picker/README.md
2026-10-09 13:45:54 +02:00

156 lines
6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Picker
A simple web app to select who pays the bills: everyone puts a finger on the
phone screen, and after a suspenseful roulette one of you is chosen.
Plain HTML, CSS and vanilla JS modules. No build step and no dependencies.
Works on any static host and installs as an offline PWA.
## Modes
- **Pick one**: one finger is chosen.
- **Pick several**: N fingers are chosen.
- **Teams**: everyone is split into 2–4 balanced teams.
- **Order**: everyone gets a number, revealed one at a time.
- **Elimination**: one finger is knocked out per round until one remains.
Letting go early counts as being knocked out.
Settings (gear button on the idle screen) also cover the stakes text
("pay the bill" → "You pay the bill!"), theme (neon, pastel, or high
contrast with a distinct shape per finger), language (English/Dutch), sound
and vibration. Settings are saved in `localStorage`.
## How a round works
1. Put two or more fingers down. Once the set of fingers has been stable for
2.5 s, a three-beat countdown starts. Adding a finger restarts the wait.
Lifting one shows a "chickened out" message.
2. The outcome is drawn with `crypto.getRandomValues` *before* the animation
starts. The roulette is only presentation and always lands on the
pre-drawn result.
3. The result stays on screen after fingers lift. Once everyone has let go,
tap to play again (the tapping finger joins the next round).
## Project layout
| File | What |
| --- | --- |
| `js/main.js` | wiring + frame loop |
| `js/input.js` | Pointer Events per `pointerId`, gesture blocking, debug fake fingers |
| `js/game.js` | state machine (idle → gathering → countdown → suspense → result) |
| `js/modes.js` | outcome planning and roulette schedules for each mode |
| `js/random.js` | unbiased crypto randomness |
| `js/renderer.js` | canvas rendering (pre-rendered glow sprites) |
| `js/reveals.js` | reveal animations: flood, confetti, spotlight, shockwave, calm |
| `js/audio.js` | Web Audio sound effects + vibration |
| `js/ui.js` | overlay text, toolbar, settings sheet |
| `js/settings.js` | persisted settings |
| `js/i18n.js` | all strings (en, nl) |
| `js/themes.js` | colour themes |
| `js/device.js` | wake lock, fullscreen |
| `sw.js` | offline caching (network-first, cache fallback) |
| `tools/make_icons.py` | regenerates the PNG icons (Python stdlib only) |
## Testing on a desktop
Open `http://localhost:8000/?debug=1`:
- **Click** to place a fake finger, **click it again** to remove it, **drag** to move it.
- **A** adds a fake finger at a random spot, **C** clears them all.
- `?debug=1&fingers=4` starts with 4 fake fingers in a circle.
- `?debug=1&open=settings` opens the settings sheet on load.
- A HUD shows the phase, finger count and fps. The service worker is not
registered in debug mode, so you always get fresh files.
Automated checks (serve the folder, then open in any browser):
- `tests/logic.html`: state machine and fairness tests.
- `tests/sim.html?mode=order&n=5&t=11500`: renders one simulated frame at a
given time (`mode`, `n`, `t`, `theme`, `style`, `k`, `teams`, `reduced`).
## Testing on your phone (local network)
Serve the folder from your PC:
```sh
python -m http.server 8000 --bind 0.0.0.0
```
Find your PC's LAN IP (`ipconfig` on Windows, `ip addr` on Linux), then open
`http://<that-ip>:8000/` on the phone. On Windows, allow Python through the
firewall for private networks when asked.
Over plain `http://` on a LAN address, the page is **not a secure context**.
These features need HTTPS (or `localhost`):
| Feature | Plain http on LAN |
| --- | --- |
| Touch, canvas, crypto randomness, sound, vibration, fullscreen | works |
| Service worker / offline mode | **needs HTTPS** |
| Install as app (PWA) | **needs HTTPS** |
| Screen wake lock | **needs HTTPS** |
To test the secure-only features:
- **Android + USB (easiest):** enable USB debugging, open `chrome://inspect`
in desktop Chrome or Edge, click *Port forwarding* and map `8000` →
`localhost:8000`. Then open `http://localhost:8000/` on the phone.
`localhost` counts as secure, so everything works.
- **Android without USB:** on the phone, open
`chrome://flags/#unsafely-treat-insecure-origin-as-secure` and add
`http://<your-ip>:8000`. Use this for testing only.
- **Anywhere, iOS included:** put it behind HTTPS (see Docker below, behind
your reverse proxy).
Browser notes:
- Browsers only allow sound and vibration after a tap or a finger *lift*.
A finger going down doesn't count. If the very first round after opening
is silent, tap the screen once (or use any toolbar button) first.
- iOS Safari has no vibration API and no fullscreen API on iPhone, so those
controls are hidden there. Add it to the home screen for a full-screen
experience.
- Devices cap simultaneous touches (iPhone: 5, most Android phones: 10).
The app handles whatever arrives.
## Docker
The image is plain unprivileged nginx serving the static files on port
**8080**, with a `/healthz` endpoint.
```sh
docker build -t picker .
docker run --rm -p 8080:8080 picker
```
On every push to `main`, the GitHub Actions workflow
(`.github/workflows/docker.yml`) publishes a multi-arch (amd64 + arm64)
image to GHCR:
- `ghcr.io/<owner>/<repo>:latest` on `main`
- `:1.2.3` and `:1.2` when you push a `v1.2.3` tag
- `:sha-abc1234` on every build
Example `docker-compose.yml` for a home server:
```yaml
services:
picker:
image: ghcr.io/<owner>/<repo>:latest
restart: unless-stopped
ports:
- "8080:8080"
```
Point your reverse proxy (Caddy, Traefik, nginx proxy manager, …) at port
8080 for your subdomain and terminate HTTPS there. HTTPS is required for
offline mode, installing and wake lock. All paths are relative, so it also
works under a sub-path.
GHCR packages are private by default. Either make the package public
(GitHub → your profile → Packages → picker → Package settings), or run
`docker login ghcr.io` on the server with a token that has `read:packages`.
## Icons
`icons/*.png` are generated: `python tools/make_icons.py`.