mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-14 16:17:43 +00:00
147 lines
4.9 KiB
Markdown
147 lines
4.9 KiB
Markdown
# 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](https://tiennm99.github.io/loto)
|
||
|
||
Android port: [`android/`](../android) — a native Kotlin + Jetpack Compose
|
||
app at feature parity with this one (this app is the behavioral spec). It
|
||
mounts `static/audio/` straight into its APK, so the voice clips exist once.
|
||
|
||
---
|
||
|
||
## 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](https://nodejs.org) 18+ and npm.
|
||
|
||
---
|
||
|
||
## Quick Start
|
||
|
||
```sh
|
||
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](https://github.com/rany2/edge-tts) (free, no API key). To regenerate
|
||
(e.g. to add a new Microsoft `vi-*` voice):
|
||
|
||
```sh
|
||
pip install edge-tts
|
||
python3 scripts/generate-audio.py
|
||
```
|
||
|
||
The script auto-discovers all available `vi-*` voices and writes an updated manifest.
|
||
|
||
---
|
||
|
||
## Regenerating Icons
|
||
|
||
`static/icons/source.svg` is the source for `icon-192.png` and `icon-512.png`;
|
||
`static/icons/source-maskable.svg` (full-bleed background, wordmark inside the
|
||
80% safe zone) is the source for `icon-maskable-512.png`. Render them with the
|
||
real **Roboto Condensed Black Italic** TTF loaded explicitly — a fallback font is
|
||
wider and clips the wordmark. Any SVG rasterizer that accepts a font file works,
|
||
e.g. [`@resvg/resvg-js`](https://github.com/thx/resvg-js) with
|
||
`font: { fontFiles: [...], loadSystemFonts: false }` and `fitTo` set to each
|
||
PNG's exact pixel width. Open the results and check nothing touches the edges.
|
||
|
||
---
|
||
|
||
## 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`](../LICENSE) at the repository root.
|