diff --git a/doc/client_web_design_proposals.md b/doc/client_web_design_proposals.md new file mode 100644 index 0000000..c0237ca --- /dev/null +++ b/doc/client_web_design_proposals.md @@ -0,0 +1,307 @@ +# client_web — UI/UX Design Proposals + +A structured critique of the current interface compared to the physical game, followed by concrete upgrade proposals. Organised from most impactful to most effort-intensive. + +--- + +## Aesthetic Direction + +**Concept: "18th-century French gaming salon"** + +The physical trictrac board is a piece of furniture — carved mahogany rails, felt or baize surface, ivory and ebony checkers, brass pegs in drilled holes, gilt scoring tokens. The online interface should feel like playing on that table under candlelight in a Parisian salon. + +- **Typography**: Pair a classical serif (e.g. [Cormorant Garamond](https://fonts.google.com/specimen/Cormorant+Garamond)) for headings and score readouts with a clean humanist sans (e.g. [Jost](https://fonts.google.com/specimen/Jost)) for UI controls and status text. +- **Palette**: Forest green felt board (`#1d3d28`), alternating ivory (`#f0e6c8`) and deep burgundy (`#7a1e2a`) triangular fields, dark mahogany rails (`#2a1508`), aged parchment panels (`#f2e8d0`), gilt gold accents (`#c8a448`). +- **Unforgettable detail**: Triangular fields (true *flèches*) rendered in CSS with a wood-grain body surrounding them. + +--- + +## 1. Board Shape: Rectangles → True Triangles + +**Current state**: Fields are 60×180px rectangles with a rounded corner. No backgammon/trictrac board looks like this. + +**Physical game**: Fields are elongated triangles (*flèches*) pointing from the rail toward the center bar, alternating two colors. + +**Proposal**: Replace `.field` `
` elements with SVG triangles, or use CSS `clip-path: polygon(50% 0%, 0% 100%, 100% 100%)` for bottom-row triangles and `clip-path: polygon(0% 0%, 100% 0%, 50% 100%)` for top-row triangles. The field background and checker stack become SVG foreignObject or positioned elements inside. This is a large structural change to `board.rs` but is the single highest-impact visual improvement. + +The board body between triangles becomes visible as the wood/felt surface — this naturally creates the physical board's "relief" without any extra decoration. + +--- + +## 2. Jan Zone Visual Identity + +**Current state**: The four quarters (small jan, big jan, return jan, last jan) are visually identical — same field color scheme, no labels or separation beyond the center bar. + +**Physical game**: Players must constantly know which quarter they are in because the rules differ radically per zone (forbidden jans, filling values, hit scoring values differ between small-jan-table and big-jan-table, corner position, exit zone). + +**Proposals**: + +### 2a. Zone labels +Add thin labels (`"petit jan"`, `"grand jan"`, `"jan de retour"`, `"dernier jan"`) beneath the board-row (or as a subtle strip above/below the quarter). These should use the serif font at very small size and low opacity — decorative, not noisy. + +### 2b. Field color shift per zone +The physical game uses alternating colors within each quarter, but different quarters can use slightly different base hues: +- Small jan (fields 1–6): warm ivory / burgundy +- Big jan / corner zone (fields 7–12): same, but field 12 gets a distinct "corner" treatment (see §4) +- Return jan (fields 13–18): very subtly cooler ivory / dark teal instead of burgundy — signals "opponent's territory" +- Last jan / exit (fields 19–24): subtly warmer, indicating checkers are "almost home" + +### 2c. Small-jan-table / big-jan-table highlight during hit scoring +When a hit is being scored, briefly tint the entire table (fields 1–12 or 13–24) to make the point value distinction (4 pts vs 2 pts) spatially obvious. This fires as a 300ms flash synchronized with the scoring notification. + +--- + +## 3. Rest Corner (Field 12 / 13) Special Appearance + +**Current state**: Field 12 looks identical to field 11. Nothing indicates its unique rules (must enter/leave with 2 checkers simultaneously, cannot be landed on by a single checker). + +**Physical game**: The corner is a corner — it is literally in the corner of the table, a distinct physical location. + +**Proposals**: +- Give field 12 (and 13 for Black) a **crown or arch shape** at the tip of the triangle, using a small SVG ornament. +- Apply a **slightly warmer gold** field color to distinguish it. +- When the player has two checkers there, show a subtle **lock icon** or a gilded ring around the checker stack to indicate "corner held." +- When the corner is available to be taken *par puissance*, add a gentle pulsing outline on field 12 to indicate the privilege is available. +- Tooltip or popover: on hover, show a brief note "Coin de repos — must enter and leave with 2 checkers." + +--- + +## 4. Checker Rendering: Static → Animated + +**Current state**: Checkers appear and disappear between `ViewState` snapshots. No movement animation. + +**Physical game**: Checkers slide across the board with a satisfying click sound. + +**Proposals**: + +### 4a. Slide animation +Diff the board array between the previous and current `ViewState`. For each checker that moved from field A to field B, apply a CSS or Web Animation API translation from `field_center(A)` to `field_center(B)` (duration ~250ms, ease-out). This requires keeping the previous `ViewState` as state in `GameScreen` and computing a diff when a new state arrives. + +### 4b. Lift effect during staging +When the player clicks an origin field and a checker becomes "selected," apply a `transform: scale(1.15) translateY(-4px)` with a subtle drop shadow increase. Visually lifts the checker off the board. + +### 4c. Checker appearance +Replace the CSS `radial-gradient` circles with SVG: +- **White**: ivory `#f5edd8` with a pearl sheen gradient, thin gilt ring border, engraved concentric circles +- **Black**: ebony `#1a0f06` with subtle grain texture, same gilt ring + +A stack of 5+ checkers can render a "perspective stack" — each checker at a slight y offset with a shadow, giving depth. + +--- + +## 5. Dice: Static → Rolling Animation + +**Current state**: Dice appear with their final value immediately. No sense of randomness or anticipation. + +**Physical game**: Dice are shaken in a cup (*cornet*) and tumbled out. The roll is a theatrical moment. + +**Proposals**: + +### 5a. Roll animation +When `SerTurnStage` transitions from `RollDice` to `Move`, animate both dice with a fast face-cycling (showing random faces for ~400ms, decelerating to final value). Pure CSS `animation` on the die-face SVG circles, cycling via `keyframes`. + +### 5b. Dice cups +Add two SVG/CSS dice cups above the dice display. During rolling, they visually "tip" (rotate 90° via CSS transform) and the dice "fall out." A subtle translate-y on the dice moves them downward into view. + +### 5c. Double visual +When both dice show the same value, add a subtle golden glow around both — visually communicating that it is a double (which affects scoring: 6 pts instead of 4, etc.). + +### 5d. Used-die visual +When one die has been consumed by a staged move, slide it slightly down and reduce opacity (current: gray-out). Animate the "used" transition with `transition: all 0.15s`. + +--- + +## 6. Scoring Notifications: Side Panel → Layered Toasts + +**Current state**: Scoring events appear as a small cream panel in the side panel column (`scoring-panel`). They are easily missed, especially opponent events. + +**Physical game**: Scoring is the central drama of every turn — points are loudly marked, bredouille doubled, holes recorded with pegs. + +**Proposals**: + +### 6a. Board-overlaid toast for holes +When a hole is won, display a large centered overlay on the board — not a modal, but a translucent toast with gilt border: `"Trou ! ×2 bredouille"`. Auto-dismiss after 1.5s or on click. This is the most important event and deserves the most visual weight. + +### 6b. Scoring event animation +When `my_scored_event` appears, animate the panel sliding in from the right with a 200ms ease-out. Jan rows stagger in (each with `animation-delay: n * 50ms`). + +### 6c. Jan hover → board highlight synchronization +The current arrow-on-hover feature is good. Extend it: when hovering a jan row, also highlight the relevant fields with a faint golden shimmer instead of (or in addition to) the arrows. This ties the abstract jan name to a concrete board location. + +### 6d. Bredouille treatment +Bredouille doubles a hole's value — a massive game event. Currently shown as a small amber badge. Proposals: +- The toast for a bredouille hole should animate in differently: bigger, gold shimmer background +- Show a small animated flag (*pavillon*) icon in the score panel when bredouille is active, matching the physical game's token + +### 6e. Hit scoring visual +When a hit is scored (*battue*), show a brief visual on the opponent's half-field checker — a faint concentric ring expanding outward (CSS `animation: ripple 0.4s ease-out`). This communicates the "fictitious" nature of the hit: something happened at that checker's position, but it didn't move. + +--- + +## 7. Score Panel: Progress Bars → Pegs and Holes + +**Current state**: Points and holes are displayed as progress bars (0–12) and numeric values. Functional but abstract. + +**Physical game**: Points are tracked with physical tokens (*jetons*) placed on the board surface at specific field tips. Holes are tracked with pegs (*fichets*) in holes drilled along the rail at each field base. + +**Proposals**: + +### 7a. Hole tracker: 12 dots/pegs +Replace the `score-bar-holes` progress bar with a row of 12 small circles ("drilled holes") in a horizontal strip. Filled holes are rendered as a gilded peg inserted (solid gold circle). Unfilled holes are empty rings. This is a `` with 12 `` elements. The filled count animates one peg at a time (sequenced `animation-delay`). + +### 7b. Point tracker: token on board +For points (0–11), show a small token image positioned at the corresponding field tip along the near rail — mirroring the physical game exactly. This is ambitious but highly authentic. A simpler approach: replace the thin progress bar with a 12-cell dot track where one glowing token is positioned. + +### 7c. Bredouille indicator +When `can_bredouille` is true for a player, show the token as a double-token (two stacked icons) or add a small flag icon next to the token. + +--- + +## 8. Status Communication: Text → Contextual Guidance + +**Current state**: A single text line (`"Select move 1"`, `"Opponent's turn"`) in the status bar. New players have no idea what to do or why they can't do something. + +**Physical game**: Human players narrate what's happening; experienced players understand the state from context. + +**Proposals**: + +### 8a. Contextual sub-prompt +Below the primary status, show a secondary hint line in smaller text: +- During `Move` stage: `"Click a highlighted field to move a checker"` +- During `HoldOrGoChoice`: `"Hold to keep points and keep playing — Go to reset and start a new setting"` +- When waiting for confirm: `"↑ Opponent scored points — click Continue when ready"` + +### 8b. Forbidden-jan visual cue +When a field is in the opponent's jan and the player cannot land there (forbidden jan rule), show those fields with a subtle `✕` pattern or darker tint rather than just being unclickable. This communicates *why* the fields aren't selectable. + +### 8c. Exit-eligible highlight +When all player checkers are in the last jan (fields 19–24), add a subtle directional glow to the exit rail (the right/left edge of the board depending on player). A small "EXIT →" arrow indicator could appear. + +### 8d. Can-take-corner indicator +When the player can take their corner (field 12 or 13 is the valid destination), add a brief pulse to that field beyond the standard `.dest` highlight — the corner rules are special enough to warrant extra visual salience. + +--- + +## 9. Bug Fix: Hold Button Is Non-Functional + +**File**: `src/components/scoring.rs` line 91 + +The "Hold" button in the `ScoringPanel` has no `on:click` handler. In the physical game, "Hold" (*tenir*) means: stay in the current setting, mark remainder points, and continue playing normally. + +`PlayerAction` does not currently include a `Hold` variant. In the current implementation, if the player simply does nothing (doesn't click Go), the game waits — but there is no message sent to the backend to confirm "staying." + +**Fix required**: Add `PlayerAction::Hold` (or reuse `Mark`) and connect the Hold button's `on:click` to send it. The backend needs to handle it by advancing past `HoldOrGoChoice` without triggering `GameEvent::Go`. + +--- + +## 10. Layout: Side Panel → Integrated Design + +**Current state**: The board and side panel sit side-by-side (`board-and-panel: flex-direction row`). The side panel (min-width 160px) contains status, dice, scoring, and buttons stacked vertically. + +**Proposals**: + +### 10a. Move dice inside the board +Place the dice display centered in the **board-bar** (the vertical divider between quarters). Currently the bar is 20px wide — widen it to ~80px and center two dice there. This puts dice physically near the board action, matching the physical game where dice land on the board surface. The bar color becomes a darker felt strip. + +### 10b. Status bar above the board +Move the primary status message to a full-width strip directly above the board, styled with the serif font at larger size. This gives it appropriate visual weight and removes it from the cramped side panel. + +### 10c. Action buttons below the board (or in score panels) +"Continue," "Go," and "Hold" buttons can live below the board in a centered button row. The side panel then becomes purely informational (scoring panels), which can slide in from the right. + +### 10d. Mobile: rotate board 90° option +The board is ~776px wide. On narrow screens, offer a portrait mode where the board is rendered rotated 90° (each player's quarters stacked vertically), with a scroll-independent panel above/below for controls. + +--- + +## 11. Login Screen: Form → Atmosphere + +**Current state**: A plain 320px-wide column with a `

Trictrac

`, a text input, and three buttons. Functional but gives no sense of what the game is. + +**Physical game**: A trictrac board is an object of beauty — players set it out, prepare the checkers, and roll for first-move privilege. + +**Proposals**: + +### 11a. Illustrated header +A high-quality SVG illustration of the board (simplified top-down view, showing the triangular fields, checker stacks at starting positions, dice) as the page hero. Possibly animated: the two stacks slowly deploying two checkers as the page loads. + +### 11b. Typography treatment +"TRICTRAC" as a large display heading in a classical-weight serif, possibly with subtle tracking and a gilt color. Below it, the French subtitle: *"Jeu de trictrac — XVIIIe siècle"* in small-caps at reduced opacity. + +### 11c. Mode selection +The three buttons (Create / Join / vs Bot) styled as wooden tiles or embossed cards rather than plain buttons. + +--- + +## 12. Game-Over Modal: Generic → Ceremonial + +**Current state**: A centered modal with "Game Over," the winner's name, and Quit/Play Again buttons. + +**Physical game**: The end of a game involves settling accounts, noting the final hole count, and potentially recording results. + +**Proposals**: +- Show a **final score parchment** — both players' hole counts displayed like a ledger entry, with the winner's name engraved in gilt text +- Animate the modal entrance with a slight downward reveal (the parchment "unrolling") +- Show the hole difference: `"8 — 3"` in large numerals with a small flourish between them +- If bredouille applied to the winning holes: `"✕ 2 bredouille"` annotation +- "Play again" styled as "Rejouer" / "Play again" with a dice icon + +--- + +## Implementation Priority + +| Priority | Proposal | Effort | Impact | +|----------|-----------|--------|--------| +| 1 | §9 Fix Hold button (bug) | Low | Correctness | +| 2 | §3 Rest corner special appearance | Low | Clarity | +| 3 | §8b–d Forbidden jan + exit + corner cues | Medium | Clarity | +| 4 | §5a–d Dice roll animation | Medium | Delight | +| 5 | §6a–b Scoring toasts + animation | Medium | Drama | +| 6 | §7a Hole tracker (12 peg dots) | Low | Authenticity | +| 7 | §2a–b Jan zone labels + color shift | Low | Orientation | +| 8 | §4a Checker slide animation | High | Polish | +| 9 | §1 Triangular fields | High | Authenticity | +| 10 | §10a–b Dice in bar + status above board | Medium | Layout | +| 11 | §6e Hit ripple animation | Medium | Comprehension | +| 12 | §11 Login redesign | Medium | First impression | +| 13 | §12 Game-over modal | Low | Finish | +| 14 | §4c SVG checkers | Medium | Aesthetics | +| 15 | §7b–c Token tracker on rail | High | Authenticity | + +--- + +## Typography and CSS Variables Proposal + +Replace the anonymous `sans-serif` body font and introduce a CSS variable system: + +```css +@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;600&family=Jost:wght@300;400;500&display=swap'); + +:root { + /* Board */ + --board-felt: #1d3d28; + --board-rail: #2a1508; + --field-ivory: #f0e6c8; + --field-burgundy: #7a1e2a; + --field-corner: #c8a030; /* rest corner accent */ + --field-exit-glow: #e8c060; + + /* Checkers */ + --checker-white: #f5edd8; + --checker-black: #1a0f06; + --checker-ring: #c8a448; /* gilt border */ + + /* UI */ + --ui-parchment: #f2e8d0; + --ui-parchment-dark: #e4d8b8; + --ui-ink: #2a1a08; + --ui-gold: #c8a448; + --ui-gold-dark: #8a6a28; + --ui-green-accent: #3a6b2a; + --ui-red-accent: #7a1e2a; + + /* Typography */ + --font-display: 'Cormorant Garamond', Georgia, serif; + --font-ui: 'Jost', system-ui, sans-serif; +} +``` diff --git a/doc/client_web_overview.md b/doc/client_web_overview.md new file mode 100644 index 0000000..980ea2f --- /dev/null +++ b/doc/client_web_overview.md @@ -0,0 +1,289 @@ +# 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/`. diff --git a/doc/trictrac_rules.md b/doc/trictrac_rules.md new file mode 100644 index 0000000..9d16e5d --- /dev/null +++ b/doc/trictrac_rules.md @@ -0,0 +1,212 @@ +# Trictrac Rules — Quick Reference + +This document summarises the rules of grand trictrac based on the 2013 Malfilâtre edition ([full text](refs/laws_and_rules_of_trictrac.md)). + +French terms follow the mapping in [vocabulary.md](refs/vocabulary.md). + +--- + +## 1. Board and Starting Position + +- 24 triangular fields (_flèches_ / _cases_), numbered 1–24 from each player's perspective. +- 4 quarters of 6 fields: **small jan** (1–6), **big jan** (7–12), **return jan** (13–18), **last jan** (19–24, exit zone). +- Field 12 (White) / 13 (Black) is the **rest corner** (_coin de repos_). +- Each player starts with all 15 checkers in a stack (_talon_) on field 1. +- Checkers always move in the same direction (White: 1→24; Black: mirror of that). + +## 2. Dice and Movement + +- Both dice are rolled together; both must be played if possible. +- If only one can be played and there is a choice, the higher number must be played. +- A single checker may play both dice successively — a **chained move** (_tout d'une_) — stopping on an intermediate resting field (_repos_) between the two dice. +- **Fields are single-color**: a checker may only land on an empty field or one already occupied by own checkers. +- Landing on a field with ≥ 1 opponent checker is **forbidden** (blocked field). +- An unplayed number is a **helpless man** (_jan-qui-ne-peut_): 2 points penalty per unplayed die, credited to the opponent. + +## 3. The Rest Corner (Field 12 / 13) + +- Must be entered **simultaneously** (_d'emblée_): exactly 2 checkers must enter together. +- Must be vacated simultaneously: exactly 2 checkers must leave together. +- Always holds ≥ 2 checkers while occupied; a single checker there is forbidden. +- Three ways to take the corner: + - **By effect** (_par effet_): normal die values land exactly on it. + - **By puissance** (_par puissance_): the opponent's corner is empty; the player could take both corners simultaneously, but by privilege takes their own instead (as if stepping back one field). + - **By chance** (_par effet_): general case when it results from the dice. +- If both by-effect and by-puissance are possible, by-effect takes priority. +- An empty corner may serve as a resting field during a chained move (not a landing). +- Placing checkers on the **opponent's** corner is always forbidden. + +## 4. Scoring: Points and Holes + +- Points are tracked with tokens (0–11); **12 points = 1 hole** (_trou_). +- A **hole won bredouille** (_bredouille_) counts as **2 holes**: the active player scored 12 consecutive points from zero without the opponent scoring anything in between. The second player to start marking takes a double token (the _pavillon_ / flag) and can also win bredouille. +- The ordinary game ends when one player reaches **12 holes**. + +## 5. Scoring Events (Jans) + +All point values: normal roll / double. + +### 5a. Opening Jans (first rolls of a setting only) + +| Jan | Condition | Points | +| --------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------ | +| **Two tables jan** | First 2 checkers deployed; roll covers both rest corners; opponent's corner is empty | 4 / 6 (to player) | +| **Contre two tables** | Same, but opponent has already taken their corner | 4 / 6 (to opponent, as false hit) | +| **Mezeas jan** | Corner just taken (2 checkers); next roll shows one or two aces; opponent's corner empty | 4 per ace / 6 for double (to player) | +| **Contre mezeas** | Same, but opponent's corner is occupied | 4 / 6 (to opponent) | +| **Six tables jan** | After 2 rolls a checker is on 4 of the first 6 fields; 3rd roll could complete all 6 | 4 (always; not possible on a double) | + +### 5b. Jan Filling and Conserving + +A jan is **full** (_plein_) when all 6 of its fields hold ≥ 2 own checkers. + +- **Filling**: the last checker is brought in to complete the jan. + - Up to 3 ways: each direct die value covering the last field, or the combined sum (chained move). + - Each way: **4 / 6** points. + - Doubles allow at most 2 ways. + - "Filling in passing" (player must break the jan to play the other die) scores nothing. +- **Conserving**: both dice can be played without disturbing any of the 12 checkers of the full jan. + - Worth **4 / 6** points (at most one way). + - Conservation by helplessness (_par impuissance_): only die value 6 triggers this (smaller values can always be played within the jan by breaking it). + - The full return jan may be conserved by exiting one or more checkers. +- After marking points for filling, the player **must** actually fill the jan with the appropriate checker(s) — failure is a false move and a school. + +### 5c. Hitting (_Jan de Récompense_) + +Hitting is **always fictitious**: a checker is "hit" when a die value could cover an opponent checker on a half-field (_demi-case_), but **no actual checker moves**. The opponent's checker stays. + +**True hit**: a die (direct or combined sum) fictitiously covers the opponent checker. + +- In the **small jan table** (fields 1–12): **4 / 6** points per way. +- In the **big jan table** (fields 13–24): **2 / 4** points per way. + +Ways to hit: + +- **1 way**: only one direct die, or only the combined sum, covers the checker. +- **2 ways**: both direct dice cover it, or one die + the combined sum. +- **3 ways**: both dice + the combined sum (requires a normal roll; doubles max at 2 ways). + +**Combined-sum hit** requires a free **resting field** between the two dice stops: the field must be empty, own, or a single opponent checker (which is then also hit). + +**False hit** (_à faux_): the combined sum could hit but no valid resting field exists (all intermediate options are full opponent fields). The opponent gains the points the player would have scored. + +- True-hit points are always marked before false-hit points. +- A checker already hit truly cannot also be hit falsely in the same move. +- Multiple checkers may be hit simultaneously (some true, some false). + +**Corner hit**: player holds their own corner; opponent's corner is empty; the dice could simultaneously take the opponent's corner. Worth **4 / 6** points. Never false. + +### 5d. Exit + +- When all 15 checkers are in the last jan (fields 19–24), the player may exit. +- The exit rail counts as one additional field value. +- **Exact exit**: die value brings the checker directly to the exit rail — allowed. +- **Overflow** (_nombre excédant_): die value would carry the farthest checker past the rail — must exit. +- **Failing number** (_nombre défaillant_): die cannot reach or overflow — must play within the jan. +- A player may choose not to use an exact exit value and play within the jan instead — but overflow must always exit. +- It is forbidden to deliberately play a die within the jan to force the second die to be played as an overflow (using a checker closer to the exit). +- When the last checker exits: **4 points** on a normal roll, **6 points** on a double. +- After exit: checkers reset; the player who exited keeps first-move privilege for the new setting. + +## 6. Forbidden Jans + +A player **may not** place a checker in the opponent's small jan or big jan as long as the opponent can still materially complete a full jan there (i.e., enough of their own checkers remain to fill it). + +Exception: during a chained move, an empty field in the opponent's big jan (including their empty corner) may serve as a resting field to pass a checker into the return jan. + +## 7. Sequence of Play + +Each turn follows this order: + +1. Mark opponent's helpless man points or contre-jans from the **previous** move. +2. Mark opponent's schools; rectify false moves if any. +3. **Roll dice.** Opponent may mark schools for steps 1–2. +4. Mark own points: opening jans, hits, fills, conserves, exit. +5. Decide to **stay** (_tenir_) or **leave** (_s'en aller_) if a hole was won on own roll. +6. If exiting: reset checkers, keep token positions, roll again. +7. Play both dice. + +Points and holes must always be marked **before** touching checkers for the next move. + +## 8. Staying or Leaving + +After winning one or more holes on **own dice roll**, the player chooses: + +- **Stay** (_tenir_): mark holes, reset opponent token to zero, mark remainder points, continue. +- **Leave** (_s'en aller_): announce it; opponent agrees or raises a fault. All checkers and tokens reset to zero; only holes remain. Player who left has first-move privilege in the new setting. Remainder points are forfeited; opponent scores nothing for that move. + +If the winning points come from the **opponent's** roll (helpless man, schools), the player **must** stay — leaving is not an option. + +--- + +## 9. Schools (Marking Penalties) — _Not Yet Implemented_ + +Schools are penalties for marking errors. They are worth exactly the number of points over- or under-marked on the faulty move. They are marked last in the turn sequence. + +Key rules: + +- A school is committed once dice are rolled or a token has been advanced too far and released. +- The opponent is never obliged to mark a school — but if they do, it must be marked in full. +- **False school**: incorrectly claiming a school — itself becomes a school for the opponent. +- **School escalation** (_augmentation d'école_): dispute over a school that escalates back and forth. +- No "school of school" exists (marking a school is never itself penalised). +- No school of holes for marking a bredouille hole as simple; a school of points applies for forgetting holes due to earned points. + +--- + +## 10. The Scored Game (_Partie à Écrire_) — _Not Yet Implemented_ + +The scored game is played for an agreed number of **rounds** (_marqués_) and supports 2, 3, or 4 players (the 3/4-player format is called _chouette_). + +### Goal of a Round + +- A player must score at least **6 holes** and then **leave** to win the round. +- If both players are tied at ≥ 6 holes when one leaves, the round is **drawn** (_refait_) and replayed immediately. +- Winner of a round = player with the most holes after a leave. + +### Bredouille in the Scored Game + +- **Small bredouille** (_petite bredouille_): ≥ 6 consecutive holes → round counts **double**. +- **Big bredouille** (_grande bredouille_): ≥ 12 consecutive holes → round counts **quadruple**. +- The second player to score holes takes the flag (_pavillon_) at their peg. If the first player scores again, they take back the flag, cancelling both bredouilles. + +### Payments + +Each round is settled in tokens: + +- Winner receives (winner's holes − loser's holes) tokens, plus **consolation** of 2 tokens. +- Small bredouille: each winner hole worth 2 tokens; consolation = 4. +- Big bredouille: each winner hole worth 4 tokens; consolation = 8. +- Loser holes always deducted at 1 token each. +- In 3-player games, the non-playing player also receives consolation from the loser. +- Replays double the consolation price each time. + +A **queue** accumulates tokens from each defeat and is paid at game end to the player with the most tokens. + +**Bets** (_paris_): rounds played beyond each player's average (the _contingent_). The first double-bet is the **postillon** (28 tokens, including 20 from the queue); each subsequent bet costs 8 tokens. + +### Multi-Player Rotation (3 or 4 Players) + +- 3 players: after each round, the winner is replaced by the third player; first-move privilege stays with the player who remained. +- 4 players: two teams of two; each player plays two rounds in a row then gives way to their partner. +- Non-active players may advise (opponents in 3-player, teammates in 4-player) but may not touch game components. + +--- + +## 11. Implementation Status Summary + +| Feature | Status | +| ----------------------------------------------- | ------------------------------ | +| Board state, movement, rest corner | Implemented | +| Helpless man | Implemented | +| True / false hits (small jan, big jan, corner) | Implemented | +| Jan filling and conserving (small, big, return) | Implemented | +| Opening jans (two tables, mezeas, six tables) | Implemented | +| Exit and exit points | Implemented | +| Bredouille (hole bredouille) | Implemented (`can_bredouille`) | +| Forbidden jans | Implemented | +| Stay / leave (_s'en aller_) | Implemented (`Go` event) | +| Big bredouille (`can_big_bredouille`) | Field exists, not used | +| Schools | Not implemented | +| Scored game / rounds | Not implemented | +| Misery pile (_pile de misère_) | Not implemented |