summaryrefslogtreecommitdiffstats
path: root/AGENTS.md
blob: e784624c3011206bf41ab040a0d838f96cabc697 (plain) (blame)
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
# 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.
- 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]`.
- 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.

## 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.
- `save_host` must not wipe the fingerprint column. Use `ON CONFLICT ...
  DO UPDATE SET last_used` — never `INSERT OR REPLACE`.

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