Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
Queue up. Duel a friend. A one-on-one arena shooter that runs in the browser at cuued.com. Shoot, block and wall-jump to win points. The loser of each round drafts one card from a hand of five, so every loss lets you build a counter to your rival.
The name is said like queued: pick a name, share a 4-letter code, and you're in a duel. No download, no account.
Play
Go to cuued.com, or open index.html in a browser. There's no build step, server or dependencies to install. A copy also runs on GitHub Pages.
The first time you open it, you pick a name. It's remembered in your browser, and you can change it from the title screen or Settings.
Online play
One player picks Play online → Host a game and gets a 4-letter room code and an invite link.
The other player opens the link, or picks Play online → Join with a code and types the code.
The match starts as soon as they connect. Both players draft at the start, and each round's loser drafts on their own screen.
Online play is peer-to-peer. The host's browser runs the match, and the guest sends its inputs and draws the host's snapshots. The guest's own fighter is predicted locally, so it responds instantly, and it's corrected against the host's acknowledged state without visible jumps. The free public PeerJS broker only introduces the two browsers, so there is no game server to run.
When two networks can't link directly (strict routers, campus or office Wi-Fi, some phone carriers), the game falls back to a relay automatically. Messages go through free public MQTT brokers (EMQX, then Mosquitto) over secure WebSockets, with no account needed. The host listens on every broker, and the guest uses the first one that answers. A relayed match adds about 40–80 ms each way, shows ONLINE (RELAY) at the top of the screen, and sends snapshots at 20 Hz instead of 30. These are free test brokers with no uptime guarantee, which suits a game played with friends. node scripts/relay-smoke.js checks them live.
Optional upgrade: Cloudflare TURN.api/turn.js is a Vercel function that can hand out short-lived Cloudflare Realtime TURN credentials (free for the first 1 TB a month). That keeps even blocked networks on a fast WebRTC path, so the MQTT fallback is rarely needed. To enable it, set two environment variables on the Vercel project and redeploy:
Variable
Where to find it
CF_TURN_KEY_ID
Cloudflare dashboard > Realtime > TURN Server > your key's Turn Token ID
CF_TURN_KEY_TOKEN
The API Token shown once when you create that key
Without them, /api/turn returns STUN only and the MQTT relay covers blocked networks. node scripts/turn-check.js confirms the credentials work by allocating (and releasing) a real relay slot.
Move
Jump
Drop through a plank
Aim
Shoot
Block
You
AD or ←→
W / Space / ↑
S / ↓
mouse
click
right-click / Shift
Press Esc to pause and M to mute. In the menus, use ↑↓ to choose, Enter to select, ←→ to change a setting and Esc to go back.
How a match works
Both players draft one card before the first shot.
A point ends when one fighter dies. Two points win a round.
The round's loser drafts one card from a hand of five. Cards stack.
The first player to win the set number of rounds (3, 5 or 7) takes the match.
If a point drags past 45 seconds, overtime starts. Bullets begin to seek their target, and damage climbs every 10 seconds until someone falls.
What's in it
Combat: 130 health, 55 damage per shot (three plain hits kill), a 3-round magazine, and a 4-second block. Movement and bullets are tuned from frame-by-frame measurements of ROUNDS gameplay: low, snappy jumps (about four body-heights, plus a small double jump and wall jumps), and slower bullets that drop hard, so long shots are lobbed. Bullets fly as glowing tracers and throw sparks where they hit. An optional aim guide (off by default: turn it on in Settings) shows the first stretch of your shot's arc. Your own explosions hurt you a little, so don't fire explosive rounds point-blank.
48 cards across gun, bullet, block and body upgrades, each with its own custom icon and animated art. Each card in a hand rolls 55% common, 32% uncommon, 13% rare, so about half of hands show a rare (and three drafts without one guarantee one). Cards stack, so combos multiply. Many change how you move or fight, not just your numbers: Limpet Mines stick to walls and blow half a second later (careful, they hurt you too), Bulwark raises a bullet-stopping wall when you block, Cluster Guard tosses bombs when you block, Frost Guard freezes a nearby rival, Gravity Well bullets drag your rival, Aegis soaks a hit.
21 arenas (ROUNDS-style: chunky platforms, cover blocks, symmetric layouts, every climb within one jump plus an easy double jump) with one-way planks, solid walls, pits, hanging blocks that swing when you shoot or land on them, moving platforms that carry you, and bounce pads. The arena edge is a glowing energy border, not a wall: touching the sides or ceiling hurts and knocks you back (you can wall-jump up the arena's blocks, never up its edges), and falling to the bottom pops you up with damage instead of killing you outright.
ROUNDS-style tricks (from studying a ROUNDS tips guide):
Duck by holding S on the ground: your hitbox squashes flat and head-height shots sail over. In the air, S cuts a jump short or drops you fast.
Grip walls by holding toward them: you barely slide, and your air jump comes back. Jump while holding into the wall to climb it; jump without holding to kick off it.
Perfect parry: a block that meets a bullet in its first 0.12 s sends it back at the shooter (PARRY).
Edge launches: block as you hit the arena border to bounce off it unhurt: off the bottom you launch about halfway up the screen with your air jump back (enough to get onto a platform from any gap; edge-save.js checks every arena), off a side you rocket back in, off the top you spike down.
Hanging blocks hurt: shoot one so it swings hard into your rival to smash them; block to knock it back.
New cards: Echo Guard (every block fires a second free block a beat later) and (blocking dashes you along your aim and rams your rival). Long Haul bullets now grow as they fly, like ROUNDS' Grow.
Project layout
src/counterpick.html the game: one self-contained file (HTML, CSS, JS)
vercel.json serves the repo as a static site on Vercel (cuued.com)
api/turn.js Vercel function: optional Cloudflare TURN credentials for online play
scripts/relay-smoke.js live check of the public MQTT relays (not in CI)
scripts/turn-check.js live check of the Cloudflare TURN relay: real authenticated allocation (not in CI)
scripts/balance.js CPU-vs-CPU win rate of every card on its own (not in CI)
scripts/pacing.js full CPU matches: point length, close matches, comebacks, overtime (not in CI)
scripts/browser-online.js a real online match between two headless Chrome windows over PeerJS: bots play with every block and
movement card; checks errors, fps, snapshot rate and host/visitor agreement on both screens (not in CI)
scripts/browser-mobile.js emulates a phone held sideways with real touch events: sticks, buttons, layout, rotate prompt (not in CI)
scripts/reach.js drives a fighter over every arena (with a 12% jump-height margin) and fails if any platform, wall top,
hanging block or moving platform can't be reached from both spawns (not in CI: ~80 s)
scripts/stress.js long CPU matches with big random card stacks: crashes, NaN, runaway bullets, stalls (not in CI)
scripts/browser-perf.js headless Chrome frame times at 1x and 2x pixel density, up to hundreds of bullets (not in CI)
scripts/browser-flow.js headless Chrome walkthrough: name, menu, draft, pause, settings, and a real online match on the live site (not in CI)
index.html generated from src/ by scripts/build.js; adds the standalone page shell
scripts/build.js node scripts/build.js [--check]
tests/ headless test suite (plain Node, no dependencies)
Edit src/counterpick.html, then run node scripts/build.js to regenerate index.html. CI fails if you forget.
Tests
node tests/run-all.js
The tests load the game headlessly with a stubbed DOM and canvas, then check:
audit.js plays CPU vs CPU and fails if a bullet spawns past a wall, a bullet tunnels through one, a body ends a frame inside a wall, a point stalls, or the game throws.
ceiling.js fires shots up out of every arena and fails unless each one flies out over the top edge and falls back in (the energy border stops fighters, not bullets).
net.js loads two copies of the game, connects them through a mock PeerJS with 40 ms latency, and has bots play a full online match. It runs at 40 ms and on a laggy, jittery link (90 ms ± 45 ms, like a cross-country connection), each also in a features mode where both fighters carry every block/movement card and the bots duck, parry, dash and launch off edges while both screens draw, and fails if the guest's own fighter ever ends up inside or passes through a wall. It checks joining by code, drafting on both screens, input relay, view sync, rematch, a full room, a wrong code and disconnects. It also plays a relayed match through a mock MQTT broker (which parses the real packets) with the direct link blocked and the first broker down.
movement.js runs the same input timeline at 30, 60, 120, 144 and 240 fps and checks that run speed, jump height, tap-jump height and long-jump distance match. Physics advances in fixed 1/120 s steps on every device.
points.js checks scoring (the first death decides the point; there are no trades, so every point goes to Orange or Blue) and the arena edges (damage and knock-back, a small pop off the bottom, no clinging to edges).
stacking.js fires every card stacked 1, 3, 6 and 9 times, plus 300 random mixed decks, and fails if a deck shoots nothing, its bullets don't fly, or anything goes NaN.
cpu-reach.js checks that the CPU can get to you and land a hit on every arena, from either spawn.
combos.js checks that you and the CPU can wall-jump up the arena's blocks but never climb its edges, that your own explosions hurt you a little, that Sandbox loadouts apply with no drafting, that stacked cards keep both effects (Limpet Mines with Splinter Rounds, double Ricochet), that the block cards work (Cluster Guard bombs, Frost Guard freeze, Double Guard's second charge), and that blocking into the bottom edge saves you and launches you high.
card-art.js renders all 44 card animations and icons for 12 seconds each.
links a Big Screen host and up to four phone controllers through the mock PeerJS (40 ms, and 90 ms ± 45 ms): phones become controllers, the lobby seats them, an at-once draft, a press reaching the screen within a few frames, Find me, a whole free-for-all, a phone leaving and getting its own seat back, a late friend taking the CPU's seat (or a new fighter when there are no CPUs), the result on every phone, back to the lobby, and Teams 2v2.
GitHub Actions runs the same suite on every push (.github/workflows/test.yml). The tests need Node 20 or newer.
Credits
Game design inspired by ROUNDS (Landfall, 2021). ROUNDS is a trademark of Landfall. This project isn't affiliated with or endorsed by Landfall.
License
CUUED is free software under the GNU General Public License v3.0. You can use, study, share and modify it. Anything you distribute that's based on it must also be released under GPL-3.0, with its source code.
Blink Guard teleports you can see: the body collapses into a ring where it was, a streak and fading afterimages trace the jump, and it pops out with a flash and a rising zip. Online, the other fighter vanishes and reappears instead of sliding, and the joining player's own blink happens instantly on its screen (aimed and timed exactly as the host will make it, and never undone by a snapshot that's a step behind on it).
Hit feedback: a white hit marker flashes on your crosshair when your shot lands (the joining player too), and the screen edges pulse red when you're under 35% health.
Shots fly past the border: the glowing arena edge stops fighters, not bullets. A shot over the top keeps flying and falls back in (a small arrow on the top edge tracks it), so high lobs are a real play; a shot out a side is gone.
Movement and hits, tuned for landing shots: fighters take about 0.1 s to reverse on the ground and steer less in the air, so jukes and jumps are readable without lowering top speed, and bullets reach fighters 3 px sooner than they look (walls still see only the core). Against a human-like shooter this lands about a quarter more shots on a dodging target; kills still take three plain hits. Auto-aim (the CPU and the phone tap) solves the arc from the gun's muzzle, so it no longer overshoots at range.
Plays on phones: on a touch screen (held sideways) a stick appears under your left thumb (run, flick up to jump, pull down to duck or drop) and an aim stick under your right, Brawl Stars style (tap to auto-aim and fire, or drag to aim and let go to fire; drag back to the middle to cancel; a tap while the gun is still cycling waits a moment for it instead of being lost, and the aim arc shows only while you drag), with Jump and Block buttons in the side margin. They drive the same inputs as a keyboard and mouse, so online play, Sandbox and every trick work the same. Upright for menus, sideways for fights: the menus, Customize and the lobby are laid out for a phone held normally (one column, big tiles); any part of a match (draft, fight, end screen, Sandbox) shows a "Game time! Turn it sideways" screen while the phone is upright, with a way back to the menu, and where the browser allows it (full screen, or CUUED opened from the home screen) the phone locks sideways for the match and unlocks for the menus (the manifest no longer forces landscape). Add to Home Screen: on a phone, a friendly sheet slides up on the menu (once a visit at most; Maybe later rests it for three days, four times at most): on Android it installs CUUED with one tap through the browser's own install prompt; on iPhone and iPad, which have no such prompt, it shows the steps (Share, Add to Home Screen) next to a little home screen with the CUUED icon hopping into place. An Add to Home Screen button on the menu opens it any time.
Customize (main menu; the coin counter opens it too): your look and the shop in one page, so there's one place for everything. Colour: your shade of orange (worn whenever you play the orange side) and of blue, from eight swatches each plus a deeper/lighter slider; shades stay inside their colour family, are checked when they arrive online, and your opponent sees yours. Hats, Patterns, Trails list everything: tap one you own to wear it, tap any other to try it on (your fighter on the stage wears it), then the big button under the stage buys it (Buy · price, Wear, Wearing, or Need N more). 13 hats (Party Hat 3 up to Wizard Hat 15), 8 patterns (up to an animated Galaxy, 16) and 10 trails (up to Plasma, 18). One hat is a secret: it shows as ??? and only a code unlocks it (the code is checked by its hash, so it isn't in the page as text). It's an original helmet in the game's style: glossy black, flared back, crest, red eyes that follow your aim.
Sandbox card picker in the same rounded panel: each card tile shows its painting (or its icon), name, a short description, type and rarity; tapping one shows its full text and stats in colour.
Pop-ups (pickups, level-ups, PARRY) start above the fighter's name tag, stay below the top HUD and on screen, and stack instead of overlapping.
Bullets are solid rounds like the card art: a warm slug in the shooter's colour tapering back from a white-hot nose, a hot centre line and two thin speed streaks, drawn as a solid thing (not added light) with a small, capped halo and a faint tracer. They feel alive: the streaks shimmer and each round throws off short, sharp sparks that streak back off its flanks (its colour and white-hot flecks); worked out on each screen while drawing (no network), on a per-frame budget so a crowd of shots never floods, and off with reduced motion.
The draft's big fighter is the in-game fighter up close (shaded, tall oval eyes, your colour, pattern and hat), and alive: it breathes, blinks, looks at the card you're on and squints happily at a pick. On a phone every card in a hand is the same height.
Sandbox arenas: the Arena button opens a grid of map pictures (Random first) to pick from.
Range and aim: default shots fly about 8% further than they used to (slightly faster, slightly less drop). The aim guide runs the bullet's own physics step for step (speed, drop, low gravity and fast-bullet twists, Accelerator, homing's lighter drop), and a single-bullet gun fires exactly along it (only spread guns scatter); a test fires real shots and checks they stay on the guide.
Fits every screen: an audit opens every screen (menu, How to play, Settings, lobby, Customize, Sandbox and its arena picker, 1v1 and team drafts, pause, end) at 667x375, 932x430, 1024x768, 1366x768 and 1920x1080 and checks nothing runs off-screen. Upright on a phone, the arena behind the menus zooms in to fill the screen top to bottom and follows the fight. The logo scales with a shorter screen's height, How to play scrolls inside its panel on a small phone, and a team draft's six cards always fit.
Smooth on slow phones: a busy 2v2 with fire-heavy builds holds 60 fps on a phone-sized screen even with the CPU slowed 8x (browser throttling).
Effects made while drawing (colour drops, sparks, blinks) use their own random stream, so how often a screen draws never changes the game's dice.
Stacking stays fun, never broken: every copy of a card still helps, with shrinking returns: bullets grow to about 2.6x, a fan holds 7 pellets, a burst 4 shots, and a gun fires at most 24 pellets a second (copies past a cap add a little damage instead). Bullet glow is capped in size and dims when the air is full of shots. Survival prices rise by 1 for every 3 things you own, so a big build costs more to grow; the shop offers a card at most 3 times (Second Wind and Aegis once).
Living fighters: each body runs on two springs worked out on every screen from what it shows (no extra network traffic): it stretches on a jump, squashes and wobbles on a landing or a hit, leans into a run and overshoots on a turn, breathes at rest, squishes flat against a wall it clings to, and blinks. Legs tuck on the way up and dangle on the way down, the free hand trails the motion, and landings, jumps, runs and hits flick drops of the fighter's own colour.
Points like ROUNDS: a kill shows HALF ORANGE (or the player's name), then ROUND ORANGE, and NAME WINS for the match; below it, a big glowing circle for each side shows how much of the round they have, and the scorer's circle fills in with a wipe (2v2: one circle per team).
Walking: on the ground each foot plants and pushes back, then lifts and swings forward with a bent knee; the body bobs once a step and the free arm swings against the stride. Same physics; worked out on every screen from what it draws, so an online opponent walks too.
End screen: your fighter up close: on a win it hops with its arms up, beaming, under confetti (ribbon: Victory, or New best in Survival); on a loss it slumps, eyes down, with a sad brow and a tear (So close if you lost by a round, else Defeated). Your build is dealt out as cards (icon, kind, rarity, ×2 for copies), stat pills (rounds, cards, streak; Survival: rounds, level, best), coins, the rival's deck, and one warm button: Play again (Rematch online, New run in Survival). A Big Screen shows the winner celebrating.
Arena pieces: platforms are drawn as chipped low-poly rock slabs (shadows are drawn first, so none falls across another platform), bounce pads are spring plates, hanging blocks hang on chains. Crates (Divider, Bunkers, Quarry, and the Canyon, Fortress and Dunes Survival arenas) are cover you can move and break: walk into one to push it (at a slower walk) to put a wall where you need it, shove a rival along with it, or push it off a ledge onto whoever is below (22 damage); three hits crack it apart and leave a small health orb (12% of max health, at least 15) for whoever grabs it first (in Survival, a hero; the CPU goes for it when hurt). The colour rule: bright rock never moves; dark iron-and-timber pieces are physical, and a hollow iron support (a frame with a glowing core) is what you shoot. Rope drops (Gallows, Cutline, Ropeyard) hang a platform under one: shoot it and the platform falls (30 damage to whoever it lands on) and stays as cover. Rigs (Chandelier, Drawbridge, Wrecking Yard, Pulley, Topple, and Foundry for four): a beam on two ropes swings from the one that's left, then falls; raised drawbridges slam down when their rope is shot (and drop away when the hinge is); an iron ball swings across the floor, pushed by shots and blasts, knocking over whoever it meets; two lifts share one rope, so the side with more fighters sinks; a keystone (two hits) brings the blocks above it down, toppling. Collision stays level (exact and in step online) while the drawn piece leans a little as it moves and settles flat at rest; blasts crack supports too; the CPU shoots them over you and lowers a raised bridge to cross. The host decides every break; guests run the same physics and are eased to the host's. Piston's middle walls are 30 px lower, and the CPU climbs a tall wall with its double jump at the top of the first, so it never gets stuck in the middle room. The rig arenas have wide versions for co-op Survival (bridges grow with the gap, so they still meet). Pulley's side ledges sit a double jump off the floor, so they stay reachable after the lifts fall. Everything resets each point and after a lost Survival life. New arenas: Quarry and Cutline. In the Survival shop a card is offered at most 3 times (Second Wind and Aegis once).
CUUED coins & Shop: win a full match to earn coins (2 for an online win or a Hard CPU, 1 for a Normal or Easy CPU; streaks tracked) and spend them in the Shop on hats, body patterns and bullet-trail colours. Cosmetic only, so nobody can buy an edge; your opponent sees what you wear online. Coins are kept in your browser.
Survival (solo): an endless run, like CoD Zombies crossed with Brotato. Choose a character first: Duelist (+35% damage, -25% health), Tank (+50% health) and Gunslinger (+45% fire rate) from the start; Sprite (fast, an extra air jump) unlocks at round 10 and Juggernaut (every block is a charge) at round 20. Then take one free pick from a hand of three cards and two power-ups, and fight waves: one enemy at first, up to four by round 12, each gaining about one card every two rounds (rare cards from round 10) and a little more health. Twist rounds (from round 6) change the rules for a round: low gravity, fast bullets, frenzy fire or glass (everyone takes double damage). Only the start is free: after the first pick every card and power-up is bought with scrap in the shop (power-ups: one slot, two from round 8). Damage and kills earn XP; each level adds +5% health and +4% damage on the spot (up to level 15). Power-up capsules drop only here: Health (+40%), Overcharge (double damage, 8 s), Rapid fire (6 s), Bubble (blocks the next hit) or Wild card (a random card until the round ends). Clearing a round pays scrap for the shop: 5 + 0.6 per round, +1.5 per kill, +4 for no damage taken, +12 for a boss (+5 for an elite), +3 teamwork in co-op when both heroes finish alive (all x the character's bonus, and x0.85 each in co-op), plus interest: 1 per 12 scrap you saved, up to 4, so holding scrap for a big buy pays; the HUD shows each card once with a x2, x3 tag for copies, which is the draft's own animated card fan with a price under each of five cards: buy any (click or 1-5), lock one to keep it for the next shop, reroll (R, costs more each time), or sell a card from your build for half price. Big arenas: from round 4 most rounds (and every round from 8) fight on Canyon, Fortress, Skyline, Dunes, Ruins or Mesa, arenas bigger than the screen, built at the screen's own 16:9 (2240 x 1260) and shown whole, like a ROUNDS map: nothing cut off, no bars, no panning (phones zoom out to 0.8 and follow you gently). Co-op plays every standard arena at 1920 x 1080. The reshape spreads platforms sideways (pillars and springs keep their width and stay flush with the edges they touch) and adds sky on top, so no jump gets taller. An arrow on the screen's edge points to anyone out of view. Duel and CPU matches keep the classic one-screen arenas. 3 lives for the whole run; your best round is saved per character and the run pays CUUED coins. survival.js plays whole runs with a CPU stand-in; drops.js checks each capsule.
The round wheel: from round 7, every five rounds go wave → swarm (one more enemy, each 20% weaker, one card fewer) → twist (low gravity, fast bullets, frenzy or glass) → elite (one fewer enemy, led by a gold Elite with two extra cards and ×1.55 health, +5 scrap) → boss. Bosses scale: +8% health per boss round before them (+20% each time the six repeat), one extra themed card from round 20 and two from round 45; the banner shows the boss's tier.
Quick guide: the first time you pick a name (that name is your account on this device: coins, look and best Survival run are saved to it), a five-page guide shows what the menu buttons do, how to host, join and use a Big Screen, the controls (touch or keyboard, whichever you're on) and how card drafts work. Swipe, Next or the arrow keys turn the pages; Skip or Esc closes it for good; How to play > Quick guide replays it. Joining from a link goes straight into the game and shows the guide when you first land on the menu.
Phones: the page is locked (no scrolling or rubber-banding; overlays scroll inside themselves only when they must), the game is sized to the visible screen and re-fits when the browser bars or the orientation change, phones default to Thumb room: the arena sits between two darkened side lanes where the move stick, Jump and Block live, so thumbs rest beside the fight instead of on it (it costs only a few percent of size on a modern phone); Settings > Phone view > Fill screen zooms the standard arenas in up to 20% to run edge to edge instead (the camera follows you; arrows point to anyone off screen), the first touch in a game goes full screen where the browser allows it (Android; on iPhone, Add to Home Screen), and the card picks and the shop fit one row on any phone held sideways.
Logo, share card and app icon: the menu logo (logo.webp), the link preview (og-card.jpg) and the home-screen icons (icon-192.png, icon-512.png) were also made with Higgsfield; the menu falls back to the lettered logo if the image can't load.
Card art: every card and character has a painted illustration (cards/*.webp, about 20 KB each, made with Higgsfield in the game's own style). On the card it drifts slowly, a light sweeps across and sparks in the card's colour rise, faster when hovered; images load on demand and warm up in idle time, and a card whose image is missing falls back to its drawn animation.
Chat (online duels and Co-op): the speech-bubble button next to Pause (or Enter on a keyboard) opens a message bar with quick messages (GG, Nice shot!, Rematch?, 😂, 🔥). Messages show for a few seconds at the bottom left (top left on phones), 120 characters at most, one every 0.7 s, always as plain text. Settings > Chat turns it off both ways. browser-online.js checks it between two real browsers.
Party · 2v2 (Multiplayer, online; and Single player > Party vs CPUs): two against two on five party arenas built for it (1920 x 1080, shown whole on any screen, phones too, two spawns per side, every level a jump apart and nothing stacked awkwardly under anything else), each with its own character: Plaza (wide open, long sightlines: long-range builds shine), Islands (shores over pits, islands, pads to high ledges: mobility and lobs), Gearworks (close quarters, a pit room with one lift: bouncing and blasting shots), Ropeyard (three rope drops, crates, a middle plank: patient, tactical play), Spire (hold the central tower: arcing and homing shots dig you out). In the lobby everyone, the host too, picks Team Orange (orange and light apricot fighters) or Team Blue (blue and light sky blue), so teammates read as one colour, and in a party name tags are tinted in the team's colour; two to a team, a friend who joins lands on the team with fewer players, and CPUs fill any empty seat at Start. Teammates' shots pass through each other. A team takes the point when the other is wiped out (the first side to fall loses: no trades); two points win the round. The losing pair draft together: one fan of six cards, three in each player's colour, in two clear sections headed by each picker's fighter and name ("You" on your own), both picks shown on every screen. CPU players go by names (Bolt, Pip, Rook, Nova), and teammates are shades of one colour (orange and gold, blue and light blue). A friend who leaves mid-match sits out and the rest play on (in a rematch their seat is a CPU). Online, the host runs the match and keeps a link to each friend: their own input queue, their own snapshot clock (60 a second on a direct link, 20 through the relay) and their own effects queue; each screen predicts its own fighter and draws the other three from the host's snapshots. Tested with four screens on a simulated network: every screen draws the others within about 2.5 px of the host's path, with judder near 1.3% (40 ms and 90 ms + jitter).
On a game website: CUUED runs inside another site's <iframe> (a Friv-style portal such as Flick). node scripts/build.js --portal packages it as dist/portal/cuued/ (+ .zip): the game page, card art, icons, a 480x360 thumbnail, a 1200x630 banner and game.json (title, description, tags, orientation, iframe settings, events). Inside a frame, invite links and Big Screen QR codes point at the site's page (?share=), ?join= joins a room, the site hears ready / gameplayStart / gameplayStop / gameOver and can send pause / resume / mute / unmute / config; arrow keys never scroll the site's page and the Add to Home Screen prompt is left to the site. The relay credentials endpoint answers extra sites listed in the PORTAL_ORIGINS env var. Full guide: PORTAL.md; scripts/browser-portal.js checks it all in real browsers with a stand-in site.
Spawns and smoothness (audited):spawns.js places every fighter in every arena and mode (a duel, a 3- or 4-player free-for-all, 2v2, Survival with 1 to 4 heroes and its waves) and checks nobody starts in a wall, over a pit, on top of another fighter or falls out. Survival spots are chosen from fixed floor under the whole body (never something that swings, falls or breaks), sized for big fighters, enemies apart from each other and well away from every hero; extra heroes start beside the first. versus.js measures how smoothly every screen (the host's too) draws the other fighters. Every phone screen keeps clear of the notch / camera island on either side.
Multiplayer = Versus or Co-op Survival. Each asks where everyone is: Own screens (everyone on their own phone or computer) or One big screen (this computer is the TV, phones are controllers); a smaller row joins any room with a code. Versus on own screens opens the same kind of lobby as a Big Screen: the code, a QR code and a Copy/Share invite link, the players (the host first, friends as they join, up to 4) and the rules: Mode (Free-for-all or Teams 2v2), rounds to win, card picks, CPU players, CPU skill, arena and health. Two players and no CPUs is a duel; a free-for-all of 2 to 4 is scored per fighter, everyone drafts on their own screen; Teams fill empty seats with CPUs; one player can start against a CPU. versus.js plays all four through the mock PeerJS.
Customize on a phone, and stacked hats: held upright, Customize puts your fighter big at the top (drawn larger on its stage everywhere), what you've picked with its Buy / Wear button beside it, then the tabs and a grid that scrolls on its own. Hats stack: one per spot on the head (on top, ears or horns, a headband, a halo; up to four), tapping one you own puts it on or takes it off, a hat for a spot you've filled swaps in, and a halo floats higher over a tall hat. Everyone sees your stack online and on a Big Screen.
Invite links are cuued.com/?join=CODE (old #CODE links still work, and a link opened in a tab where CUUED is already running joins too); on a phone, Share link opens the share sheet.
Survival, later rounds: the swarm slot in the five-round wheel takes turns with waves (three waves of a few raiders each, the next storming in about a second after the last is down, +2 scrap per wave). From round 10 raiders can carry a trait (Armored, Swift, Venomous, Bouncy, Blinking, Vampiric: a name, an aura colour and a matching card), more often as the run goes on; enemy health climbs faster after round 12; and every beaten boss drops boss loot, one of three rare cards free for every hero before the shop. Revives: a downed co-op hero has a window (12 s, then 10, then 8 in the same round) for a teammate to stand in the circle for 2 s; if nobody does, they're out until the round ends. The circle is filled, ringed in their colour, with a clock running round it (red near the end) and the countdown above. CPU packs spread out: each keeps its own range, every other one flanks round the far side, a crowded one steps away, and one stuck against a moving piece tries the other way.
Co-op Survival on a Big Screen (Multiplayer > Co-op Survival > One big screen, or Mode: Co-op Survival in the Big Screen lobby): the laptop or TV runs the run and 1 to 4 phones are the heroes, each its own colour. Every phone chooses its character and free card on its own screen while the big screen shows a column per hero (who is still choosing, their build); between rounds every phone gets its own shop (five full-size offers to swipe through: tap to buy, Lock, Reroll, tap a build card twice to sell it for half, then Ready), and the next round starts when every phone is ready. The phone's top bar shows the round, the team's lives, its scrap and level; a downed hero's phone says how long until they're back (or that a teammate is reviving them). Waves grow with the team (one more enemy per extra hero from round 4, tougher bosses). A friend who scans the code mid-run joins at the next round, as levelled and built as the hero with the least; a phone that leaves is out (nobody waits on it) and gets its hero back when it rejoins. When the lives run out every phone gets its CUUED coins and its own record (which unlocks characters), and the screen offers a New run or Back to lobby. bigsurv.js plays it through the mock PeerJS; scripts/browser-bigsurv.js with real browsers.
Big Screen (a way to play Duel or Party: Multiplayer > Duel a friend or Party · 2v2 > One big screen; phones use Multiplayer > Join with a code, which joins any room): a laptop or TV is the screen everyone watches and has no fighter of its own; 1 to 4 friends join on their phones by scanning the QR code in the lobby (or typing the 4-letter code at cuued.com), and each phone becomes a controller. The phone draws no game: it shows your colour and name, your health bar, the score, Find me (rings and an arrow over your fighter on the screen, also shown at every point's start), the same twin-stick controls as phone play (left thumb moves and flicks up to jump; right thumb Brawl Stars style: drag to aim with the aim guide drawn on the big screen, let go to fire, a quick tap auto-fires at the nearest rival), big Jump and Block panels down the right edge (Block fills as it cools down), vibration on hits, being knocked out and winning a point (Android), and keeps the screen awake. The lobby on the screen has the seats (with each phone's ping) and the game settings as tiles you step through either way (‹ ›, the arrow keys, or a click): Free-for-all (four colours: orange, blue, green, pink) or Teams 2v2 (phones choose their team), rounds to win, who picks cards after a round (the losers, everyone, or nobody), CPU players (none by default; the host adds 1 to 3, and their skill), arena (all party arenas or one), health (75 / 100 / 150%) and round twists. Card drafts happen on the phones, everyone at once (25 s; a card is picked for anyone who doesn't), five full-size cards in a row you swipe through (dots show where you are; tap one to pick it): the screen shows a face-down card per picker that ticks as each locks in, then flips them face up one by one so the room sees every pick. A friend who joins mid-match takes a CPU's seat, or gets a new fighter from the next point (with as many cards as the player with the fewest); a phone that drops and comes back gets its own seat back. The screen can pause (the phones say so), rematch, or go back to the lobby to change settings with everyone still connected. Each phone sends its controls on the unordered fast channel on every touch and every frame (presses, shots and taps as running counters, so a lost packet loses nothing), and the host applies the newest one the moment it arrives, with no buffer; the phone gets a small status 15 times a second instead of game snapshots.
Co-op Survival (Multiplayer, online): the same run with a friend. The host's room is opened from Multiplayer > Co-op Survival; the friend joins with the code or link like a duel. The host runs the whole fight (every enemy, boss telegraphs, capsules, twists, strikes) and sends it to the guest; each hero has their own character, XP, levels, scrap and shop, and picks on their own screen at the same time (the next round starts when both are done). A downed hero comes back after 8, 12, then 16 s, or at once when the partner stands within reach of the spot for 2 s; both down at once costs a life. Raiders are 30% tougher and from round 4 there is one more of them; bosses are 75% tougher (they face two guns). Co-op fights on wider arenas: every standard arena 35% wider (except Springs, Piston and Chimes, built for their width) plus the big ones, and each boss's own arena widened, so two heroes and a boss have room; solo keeps the standard arenas, which are built for one-on-one. coop.js plays runs over the mock network (40 ms, and 90 ms with jitter).
Bosses (in Survival, every 5th round): a boss has a signature deck, more health and size, and a special attack it always warns you about. The Sniper tracks you with a red laser for 0.85 s (still for the last 0.2 s), then fires a fast, heavy shot, and stands still while aiming; The Brute crouches and shakes with a warning lane for 0.75 s, then charges: jump it, block it at the last moment to stagger it, or bait it into a wall or the arena edge (a staggered boss takes 50% more damage). At 66% and 33% health it slams down new cards. The Bombardier (Bunkers) marks red circles on the ground for 1 s, then shells them. The Phantom (Archipelago) shimmers for 0.7 s, then blinks behind you. The Warden (Crossroads) raises a dome for 2.4 s that reflects your shots, then is left tired and open. The order of the five is shuffled each run; every 25th round is the Crown (Towers), which uses the snipe, then the charge, then the barrage as its health falls.
Sandbox: hand yourself or the CPU any cards you like, as many copies as you want, and try them out with no drafting. Press Tab mid-fight to change cards; the CPU can fight back, dodge without shooting, or stand still as a target, and you can lock the arena.
Instant shots for the joining player: their bullet leaves the gun the moment they click (a predicted bullet with the same speed, drop, walls and bounces), and the host fires the real one on the exact same physics step; when it arrives it takes over the predicted bullet's place and trail. Every bullet on the joining player's screen is placed on the host's clock and glides between updates.
Shots leave the gun on both screens: the joining player draws the host's fighter a moment in the past, so the host's bullets are drawn on that same timeline when fired and ease onto the present over 0.2 s (so they still meet the joining player where it really is); snapshots carry each bullet's age so it stays hidden until the moment it was fired. Median distance from the host's gun when a shot first appears: 24 -> 20 px at 40 ms, 44 -> 10 px at 90 ms. The fighters themselves are always a network trip apart between the two screens (40-80 px while running), as in any online shooter; lag compensation makes shots count against what each player sees.
Smooth on any screen: physics runs in fixed 1/120 s steps, and both screens draw every stepped thing (fighters, moving platforms) at the exact point between its last two steps, so 60, 120 and 144 Hz screens and uneven frame times never show 1-, 2- or 3-step jumps. Snapshots are stamped with the time their physics step represents (not the frame that sent them), and the joining player's playback clock eases toward the host's instead of jumping when a packet is early or late. A stalled frame (up to 0.25 s) is caught up rather than dropped, so neither side's simulation falls behind. net.js scores drawn movement on both screens under realistic timing (NET_REAL: 144 Hz screens with jitter; a 30 fps visitor with 150 ms stalls).
Switching tabs mid-match: browsers pause a hidden tab's game, so each side tells the other ("…'s game is in the background"), and a visitor who switches away stands still on the host instead of repeating its last input.
Fair hits for the joining player (lag compensation): the joining player sees the host's fighter a moment in the past, so each of its input steps says which moment it was showing, and the host checks its shots against where the host's fighter was then (up to 300 ms back). A shot that hits on the joining player's screen hits. In the bots' online test the joining player went from landing about half as many hits as the host to about the same.
60 updates a second: on a direct or TURN link the host sends 60 snapshots a second (about 26 KB/s) instead of 30, and the joining player's smoothing buffer drops from 55 to 38 ms, so the host's fighter, bullets, health and hits show up sooner and move more smoothly. A relayed match stays at 20.
No lag spikes from lost packets: PeerJS's data channel is fully reliable and ordered, so on real Wi-Fi one lost packet froze every later snapshot until it was resent (about a quarter second), then everything arrived at once. Snapshots, the visitor's inputs and pings now ride a second, lossy and unordered WebRTC channel (negotiated on the same connection, no extra signalling): a lost one is skipped and the next arrives 16-33 ms later. Picks, results and effects stay reliable. The visitor also draws the other fighter a little further in the past when snapshots arrive unevenly, so it always has a real position to glide to. net.js runs with 2% packet loss to keep it that way.
Smooth online play: the joining player's game runs hanging blocks and moving platforms itself and steers toward the host's, and draws the other fighter a few milliseconds in the past along its true path, so nothing shakes between network updates. net.js checks both.
Online 1v1 by room code or invite link, peer-to-peer, with no accounts.
CPU opponent at three skill levels. It reacts to where you were a moment ago, swings its aim at a human speed, crosses gaps, climbs walls and blocks.
Original music and sound, generated live with the Web Audio API: a looping minor-key groove that ducks during card picks, with a surprise drum fill now and then (a snare roll, a hi-hat stutter or an off-beat snare groove at the end of a phrase, landing on a light splash: about one every 40 s, a little more often on a boss, a last life or match point), and effects with a room reverb so impacts boom. Music and volume settings; no audio files.
The sky tells the score: each match starts at night and moves toward dawn as the leader nears the winning score. The last round is announced as FINAL ROUND.
Reduced-motion support: screen shake and flashes are toned down when the OS asks for less motion.
bigscreen.js
drill.js checks that Ghost Rounds drill like Drill Ammo (through a thin wall or a thick block, stopped by very thick rock and by a block, deeper with two copies, at a reload cost) and that Survival opponents pick up power-ups.
rigs.js drives every rig arena: each support breaks and its piece reacts (beam swings then falls, bridges come down and meet then drop away, the ball swings then drops, lifts trade height under a fighter then fall, a keystone brings its stack down sideways), a falling beam hurts, a fighter walks across the lowered bridges, nothing ends up inside a block (also through a minute of CPU play on each), and a guest fed the host's snapshots 90 ms late shows every piece where the host has it.
audio.js plays every sound effect into a mock Web Audio graph and checks its parameters.