summaryrefslogtreecommitdiffstats
path: root/docs/adr/0001-gui-frontend-architecture.md
blob: e4dd5acd6766ee2e28fe9f388b505890e9091aa9 (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
# 0001 — GUI is a second frontend over a shared core

Status: accepted, 2026-11-01; the "no browser" constraint is superseded by
[0003](0003-web-gateway.md), which adds a browser frontend through a gateway.

## Context

The client is a terminal UI built around `ClientCore`, which is already
headless and tested without a terminal (`tests/client_core.rs` drives it with a
fake screen). The GUI must add a second presentation of the same product with
these constraints:

- Installed desktop application. No browser, no webview, no macOS.
- Linux x86_64 and Windows x86_64, cross-compiled where possible; i686 stays
  TUI-only. Windows 10 is the floor.
- "Lightweight": ~40 MB binary, ~150 MB idle memory, ~1 s cold start.
- Effects are tweened 2D animation: reels, wheel, cards, chips, particles, a
  live crash curve. No 3D, no audio for now.
- egui is chosen for rendering; the repository is GPL-3.0-or-later.

The existing boundary is terminal-shaped: `Effect` carries lines of text,
`handle_key` carries arrow keys, and `Game::render` returns a formatted board.
None of it can be reused by a GUI as-is.

## Decision

1. **One brain, two frontends.** `ClientCore` owns state, navigation, and
   flows. Frontends send `Command` and render `View`. The TUI is the first
   adapter and stays green throughout the refactor; roulette is ported first
   because it has the largest UI surface.
2. **The boundary is semantic, not textual.** `Command` covers app flows
   (connect, trust, login, play, leave) and game actions (`PlaceBet(BetTarget)`,
   `Spin`, `Hit`, `CashOut`). `View` covers app screens
   (`Connect`, `Trust`, `TrustMismatch`, `Login`, `Lobby`, `History`,
   `Game(GameView)`) and per-game state (reels, felt with chips and cursor,
   hand with legal actions, crash phase and multiplier).
3. **The core owns navigation.** Which screen is showing and which game is open
   is state, not presentation; both frontends must behave identically.
4. **egui (eframe).** Immediate mode suits per-frame custom drawing; forms,
   text input, and scrolling come free; MIT/Apache; ~10–20 MB. Slint was
   rejected for its retained/declarative model, which fights per-frame drawing
   of the crash curve and particles. Macroquad was rejected because every input
   field and list would be hand-built. Webviews were rejected on memory and
   system dependencies.
5. **A `gui/` workspace member.** The existing package keeps the core, TUI, and
   server bins and all current test paths. egui's ~150 transitive crates must
   never enter the TUI or server build graph — the same mistake as the
   package-wide `native-tls` dependency.
6. **A blocking network thread with channels.** The core is I/O-free; the
   adapter owns the TLS stream. One thread reads/writes, forwards every line,
   and wakes the UI via `egui::Context::request_repaint`. No async runtime:
   the protocol has one request in flight plus pushes. Request deadlines live
   in the shared adapter so both frontends time out identically.
7. **First contact is explicit.** The handshake completes with a record-only
   decision, then the adapter asks before sending anything. New key: modal with
   the fingerprint. Changed key: hard stop with an explicit "forget and
   reconnect". The TUI keeps its print-and-accept prompt via the same state
   machine.
8. **Single-file binaries with embedded assets**, named
   `zagan-bet-gui-<version>-<platform>-<arch>[.exe]`, plus bash/PowerShell
   installers. The site lists both. No auto-update; the lobby points at the
   download page when the server advertises a newer release.

## Consequences

- The client core is rewritten at its boundary before any GUI code exists:
  `Effect`, the `Game` trait, and `handle_key` all change. This is the cost of
  keeping one implementation of reserve/settle/forfeit and payout rules.
- The TUI's terminal concerns (prompt editing, wheel animation, interactive
  input) move into the TUI adapter, where they belong.
- The GUI must be themed deliberately; egui's defaults look like a debug tool.
  A style spike on one reference screen gates the game ports.
- Windows binaries are unsigned; SmartScreen will warn until a certificate is
  bought. No auto-update means players download again.
- The refactor is testable headlessly: the TUI adapter and the core tests
  remain the regression net, and `View` can be asserted without pixels.

## Alternatives considered

- **Separate GUI client** reusing only rules and protocol: faster to start,
  duplicates connection/session/timeout flows and lets the two clients drift on
  the invariants in `AGENTS.md`. Rejected.
- **Hybrid: GUI reads `GameState` directly**: avoids the refactor but creates a
  second parser of stringly-typed data, guaranteeing renderer drift. Rejected.
- **Slint**: best declarative animation story and smallest runtime, but the
  retained model is wrong for per-frame custom drawing. Rejected after the
  license objection (GPL) was removed by open-sourcing.
- **Tauri / webview**: easiest effects, but a system webview breaks the memory
  budget and adds a distro-dependent dependency. Rejected.
- **tokio**: no concurrency to exploit. Rejected.