{"slug": "throw-hands-a-webcam-fighting-game-where-your-body-is-the-controller", "title": "Throw Hands: a webcam fighting game where your body is the controller", "summary": "A solo developer built Throw Hands, a browser-based 3D fighting game that uses a webcam and MediaPipe Pose Landmarker to map a player's body movements onto an in-game fighter, with no download or controller required. The developer replaced an arm-extension jab detector with elbow-angle peak detection after the original approach missed punches thrown at the lens, and validated the game with automated tests that replay real fighter footage as a fake webcam, logging 2,160 shared ticks with zero state mismatches across two clients. The project, built in roughly 24 hours at HackTX 2026 with two Claude Code agents, also uses Meta's Muse models for AI-generated fighters and Gemini Flash-Lite for live fight commentary.", "body_md": "*This is a submission for the [MLH x DEV Writing Challenge](https://dev.to/mlh-hackathon)*\n\n**Throw Hands** is a 3D fighting game that runs in a browser tab. Your webcam tracks your body, and a fighter in a UFC-style octagon copies your arms, torso and head. Throw a real jab and it jabs; raise your forearms and it blocks; step toward the camera and it walks forward. You can fight a CPU, a friend on the same camera, or friends on other laptops and phones by room code. It's live at [https://throwhands.tech](https://throwhands.tech), with no download, account or controller.\n\nAI shows up in three places. A pose model in the browser (MediaPipe Pose Landmarker) is the controller. Fighter Forge turns a sentence like \"lava golem with a crown\" into a fighter with a name, backstory and card art, using Meta's Muse Spark and Muse Image. Gemini commentates the fight (details below).\n\nI built it solo at HackTX 2026 in about 24 hours, working with two Claude Code agents.\n\nMy first jab detector measured arm extension, the distance from shoulder to wrist. It missed punches thrown straight at the lens. One webcam measures depth poorly: on a labeled clip, a full jab only changed the measured arm length from 0.24 m to 0.34 m.\n\nThe camera measures angles well. The elbow snaps open from about 75° to 120° in a tenth of a second, whatever the depth. Detecting **peaks in elbow angle** took recall on the labeled jab clip from 4 of 5 to 6 of 6.\n\nThe second detector used velocity over a 100 ms window. At 30 fps it worked. At 15 fps, which a phone or a busy laptop delivers, its recall fell to 20%, because a window holding one frame can't give a velocity.\n\nStrikes are now **peaks**: a local maximum of elbow angle (jab), wrist height (uppercut) or sideways sweep (hook) that rose far enough within 350 ms. That costs one frame of latency and behaves the same at 10, 15 and 30 fps. An evaluation tool replays recorded body points at any frame rate, so every change is scored at both 30 and 15 fps.\n\nI couldn't stand in front of a camera all night, so the end-to-end tests use **downloaded video of real people as the webcam**. Chromium's `--use-file-for-fake-video-capture` plays an MJPEG clip as a camera. For WebKit, the test injects a `getUserMedia` that paints a clip into a canvas stream.\n\nThe best test opens two browsers driven by two pro fighters' shadowboxing footage. They create and join a room through the real UI and play a full best-of-3 while each client logs every state snapshot it receives. Comparing the two logs: 2,160 shared ticks, 0 mismatches in health, round or timer.\n\nThe tests found real bugs. Safari never started pose tracking, for three separate reasons:\n\n`<video>` sat inside a `display:none` element, and WebKit won't load a stream into it.\nShadowboxing clips got the gestures working, but my first session at a laptop broke movement: leaning back walked my fighter *forward*. I added a drill mode that shows one prompt at a time (\"stand still\", \"big hook\", \"step back\") and records the body points, with the prompt as the label. Replaying a recording through the same recognizer reproduced each bug before I fixed it.\n\nOne deterministic TypeScript simulation handles health, guard, supers, grappling, cage collisions and KOs. It runs in the browser for local play and on the server for online rooms. Every message carries a fighter slot ID, and bots send the same inputs humans do, so a 1v1 duel, an 8-player Brawl and winner-stays-on are all configurations of one room.\n\nClaude Code made this video too. The implementer agent recorded the gameplay in a headless browser, rendered the motion graphics, generated the voiceover with ElevenLabs and cut it together with ffmpeg. I reviewed every cut and sent back fixes with timestamps.\n\n**Gemini API.** Every 3 to 4 seconds, the server sends a batch of match events (hits, guard breaks, supers, cage slams, KOs, health) to a Gemini Flash-Lite model, which returns one roast or hype line about the fight. In online rooms the server writes one line per room, so every player and spectator sees the same call and the free tier lasts longer. Gemini TTS was supposed to voice the lines, but its free quota ran out during the hackathon. Live lines now appear as captions while a matching pre-recorded announcer roast plays. Next time I'd build the caption fallback first and add the voice on top.\n\n**Meta Muse API.** Fighter Forge sends the player's one-line prompt to Muse Spark (`muse-spark-1.3`), which returns JSON with a name, backstory, three move names and two colors in about 2 seconds. Muse Image (`muse-image-1.0`) then paints the fighter card; setting `reasoning_strength: \"low\"` cut that from about 17 seconds to about 10. The fighter's suit takes the card's dominant color. After a two-player online match, the server makes one rivalry card per match (a trash-talk line from Muse Spark and a fight poster from Muse Image) and sends it to both players. Three things I learned:\n\n`model_not_found` while 1.2 kept working, so text calls fall back to 1.2.\n**Tiger Data.** Hits, blocks and supers go into a `match_events` hypertable, and continuous aggregates compute the Elo leaderboard. Players can opt in to share their body-point coordinates (never video) for a motion-data program, and those streams go into a `pose_streams` hypertable. My first schema stored each frame as an array, and compression only reached 1.7×. Storing one `smallint` column per coordinate let TimescaleDB's columnar compression work: 19.7 MB became 5.9 MB per player-hour, 3.4×.\n\n**Vultr.** One Vultr VM in Dallas, the smallest plan (1 vCPU, 1 GB RAM), serves the game over HTTPS through Caddy. It also runs the WebSocket server and the simulation for online rooms, and it makes the Gemini, Meta and Tiger Data calls so no API key reaches the browser. An 8-browser Brawl test ran against it with 0 mismatches between clients.\n\n**.Tech domain.** \"Throw hands\" is slang for fighting, so the game lives at throwhands.tech.\n\nHackTX 2026 ran October 24 to 25 at the University of Texas at Austin, with about 695 hackers. I entered solo and split the work between two Claude Code agents:\n\nThey talked through files in the public repo: `SPEC.md` for what to build, `REVIEW.md` for my feedback, `STATUS.md` for progress. My job was the part neither agent could do: standing in front of the webcam at 4 AM, throwing hooks that didn't register, and recording drills until they did.\n\nWhat I'll remember most is how bad AI can be at perceiving videos and animations until you review them yourself. In one cut of the demo video, two voiceovers played over each other at 50 seconds; in a later cut, the Shoulder Throw was still animated wrong at 56 to 58 seconds. Both agents got better once I gave them specific pointers, which I passed through the planner agent so it could build a better, more specific prompt, and once the agent reviewing the video looked at it at a finer grain, frame by frame. The game's own visual tests work the same way: scenes are rendered frame by frame and measured (foot slide, fist-to-target distance) before a fresh AI reviewer scores them. Frame-by-frame review is really slow across an entire video, though, so a human still has to step in periodically to review UI, animation and video changes.", "url": "https://wpnews.pro/news/throw-hands-a-webcam-fighting-game-where-your-body-is-the-controller", "canonical_source": "https://dev.to/andrewyin/throw-hands-a-webcam-fighting-game-where-your-body-is-the-controller-56e7", "published_at": "2026-10-26 05:36:44+00:00", "updated_at": "2026-10-26 05:43:24.806083+00:00", "lang": "en", "topics": ["artificial-intelligence", "computer-vision", "ai-agents", "generative-ai", "ai-tools"], "entities": ["Throw Hands", "MediaPipe Pose Landmarker", "Google", "Meta", "Muse Spark", "Muse Image", "Gemini", "Claude Code"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/throw-hands-a-webcam-fighting-game-where-your-body-is-the-controller", "markdown": "https://wpnews.pro/news/throw-hands-a-webcam-fighting-game-where-your-body-is-the-controller.md", "text": "https://wpnews.pro/news/throw-hands-a-webcam-fighting-game-where-your-body-is-the-controller.txt", "jsonld": "https://wpnews.pro/news/throw-hands-a-webcam-fighting-game-where-your-body-is-the-controller.jsonld"}}