# client_web Crate Overview A Leptos-based WASM frontend for trictrac. Builds to a single-page app served by Trunk on port 9092. --- ## File Structure ``` client_web/ ├── Cargo.toml # Dependencies and i18n locale config ├── Trunk.toml # Serve port 9092 ├── index.html # Shell: mounts WASM + links CSS ├── assets/style.css # All styles (~472 lines, no framework) ├── locales/ │ ├── en.json # 52 English keys │ └── fr.json # 52 French keys └── src/ ├── main.rs # load_locales!() macro + mount_to_body ├── app.rs # Root App component, state, network loop (571 lines) ├── components/ │ ├── mod.rs │ ├── game_screen.rs # Main in-game UI, move staging (324 lines) │ ├── board.rs # Board rendering and click handling (372 lines) │ ├── die.rs # SVG die face │ ├── score_panel.rs # Points/holes bar for one player │ ├── scoring.rs # Jan-by-jan scoring notification panel │ ├── login_screen.rs # Room create/join │ └── connecting_screen.rs └── trictrac/ ├── mod.rs ├── types.rs # Protocol types: ViewState, JanEntry, PlayerAction, … (217 lines) ├── backend.rs # BackEndArchitecture impl, engine bridge (332 lines) └── bot_local.rs # Local bot: random moves, always Go (34 lines) ``` --- ## Component Tree ``` App ← manages screen, pending queue, network task └─ I18nContextProvider ├─ LoginScreen ← room name input, create/join/bot buttons ├─ ConnectingScreen ← spinner while connecting └─ GameScreen ← in-game UI; receives GameUiState prop ├─ PlayerScorePanel ← opponent score (above board) ├─ Board ← 24 interactive fields; SVG arrow overlay ├─ side panel │ ├─ status bar ← localised turn/action prompt │ ├─ dice bar ← two Die components │ ├─ ScoringPanel (me) ← my jans this turn, hold/go buttons │ ├─ ScoringPanel (opponent) ← opponent jans (shown during pause) │ └─ action buttons ← Continue / Go / Empty Move └─ PlayerScorePanel ← my score (below board) [game-over overlay modal] ``` --- ## Screens and Transitions ``` Login ──(connect)──→ Connecting ──(game start)──→ Playing ↑ │ └──(reconnect)─────┘ Playing ──(disconnect / game over)──→ Login ``` `app.rs` drives transitions via `RwSignal`. --- ## State Management ### Root signals (live in `App`, provided via Leptos context) | Signal | Type | Purpose | |--------|------|---------| | `screen` | `RwSignal` | Which screen is shown | | `pending` | `RwSignal>` | Buffered states awaiting "Continue" | | `cmd_tx` | `UnboundedSender` | UI → network command channel | Both `pending` and `cmd_tx` are provided as context so any descendant can read/write them without prop-drilling. ### GameScreen-local signals | Signal | Type | Purpose | |--------|------|---------| | `selected_origin` | `RwSignal>` | First clicked field during move staging | | `staged_moves` | `RwSignal>` | Accumulated (origin, dest) pairs for this turn | | `hovered_jan_moves` | `RwSignal>` | Moves to draw arrows for on hover | ### Data flow ``` Network task (async in App) ↓ SessionEvent::Update push_or_show() → pending queue or screen.set() ↓ GameScreen re-renders (GameUiState prop) ↓ User clicks field → staged_moves effect → NetCommand::Action(Move) User clicks Go/Continue → cmd_tx.send or pending.pop_front() ``` --- ## Network and Session The multiplayer layer is provided by `backbone-lib` (local fork at `../../forks/multiplayer/`). `App` spawns an async task (via `spawn_local`) that multiplexes: - `cmd_rx`: commands from UI components - `session.next_event()`: updates from the server ### StoredSession (localStorage key: `"trictrac_session"`) ```rust struct StoredSession { relay_url: String, game_id: String, room_id: String, token: u64, // reconnect token issued by server is_host: bool, view_state: Option, // host saves last known state; guest saves None } ``` On page load, if a stored session exists, App goes directly to Connecting and sends `NetCommand::Reconnect`. Failed reconnects clear the session and return to Login. --- ## Pause / Confirmation Flow Certain opponent events are paused so the local player can see what happened before their turn starts. Pause triggers (`infer_pause_reason()` in `app.rs`): | Reason | Condition | |--------|-----------| | `AfterOpponentRoll` | Opponent is active; dice values changed | | `AfterOpponentGo` | Opponent chose Go (HoldOrGoChoice→Move transition) | | `AfterOpponentMove` | Turn switched to us | While a state is in the pending queue, `GameScreen` shows a "Continue" button. Clicking it calls `pending.pop_front()`; if the queue empties, the live state is displayed. --- ## Game Engine Integration **File**: `src/trictrac/backend.rs` `TrictracBackend` implements the `BackEndArchitecture` trait. It owns a `GameState` from `trictrac-store` and translates between the UI protocol and the engine's event model. ### PlayerAction → GameEvent mapping | PlayerAction | GameEvents emitted | |---|---| | `Roll` | `GameEvent::Roll`, `GameEvent::RollResult(d1, d2)` | | `Move(m1, m2)` | `GameEvent::Move` (after validation) | | `Go` | `GameEvent::Go` | | `Mark` | internal; drives `MarkPoints`/`MarkAdvPoints` loop automatically | `drive_automatic_stages()` loops through scoring stages without waiting for player input — these are not interactive in the current implementation (schools are not implemented). ### ViewState construction `ViewState::from_game_state()` in `types.rs` converts the engine state to the serialisable snapshot sent to clients: - `board: [i8; 24]` — direct copy of `Board::positions` - `dice: [u8; 2]` — current dice values - `stage / turn_stage` — serialisable enums (`SerStage`, `SerTurnStage`) - `scores: [PlayerScore; 2]` — points, holes, `can_bredouille` - `dice_jans: Vec` — scoring events for the current turn, sorted descending by points - `active_player_index: usize` — 0 = host, 1 = guest ### Bot `bot_local.rs` runs in the browser (no server call). It inspects `GameState` directly and returns a `PlayerAction`: - **RollDice**: always Roll - **HoldOrGoChoice**: always Go - **Move**: picks a random legal sequence from `MoveRules::get_possible_moves_sequences()`; mirrors moves because Black's board is mirrored --- ## Board Rendering (`board.rs`) ### Layout The 24 fields are split into 4 quarters of 6. Each player sees the board from their own perspective: ``` White's view: TOP-LEFT [13–18] | TOP-RIGHT [19–24] ───────────────────────────────────────── BOT-LEFT [12–7] | BOT-RIGHT [6–1] Black's view (mirror): TOP-LEFT [1–6] | TOP-RIGHT [7–12] ───────────────────────────────────────── BOT-LEFT [24–19] | BOT-RIGHT [18–13] ``` Fields are 60 × 180 px, alternating gold (`#d4a843` / `#c49030`). Checkers are 40 px SVG circles (radial gradient). Up to 4 are stacked visually; a text label is shown when count > 4. ### Highlighting Field CSS classes are computed reactively inside the `view!` macro closure: | Class | Meaning | |-------|---------| | `.clickable` | Valid origin during Move stage (lime green) | | `.selected` | Currently selected origin (darker green + outline) | | `.dest` | Valid destination for the selected origin | `valid_sequences` (from `MoveRules`) is computed once per render and used to derive `valid_origins_for()` and `valid_dests_for()`. The displayed checker count (`displayed_value()`) accounts for staged-but-not-yet-sent moves so the board previews the move visually. ### SVG arrow overlay When the player hovers a row in the ScoringPanel, the corresponding checker moves are drawn as gold arrows over the board. `field_center()` maps field numbers to pixel coordinates; `arrow_svg()` renders the path with a drop-shadow. --- ## Scoring Display (`scoring.rs`, `score_panel.rs`) `compute_scored_event()` in `app.rs` diffs consecutive `ViewState` snapshots to produce a `ScoredEvent`: - `points_earned: i32` - `holes_gained: u8` - `jans: Vec` — only events relevant to the beneficiary `ScoringPanel` renders one `JanEntry` per row. Hovering a row writes that entry's moves into `hovered_jan_moves`, triggering the arrow overlay on the board. `PlayerScorePanel` shows a colour-filled bar (animated via CSS `transition: width 0.3s`) for points (0–12) and holes (0–12). Bredouille state is shown with a small indicator. --- ## Internationalisation `leptos_i18n::load_locales!()` is a compile-time macro that reads `locales/en.json` and `locales/fr.json` and generates a typed `i18n` module. There are 52 keys covering UI labels, game-state prompts, jan names, and status messages. Usage in components: ```rust let i18n = use_i18n(); t!(i18n, your_turn_roll) // → reactive View t_string!(i18n, scored_pts, pts = 4) // → String with interpolation ``` The language switcher (top bar and login screen) calls `i18n.set_locale(Locale::en | Locale::fr)`, which triggers a full reactive re-render. --- ## Styling `assets/style.css` is a single hand-written stylesheet (~472 lines). No CSS framework. Key design tokens: - Body background: `#c8b084` (tan) - Board background: `#2e6b2e` (dark green) - Fields: `#d4a843` / `#c49030` (gold alternating) - Interactive fields: `#aad060` (lime, clickable) / `#709a20` (darker, selected) - UI panels: `#f5edd8` (cream) Layout uses Flexbox and CSS Grid throughout. Score bars animate with `transition: width 0.3s`. Field clicks give immediate feedback via `transition: background 0.1s`. No media queries — the layout is designed for desktop/tablet. --- ## Protocol Types (`types.rs`) | Type | Role | |------|------| | `PlayerAction` | `Roll \| Move(CheckerMove, CheckerMove) \| Go \| Mark` — UI → backend | | `GameDelta` | `{ state: ViewState }` — broadcast to all clients on every change | | `ViewState` | Full serialisable snapshot of engine state | | `JanEntry` | One scoring event: jan type, points, ways, moves, is_double | | `ScoredEvent` | Points/holes delta + jan list for one player in one turn | | `PlayerScore` | name, points (0–11), holes (0–12), can_bredouille | | `SerStage` | `PreGame \| InGame \| Ended` | | `SerTurnStage` | `RollDice \| RollWaiting \| MarkPoints \| HoldOrGoChoice \| Move \| MarkAdvPoints` | `CheckerMove` comes directly from `trictrac-store`; fields are 1-indexed (0 = stack/exit). --- ## Build ```bash trunk serve # dev server at http://localhost:9092 trunk build --release # WASM release bundle ``` `index.html` uses Trunk's `data-trunk` attributes: `rel="rust"` compiles `src/main.rs` to WASM; `rel="css"` copies `assets/style.css`. The WASM binary and generated JS glue land in `dist/`.