# 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 ` 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`).