package casino import "fmt" // betRangeText describes the accepted wager range; the unlimited sentinel is // rendered as "no maximum" instead of a machine-sized integer. func betRangeText(minBet, maxBet int) string { if maxBet == maxBetUnlimited { return fmt.Sprintf("%d or more (no maximum)", minBet) } return fmt.Sprintf("between %d and %d", minBet, maxBet) } type GameName string const ( GamePaiGow GameName = "paigow" GameVideoPoker GameName = "video_poker" GameSlots GameName = "slots" GamePlinko GameName = "plinko" GamePachinko GameName = "pachinko" GameCashHunt GameName = "cash_hunt" GameCoinFlip GameName = "coin_flip" GameBlackjack GameName = "blackjack" GameBaccarat GameName = "baccarat" GameRoulette GameName = "roulette" GameBigSix GameName = "big6" GameCrazyTime GameName = "crazy_time" GameCraps GameName = "craps" ) type BlackjackConfig struct { Decks int `yaml:"decks,omitempty"` ShuffleAt float64 `yaml:"shuffle_at,omitempty"` BlackjackPayout float64 `yaml:"blackjack_payout,omitempty"` DealerHitSoft17 *bool `yaml:"dealer_hit_soft_17,omitempty"` MaxSplitHands int `yaml:"max_split_hands,omitempty"` ResplitAces *bool `yaml:"resplit_aces,omitempty"` DoubleAfterSplit *bool `yaml:"double_after_split,omitempty"` DoubleSplitBlackjack *bool `yaml:"double_split_blackjack,omitempty"` InsurancePayout float64 `yaml:"insurance_payout,omitempty"` Surrender *bool `yaml:"surrender,omitempty"` ActionTicks int `yaml:"action_ticks,omitempty"` SplitUnlikeTenValues *bool `yaml:"split_unlike_ten_values,omitempty"` } func ruleBool(value *bool, defaultValue bool) bool { if value == nil { return defaultValue } return *value } func (c BlackjackConfig) DealerHitsSoft17() bool { return ruleBool(c.DealerHitSoft17, false) } func (c BlackjackConfig) AllowsResplitAces() bool { return ruleBool(c.ResplitAces, false) } func (c BlackjackConfig) AllowsDoubleAfterSplit() bool { return ruleBool(c.DoubleAfterSplit, true) } // CountsDoubleSplitBlackjack reports whether a 3+ card hand that reaches 21 // after a split and double is paid as a blackjack (3:2). When true (the // default) this raises the table RTP above the nominal blackjack payout; // standard play pays 1:1 for any 21 on a split hand, doubled or not. func (c BlackjackConfig) CountsDoubleSplitBlackjack() bool { return ruleBool(c.DoubleSplitBlackjack, true) } func (c BlackjackConfig) AllowsSurrender() bool { return ruleBool(c.Surrender, false) } // AllowsUnlikeTenSplit reports whether any two 10-valued cards may split // (K+Q, 10+J). Casinos vary on whether identical ranks are required; true // (the default) is the more permissive setting. func (c BlackjackConfig) AllowsUnlikeTenSplit() bool { return ruleBool(c.SplitUnlikeTenValues, true) } func (c BlackjackConfig) Normalize() BlackjackConfig { if c.Decks <= 0 { c.Decks = 8 } if c.ShuffleAt <= 0 || c.ShuffleAt >= 1 { c.ShuffleAt = 0.25 } if c.BlackjackPayout <= 0 { c.BlackjackPayout = 1.5 } if c.MaxSplitHands <= 0 { c.MaxSplitHands = 4 } if c.InsurancePayout <= 0 { c.InsurancePayout = 2 } if c.ActionTicks <= 0 { c.ActionTicks = 20 } return c } // BaccaratConfig holds punto banco rules, including no-commission exceptions // and optional wagers. Commission tables pay BankerPayout:1 (0.95 = 5% fee); // no-commission variants pay even money except for their specified exception. type BaccaratConfig struct { Variant string `yaml:"variant,omitempty"` // commission, super_6, ez_baccarat PairBets bool `yaml:"pair_bets,omitempty"` Dragon7 bool `yaml:"dragon_7,omitempty"` Panda8 bool `yaml:"panda_8,omitempty"` Super6Bet bool `yaml:"super_6_bet,omitempty"` Decks int `yaml:"decks,omitempty"` ShuffleAt float64 `yaml:"shuffle_at,omitempty"` TiePayout float64 `yaml:"tie_payout,omitempty"` BankerPayout float64 `yaml:"banker_payout,omitempty"` } func (c BaccaratConfig) Normalize() BaccaratConfig { if c.Variant == "" { c.Variant = BaccaratCommission } if c.Decks <= 0 { c.Decks = 8 } if c.ShuffleAt <= 0 || c.ShuffleAt >= 1 { c.ShuffleAt = 0.25 } if c.TiePayout <= 0 { c.TiePayout = 8 } if c.Variant == BaccaratSuper6 || c.Variant == BaccaratEZ { c.BankerPayout = 1 } else if c.BankerPayout <= 0 { c.BankerPayout = 0.95 } return c } type TableConfig struct { Plinko PlinkoConfig `yaml:"plinko,omitempty"` ID string `yaml:"id"` Game GameName `yaml:"game"` MaxPlayers int `yaml:"max_players,omitempty"` BettingTicks int `yaml:"betting_ticks,omitempty"` MinBet int `yaml:"min_bet,omitempty"` MaxBet int `yaml:"max_bet,omitempty"` Blackjack BlackjackConfig `yaml:"blackjack,omitempty"` Baccarat BaccaratConfig `yaml:"baccarat,omitempty"` Roulette RouletteConfig `yaml:"roulette,omitempty"` Craps CrapsConfig `yaml:"craps,omitempty"` VideoPoker VideoPokerConfig `yaml:"video_poker,omitempty"` PaiGow PaiGowConfig `yaml:"pai_gow,omitempty"` } type MachineConfig struct { ID string `yaml:"id"` Game GameName `yaml:"game"` Variant string `yaml:"variant,omitempty"` MinBet int `yaml:"min_bet,omitempty"` MaxBet int `yaml:"max_bet,omitempty"` SpinTicks int `yaml:"spin_ticks,omitempty"` Payout float64 `yaml:"payout,omitempty"` } func (c MachineConfig) Normalize() MachineConfig { if c.MinBet <= 0 { c.MinBet = 1 } // An unset MaxBet defaults to the economy's bounded cap; the explicit // unlimited sentinel is still honored for authored configurations. if c.MaxBet <= 0 { c.MaxBet = DefaultMaxBet } // Never leave a normalized config self-contradictory: a min_bet above // the default cap must widen the effective maximum rather than produce // a game every wager is rejected from. if c.MaxBet < c.MinBet { c.MaxBet = c.MinBet } if c.SpinTicks <= 0 { c.SpinTicks = 2 } if c.Variant == "" { c.Variant = "huff_and_even_more_puff" } if c.Payout <= 0 { c.Payout = 0.97 } return c } func (c TableConfig) Normalize() TableConfig { if c.MaxPlayers <= 0 { c.MaxPlayers = 8 } if c.BettingTicks <= 0 { c.BettingTicks = 20 } if c.MinBet <= 0 { c.MinBet = 1 } if c.MaxBet <= 0 { c.MaxBet = DefaultMaxBet } // Normalize the game's own rule block through its registration, so a new // game gets its defaults without a third switch to remember. if def, ok := tableGameDefFor(c.Game); ok { c = def.normalize(c) } if c.MaxBet < c.MinBet { c.MaxBet = c.MinBet } return c } type Wager struct { Total int Chips int Credits int // ID is the wallet's opaque handle for the committed stake. It names the // stake when a settlement resolves it (PayCredits) or consumes it // (ResolveStake). ID string } type Wallet interface { // Wager debits the stake and records it as pending. ErrInsufficientFunds // is a normal rejected bet; ErrWalletPersistence means the debit could // not be made durable and the game must not proceed as if it succeeded. Wager(amount int) (Wager, error) // PayCredits banks a payout durably and, in the same save, resolves the // listed stakes (stakeIDs may be empty, for a payout that returns no // stake). A table passes the hand's stake IDs so the payout and the // stake return are one durable step; a slot win passes its triggering // stake. A failed save leaves the stakes pending — refunded at the next // startup, never paid on top of the refund — and the game must not // announce the win. PayCredits(amount int, stakeIDs ...string) error // ResolveStake consumes ONE pending stake by wallet handle (a single // hand paid or lost while a sibling hand's round is still in flight). ResolveStake(stakeID string) // ResolveStakesByIDs consumes the listed pending stakes in ONE save. It is // the batched form of ResolveStake used when a losing round has several // stakes (split/double hands) so they are not committed one save at a time. ResolveStakesByIDs(stakeIDs ...string) // RefundOutstanding returns every pending stake to the player (chips as // chips, credits as credits) and clears the ledger. An error means the // refund could not be made durable: nothing was returned and the stakes // remain pending for a later attempt or startup recovery. RefundOutstanding() (chips, credits int, err error) // RefundStakes returns the named pending stakes in kind — chips as chips, // credits as credits — and removes them from the ledger in one save. It is // the take-back path for a seat that holds specific handles; unlike // PayCredits it never converts casino-only chips into spendable credits. RefundStakes(stakeIDs ...string) error } type EventType string // Only the event types the games actually construct are defined. Payout and // win styling travels on Event.Color, so there is no separate payout event // type to keep in sync. const ( EventJoined EventType = "joined" EventResult EventType = "result" EventLeft EventType = "left" ) // MenuView carries a structured menu alongside its plain-text Message so the // presentation layer can enrich it (balances, command colors) without parsing // message prefixes. It is deliberately game-agnostic: every game supplies a // heading and its command list, and the renderer has no per-game switch that // could silently drop a new game's commands. type MenuView struct { Heading string // prompt heading shown above the command list ShowBalance bool // prepend the player's chips/credits balance Commands []MenuCommand Repeat bool // returning bettor: compact clients can show a short reminder // ShowHints marks a menu that always carries its command list under the // "casino_hints once" option: the join menu and an explicit look/status. ShowHints bool } // MenuCommand is one selectable command shown in a casino menu: the typed // command text plus a short human-readable description. type MenuCommand struct { Command string Desc string Cost int // additional stake; presentation can mark unaffordable actions } type Event struct { RoomID int TableID string Type EventType Player string Message string PrivateMessage string PrivateTo string Color string Public bool // Snapshot is the optional per-game view attached to the event. A game // implements the Snapshot interface for its view type; the presentation // layer type-switches on it. Because the field is typed, a new game's // snapshot can never be assigned without implementing the interface, and // the event filter can test it with a single nil check instead of // enumerating every game's field. // Settlement is private to PrivateTo, never broadcast to spectators. // Message/PrivateMessage remain a plain-text fallback for non-UI consumers. Settlement []SettlementLine SettlementNote string // warning displayed alongside a partial settlement Snapshot Snapshot Menu *MenuView ShowBalance bool // append the recipient's current available funds Detail bool // action-by-action public output, only for participants/watchers } // Snapshot is the marker interface for a game's structured event view. The // method is unexported so only types in this package can implement it. type Snapshot interface{ snapshot() } type SlotSnapshot struct { Grid [slotGridRows][slotGridReels]string StoppedReels int } func (*SlotSnapshot) snapshot() {} // CardView renders one card for any card-based table game. type CardView struct { Rank string Suit string Hidden bool } type BlackjackPlayerView struct { Name string Cards []CardView Score string Hand int Hands int Bet int Active bool Status string } type BlackjackSnapshot struct { Dealer []CardView DealerShownScore string DealerScore string DealerRevealed bool Players []BlackjackPlayerView } func (*BlackjackSnapshot) snapshot() {} // BlackjackRulesView ships a table's configuration so the presentation layer // can draw the house-rule felt instead of a raw rules dump. type BlackjackRulesView struct { Config TableConfig } func (*BlackjackRulesView) snapshot() {} // BaccaratBetView lists one participant's wager on a baccarat outcome. type BaccaratBetView struct { Name string Spot string Amount int } type BaccaratSnapshot struct { Player []CardView Banker []CardView PlayerScore string BankerScore string Bets []BaccaratBetView } func (*BaccaratSnapshot) snapshot() {} // BaccaratRulesView ships a table's configuration so the presentation layer // can draw the house-rule felt instead of a raw rules dump. type BaccaratRulesView struct { Config TableConfig } func (*BaccaratRulesView) snapshot() {} type Participant struct { Name string Connected bool HasBet bool Wager Wager Wallet Wallet SittingOut bool SitOutAfter bool LeaveAfter bool } // SettlementBalanceNeeded reports whether a settlement should print the seat's // balance: the seat is leaving, sitting out after the round, or disconnected, // and the payout did not fail. Shared by every table so the rule lives in one // place instead of being copy-pasted per game. func (p *Participant) SettlementBalanceNeeded(payoutFailed bool) bool { return !payoutFailed && (p.LeaveAfter || p.SitOutAfter || !p.Connected) }