Plans were split across three trees: plans/, web/plans/, and android/plans/. Merge them into plans/ at the repo root. Sweep the shipped Android experience pass and the two reports for it, carrying the residual forward into plans/todo.md: the device QA checklist (open only because this environment has no device or emulator), the clipped web PWA icons defect, and the untested native assumptions. Merge android/plans/todo.md in the same pass, dropping items the experience pass already delivered -- haptics, back button, safe-area insets, launcher icon, and splash -- each verified against MainActivity.java, web/src/app.css, and the res/ tree. Correct two stale claims while merging: web/ is a plain directory in this monorepo, not a git submodule, and the serialize-javascript override is held by @rollup/plugin-terser rather than workbox-build. The retired native Kotlin/Compose port plan moves as-is; it is superseded by the Capacitor wrapper but not yet swept.
Lô Tô
Web version of Lô tô hội chợ — Vietnamese fairground bingo. Built with SvelteKit (static export). The caller draws numbers one at a time; a host panel manages the game and called numbers are announced aloud in Vietnamese using bundled audio clips. No runtime TTS or server required.
Live: tiennm99.github.io/loto
Android port: android/ — a Capacitor wrapper that bundles this
app into a fully-offline APK.
Features
- Number board — 1–90 grid; called numbers highlight as they are drawn
- Host panel — enable "Chế độ quản trò" (host mode) in settings to reveal the master draw panel inline below your player card
- Vietnamese voice callouts — pre-generated MP3 clips for every number using
Microsoft Edge TTS (
edge-tts); multiple voices selectable in settings; no API key or internet connection needed at runtime - Auto-cross player card — optional automatic crossing of numbers on the player's bingo card as they are called
- Auto-countdown — configurable delay between number draws for unattended calling
- PWA — installable as a progressive web app; works offline after first load
- Static export — no server; deploys to any static host or GitHub Pages
Requirements
Node.js 18+ and npm.
Quick Start
git clone https://github.com/tiennm99/loto
cd loto
npm install
npm run dev # dev server at http://localhost:5173
Commands
| Command | Description |
|---|---|
npm run dev |
Start development server |
npm run build |
Production build (root base path) |
npm run build:gh |
Production build for GitHub Pages (/loto base path) |
npm run preview |
Preview production build locally |
npm test |
Run unit tests |
npm run lint |
ESLint check |
Configuration
Copy .env.example to .env.local and set:
| Variable | Description |
|---|---|
CODESERVER_HOST |
Hostname for code-server reverse-proxy dev (optional) |
CODESERVER_PORT |
Port for code-server proxy (optional) |
When running inside code-server use npm run dev:codeserver and open
https://<CODESERVER_HOST>/absproxy/<CODESERVER_PORT>/ (use /absproxy/, not
/proxy/ — the latter strips the path prefix and breaks SvelteKit routing).
Architecture
Single-page SvelteKit app, statically exported. No backend.
src/
├── routes/ # Single route: /
├── lib/
│ ├── game-logic.js # Draw state, number pool, called-number tracking
│ ├── master-store.svelte.js # Host panel state (Svelte 5 runes store)
│ ├── settings-store.svelte.js
│ ├── voice.js # Audio manifest loader + clip player
│ ├── player-auto-cross.js # Auto-cross logic
│ ├── MasterPanel.svelte # Host draw UI
│ ├── PlayerBoard.svelte # Bingo card + number grid
│ └── AutoCountdown.svelte # Countdown timer between draws
static/
└── audio/{voiceId}/ # Pre-generated MP3 clips (one per number per voice)
scripts/
└── generate-audio.py # Regenerates audio clips via edge-tts
Game state lives entirely in Svelte 5 rune stores; no server round-trips. Audio manifest is generated once at build prep time — the app reads a JSON manifest on load and plays the appropriate clip for each drawn number.
Regenerating Audio
Voice clips in static/audio/ were generated with
edge-tts (free, no API key). To regenerate
(e.g. to add a new Microsoft vi-* voice):
pip install edge-tts
python3 scripts/generate-audio.py
The script auto-discovers all available vi-* voices and writes an updated manifest.
Deployment
Deployed to GitHub Pages by the deploy-pages job in
.github/workflows/ci.yml on push to main. Build command: npm run build:gh
(sets /loto base path). Static output in build/.
The same workflow also deploys the base-"" build to Firebase Hosting and
bundles it into the debug APK, so web/ is built once per base path per run.
License
Apache-2.0 — see LICENSE at the repository root.