How a TV and up to four phones share a room on doozer.games. The code, the TV-authoritative message protocol, the interim long-poll relay on the origin, the Cloudflare Worker waiting to replace it, and the rules that keep it from ever being the server's load or getting a venue banned.
Z1 and Z2 of Doozer Games, live in v2.2.656 (Oct 25 2026).
doozer.games/<CODE> (the existing ?e=qr). Codes use BCDFGHJKMNPQRSTVWXZ: no vowels, so no words; no I, L, O, U or Y to misread. With nobody playing, four gardens rake themselves.
three or four get a quarter each; an open garden keeps raking. A fifth phone is told all four gardens are in use. A taken color or a repeated nickname ("Moss 2") is adjusted and the player is told. Away for two minutes, the garden opens up again.
The code on the screen is the only way in. It is never published anywhere else (not on The One, not in a sitemap). /tv, /join and /<CODE> are noindex and the landing links /tv nofollow. robots.txt disallows /tv, /join and the relay. GOTCHA (fixed v2.2.659): index.php answers /robots.txt and /sitemap.xml for every host first; doozer.games and doozer.shop are now handed to their own includes there, so edit those, not the generic branch. Cloudflare caches robots.txt. The room cap counts only TVs heard in the last minute, so a crawler that opens /tv and leaves cannot keep a venue waiting.
| File | What |
|---|---|
includes/doozer-zen.php |
the garden; $zenRoom (route /tv) adds the room: players, Hand, labels, join card |
includes/doozer-pad.php |
the phone: code entry (/join), join screen, touchpad, tray, minimap |
assets/js/doozer-relay.js |
DZRelay.tv() / DZRelay.phone(), both carriers behind one API |
games-relay.php + includes/games-relay.php |
the interim relay, a root file Apache runs directly |
cloudflare/games-relay/ |
the Worker + Durable Object (not deployed until Jon connects Cloudflare) |
game_rooms |
one row per room: code, live_code (unique while live), game, business_id (Z3), state, joins, players_peak. Never who played. |
Phones send intent only, allowlisted and re-encoded by the relay: h {s: [[x, y, down, dt]...]} (hand samples, 0..10000 across the garden), tool, lift, spawn, rot, smooth, hello, leave; for the menu and Town Trivia, pick {g}, town {id}, start, again, ans {a}. join is written by the relay, never by a phone. The TV answers with welcome (garden, color, deduped nickname, hand), state (that garden's objects for the phone's minimap, 4/s at most), tick (lift/set, for haptics), hand (a new stone landed beside you: move onto it), full, who (the TV reloaded: join again).
The phone owns its hand position (a relative touchpad with mild acceleration, like a laptop's): slide moves, hold 220 ms then slide presses (rake or push), hold still 560 ms on a stone lifts it. The press sample must be sent the moment the press begins, before any slide: the TV only lifts from a press it has seen, and a lift that arrives first is ignored (found in testing: the stone was pushed instead). The TV plays each hand back through a 90 ms jitter buffer keyed by the phone's own sample spacing, so strokes keep their shape over a bumpy network, and catches up if it falls more than 420 ms behind.
900 ms idle) and gets its mail in the same reply. The TV long-polls, held at most 0.8 s and only while one of 6 server-wide wait slots is free. Measured through Cloudflare from a laptop: phone posts median 107 ms, p90 205 ms.
ticket (grWsTicket: HMAC-SHA256 over code, role, cid, cleaned nickname, color, expiry) only when platform setting games_relay_ws and api key games_relay both exist. The TV keeps polling even on the socket, so a phone on either carrier is heard, and answers each phone on the carrier it last used. Tested locally against the Worker's real Room class (test/room.test.mjs, 23 checks, plus a mock edge with fallback when it dies). Steps to go live: cloudflare/games-relay/README.md.
no autoload, no shutdown job tick on a path hit 15 times a second per phone. Rooms are folders of small files in the temp dir (never web served; cache/ is). A message is one appended line under flock; a queue is emptied under the same lock once its only reader has caught up. The database is touched only to make a room, count a join (after the reply), and close. DDL runs once, then a flag file says so. Caps: 12 live rooms, 8 phone connections a room, 6 held polls server-wide, 32 KB bodies, 12 rooms and 40 joins per address per hour.
{ok:false, error}. A run of 4xx or a ModSecurity matchfrom one address feeds fail2ban, whose bans escalate to a week (recidive) and then forever (see The 521 Root Cause). Oct 25 2026: a relay test with an HTML nickname got the operator's laptop banned from the origin for over half an hour.
nick64) in both directions, so text a person typed neversits in a request body where the WAF can mistake it for an attack and ban a venue's network. The relay strips tags and control characters, caps at 16, and swaps anything on a short list of slurs and profanity for a default nickname (it is shown on a TV in a public room).
are whitelisted in fail2ban and nginx restores the real address, so whatever the WAF decides about one request, a ban cannot lock a room out.
with it; addresses only as a salted hash in hourly limit files.
tv.q) is emptied only onceit passes 256 KB, and every page that opens (the app starting, the menu after Back, a game after a pick) reads it from the start. Of that first read, DZRelay.tv keeps each phone's latest join, if the phone has not left since and the relay heard from it in the last 20 s (seen); everything after it is live. GOTCHA (found on the Fire TV stick, v2.2.693): acting on all of it seated phones long gone ("Room is full" with nobody there, again after every page change, until each went quiet two minutes later), and replayed an old menu pick, so Back jumped straight back into the game; Zen re-raked old strokes. Tests that start fresh rooms never see it: test a resumed one.
doozer-tv-room.js, pageshow withpersisted). Desktop Chrome restores the menu on Back with the players of then, and the phones stay on the game; the Fire TV app's WebView reloads it anyway.
curl -H "X-Dev-Token: …" https://doozer.work/games-relay.php?stats=1 (or through doozer.games): live rooms, the week's counts, caps; ?stats=1&relay=off|on is the switch; &sweep=1 sweeps. In a TV's console, dzZenRoom() shows the carrier and who sits where. Verify by LOOKING: headless Chrome over the DevTools protocol, a TV tab and real phone tabs driven with Input.dispatchMouseEvent, screenshots of the TV (the harness lives in the session scratchpad; rebuild it from this description).
Related: The 521 Root Cause, Shortlinks and QR, Doozer Games and Doozer Shop.