# 🎭 The Imposer Game

A lightweight, fully offline-capable word-deduction party game that runs as a
Progressive Web App (PWA) — no account, no install required to try it. Play in
1-tap game modes, gather in a lobby, then figure out the secret word: one player
is The Imposter and needs to blend in while everyone tries to flush them out.

## Quick start (local / offline)

Serve the folder with any static file server and open it in a browser:

```bash
# from the project root
python3 -m http.server 8000
# open http://localhost:8000
```

No Node, no build step, no network needed after first load (it's a PWA with a
service worker for offline). Everything — words, images, sounds, icons — is
bundled locally.

## PWA / Installer

- `manifest.webmanifest` + service worker enable *"Install app"* on mobile/desktop.
- In-app **Installer** (bottom bar 🛠) detects your OS, captures the browser's
  native install prompt, and can store platform bundles (keeps other platforms
  compressed to stay light).
- Share invites via **QR code** (Lobby/Board) — no ads needed to invite others.

## The 5 game modes

| Mode | How it works |
|------|--------------|
| 👥 **Hot Seat** | Same word for everyone, one imposter; take turns reading it aloud. |
| 🕵️ **Hardcore** | Different words per player, same category lane. |
| 🤖 **Family (Bots)** | N human players + bots fill the table. |
| 🌐 **Online (Strangers)** | Matchmade room; fill with players or bots. |
| 🎯 **Snap / Speed** | Fast rounds with shorter timer. |

Plus a configurable **Custom** setup inside the lobby (players, imposters,
category, timer).

## Gameplay rules

- **Imposter count** scales with players: n imposters need at least **n³** players
  (e.g. 2 imposters → 27 players, 3 → 64).
- **Survivor elimination voting:** each round the table votes *out* one player.
  - Crew wins if The Imposter is voted out.
  - The Imposter wins when only 2 players remain (or fewer alive than imposters).
- Voted-out players stay **muted** (no chat/voice) until the match ends.
- **Read-aloud (TTS)** is intentionally limited so it never spoils the action:
  turn notices, voting announcements, eliminations, the pick prompt, and the
  final reveal — never the word/identity mid-game. The final reveal uses a
  gold **typewriter** animation ("The real word was X, and the imposter was Y…").

## Tokens & economy

- New players get a **+10 💎** weekly grant.
- Starting a match costs **1 💎**; watch a real **ad** for tokens:
  - **Offline / no backend:** **+1 💎** per full ad, capped at **+30/week**.
  - **Online (server linked):** **+5 💎** per full ad, credited to your account.
  - Reward is only paid on a *completed* watch (skipping grants nothing).
- Hard token gates and "last token" protections are enforced.
- Local-first, mirrored to your server account when online.

## Themes, audio, chat

- **6 color themes** (dark default, plus light/neon etc. — switch in Settings).
- **Web-Audio synthesized** SFX + ambient music + TTS read-aloud, each with
  per-channel volume sliders (music / SFX / read-aloud) and a master volume.
- A **"Hide 'I am imposter'"** setting hides your imposter identity privately
  (good for emotions/table-balance).
- In-game **chat** with bot bubbles; voice toggle per player.

## Content & admin

- **10 categories × 100 words** each (1,000 words) across `core/words.js`, with
  an emoji-as-image per category. Duplicate `[word, impost]` pairs are deduped at
  load and again whenever a server pack is applied.
- Custom words and ads are stored as JSON and merged at runtime:
  - `data/words-custom.json` — your own words/hints
  - `data/ads.json` — ad placements (id, type, reward, platform)
- **`python3 admin.py`** — a tkinter GUI for managing words/ads, publishing a
  **compressed monthly word pack** to the server, pushing app **broadcasts**,
  viewing **weekly player reports**, and saving the app version + **GitHub repo
  URL** used by the in-app self-update check.

## Online backend (optional; still fully playable offline)

The included `server/` is a **zero-dependency Node server** (`http` only) that
adds *stranger matchmaking*, *server-backed tokens*, an *ad feed*, a *monthly
word pack*, *admin broadcasts* and *weekly player reports*. It deploys to any
free Node host (Render / Railway / Fly).

```bash
cd server && node index.js          # listens on $PORT or 3000
```

Endpoints:

```
GET   /health                  health check
GET   /version.json            app version + pack version + GitHub repo (drives self-update)
GET   /ads.json                ad feed
POST  /account/create          {userId, tokens:10, week}
POST  /account/balance         {tokens, timeToWeekReset}
POST  /account/spend           spend n -> {ok,tokens}
POST  /account/adReward        +reward -> {ok,tokens}
POST  /match/queue             join matchmaking -> {room, host}
POST  /match/status            {players, locked, started, fill}
POST  /match/leave             leave queue
POST  /match/sync              push host game snapshot for guests
POST  /match/state             poll guest game snapshot
GET   /words.json             get fused word DB (published + submitted)
POST  /admin/publish           upload compressed monthly word pack {zip,lz,version}
POST  /admin/config            save appVersion / github repo / appZipUrl
POST  /admin/broadcast         push a one-time announcement popup
POST  /admin/broadcast/clear   clear auto announcements
POST  /admin/players           list weekly player reports
POST  /admin/players/flag      flag a player report
POST  /announce                fetch current announcements
```

To point the PWA at it, set the backend URL before the scripts load:

```html
<script>window.ServerURL = 'https://your-host.example';</script>
```

Tunable via env: `PORT`, `ADMIN_TOKEN`, `MATCH_LOCK` (secs to lock at 3 players),
`MATCH_MIN`, `MATCH_TIMEOUT` (secs before prompting for bots).

### Hosting on GitHub Pages

The **static PWA (everything except the Node backend) runs fine on GitHub Pages**
— the game is offline-first and needs no server to play locally, with bots,
hot-seat and QR/lobby play all working. The live/online features (stranger
matchmaking, server tokens, monthly packs, broadcasts, weekly reports) need the
Node `server/` on a separate small host (Render/Railway/Fly). Add
`window.ServerURL` to point the Pages build at that host.

> ⚠️ **Private repos:** GitHub Pages **cannot** publish a site to the public from
> a private repo. If you want to keep your source repo private but still give
> players a working link, deploy the site to **Netlify** instead — the admin has
> a **"Deploy to Netlify"** button that uses your Netlify token to upload the
> static app and return a public `*.netlify.app` URL (your GitHub repo can stay
> private, and the app's self-update point is set to that public URL).

**From the admin (Publish tab):**
- **"Continue with GitHub"** — opens GitHub; you paste a Personal Access Token,
  the admin validates it, lists your repos, you pick one (or create "New repo…"),
  choose a branch, and **"Push whole site + pack"** uploads every static file plus
  the compressed monthly word pack to that repo (auto-updates the hosted app).
- **"Deploy to Netlify"** — paste a Netlify Personal Access Token and it uploads
  the site for a public URL while the GitHub repo stays private.

## Tests

```bash
node --test        # or: npm test   (JS: words/game/bots/wallet/compress/server)
python3 test/admin_github_test.py   # Python: admin GitHub + Netlify helpers (mocked)
```

## Online backend (3-account Cloudflare shards)

Ever since the old local server was retired, all online features (friend rooms,
stranger matchmaking, word relay, rewarded-ad verification, SSV) run through
**Cloudflare Workers**. The backend is spread across **three Cloudflare
accounts** so the free-tier KV write quota (1,000 puts/day ≈ 11–12 rewarded-ad
games) doesn't all land on one namespace:

| dir             | worker             | role                                        |
|-----------------|--------------------|---------------------------------------------|
| `worker-shard-a`| `imposter-friend-1`| friend rooms, room-code prefix bucket 0      |
| `worker-shard-b`| `imposter-friend-2`| friend rooms, room-code prefix bucket 1      |
| `worker/`       | `imposter-ads`     | AdMob SSV, ad tokens, stranger matchmaking   |

Every account deploys the **same** `worker.js` with its **own** KV namespace
`AD_TOKENS` and **identical** secrets: `ADTOKEN_HMAC_KEY`, `ADMOB_SSV_PUBKEY`
(and optional `GITHUB_PAT`). `ably-config.js` holds the shard table
(`IMPOSTER_SHARDS`); a room's worker is chosen deterministically from its
room-code prefix, so hosts and joiners always hit the same shard. Stranger
rooms and ads/SSV always go to the ads worker. Deploying a shard (browser
login — you click Allow once per account):

```bash
cd worker-shard-a
npx wrangler login                      # browser -> Allow
npx wrangler kv namespace create AD_TOKENS   # paste id into wrangler.toml
npx wrangler secret put ADTOKEN_HMAC_KEY    # SAME on all 3
npx wrangler secret put ADMOB_SSV_PUBKEY    # SAME on all 3
npx wrangler deploy                    # -> https://imposter-friend-1.<you>.workers.dev
```

`deploy_shards.sh` automates all of the above except the browser Allow click.

## File map

```
index.html            PWA shell (all screens/modals)
app.js                main app (screens, flows, reveal, voting, chat, wave, onboarding)
core/  words.js compress.js bots.js game.js transport.js room.js   game logic
token/wallet.js       tokens/ads/gates
ui/   icons.js style.css profile.js audio.js qr.js qrcode.js installer.js
server/ index.js api.js   optional online backend
admin.py              tkinter admin GUI
data/ words-custom.json ads.json
assets/icons/         app icon/logo
manifest.webmanifest sw.js    PWA offline/install
```

---

The Imposer Game — spot the fake, keep your word, survive the vote.
