# Games and the play audit ## Implemented | Game | Status | Notes | | --- | --- | --- | | Slots | Done | 5 reels x 3 rows, 5 paylines, 98.6% RTP | | Roulette | Done | European wheel, split bets, 97.3% RTP | | Blackjack | Done | Six-deck shoe per hand, S17, 3:2, hit/stand/double | | Crash | Done | Local multiplier, server-owned clock and crash event, 99% RTP | ## Planned | Game | Shape | Main open decisions | | --- | --- | --- | | — | — | Provable fairness for the crash point (`hash` commit/reveal) | Crash rounds are private to one player and use the same request/response socket plus one server-initiated line. A shared multiplayer round would need a lobby and broadcast, which is a different product. ## Crash - The multiplier is `exp(0.1386294 * t)`: 1.00x at the start, 2.00x at 5s, 10x at ~16.6s, capped at 1000x. - The crash point is drawn when the round starts: `max(1.0, (1 - 0.01) / (1 - u))` for a uniform `u`. That gives `P(crash >= m) = 0.99 / m`, so a fixed-target cashout returns 99% of the stake. The cap trims the RTP by a hair. - The crash point is **not** sent to the client until the round ends. Knowing it early would let a patched client cash out at `crash - epsilon` every round. - The client draws the multiplier from its own monotonic clock, anchored when `CRASH_START` arrives. That number is display only. - The server computes the cashout multiplier from its own clock when the request arrives. The client never sends a multiplier. A cashout that arrives after the crash time loses the bet. - Because `P(crash = 1.00x) = 1%`, an instant cashout returns the stake 99% of the time and nothing 1% of the time. There is no risk-free window, which is why there is no cashout lockout. - Payouts are `floor(bet * multiplier)` in whole PLN, so anything under 1.10x on a 10 PLN bet returns 10. - When the crash time passes, the server pushes `CRASH_BUSTED` on the same connection without being asked. The client's event loop polls for keys and drains the socket every 25ms. - Leaving with Q or disconnecting during a round forfeits the reserved bet and writes a `result = forfeit` audit row. Client controls: ``` C cash out at the server's current multiplier SPACE start the next round + or = / - change the next round's bet Q forfeit an active round (bet is lost), or leave when idle ``` ## Blackjack - Six-deck shoe, shuffled fresh for every hand and discarded afterwards. - Dealer stands on all 17s (including soft 17). - Blackjack pays 3:2, rounded down to whole PLN. - Hit, stand, and double; no split, insurance, or surrender. - The bet is reserved when the hand starts. Leaving an active hand with Q, or disconnecting mid-hand, forfeits the reserved bet; it is not refunded. A settled hand is unaffected. - A natural or dealer blackjack settles immediately. Client controls: ``` H hit S stand D double (on the first two cards) SPACE deal the next hand + or = / - change the next hand's bet Q forfeit an active hand (bet is lost), or leave when no hand is active ``` ## Roulette - European wheel: 0-36, one green zero. - Bets: red, black, even, odd, and straight numbers. - Bets can be split across all of those; every winning bet is added together. - Straight pays 35:1 (36x returned), outside bets pay 1:1 (2x returned). - 0 loses all outside bets. RTP is 36/37 = 97.30%. - Numbers are color-coded in the TUI: red numbers are red, 0 is green, and black numbers stay at the default terminal color. Bet amounts and balances are not colored. Client controls inside roulette: ``` R / B / E / O add the current chip to red/black/even/odd digits + Enter add the current chip to that number C clear all bets + or = / - change the chip size SPACE spin Q leave the table ``` The result is shown as a CS:GO-style horizontal strip: the ball marker stays fixed and the wheel numbers scroll beneath it, ending on the winning number. ## Audit schema Every settled play is one row in `plays`: ``` plays ( session_id TEXT PRIMARY KEY, username TEXT NOT NULL, game_type TEXT NOT NULL, bet_amount INTEGER NOT NULL, winnings INTEGER NOT NULL, balance_after INTEGER NOT NULL, details TEXT NOT NULL DEFAULT '{}', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ``` `details` is game-specific JSON: ```json // slots {"reels":"CCCCLLCLODGSDGS","win_type":"win","win_lines":"1","win_cells":"0,3,6"} // roulette {"number":"17","color":"black","bets":"{\"red\":10,\"black\":10,\"even\":10,\"odd\":10,\"numbers\":[{\"number\":17,\"amount\":5}]}"} // blackjack {"player":["10S","6H","5D"],"dealer":["9D","8C"],"player_total":21,"dealer_total":17,"bet":10,"result":"win","winnings":20} // crash {"bet":10,"crash_point":3.0,"started_at":12.44,"phase":"cashed","result":"cashed","multiplier":2.0,"payout":20} ``` The balance update and the `plays` insert happen in one transaction (`database::apply_play`). ## Wire protocol Slots keeps its original exchange: ``` PLAY_START slots GAME_RESULT ``` Roulette adds a compact bets payload and a roulette-specific result: ``` PLAY_START roulette ROULETTE_RESULT ``` Blackjack is a multi-step session: ``` PLAY_START blackjack BLACKJACK_STATE BLACKJACK_ACTION hit|stand|double|cancel BLACKJACK_STATE ``` `BLACKJACK_STATE` carries the player/dealer cards, totals, actions, balance, result and winnings. The dealer's hole card is `?` until the hand is done. Crash sends one line when the round starts, one line when it crashes (without a request), and answers the cashout: ``` PLAY_START crash CRASH_START CRASH_CASHOUT CRASH_STATE CRASH_BUSTED # pushed when the crash time passes CRASH_FORFEIT CRASH_STATE ``` `CRASH_STATE` carries `session_id`, `phase` (`running`, `cashed`, `crashed`, `forfeit`), `bet`, `balance`, the current/final `multiplier`, `potential`, `crash_point` (null while running), `payout` and `result`.