aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-31 17:44:10 -0400
committerhistoria <[not public]>2026-07-31 17:44:10 -0400
commitf51424c90dbce7191a31b6e675818c985f0f4539 (patch)
treea7845a2e09bd6e83d2d034f5e702a4940f38126a /internal/game/casino.go
parent58758d41488a777d2bc0e787be655363bdd9eb60 (diff)
downloadthehouseoficarus-f51424c90dbce7191a31b6e675818c985f0f4539.tar.gz
feat: casino framework and basic slot machine
Diffstat (limited to 'internal/game/casino.go')
-rw-r--r--internal/game/casino.go319
1 files changed, 319 insertions, 0 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go
new file mode 100644
index 0000000..c6fb63b
--- /dev/null
+++ b/internal/game/casino.go
@@ -0,0 +1,319 @@
+package game
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "thehouseoficarus/internal/casino"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+type casinoWallet struct {
+ g *Game
+ p *player.Player
+}
+
+func (w casinoWallet) Wager(amount int) (casino.Wager, bool) {
+ chips := countChips(w.p)
+ if chips > amount {
+ chips = amount
+ }
+ credits := amount - chips
+ if w.p.Credits < credits {
+ return casino.Wager{}, false
+ }
+ if chips > 0 {
+ w.p.RemoveItem("chips", chips)
+ }
+ w.p.Credits -= credits
+ w.g.AccountStore.SaveCharacter(w.p)
+ return casino.Wager{Total: amount, Chips: chips, Credits: credits}, true
+}
+
+func (w casinoWallet) PayCredits(amount int) {
+ w.p.Credits += amount
+ w.g.AccountStore.SaveCharacter(w.p)
+}
+
+func (g *Game) casinoTable(roomID int, gameName string) (*casino.Table, bool) {
+ room, err := g.World.LoadRoom(roomID)
+ if err != nil {
+ return nil, false
+ }
+ for _, cfg := range room.CasinoTables {
+ if strings.EqualFold(string(cfg.Game), gameName) {
+ return g.Casino.EnsureTable(roomID, cfg), true
+ }
+ }
+ return nil, false
+}
+
+func (g *Game) casinoMachine(roomID int, gameName string) (*casino.Machine, bool) {
+ room, err := g.World.LoadRoom(roomID)
+ if err != nil {
+ return nil, false
+ }
+ for _, cfg := range room.CasinoMachines {
+ if strings.EqualFold(string(cfg.Game), gameName) || strings.EqualFold(cfg.Variant, gameName) {
+ return g.Casino.EnsureMachine(roomID, cfg), true
+ }
+ }
+ // Migrate older room definitions that declared slots under casino_tables;
+ // slots are still always instantiated as one-player machines.
+ if strings.EqualFold(gameName, "slots") {
+ for _, cfg := range room.CasinoTables {
+ if strings.EqualFold(string(cfg.Game), "slots") {
+ return g.Casino.EnsureMachine(roomID, casino.MachineConfig{
+ ID: cfg.ID, Game: casino.GameSlots, MinBet: cfg.MinBet, MaxBet: cfg.MaxBet,
+ }), true
+ }
+ }
+ }
+ return nil, false
+}
+
+func (g *Game) executePlay(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ if p == nil || len(args) == 0 {
+ sess.WriteLine("Play what?")
+ return
+ }
+ if _, playing := g.Casino.Participation(p.Name); playing {
+ sess.WriteLine("You're already playing a game. Type \"stop\" between rounds to leave it.")
+ return
+ }
+ g.startCasinoGame(sess, p, args[0])
+}
+
+func (g *Game) startCasinoGame(sess *net.Session, p *player.Player, gameName string) {
+ if p == nil || sess == nil {
+ return
+ }
+ if _, playing := g.Casino.Participation(p.Name); playing {
+ sess.WriteLine("You're already playing a game. Type \"stop\" between rounds to leave it.")
+ return
+ }
+ if machine, ok := g.casinoMachine(p.RoomID, gameName); ok {
+ g.emitCasinoEvents(g.Casino.JoinMachine(machine.RoomID, machine.Config, p.Name))
+ return
+ }
+ table, ok := g.casinoTable(p.RoomID, gameName)
+ if !ok {
+ sess.WriteLine(fmt.Sprintf("There is no %s table here.", gameName))
+ return
+ }
+ g.emitCasinoEvents(g.Casino.Join(table.RoomID, table.Config, p.Name))
+}
+
+func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ if p == nil {
+ sess.WriteLine("Bet how much?")
+ return
+ }
+ if machine, ok := g.Casino.MachineFor(p.Name); ok {
+ if len(args) == 0 {
+ g.emitCasinoEvents(g.Casino.StartMachine(machine.RoomID, machine.Config.ID, p.Name, casinoWallet{g: g, p: p}))
+ return
+ }
+ if len(args) == 1 && args[0] == "max" {
+ g.emitCasinoEvents(g.Casino.SetMachineBet(machine.RoomID, machine.Config.ID, p.Name, machine.Config.MaxBet))
+ return
+ }
+ if len(args) == 1 {
+ if amount, err := strconv.Atoi(args[0]); err == nil && amount > 0 {
+ g.emitCasinoEvents(g.Casino.SetMachineBet(machine.RoomID, machine.Config.ID, p.Name, amount))
+ return
+ }
+ }
+ g.emitCasinoEvents(g.Casino.StartMachine(machine.RoomID, machine.Config.ID, p.Name, casinoWallet{g: g, p: p}))
+ return
+ }
+ if len(args) == 0 {
+ sess.WriteLine("Bet how much?")
+ return
+ }
+ amount, err := strconv.Atoi(args[0])
+ if err != nil || amount <= 0 {
+ sess.WriteLine("Your bet must be a positive whole number.")
+ return
+ }
+ table, playing := g.Casino.Participation(p.Name)
+ if !playing {
+ sess.WriteLine("You're not playing a game.")
+ return
+ }
+ g.emitCasinoEvents(g.Casino.Bet(table.RoomID, table.Config.ID, p.Name, amount, casinoWallet{g: g, p: p}))
+}
+
+func (g *Game) executeAutoBet(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ if p == nil {
+ return
+ }
+ if machine, ok := g.Casino.MachineFor(p.Name); ok {
+ g.emitCasinoEvents(g.Casino.EnableMachineAutoBet(machine.RoomID, machine.Config.ID, p.Name, casinoWallet{g: g, p: p}))
+ return
+ }
+ sess.WriteLine("Autobet is only available at games without player choices.")
+}
+
+func (g *Game) CasinoTick() {
+ if g.Casino != nil {
+ g.emitCasinoEvents(g.Casino.Tick())
+ }
+}
+
+func (g *Game) emitCasinoEvents(events []casino.Event) {
+ for _, event := range events {
+ if event.Message == "" {
+ continue
+ }
+ category := event.Color
+ if category == "" {
+ category = casinoEventColor(event.Type)
+ }
+ if event.Public && g.Hub != nil {
+ for _, sess := range g.Hub.PlayersInRoom(event.RoomID) {
+ if event.PrivateTo != "" && sess.Player != nil && sess.Player.Name == event.PrivateTo {
+ continue
+ }
+ message := event.Message
+ if category == "casino_menu" && sess.Player != nil && sess.Player.Name == event.PrivateTo {
+ message = g.casinoMenuBalance(sess, message)
+ }
+ if event.Slot != nil && sess.Player != nil {
+ message += "\n" + renderSlotSnapshot(event.Slot, g.colorMode(sess), sess.Player.OptionBool("unicode"))
+ }
+ sess.WriteLine(g.colorize(sess, category, message))
+ }
+ }
+ if event.PrivateMessage != "" && event.PrivateTo != "" {
+ g.charsMu.Lock()
+ sess := g.loggedInChars[event.PrivateTo]
+ g.charsMu.Unlock()
+ if sess != nil {
+ privateMessage := event.PrivateMessage
+ if category == "casino_menu" {
+ privateMessage = g.casinoMenuBalance(sess, privateMessage)
+ }
+ sess.WriteLine(g.colorize(sess, category, privateMessage))
+ if category == "casino_menu" {
+ g.writePrompt(sess)
+ }
+ }
+ }
+ if event.PrivateTo != "" {
+ g.charsMu.Lock()
+ sess := g.loggedInChars[event.PrivateTo]
+ g.charsMu.Unlock()
+ if sess != nil && event.PrivateMessage == "" {
+ privateMessage := event.Message
+ if category == "casino_menu" {
+ privateMessage = g.casinoMenuBalance(sess, privateMessage)
+ }
+ sess.WriteLine(g.colorize(sess, category, privateMessage))
+ if category == "casino_menu" {
+ g.writePrompt(sess)
+ }
+ }
+ }
+ }
+}
+
+func casinoEventColor(eventType casino.EventType) string {
+ if eventType == casino.EventPayout {
+ return "casino_win"
+ }
+ return "casino_action"
+}
+
+func (g *Game) casinoMenuBalance(sess *net.Session, message string) string {
+ if sess == nil || sess.Player == nil {
+ return message
+ }
+ chips := g.colorize(sess, "casino_chips", fmt.Sprintf("%d", countChips(sess.Player)))
+ credits := g.colorize(sess, "casino_credits", fmt.Sprintf("%d", sess.Player.Credits))
+ line := fmt.Sprintf("You have %s chips and %s credits.", chips, credits)
+ return strings.Replace(message, "Slot machine commands:", line+"\n\nSlot machine commands:", 1)
+}
+
+func renderSlotSnapshot(snapshot *casino.SlotSnapshot, mode string, unicode bool) string {
+ if snapshot == nil {
+ return ""
+ }
+ left, junction, right, horizontal, vertical := '+', '+', '+', '-', '|'
+ bottomLeft, bottomJunction, bottomRight := '+', '+', '+'
+ if unicode {
+ left, junction, right, horizontal, vertical = '\u250c', '\u252c', '\u2510', '\u2500', '\u2502'
+ bottomLeft, bottomJunction, bottomRight = '\u2514', '\u2534', '\u2518'
+ }
+ separator := string(left)
+ for reel := 0; reel < 5; reel++ {
+ if reel > 0 {
+ separator += string(junction)
+ }
+ separator += strings.Repeat(string(horizontal), 9)
+ }
+ separator += string(right)
+ lines := []string{casinoFrame(mode, separator)}
+ frameVertical := casinoFrame(mode, string(vertical))
+ for row := 0; row < 3; row++ {
+ line := frameVertical
+ for reel := 0; reel < 5; reel++ {
+ value := "..."
+ if reel < snapshot.StoppedReels {
+ value = snapshot.Grid[row][reel]
+ }
+ line += " " + colorSlotSymbol(mode, value) + strings.Repeat(" ", 7-len(value)) + " " + frameVertical
+ }
+ lines = append(lines, line)
+ }
+ bottom := string(bottomLeft)
+ for reel := 0; reel < 5; reel++ {
+ if reel > 0 {
+ bottom += string(bottomJunction)
+ }
+ bottom += strings.Repeat(string(horizontal), 9)
+ }
+ bottom += string(bottomRight)
+ lines = append(lines, casinoFrame(mode, bottom))
+ return strings.Join(lines, "\n")
+}
+
+func casinoFrame(mode, text string) string {
+ spec := color.NoColor()
+ spec.Fg = 244
+ return color.Render(mode, spec, text)
+}
+
+func colorSlotSymbol(mode, symbol string) string {
+ spec := color.NoColor()
+ spec.Fg = 250
+ switch symbol {
+ case "straw":
+ spec = color.NoColor()
+ spec.Gradient = []int{220, 226}
+ case "stick":
+ spec = color.NoColor()
+ spec.Gradient = []int{34, 46}
+ case "brick":
+ spec = color.NoColor()
+ spec.Gradient = []int{160, 196}
+ case "hat":
+ spec = color.NoColor()
+ spec.Gradient = []int{129, 201}
+ case "wolf":
+ spec = color.NoColor()
+ spec.Gradient = []int{27, 39}
+ case "scatter":
+ spec = color.NoColor()
+ spec.Gradient = []int{196, 220, 46, 51, 129, 201}
+ default:
+ spec.Dim = true
+ }
+ return color.Render(mode, spec, symbol)
+}