Both subprojects ship from the same commit, so ordinary CI is now a single ci.yml; only the tag-driven release stands apart. The web app is built twice per run — once per base path — and every consumer downloads the artifact instead of rebuilding, replacing three redundant base-"" builds on main. Nothing deploys unless the test job is green, and android-release runs the suite before signing (ci.yml does not fire on tags, so it was the only gap). Shared toolchain setup moves into composite actions, which puts the web build and the APK on the same Node version for the first time. The Firebase PR path was still on npm ci against a stale web/package-lock.json that could resolve a different tree than pnpm-lock.yaml; drop the lockfile and the npm path with it. Also: least-privilege permissions widened per job, persist-credentials off on every checkout, concurrency groups that cancel superseded PRs but never a live deploy, npm caching for android, and the Firebase action pinned by commit SHA to match how the release actions were already pinned.
4.1 KiB
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
Quick Start
git clone https://github.com/tiennm99/loto
cd loto
pnpm install
pnpm dev # dev server at http://localhost:5173
Commands
| Command | Description |
|---|---|
pnpm dev |
Start development server |
pnpm build |
Production build (root base path) |
pnpm build:gh |
Production build for GitHub Pages (/loto base path) |
pnpm preview |
Preview production build locally |
pnpm test |
Run unit tests |
pnpm 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 pnpm 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: pnpm 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.