package casino import ( "fmt" "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 // 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 heading for a table // game; only the command list differs per game. func (b *tableBase) betMenuEvent(name string, commands []MenuCommand) Event { heading := fmt.Sprintf("New %s hand! Place your bet.", b.Config.Game) e := b.private(name, heading) e.Color = "casino_menu" e.Menu = &MenuView{Heading: heading, 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) } 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() }