The TV runs the game. The phones just hold the buttons.

Playcade puts an arcade on any screen with a browser and turns every phone in the room into its controller. Most of the work went into the parts nobody sees: the network path, the weakest TV in the house, and the browser quirks between them.

Vue 3ViteSocket.ioWebRTCCanvas 2DExpressMongo

The Playcade TV screen: the arcade-cabinet logo, a QR code and the room code XEUZ on a gold card, and a row of game cards with googly-eyed critters on a warm graphite background.

Playcade turns the TV into a party game console and everyone's phone into a controller. Open playcade.party on anything with a big screen and a browser: a smart TV, a laptop on HDMI, a streaming stick. It shows a four-letter room code and a QR code. Everyone scans it, picks a name and a colour, and their phone becomes the gamepad. The first phone in holds the remote and picks the game. There are eleven games, from Fuse (lock in as late as you dare) to Turbo (twelve karts on one screen) and Sketchy (draw it on your phone, the room votes on the TV), for two to twelve players. No console, no app, no account.

The TV is the console

The design doc opens with the one decision everything else hangs on: where the game actually runs.

Game server Node · Express + Socket.io Rooms in memory, 4-letter codes WebRTC signaling Turn-based games (server reducers) Input relay when WebRTC fails MongoDB analytics, admin wss: lobby, signaling, turn-based state wss: actions, relay fallback TV stage /tv · any big-screen browser Lobby, room code, QR Action games: simulation + render Game audio Phones · 2-12 players /p/CODE · controller Gamepad for the current game Private info: hands, roles Name and colour WebRTC DataChannel LAN direct, typ. 5-30ms unordered, no retransmits
The TV runs the only simulation and the only render; phones send input and show their own controls. Turn-based games run on the server. Action games talk to the TV directly over WebRTC, and when that fails the same messages go through the Socket.io relay. There are no TURN servers.

Turn-based games like trivia, voting and drawing only need an action every few seconds, so they run on the server as pure reducers: a phone sends an action over Socket.io and the server sends back a view for each player and one for the TV. Real-time games are different. A racer needs input on the screen in well under 50ms, and a round trip from phone to cloud to TV costs two internet legs.

So for action games the TV is the console. It runs the only simulation and draws the only world. Phones send inputs straight to the TV over a WebRTC DataChannel set to unordered, with no retransmits, so a stale input is dropped instead of blocking a fresh one. The server only helps the two find each other. When WebRTC can't connect, on hotel wifi or a phone on mobile data, the same messages go through the Socket.io relay instead, and the game code never knows which path it's on. There is no TURN server: the relay is the fallback.

Because phones never render the world, there is nothing to predict or reconcile. The architecture doc guards that as a rule: never make a phone render or predict world state.

The TV lobby after one phone has joined: the room code, the game rail with Fuse marked UP NEXT, and a seat with the REMOTE badge.

Built for the weakest screen in the room

The docs assume a 2019 mid-range TV as the baseline, not a dev machine, and that assumption got tested early. A real TV reported 8fps on the lobby while a desktop throttled 6x held 59.5. The expensive effects, glow halos on two dozen critters and drifting background dots, were bound by fill rate, not CPU, so no desktop profile could reproduce them.

The fix was to let the app measure itself. It samples its own frame rate in 2.5-second windows, and after three windows in a row under 32fps it switches to a lighter look for good: same critters, colours and layout, without the costly extras. It never switches back, because a screen that flips between two looks in front of a room is worse than one that stays plain. At 20x CPU throttle with twelve players seated, that took the lobby from 14.6fps to 60.2.

A later performance pass has the most useful line in the repo: trust work, distrust time. Frame rates on a shared machine swing wildly (the same setup measured 18.1 and 5.8fps minutes apart), so the performance gate checks draw calls, overdraw and DOM nodes instead. Baking Flick Carnival's static scenery into sprites took it from 330 draw calls a frame to 72. The pass also records what didn't work: a 30fps render fallback, planned since week one, was built, measured and dropped, because these games are limited by the cost of each frame, not by how many frames they draw.

The phone join screen: pick a name and one of twelve critter colours, then JOIN THE GAME.
The host phone as the remote: a game rail, a big START button, and a TV SIZE control.

A television is not a phone

Phones and TVs load the same site, so it has to tell them apart. The first version got a Fire TV Stick wrong: it saw a touch-style pointer and a short screen, decided it was a phone, and asked a television to rotate itself to portrait.

The fix changed what counts as evidence. A device is only treated as a phone with positive proof: a touchscreen and a coarse pointer and a short side, and a browser that says it is not mobile is believed. Anything still ambiguous falls on the side of big screen, because a phone mistaken for a TV can still join by QR, but a TV mistaken for a phone can't start a room at all. There is also a manual override for the TV boxes nobody has tested yet.

Tests that drive the element, not the function

The join screen broke in production twice while every unit test passed. Code that pulled a room code out of a pasted link had passing tests, but the input box had a four-character limit, and the browser cuts a paste down to that limit before the code ever sees it. Pasting playcade.party/NHTD gave the field "PLAY".

The repo's rules now say it plainly: if a behaviour depends on what the browser does to input first (paste, focus, keyboards, the viewport), it gets an end-to-end test in a real browser. New tests are checked by putting the bug back and making sure they fail.

Twelve seats, twelve colours

Every game takes up to twelve players, because a group should never have to shrink to play the next game. Each seat is a colour and a googly-eyed critter, and the twelve colours were tuned until the closest pair was as easy to tell apart as the original eight. That is also why the background is a neutral warm graphite: an earlier plum background had the same hue as two of the player colours, and those two players sank into it.

What's next

The design doc's build order still has audience mode (phones beyond the player limit vote and cheer), a daily race with the same track for everyone that day, and optional profiles. The performance notes keep an open list too, led by an 8-byte binary input frame to replace the JSON packets the busiest games send today.