156 lines
6 KiB
Markdown
156 lines
6 KiB
Markdown
# 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`.
|