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
|
# Project Conventions for zagan-bet
## Type Safety
- SQLite `INTEGER` is `i64`. Never cast to `u64` without a bounds check.
- Balance, bet, and winnings fields must agree on `i64` or `u64` across the
entire codebase — pick one and enforce it.
- Negative balance is a state the DB can represent; the application must reject
plays that would produce one, not silently wrap via `as u64`.
## Game Logic
- Never compute the same payout/result in two places. `process_action` and
`calculate_result` must not both independently derive winnings.
The canonical split: `process_action` mutates state and stores raw data
(reels, win_type); `calculate_result` reads that data and produces the
`GameResult`. One path, one formula.
- When checking symmetry (e.g. two-of-a-kind), enumerate **all** pairs, not
just adjacent indices. `[a, b, a]` is a valid match.
## Session Tracking
- Every `PLAY_START` must produce a session entry before the balance is
touched. The session is the audit trail for the deduction; without it the
deduction is orphaned.
- Multi-step games (blackjack, crash) reserve with `database::reserve_play`:
the debit and the `pending_plays` row are one transaction. Settlement deletes
that row and writes the `plays` row in the same transaction.
- Sweep orphaned reservations only at startup (`recover_orphaned_reservations`),
never per connection. A per-connection sweep would settle another player's
live hand. An orphaned reservation is a forfeit, never a refund.
- Clean up sessions on client disconnect. If a game is in progress when the
connection drops, settle it as a forfeit (the reserved bet is lost) and
record the play; never refund an active hand.
## Server Handler
- Never hold a lock, drop it, then re-acquire it in the same function to
mutate the same resource. Acquire once, scope the lock to the operation.
- All balance mutations (`SET balance = balance ± ?`) must happen in a
transaction.
## Crash
- The client draws the multiplier from its own clock for display only. A
cashout carries a session id and nothing else; the multiplier and payout come
from the server's clock at receipt.
- Never send the crash point to the client before the round ends. A client that
knows it can cash out at `crash - epsilon` every round for a guaranteed win.
- The crash point is `max(1.0, (1 - edge) / (1 - u))`; do not "fix" the 1% of
rounds that crash at 1.00x. That is where the house edge is charged on an
instant cashout, and it is why no cashout lockout is needed.
- The crash event is a server-initiated line (`CRASH_BUSTED`), not a reply.
Client code must drain the socket on a timer and route pushed lines by
prefix instead of assuming the next line answers the last request.
- `ServerCore::poll` must be idempotent: it settles the round, removes the
session, and pushes once. A second poll must produce nothing.
## GUI
- The GUI is a second frontend over `ClientCore`, not a second client.
Frontends send `Command` and render `View`; the core owns navigation and
state and contains no I/O or terminal types.
- The command-line editor and key mapping live in `client::input`. The core
receives `Command` or a finished line through `submit_line`, never a key.
- egui lives in the `gui/` workspace member. Its dependencies must never
enter the TUI or server build graph: no GUI crate in the root package's
`[dependencies]`. The GUI may depend on the core, never the reverse.
- Build the GUI with `cargo build -p zagan-bet-gui --profile gui-release`;
the profile lives at the workspace root because a member's is ignored.
- Colours, spacing and font sizes come from `gui/src/theme.rs`; screens must
not hard-code them. The embedded fonts are Open Sans (OFL) and Hack (MIT);
ship their licence files with any change.
- GUI integration tests live in `gui/tests/`, because the root package cannot
depend on the GUI.
- First contact shows the fingerprint and waits for a decision before sending
any credentials. A changed key is a hard stop; forgetting it is explicit.
- The GUI is themed deliberately. Default egui styling is not an acceptable
look; keep a style module and a reference screen. The palette is derived
from the terminal's ANSI colours, and felt green is only ever the roulette
table.
- The GUI felt is pointer-first: the hover snaps to the shared fine lattice
(cell centre, edge, crossing), left click adds the chip and right or middle
click takes one back. The terminal keeps its arrow-key cursor; the GUI does
not paint it.
- The GUI network worker (`gui/src/net.rs`) owns the TLS stream and the request
deadline; `ClientCore` stays I/O-free. One thread with channels, no async
runtime.
- Game sidebars pin their action block with `widgets::sidebar_layout`. An
action that moves as the bet slip grows is a bug.
- The embedded fonts have no suit or arrow glyphs. Draw card suits as shapes
and use plain words for navigation; a glyph that is not in Open Sans or Hack
renders as tofu.
- `scripts/publish.sh` builds the GUI with `gui-release`; the installers are
stable site files (`web/install.sh`, `web/install.ps1`) that read
`web/latest.php` for the newest build and its SHA-256. Never regenerate or
version the installers per release, install system-wide, or skip the hash
check. The Windows installer waits for Enter before closing; `-NoPause` is
only for automation.
- Windows renders through wgpu on Direct3D 12, including the WARP software
adapter, so a machine with only OpenGL 1.1 (VM, Remote Desktop) still runs.
Linux keeps the smaller glow renderer. Do not assume OpenGL 2.0 on Windows.
- The download page links the public git repository
(https://git.2137697.xyz/zagan-bet.git) for the GPL corresponding source.
Run publish from a clean tree so the linked history matches the binaries.
## Client TUI
- `render_lines()` and `render()` must agree. If `render()` uses `\r\n`,
count lines against the same string the cursor will erase.
Prefer returning a fixed known count for the game's static layout and
letting dynamic content fill it, rather than inferring the count.
- The client refuses to start below 80x24 (`terminal_too_small`). Every game
board must fit in that size; a new board wider or taller than the minimum
must raise the minimum in the same change.
- A resize below the minimum warns, but never during a live round
(`ClientCore::is_live_game`): a reserved bet must be finishable.
## Schema
- The app is in alpha: do not write schema migrations. `init_db` creates the
current shape; a schema change means deleting `server.db` and `client.db`.
Files written by an older build are not supported.
## Error Paths
- When a game action fails after a balance deduction, the refund must happen
in the same transaction scope as the deduction, not as a separate
`execute()` call.
- Never silently ignore `Err` on balance mutations. If the refund fails,
log the username, balance, and operation; surface to the operator.
## Tests
- Every `calculate_payout` / `calculate_result` edge case gets a unit test
in `tests/`. Tests that exercise game logic use a deterministic seed or
a test helper that calls the function directly, not the network.
- A change to game logic that breaks an existing payout pattern must fail
a test before it reaches the server.
## TLS / TOFU
- See `src/tofu.rs` for the trust model. The fingerprint is over the
certificate's SPKI, not the whole certificate.
- Credentials are keyed by the SPKI fingerprint, not by address: one login
covers every address that presents the same key. Never look credentials up
by host.
- `forget_host` removes one address. Drop the credential only when no address
with that fingerprint remains, or two addresses for one server would stop
sharing a login. `forget-login` is the narrower action: it drops the
credential and keeps the pinned key.
- `save_host` must not wipe the fingerprint column. Use `ON CONFLICT ...
DO UPDATE SET last_used` — never `INSERT OR REPLACE`.
- The client transport lives in `client::transport`. It records the key during
the handshake, then asks the frontend (`TrustPrompt`) before sending `HELLO`
or credentials. A changed key is a hard error, and forgetting it is explicit.
## Versioning
Use semantic versioning (`MAJOR.MINOR.PATCH`) in `Cargo.toml`.
- **MAJOR** — breaking protocol changes (wire format, TLS trust model,
database schema migration that can't auto-upgrade).
- **MINOR** — new features, new game types, new commands, or any change to
shared semantics.
- **PATCH** — bug fixes, payout corrections, refactors that don't change
observable behavior.
Client and server are **MAJOR.MINOR lockstep**: they may differ only in the
patch component. `HELLO <version>` is the first line after TLS; the server
refuses a mismatch before any credentials are sent. A feature that changes
the wire format is a MAJOR bump and a simultaneous release of both sides, not
a compatible MINOR.
Bump the version in `Cargo.toml` as the **last** step before committing
a release-worthy change. The commit message should contain the new version
number (e.g. `v0.2.0`).
|