1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
|
# 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`).
### Reserved bets and restarts
Blackjack and crash reserve the bet before the round can settle. The debit and
a `pending_plays` row are written in one transaction
(`database::reserve_play`); settling deletes that row and writes the `plays`
audit row in the same transaction.
```
pending_plays (
session_id TEXT PRIMARY KEY,
username TEXT NOT NULL,
game_type TEXT NOT NULL,
bet_amount INTEGER NOT NULL,
details TEXT NOT NULL DEFAULT '{}',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
```
If the server dies with a round in flight, the reservation survives the process.
The next startup calls `recover_orphaned_reservations` before the listener
accepts connections, and each leftover is settled as a **forfeit**: the bet is
not refunded, and the audit row gets `winnings = 0`,
`details.result = "abandoned"`, `details.reason = "server_restart"`.
The sweep runs once per process, never per connection. A per-connection sweep
would settle another player's live hand, and only one server process may own a
database.
## Wire protocol
Slots keeps its original exchange:
```
PLAY_START slots <bet>
GAME_RESULT <session> <winnings> <bet> <balance> <reels> <win_type> <win_lines> <win_cells>
```
Roulette adds a compact bets payload and a roulette-specific result:
```
PLAY_START roulette <total> <bets-json>
ROULETTE_RESULT <session> <winnings> <bet> <balance> <number> <color> <bets-json>
```
Blackjack is a multi-step session:
```
PLAY_START blackjack <bet>
BLACKJACK_STATE <json>
BLACKJACK_ACTION <session> hit|stand|double|cancel
BLACKJACK_STATE <json>
```
`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 <bet>
CRASH_START <json>
CRASH_CASHOUT <session>
CRASH_STATE <json>
CRASH_BUSTED <json> # pushed when the crash time passes
CRASH_FORFEIT <session>
CRASH_STATE <json>
```
`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`.
|