diff options
Diffstat (limited to 'internal/game/casino.go')
| -rw-r--r-- | internal/game/casino.go | 47 |
1 files changed, 36 insertions, 11 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go index 133eb34..bc702e1 100644 --- a/internal/game/casino.go +++ b/internal/game/casino.go @@ -124,13 +124,15 @@ func (w casinoWallet) payCreditsFlagged(amount int, refund bool, stakeIDs []stri } w.p.Credits = creditsAfter // The character file is the only durable record, so it must also - // carry the resolution evidence for this player's open stakes: a - // crash before the games' own ResolveStakes call would otherwise - // leave the wager records open with checkpoint balances that no - // longer match, and recovery would refund chip stakes the paid - // settlement had already consumed. The single save below is the - // commit point for the payout AND the resolution. - resolved := w.recordStakeEvidence() + // carry the resolution evidence for the stakes this settlement + // resolves: a crash before the games' own ResolveStakes call would + // otherwise leave the wager records open with checkpoint balances + // that no longer match, and recovery would refund chip stakes the + // paid settlement had already consumed. Only the settled stakes are + // recorded (see recordStakeEvidence), so a sibling hand's stake stays + // refundable. The single save below is the commit point for the + // payout AND the resolution. + resolved := w.recordStakeEvidence(stakeIDs) if serr := w.g.AccountStore.SaveCharacter(w.p); serr != nil { w.p.Credits = creditsBefore if resolved > 0 { @@ -218,15 +220,25 @@ func (w casinoWallet) ResolveStake(stakeID string) { w.recordResolutionEvidence(unresolved) } -// recordStakeEvidence adds every currently open stake of the player to the +// recordStakeEvidence adds the stakes the current settlement resolves to the // file-side resolution evidence list and returns how many IDs were newly // recorded. Used when the economy journal is unavailable: the open wager // record IDs in the character file prove the round resolved even though no // settled marker (or payout record) could ever be journaled. The caller // must persist the character file for the evidence to count. -func (w casinoWallet) recordStakeEvidence() int { +// +// stakeIDs are the stakes the payout settles (a table hand lists its own +// hand's stakes). Only those are recorded: a sibling hand's stake is settled +// by its own payout, so marking it resolved here would suppress the refund a +// crash before that payout must still produce. An empty list (a slots payout +// links no stake) settles the round that consumed the stake, so every open +// stake of the player is evidence. +func (w casinoWallet) recordStakeEvidence(stakeIDs []string) int { added := 0 for _, rec := range w.g.Economy.openStakes(economyKey(w.p)) { + if len(stakeIDs) > 0 && !containsString(stakeIDs, rec.ID) { + continue + } if !resolvedStakeRecorded(w.p, rec.ID) { w.p.ResolvedStakes = append(w.p.ResolvedStakes, rec.ID) added++ @@ -315,11 +327,15 @@ func (g *Game) leaveCasinoGames(p *player.Player) bool { if table != nil { g.emitCasinoEvents(g.Casino.Leave(table.RoomID, table.Config.ID, p.Name)) } else if machine, ok := g.Casino.MachineFor(p.Name); ok { + // Stop autobet first, then take the machine's own leave path: a + // machine between spins has no committed stake, so one quit must + // free it (and must not warn about a forfeit that cannot happen). + // A machine mid-spin refuses the leave, the player stays seated, + // and the caller warns about the stake that is really committed. if events, stopped := g.Casino.StopMachineAutoBet(machine.RoomID, machine.Config.ID, p.Name); stopped { g.emitCasinoEvents(events) - } else { - g.emitCasinoEvents(g.Casino.LeaveMachine(machine.RoomID, machine.Config.ID, p.Name)) } + g.emitCasinoEvents(g.Casino.LeaveMachine(machine.RoomID, machine.Config.ID, p.Name)) } _, stillPlaying := g.Casino.Participation(p.Name) return !stillPlaying @@ -337,6 +353,11 @@ func (g *Game) inCasinoGame(name string) bool { return playing } +// casinoTable returns the room's table for the named game. A room addresses +// at most one table (or machine) per game name — "play blackjack" carries no +// table id — so a second table for the same game would be unreachable +// content; ValidateRoomConfigs rejects that configuration instead of silently +// dropping it. func (g *Game) casinoTable(roomID int, gameName string) (*casino.Table, bool) { room, err := g.World.LoadRoom(roomID) if err != nil { @@ -357,6 +378,10 @@ func (g *Game) casinoTable(roomID int, gameName string) (*casino.Table, bool) { return nil, false } +// casinoMachine returns the room's machine for the named game or variant. +// Like casinoTable, only the first match is reachable, which is why +// ValidateRoomConfigs rejects a room with two entries addressing the same +// game name or variant. func (g *Game) casinoMachine(roomID int, gameName string) (*casino.Machine, bool) { room, err := g.World.LoadRoom(roomID) if err != nil { |
