package casino import ( "math/rand" ) // tableGame is a multiplayer table game implementation. Table routes every // player interaction through this interface so new games plug in without // changing the dispatch layer. type tableGame interface { join(name string) []Event leave(name string) []Event bet(name, spot string, amount int, wallet Wallet) []Event tick() []Event action(name, action string) []Event rebet(name string) (spot string, amount int) markDisconnected(name string) []Event hasParticipant(name string) bool refundAll() []Event idle() bool status(name string) []Event overview() string // seats and state expose the live occupancy and lifecycle phase for the // look command's table-details view. They are read-only. seats() []string state() string // wagers is the game's live list of bets the player could make right // now (the "list" command's data; empty when nothing is worth naming, // which also suppresses the "list" hint). hints is the full live hint // list, finished with the shared "rules" and (multi-wager) "list". wagers(name string) []WagerRow hints(name string) []MenuCommand // base returns the table identity shared with the Table handle. base() *tableBase } // tableBase is the identity every table game shares with its Table handle. // The Table and the embedded tableGame hold the SAME *tableBase, so the room // ID has a single source of truth: a rekey updates it once and every event the // game builds follows, with no second field to keep in sync. type tableBase struct { RoomID int Config TableConfig } func (b *tableBase) event(message string, public bool) Event { return Event{RoomID: b.RoomID, TableID: b.Config.ID, Type: EventResult, Message: message, Public: public, Color: "casino_action"} } func (b *tableBase) private(name, message string) Event { e := b.event(message, false) e.PrivateTo = name return e } func (b *tableBase) playerAction(name, publicMessage, privateMessage string) Event { e := b.event(publicMessage, true) e.PrivateTo = name e.PrivateMessage = privateMessage return e } // betMenuEvent builds the shared "place your bet" menu event for a table // game; only the command list differs per game. The felt already names the // game and its limits, so the menu carries just the balance and commands. func (b *tableBase) betMenuEvent(name string, commands []MenuCommand) Event { e := b.private(name, "") e.Color = "casino_menu" e.Menu = &MenuView{ShowBalance: true, Commands: commands} return e } // Table is the manager-facing handle for a multiplayer table. It only holds // routing state; all game logic lives in the tableGame implementation. It // shares one *tableBase with the game, so RoomID/Config are a single source of // truth for both. type Table struct { *tableBase game tableGame } // newTable builds the table game for cfg.Game, or nil when the game has no // multiplayer table implementation. func newTable(roomID int, cfg TableConfig, rng *rand.Rand) *Table { cfg = cfg.Normalize() def, ok := tableGameDefFor(cfg.Game) if !ok { return nil } game := def.new(roomID, cfg, rng) if game == nil { return nil } // The Table and the game share the game's base, so RoomID/Config are a // single source of truth for both. return &Table{tableBase: game.base(), game: game} } func (t *Table) join(name string) []Event { return t.game.join(name) } func (t *Table) leave(name string) []Event { return t.game.leave(name) } func (t *Table) bet(name, spot string, amount int, wallet Wallet) []Event { return t.game.bet(name, spot, amount, wallet) } func (t *Table) tick() []Event { return t.game.tick() } func (t *Table) action(name, action string) []Event { return t.game.action(name, action) } func (t *Table) rebet(name string) (string, int) { return t.game.rebet(name) } // RebetAll repeats every remembered wager for a game that keeps more than one // (a spread of roulette, Big 6, Crazy Time or baccarat wagers). It returns nil // for games whose bare "bet" is a single-wager repeat, so the caller falls // back to the singular rebet path. func (t *Table) RebetAll(name string, wallet Wallet) []Event { if b, ok := t.game.(interface { rebetAll(string, Wallet) []Event }); ok { return b.rebetAll(name, wallet) } return nil } func (t *Table) markDisconnected(name string) []Event { return t.game.markDisconnected(name) } func (t *Table) hasParticipant(name string) bool { return t.game.hasParticipant(name) } // refundAll resolves every committed wager at this table (graceful // shutdown): stakes return to their players through the wallet. func (t *Table) refundAll() []Event { return t.game.refundAll() } // idle on the Table wrapper delegates to the game. func (t *Table) idle() bool { return t.game.idle() } // Status is a read-only view: it never restarts a deadline or changes a // wager. It is an explicit look/watch, so its menu always carries hints. func (t *Table) Status(name string) []Event { return markHints(t.game.status(name)) } // Board returns the game's table-only board for the table command: the live // wheel, dice layout or paytable, with no balance, command menu, or state // prose. Games whose table is a printed felt return nil so the caller can // render the felt instead. func (t *Table) Board(name string) Snapshot { if b, ok := t.game.(interface{ board(name string) Snapshot }); ok { return b.board(name) } return nil } func (t *Table) Overview() string { return t.game.overview() } // Busy reports whether a table action for the named player is still in // flight (for craps, the dice are in the air). The command loop skips the // prompt while busy so a pending result is not split by a stray prompt. func (t *Table) Busy(name string) bool { if b, ok := t.game.(interface{ busy(name string) bool }); ok { return b.busy(name) } return false } // stakeMultiple is the optional interface for a game whose wallet debit is a // multiple of the player's stated stake. Coin Flip reserves 100x so a -100 // swing is fully covered; every other game debits the stake itself. type stakeMultiple interface{ stakeMultiple() int } // StakeMultiple reports how many credits the game debits per unit of the // player's stated stake. It sizes "bet max" and affordability previews. func (t *Table) StakeMultiple() int { if m, ok := t.game.(stakeMultiple); ok { return m.stakeMultiple() } return 1 } // Seats lists the seated players in seat order for the look command. func (t *Table) Seats() []string { return t.game.seats() } // State is the table's live lifecycle phase for the look command. func (t *Table) State() string { return t.game.state() } // RulesText is this table's full rule text (the "table rules" wording). func (t *Table) RulesText() string { return rulesTextOnTable(t.Config) } // HelpTopic is the help page the "rules" command opens while seated here. func (t *Table) HelpTopic() string { return t.Config.HelpTopic() } // Wagers is the game's live list of bets the player could make right now. // Empty when nothing is worth naming (a lone "bet N" wager, a // committed round) — the same signal that suppresses the "list" hint. func (t *Table) Wagers(name string) []WagerRow { return t.game.wagers(name) } // Hints is the game's live hint list, finished with the shared "rules" and // (multi-wager) "list" entries so look and the menus advertise both. func (t *Table) Hints(name string) []MenuCommand { return t.game.hints(name) }