mirror of
https://github.com/tiennm99/loto.git
synced 2026-09-05 18:16:53 +00:00
feat: three-mode display (player/master/both) with master auto-tick
- Add `mode` setting to replace legacy `masterMode` with migration path - Implement 3-button display mode picker (player-only, master-only, both) - Auto-increment master when card drawn via call-bus event system - Add voice hint on settings button, gate auto-call on mode changes - Broadcast draw events from MasterPanel, reset call-bus on new game - Broadened announce condition to cover both modes - New call-bus.svelte.js module for event coordination - Update settings-store tests to cover mode migration - Update codebase docs for mode setting and call-bus architecture
This commit is contained in:
@@ -6,24 +6,25 @@
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/routes/+layout.svelte` | Root HTML layout. Sets Vietnamese lang, imports Geist font, applies global flex layout. |
|
||||
| `src/routes/+page.svelte` | Player page (`/`) — the ONLY page. Header + SettingsButton, PlayerBoard, conditional MasterPanel (when `settings.masterMode`), PageFooter. Indigo→purple gradient branding. |
|
||||
| `src/routes/+page.svelte` | Single page (`/`). Header + SettingsButton, renders player/master/both via `settings.mode`. Indigo→purple gradient branding. |
|
||||
|
||||
### Shared Components
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/lib/PlayerBoard.svelte` | Reusable player card (9×9 grid rendered as 3 stacked 3×9 mini-cards: Tân Tân / An khang thịnh vượng / Tân Tân tốt nhất). Tall (3:4 on mobile; 3:5 on sm+) cells with condensed bold black numbers (`tan-tan-num` font stack), white number cells, purple empty cells (dark mode dims via `filter:brightness(0.85)`). Handles crossed state, animated cross-out (200 ms `cross-draw` keyframe), `active:scale-90` press, 10 ms haptic on tap. Two header actions: "Tạo bảng mới" / "Xoá đánh dấu". First-run state shows a faded preview card. Bingo popup tiers: row 1 = standard celebration; row 3+ = falling-emoji confetti rain via CSS `confetti-fall`. Toast "Chờ N" + audio. Accepts `storagePrefix` prop for multi-card isolation. |
|
||||
| `src/lib/SettingsButton.svelte` | Gear icon + modal (responsive `max-w-sm sm:max-w-md`). 5 fieldsets: Giao diện (theme pills), Chế độ quản trò (switch row), Tự động xổ (switch + speed slider), Âm thanh (two switches + voice picker), Màu ô trống (10 Excel color swatches + custom picker). Boolean toggles use a shared `switchRow` snippet (`role="switch"` + keyboard support). Reset-to-default button. Mounted on `/`. |
|
||||
| `src/lib/MasterPanel.svelte` | Host controls. New game / draw, large "Số vừa xổ" hero token (160 px mobile, 224 px sm+) with `aria-live="assertive"` + auto `scrollIntoView` on each new draw, "Thứ tự đã xổ" history list, 11×9 last-digit-aligned tracking grid (with circular tokens + draw-order overlay). No host's own player card (the player already has one above). "Xổ số" / "Bắt đầu / Dừng" button bound to auto-call. Mounted conditionally on `/` when `settings.masterMode === true`; the wrapping section uses `transition:slide` for smooth toggle-in. |
|
||||
| `src/lib/MasterPanel.svelte` | Host controls. New game / draw, large "Số vừa xổ" hero token (160 px mobile, 224 px sm+) with `aria-live="assertive"` + auto `scrollIntoView` on each new draw, "Thứ tự đã xổ" history list, 11×9 last-digit-aligned tracking grid (with circular tokens + draw-order overlay). Publishes draws to `call-bus` for player auto-tick. "Xổ số" / "Bắt đầu / Dừng" button bound to auto-call. Mounted conditionally on `/` when `settings.mode !== "player"`; the wrapping section uses `transition:slide` for smooth toggle-in. |
|
||||
| `src/lib/PageFooter.svelte` | Footer with tagline ("Made by miti99 with ❤️ SVG icon") + link. Mounted on `/`. |
|
||||
|
||||
### Game Logic
|
||||
### Game Logic & Coordination
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/lib/game-logic.js` | Stateless utilities: generateGrid (constraint-aware picker — exact 5 per row & per col, ascending-sorted columns, soft "no 3 consecutive filled cols per row" via rejection sampling), saveGrid, loadGrid, saveCrossedState, loadCrossedState, isRowComplete, getWaitingNumber. |
|
||||
| `src/lib/call-bus.svelte.js` | Pub/sub for master draws → player auto-tick. Reactive `bus.lastDrawn` slot (emits `{ num, at }`). Used in `mode: "both"` to auto-mark master-called numbers on player board. |
|
||||
| `src/lib/vietnamese-number.js` | `numberToVietnamese(n)` — pure utility mapping 0..90 to spoken Vietnamese, with tonal exceptions (15 → "mười lăm", 21 → "hai mươi mốt", 25 → "hai mươi lăm"). Out-of-range falls back to `String(n)`. |
|
||||
| `src/lib/voice.js` | Bundled-MP3 playback. Exports `playNumber(n)`, `playWaiting(n)` (sequences cho + N), `playBingo()`, `cancelPlayback()`. Lazy `<audio>` cache, cancel-then-play, token-based cancel ensures stale promises can't resume after a new event. Reads active voice from `settings.voice`; URLs go through `import { base } from "$app/paths"` for basePath safety. |
|
||||
| `src/lib/audio-manifest.js` | Re-exports `static/audio/manifest.json` as `VOICES` (array) + `VOICE_IDS` (Set) + `DEFAULT_VOICE`. Manifest is generated by `scripts/generate-audio.py`. |
|
||||
| `src/lib/settings-store.svelte.js` | Reactive global UI settings via Svelte 5 runes. Stores 8 keys: `theme` (enum: "auto" / "light" / "dark"), `masterMode` (bool), `autoCallEnabled` (bool), `autoCallSpeed` (1–10), `emptyCellColor` (hex), `voiceEnabledMaster` (bool), `voiceEnabledPlayer` (bool), `voice` (string id matching audio manifest). Persisted to localStorage `loto_settings`. Pushes values to CSS vars and `<html class="dark">` on `:root`. Per-key validators preserve old data. |
|
||||
| `src/lib/settings-store.svelte.js` | Reactive global UI settings via Svelte 5 runes. Stores 9 keys: `theme` (enum: "auto" / "light" / "dark"), `mode` (enum: "player" / "master" / "both"; replaces legacy `masterMode`), `autoCallEnabled` (bool), `autoCallSpeed` (1–10), `emptyCellColor` (hex), `voiceEnabledMaster` (bool), `voiceEnabledPlayer` (bool), `voiceWaitingNumber` (bool), `voice` (string id matching audio manifest). Persisted to localStorage `loto_settings`. Pushes values to CSS vars and `<html class="dark">` on `:root`. Per-key validators preserve old data. One-shot migration: `masterMode: true` → `mode: "both"`. |
|
||||
|
||||
### Styling
|
||||
| File | Purpose |
|
||||
@@ -65,18 +66,19 @@
|
||||
| `loto_grid` | Player's card numbers. |
|
||||
| `loto_crossed` | Player's marked cells. |
|
||||
| `loto_master` | Host's drawn/remaining numbers. |
|
||||
| `loto_settings` | Global UI settings: `{ theme, masterMode, autoCallEnabled, autoCallSpeed, emptyCellColor }`. |
|
||||
| `loto_settings` | Global UI settings: `{ theme, mode, autoCallEnabled, autoCallSpeed, emptyCellColor, voiceEnabledMaster, voiceEnabledPlayer, voiceWaitingNumber, voice }`. |
|
||||
|
||||
`loto_master_card_*` keys are no longer written (host's own player card removed) but old saved data is left untouched.
|
||||
Old `masterMode` bool is migrated on load: `masterMode: true` → `mode: "both"`. `loto_master_card_*` keys no longer written but old saved data untouched.
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
```
|
||||
RootLayout
|
||||
└── HomePage (/) ← single page; any other URL redirects to /
|
||||
├── PlayerBoard (storagePrefix="loto")
|
||||
├── [if settings.masterMode]
|
||||
│ └── MasterPanel (controls, history, 11×9 tracking grid)
|
||||
├── [if settings.mode !== "master"]
|
||||
│ └── PlayerBoard (storagePrefix="loto")
|
||||
├── [if settings.mode !== "player"]
|
||||
│ └── MasterPanel (publishes to call-bus on draws)
|
||||
└── PageFooter
|
||||
```
|
||||
|
||||
|
||||
@@ -62,6 +62,16 @@ All colors have corresponding `dark:` variants. Tailwind's `prefers-color-scheme
|
||||
- **Max width**: `max-w-lg` (player), `max-w-2xl` (host) — centered with `mx-auto`.
|
||||
- **Flex wrap**: Use `flex flex-wrap gap-1.5` for number history chips.
|
||||
|
||||
## Settings & UI Toggles
|
||||
|
||||
### Mode Picker (SettingsButton)
|
||||
Three-way toggle in settings modal:
|
||||
- **Player mode** (`mode: "player"`) — PlayerBoard only, default UX for players
|
||||
- **Master mode** (`mode: "master"`) — MasterPanel only, host-only view (e.g., projector)
|
||||
- **Both mode** (`mode: "both"`) — PlayerBoard + MasterPanel stacked inline, master auto-ticks player board on draw
|
||||
|
||||
Mode selection is persisted to localStorage and applied immediately.
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Button Styles
|
||||
|
||||
@@ -4,6 +4,14 @@ This document tracks **future work only**. Completed features live in
|
||||
git commit history and `plans/`, not here. For "what's in the app right
|
||||
now", see `docs/project-overview-pdr.md` and `docs/codebase-summary.md`.
|
||||
|
||||
## Shipped
|
||||
|
||||
### Three-Mode Rendering + Master Auto-Tick (2026-04-27)
|
||||
- `mode: "player" | "master" | "both"` replaces `masterMode: boolean`
|
||||
- New `call-bus.svelte.js` coordinates master draws → player auto-tick in "both" mode
|
||||
- "Quản trò đọc số" voice now speaks Chờ/Kinh on master in "both" mode
|
||||
- One-shot migration: `masterMode: true` → `mode: "both"`
|
||||
|
||||
## Idea Phase
|
||||
|
||||
### Undo Last Cell
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
|
||||
```
|
||||
Entry (+layout.svelte)
|
||||
├─ onMount: loadSettings() — restore all 5 settings keys from loto_settings,
|
||||
├─ onMount: loadSettings() — restore all 9 settings keys from loto_settings,
|
||||
│ apply CSS vars, toggle <html class="dark"> on theme/OS pref, setup auto-call effect
|
||||
│
|
||||
└─ / (single page)
|
||||
├─ Load loto_grid, loto_crossed from localStorage
|
||||
├─ Display 9×9 PlayerBoard (empty cells use --empty-cell-bg from settings)
|
||||
├─ Generate new grid on button click
|
||||
├─ Mark/unmark cells on click
|
||||
├─ Show bingo popup + "Chờ X" toasts
|
||||
├─ [if settings.masterMode === true]
|
||||
│ └─ Mount MasterPanel (host controls + draw history)
|
||||
├─ [if settings.mode !== "master"]
|
||||
│ ├─ Load loto_grid, loto_crossed from localStorage
|
||||
│ ├─ Display 9×9 PlayerBoard (empty cells use --empty-cell-bg from settings)
|
||||
│ ├─ Listen to call-bus for auto-tick when master draws
|
||||
│ ├─ Generate new grid on button click
|
||||
│ ├─ Mark/unmark cells on click
|
||||
│ └─ Show bingo popup + "Chờ X" toasts
|
||||
├─ [if settings.mode !== "player"]
|
||||
│ └─ Mount MasterPanel (controls + draw history, publishes to call-bus)
|
||||
└─ PageFooter (tagline + miti99 link)
|
||||
```
|
||||
|
||||
@@ -32,11 +34,15 @@ ascending top-to-bottom (lô tô hội chợ Tân Tân convention).
|
||||
|
||||
### Settings (`loto_settings`)
|
||||
```
|
||||
theme: "auto" | "light" | "dark" // Display mode
|
||||
masterMode: boolean // Show MasterPanel on /
|
||||
autoCallEnabled: boolean // Enable auto-call timer
|
||||
autoCallSpeed: number (1–10) // Speed in seconds
|
||||
emptyCellColor: "#rrggbb" // Hex color (default #7030A0 Excel Purple)
|
||||
theme: "auto" | "light" | "dark" // Display mode
|
||||
mode: "player" | "master" | "both" // Panel visibility (replaces masterMode)
|
||||
autoCallEnabled: boolean // Enable auto-call timer
|
||||
autoCallSpeed: number (1–10) // Speed in seconds
|
||||
emptyCellColor: "#rrggbb" // Hex color (default #7030A0 Excel Purple)
|
||||
voiceEnabledMaster: boolean // Speak called numbers (master view)
|
||||
voiceEnabledPlayer: boolean // Speak "Chờ"/"Kinh" (player events)
|
||||
voiceWaitingNumber: boolean // Include number after "Chờ"
|
||||
voice: string // Voice ID from audio manifest
|
||||
```
|
||||
|
||||
### Host State (`storagePrefix="loto_master"`)
|
||||
@@ -107,7 +113,19 @@ Files that are client-only:
|
||||
- `src/lib/PlayerBoard.svelte` (player card component)
|
||||
- `src/lib/MasterPanel.svelte` (host panel, mounted when `settings.masterMode`)
|
||||
|
||||
## Data Flow: Mark a Cell
|
||||
## Data Flow: Master Draw (mode: "both")
|
||||
|
||||
```
|
||||
1. Master taps "Xổ số" button in MasterPanel
|
||||
2. Draw logic removes number from remaining
|
||||
3. broadcastDraw(num) publishes to call-bus
|
||||
4. PlayerBoard's $effect listens to bus.lastDrawn
|
||||
5. Auto-marks cell in player's grid (if it exists)
|
||||
6. If row completes → bingo popup + auto "Kinh" voice
|
||||
7. MasterPanel displays hero token, appends to history
|
||||
```
|
||||
|
||||
## Data Flow: Mark a Cell (Player)
|
||||
|
||||
```
|
||||
1. User clicks button in PlayerBoard
|
||||
@@ -116,7 +134,7 @@ Files that are client-only:
|
||||
4. $effect listens to crossed → saveCrossedState()
|
||||
5. localStorage updated with new crossed state
|
||||
6. $derived updates rowCompleteness matrix
|
||||
7. If row complete → bingo popup; if waiting → toast
|
||||
7. If row complete → bingo popup; if waiting → "Chờ X" toast
|
||||
```
|
||||
|
||||
## Data Flow: Initial Load
|
||||
@@ -141,9 +159,27 @@ Files that are client-only:
|
||||
| toast | 5s forwards | "Chờ X" notification fade in/build |
|
||||
| cell-crossed::after | instant | Red diagonal line in marked cells |
|
||||
|
||||
## Call Bus (Master ↔ Player Wiring)
|
||||
|
||||
In `mode: "both"`, MasterPanel and PlayerBoard communicate via `call-bus.svelte.js`:
|
||||
|
||||
```
|
||||
call-bus.svelte.js:
|
||||
├─ bus.lastDrawn = { num, at } // Published by master draw
|
||||
├─ broadcastDraw(num) // Called by MasterPanel on each draw
|
||||
└─ resetBus() // Called on new game
|
||||
|
||||
PlayerBoard:
|
||||
└─ $effect(() => { bus.lastDrawn }) // Listens for draw broadcast
|
||||
└─ Auto-marks cell if number exists on board
|
||||
└─ Triggers bingo popup if row completes
|
||||
```
|
||||
|
||||
Each draw creates a new object (even repeat numbers) to ensure reactive re-fire.
|
||||
|
||||
## Offline Capability
|
||||
|
||||
All state is localStorage. No API calls. Fully functional offline after initial load.
|
||||
|
||||
Last reviewed: 2026-04-27
|
||||
Last synced: 2026-04-27 (6-phase refactor)
|
||||
Last synced: 2026-04-27 (three-mode + auto-tick feature)
|
||||
|
||||
Reference in New Issue
Block a user