# 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://: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://: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//: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//: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`.