From 1f118d79e319da4e765502e9ca5b661772de94f1 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 18:38:35 -0400 Subject: refactor: casino table core; fix: stake/migration/validation gaps --- internal/casino/table.go | 62 +++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 32 deletions(-) (limited to 'internal/casino/table.go') diff --git a/internal/casino/table.go b/internal/casino/table.go index 09c19de..4be60a0 100644 --- a/internal/casino/table.go +++ b/internal/casino/table.go @@ -1,6 +1,7 @@ package casino import ( + "fmt" "math/rand" ) @@ -17,55 +18,71 @@ type tableGame interface { markDisconnected(name string) []Event hasParticipant(name string) bool refundAll() []Event - setRoom(roomID int) idle() bool + // base returns the table identity shared with the Table handle. + base() *tableBase } -// tableBase carries the identity every table game needs to build events. +// 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 { +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 { +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 { +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 menu kind and command list differ per game. +func (b *tableBase) betMenuEvent(name string, kind MenuKind, commands []MenuCommand) Event { + e := b.private(name, fmt.Sprintf("New %s hand! Place your bet.", b.Config.Game)) + e.Color = "casino_menu" + e.Menu = &MenuView{Kind: kind, 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. +// 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 { - RoomID int - Config TableConfig - game tableGame + *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() - t := &Table{RoomID: roomID, Config: cfg} + var game tableGame switch cfg.Game { case GameBlackjack: - t.game = newBlackjackTable(roomID, cfg, rng) + game = newBlackjackTable(roomID, cfg, rng) case GameBaccarat: - t.game = newBaccaratTable(roomID, cfg, rng) + game = newBaccaratTable(roomID, cfg, rng) default: return nil } - return t + // 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) } @@ -86,24 +103,5 @@ func (t *Table) hasParticipant(name string) bool { return t.game.hasPartici // shutdown): stakes return to their players through the wallet. func (t *Table) refundAll() []Event { return t.game.refundAll() } -// setRoom updates the table game's room for event routing after a room-ID -// rekey. Games keep their state; only their location moves. -func (b *tableBase) setRoom(roomID int) { b.RoomID = roomID } - -// idle reports whether no player is seated (or mid-round): only an idle -// table may have its live rules replaced by new room configuration. -func (t *blackjackTable) idle() bool { - t.mu.Lock() - defer t.mu.Unlock() - return t.players.count() == 0 -} - -// idle reports whether no player is seated (or mid-round). -func (t *baccaratTable) idle() bool { - t.mu.Lock() - defer t.mu.Unlock() - return t.players.count() == 0 -} - // idle on the Table wrapper delegates to the game. func (t *Table) idle() bool { return t.game.idle() } -- cgit v1.2.3