diff options
Diffstat (limited to 'docs/adr')
| -rw-r--r-- | docs/adr/0001-gui-frontend-architecture.md | 89 | ||||
| -rw-r--r-- | docs/adr/0002-version-lockstep.md | 55 |
2 files changed, 144 insertions, 0 deletions
diff --git a/docs/adr/0001-gui-frontend-architecture.md b/docs/adr/0001-gui-frontend-architecture.md new file mode 100644 index 0000000..44dca32 --- /dev/null +++ b/docs/adr/0001-gui-frontend-architecture.md | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | # 0001 — GUI is a second frontend over a shared core | ||
| 2 | |||
| 3 | Status: accepted, 2026-11-01 | ||
| 4 | |||
| 5 | ## Context | ||
| 6 | |||
| 7 | The client is a terminal UI built around `ClientCore`, which is already | ||
| 8 | headless and tested without a terminal (`tests/client_core.rs` drives it with a | ||
| 9 | fake screen). The GUI must add a second presentation of the same product with | ||
| 10 | these constraints: | ||
| 11 | |||
| 12 | - Installed desktop application. No browser, no webview, no macOS. | ||
| 13 | - Linux x86_64 and Windows x86_64, cross-compiled where possible; i686 stays | ||
| 14 | TUI-only. Windows 10 is the floor. | ||
| 15 | - "Lightweight": ~40 MB binary, ~150 MB idle memory, ~1 s cold start. | ||
| 16 | - Effects are tweened 2D animation: reels, wheel, cards, chips, particles, a | ||
| 17 | live crash curve. No 3D, no audio for now. | ||
| 18 | - egui is chosen for rendering; the repository is GPL-3.0-or-later. | ||
| 19 | |||
| 20 | The existing boundary is terminal-shaped: `Effect` carries lines of text, | ||
| 21 | `handle_key` carries arrow keys, and `Game::render` returns a formatted board. | ||
| 22 | None of it can be reused by a GUI as-is. | ||
| 23 | |||
| 24 | ## Decision | ||
| 25 | |||
| 26 | 1. **One brain, two frontends.** `ClientCore` owns state, navigation, and | ||
| 27 | flows. Frontends send `Command` and render `View`. The TUI is the first | ||
| 28 | adapter and stays green throughout the refactor; roulette is ported first | ||
| 29 | because it has the largest UI surface. | ||
| 30 | 2. **The boundary is semantic, not textual.** `Command` covers app flows | ||
| 31 | (connect, trust, login, play, leave) and game actions (`PlaceBet(BetTarget)`, | ||
| 32 | `Spin`, `Hit`, `CashOut`). `View` covers app screens | ||
| 33 | (`Connect`, `Trust`, `TrustMismatch`, `Login`, `Lobby`, `History`, | ||
| 34 | `Game(GameView)`) and per-game state (reels, felt with chips and cursor, | ||
| 35 | hand with legal actions, crash phase and multiplier). | ||
| 36 | 3. **The core owns navigation.** Which screen is showing and which game is open | ||
| 37 | is state, not presentation; both frontends must behave identically. | ||
| 38 | 4. **egui (eframe).** Immediate mode suits per-frame custom drawing; forms, | ||
| 39 | text input, and scrolling come free; MIT/Apache; ~10–20 MB. Slint was | ||
| 40 | rejected for its retained/declarative model, which fights per-frame drawing | ||
| 41 | of the crash curve and particles. Macroquad was rejected because every input | ||
| 42 | field and list would be hand-built. Webviews were rejected on memory and | ||
| 43 | system dependencies. | ||
| 44 | 5. **A `gui/` workspace member.** The existing package keeps the core, TUI, and | ||
| 45 | server bins and all current test paths. egui's ~150 transitive crates must | ||
| 46 | never enter the TUI or server build graph — the same mistake as the | ||
| 47 | package-wide `native-tls` dependency. | ||
| 48 | 6. **A blocking network thread with channels.** The core is I/O-free; the | ||
| 49 | adapter owns the TLS stream. One thread reads/writes, forwards every line, | ||
| 50 | and wakes the UI via `egui::Context::request_repaint`. No async runtime: | ||
| 51 | the protocol has one request in flight plus pushes. Request deadlines live | ||
| 52 | in the shared adapter so both frontends time out identically. | ||
| 53 | 7. **First contact is explicit.** The handshake completes with a record-only | ||
| 54 | decision, then the adapter asks before sending anything. New key: modal with | ||
| 55 | the fingerprint. Changed key: hard stop with an explicit "forget and | ||
| 56 | reconnect". The TUI keeps its print-and-accept prompt via the same state | ||
| 57 | machine. | ||
| 58 | 8. **Single-file binaries with embedded assets**, named | ||
| 59 | `zagan-bet-gui-<version>-<platform>-<arch>[.exe]`, plus bash/PowerShell | ||
| 60 | installers. The site lists both. No auto-update; the lobby points at the | ||
| 61 | download page when the server advertises a newer release. | ||
| 62 | |||
| 63 | ## Consequences | ||
| 64 | |||
| 65 | - The client core is rewritten at its boundary before any GUI code exists: | ||
| 66 | `Effect`, the `Game` trait, and `handle_key` all change. This is the cost of | ||
| 67 | keeping one implementation of reserve/settle/forfeit and payout rules. | ||
| 68 | - The TUI's terminal concerns (prompt editing, wheel animation, interactive | ||
| 69 | input) move into the TUI adapter, where they belong. | ||
| 70 | - The GUI must be themed deliberately; egui's defaults look like a debug tool. | ||
| 71 | A style spike on one reference screen gates the game ports. | ||
| 72 | - Windows binaries are unsigned; SmartScreen will warn until a certificate is | ||
| 73 | bought. No auto-update means players download again. | ||
| 74 | - The refactor is testable headlessly: the TUI adapter and the core tests | ||
| 75 | remain the regression net, and `View` can be asserted without pixels. | ||
| 76 | |||
| 77 | ## Alternatives considered | ||
| 78 | |||
| 79 | - **Separate GUI client** reusing only rules and protocol: faster to start, | ||
| 80 | duplicates connection/session/timeout flows and lets the two clients drift on | ||
| 81 | the invariants in `AGENTS.md`. Rejected. | ||
| 82 | - **Hybrid: GUI reads `GameState` directly**: avoids the refactor but creates a | ||
| 83 | second parser of stringly-typed data, guaranteeing renderer drift. Rejected. | ||
| 84 | - **Slint**: best declarative animation story and smallest runtime, but the | ||
| 85 | retained model is wrong for per-frame custom drawing. Rejected after the | ||
| 86 | license objection (GPL) was removed by open-sourcing. | ||
| 87 | - **Tauri / webview**: easiest effects, but a system webview breaks the memory | ||
| 88 | budget and adds a distro-dependent dependency. Rejected. | ||
| 89 | - **tokio**: no concurrency to exploit. Rejected. | ||
diff --git a/docs/adr/0002-version-lockstep.md b/docs/adr/0002-version-lockstep.md new file mode 100644 index 0000000..1e7f8ba --- /dev/null +++ b/docs/adr/0002-version-lockstep.md | |||
| @@ -0,0 +1,55 @@ | |||
| 1 | # 0002 — Client and server must share MAJOR.MINOR | ||
| 2 | |||
| 3 | Status: accepted, 2026-11-01 | ||
| 4 | |||
| 5 | ## Context | ||
| 6 | |||
| 7 | `AGENTS.md` previously described MINOR as backwards compatible: an old client | ||
| 8 | should work with a newer server and vice versa. In practice the wire format, | ||
| 9 | game messages, and client flows have changed in every MINOR so far | ||
| 10 | (`PLAY_START` payloads, `CRASH_*`, `HISTORY`, cursor and bet semantics). Silent | ||
| 11 | half-compatibility produces confusing failures: a new client sends a command | ||
| 12 | the old server echoes back as text, or an old client misparses a new field. | ||
| 13 | |||
| 14 | The project now ships multiple frontends (TUI and GUI) and a server, all | ||
| 15 | released together from one repository. | ||
| 16 | |||
| 17 | ## Decision | ||
| 18 | |||
| 19 | 1. **Version compatibility is MAJOR.MINOR lockstep.** A client and server may | ||
| 20 | differ only in PATCH. Any difference in MAJOR or MINOR refuses the | ||
| 21 | connection. | ||
| 22 | 2. **The first line after the TLS handshake is `HELLO <version>`.** The server | ||
| 23 | replies with its own version, the latest release version, and the download | ||
| 24 | URL. Any other message before `HELLO` is rejected. No credentials are sent | ||
| 25 | before the versions agree. | ||
| 26 | 3. **The server is authoritative about the latest release.** It advertises | ||
| 27 | `latest` and the download URL in the handshake; the client compares its own | ||
| 28 | `CARGO_PKG_VERSION` and offers a link when it is behind. The download site | ||
| 29 | is not queried by clients: that would add a second TLS stack and a web PKI | ||
| 30 | trust domain for one string. | ||
| 31 | 4. **A MAJOR bump covers a wire change; a MINOR bump covers a feature that | ||
| 32 | changes the protocol or the shared semantics.** Both require releasing | ||
| 33 | clients and server together. | ||
| 34 | |||
| 35 | ## Consequences | ||
| 36 | |||
| 37 | - Every MINOR release must update the server and all clients simultaneously; | ||
| 38 | a stale server is a hard failure, not a degraded experience. Deployment order | ||
| 39 | matters. | ||
| 40 | - `AGENTS.md`'s MINOR rule is rewritten; the old "old clients work with new | ||
| 41 | servers" promise is withdrawn. | ||
| 42 | - The handshake is itself a breaking wire change: this lands as v4.0.0. | ||
| 43 | - The update check needs no extra dependency and works for both frontends, at | ||
| 44 | the cost of only being as fresh as the server you connect to. | ||
| 45 | |||
| 46 | ## Alternatives considered | ||
| 47 | |||
| 48 | - **Keep MINOR backwards compatible** and negotiate per-feature: more work, | ||
| 49 | and every feature so far would have needed a capability flag. Rejected. | ||
| 50 | - **Fetch `version.json` from the download site**: site-authoritative updates, | ||
| 51 | but adds an HTTP client and a CA trust store with different rules from the | ||
| 52 | pinned game server. Rejected. | ||
| 53 | - **Semver-range matching**: allows broader compatibility but makes the | ||
| 54 | failure modes depend on which optional features each side implemented. | ||
| 55 | Rejected for a small, tightly released product. | ||
