60 seconds to save the wedding. Hold to walk. Move to balance.
___________
'._==_==_=_.' .------------------------------------.
.-\: /-. | |
| (|:. 1st |) | | * W I N N E R * |
'-|:. |-' | |
\::. / | TINKERHUB KOCHI |
'::. .' | DEVIN EVENT - 1ST PRIZE |
) ( | |
_.' '._ | 7 TIERS . 60 SECONDS . 0 DROPPED |
'-------' '------------------------------------'
🏆 1st prize winner at the TinkerHub x Devin event in Kochi 🏆
Seven tiers, sixty seconds, and not a single cake dropped.
You are a nervous waiter carrying a 7-tier wedding cake from the kitchen to the cake table across a chaotic reception. The best man's toast happens at 0:00. If the cake isn't on the table by then, or if it falls, the wedding is ruined.
Made for the "60 Seconds to Save It" game jam (Night Out with Devin): one minute, one level, two inputs, a clear win or loss, and a cake that almost falls a lot but rarely actually does.
Play: https://cake-walk-two.vercel.app
Both inputs act on the same horizontal axis: speeding up tips the cake back, stopping tips it forward. Slide the tray under the lean.
| Desktop | Mobile | |
|---|---|---|
| Walk | Hold Space / W / ↑ or the left mouse button |
Touch and hold the right half of the screen |
| Balance | Move the mouse left/right, or A/D / ←/→ |
Tilt the phone (tap Enable tilt controls), or drag on the left half |
| Retry | R |
Retry button |
| Pause | P / Esc (also automatic when the tab loses focus) |
❚❚ button |
| Mute | M |
♪ button |
| Fullscreen | F |
⛶ button |
Stop inside the glowing zone in front of the cake table, slow and upright, and hold still for half a second to set the cake down.
Pick a world on the title screen with the three square picture buttons under the menu (or
← / → on a keyboard). The choice is remembered, and ?world=pirate or ?world=space links
straight to it.
- Kitchen doors - calm tutorial zone with floating hints.
- Champagne spill - slippery floor: stopping takes ~3x longer and you get random slips.
- Dancing Uncle - hip-bumps into your path on a (seeded) rhythm. Wait for the gap or power through.
- Roomba - patrols an ellipse; running into it makes you hop.
- Toddler Dash - once you cross the trigger a toddler sprints across, aiming at you. Stop in time!
- Grandma with a walker - blocks the lane for a few seconds. Patience, dear.
- Bass Drop - at 20 s remaining the DJ counts 3-2-1 and drops the bass (wherever you are).
- Bouquet Toss - the bouquet lands on the top tier, raising the centre of mass.
- Conga Line - dancers cross the lane in sequence; each one that overlaps bumps you.
- Cake Table - stop, hold still, deliver.
The same 60-second walk across two ships lashed together at sunset, to the captain's table.
- Galley - calm tutorial zone.
- Rolling deck - the ship rocks on the swell the whole way, nudging the cake one way, then the other. Watch the horizon tilt.
- Loose cannonballs - roll from rail to rail with the swell. Running into one makes you hop.
- Swinging rum barrel - swings from a crane at cake height and clips the top tiers (a shorter cake ducks under it).
- Walk the plank - a springy gangplank over shark water. Every step bounces it; cross slowly or the bounces throw the cake about.
- The captain's parrot - perches on the top tier, flaps about, then flies off.
- Kraken - a tentacle bursts through the deck and blocks the way, then slams the deck as it sinks. Wait at a respectful distance.
- Rogue wave - at about 20 s left the lookout rings 3-2-1 and a wave breaks over the rail, leaving the deck slippery for a few seconds.
- Captain's table - stop, hold still, deliver.
An astronaut is marrying an alien on the observation deck. Carry the cake there from the docking bay, past windows full of planets, while the station does its best to stop you.
- Docking bay - calm tutorial zone.
- Moving walkway - a conveyor belt that speeds you up. Getting on and off jolts the tray, so step on gently.
- Laser security gate - a beam that blinks on and off. Get zapped and you hop. Watch the warning flicker and time your crossing.
- Gravity glitch - the gravity generator cycles normal, 30%, normal, heavy. In low gravity the cake barely feels the tray; in heavy gravity every lean falls twice as fast. The sign overhead and the floor colour tell you which is coming.
- Teleporter pad - beams you (and the cake) further down the corridor in one jump. The cake arrives a little shaken.
- Flying saucer - parks outside and points its tractor beam at the cake, tugging it side to side and making it lighter. Keep walking.
- Meteor shower - at about 42 s left the klaxon counts 3-2-1, meteors pound the hull from alternating sides, and the last one punches a hole that sucks the cake towards space until the shutters slam. (The saucer flees as soon as the alarm starts.)
- Observation deck - stop, hold still, deliver.
Every obstacle telegraphs itself about a second ahead with a ! bubble or animation.
- Daily Challenge - everyone gets the same variation for today's date (
YYYY-MM-DD). - Random - a random seed every round.
?seed=anythingforces a seed (great for replaying a friend's run).
The layout is fixed; the seed varies the uncle's rhythm, the Roomba's route, the toddler's delay, grandma's patience, the bass-drop direction, the bouquet's landing offset and the conga timing. On the ship it varies the swell, where the cannonballs, barrel, plank and kraken sit, the barrel's swing, how long the parrot stays and when the wave breaks. On the station it varies the walkway's length and speed, the laser's rhythm, the gravity cycle, how far the teleporter throws you, the saucer's sway and the meteors' count, timing and first side.
-
Win: cake set down before 0:00 with at least 4 tiers.
-
Lose: the tower topples ("The cake has left the building."), fewer than 4 tiers remain ("That's not a wedding cake. That's a cupcake."), or time runs out ("The best man toasted an empty table.").
-
Every try scores, win or lose, with no ceiling. The score adds up:
- Distance up to 3,000 for how far you got, and Cargo 500 per tier carried the whole way (a tier lost halfway keeps half).
- Delivery on a win: 2,000 + 1,000 per tier delivered, plus Pace of 100 per second left (a loss earns a little pace too, scaled by how far it got).
- Poise up to 2,000 for a level cake (halved at an average lean of 6°), Clutch 250 per save (recovering from a lean past 30° without toppling), minus Bumps for every hit (up to 200 by strength) and spill skid (40).
So two runs that end at the same spot still score differently. The parts are on the results card; the formula is
src/score/formula.tsand its constants live intuning.ts. -
Grades: S = 7 tiers and ≥ 15 s left, A = 6+, B = 5, C = 4, F = loss.
-
Personal best: one per world (Daily Challenge and Random share it), kept in IndexedDB with the player's name and id. The title shows it next to a link to the scoreboard.
-
Scoreboard: top 10 per world, this week (ISO week, UTC) and all time. Each player gets a random name (e.g.
WobblyOtter42) that they can change under Controls or on the scoreboard. See Scoreboard for how it is stored. -
Share sends a score card (the final frame plus the score) through the Web Share API, copies it to the clipboard on desktop, or saves it, along with text like
🎂 CAKE WALK — Daily Challenge 2026-09-30 — 19,880 pts — Grade S — 7/7 tiers — 17.3s left.
Requires Node 20+.
npm install
npm run dev # http://localhost:5173| Script | What it does |
|---|---|
npm run dev |
Vite dev server |
npm run build |
Type-check and build to dist/ |
npm run preview |
Serve the production build |
npm test |
Vitest unit tests + the 30-seed headless bot run |
npm run lint |
ESLint + Prettier check |
npm run typecheck |
tsc --noEmit (strict) |
npm run e2e |
Playwright smoke test against the built site (run npx playwright install chromium once) |
?debug=1- overlay with FPS, θ, ω, L, aTray, per-tier slides, hitboxes, block line and the seed. Keys1-9,0skip to each zone.?bot=1- the autopilot plays the real round (handy for demos and recording GIFs).- Every gameplay constant lives in
src/game/tuning.ts, commented.
- Vite + strict TypeScript + Canvas 2D. No engine, no framework, no runtime dependencies. The whole game is ~30 KB gzipped.
- Fixed 120 Hz simulation with an accumulator; rendering every animation frame; frame dt
clamped to 100 ms. The back buffer matches the window's aspect ratio, so the game fills every
screen: landscape shows the full 270 px scene height and a wider slice of the hall, portrait
shows 300 px across with more wall above and floor below. Integer-scaled when that is close
to the ideal fit,
imageSmoothingEnabled = false, devicePixelRatio aware. - Physics (
src/physics/, pure and unit-tested): the cake is an inverted pendulum on the tray,θ'' = (G/L)·sinθ − (aTray/L)·cosθ − DAMP·ω − GLUE·θ + impulses, withLrecomputed from the tiers still standing (so losing tiers makes the cake twitchier). Each tier slides on the one below with a damped spring; past 45 % of the lower tier's width it falls off along with everything above it. - Input (
src/input/) reduces keyboard, mouse, touch and gyro into oneIntent { walk, trayTarget }per tick - the simulation never sees raw input. - World (
src/game/World.ts) is completely headless, so the autopilot (src/bot/autopilot.ts, a PD controller on the lean plus predictive hazard gating) can play it in tests and in the title-screen attract mode. - Art is procedural pixel art: string-array pixel maps plus palettes rendered to offscreen canvases at load. The cake is drawn upright offscreen and rotated with nearest-neighbour sampling for a crunchy pixel look.
- Audio is synthesized with the Web Audio API (no files): footsteps, a creak that tracks the lean, thumps, splats, crowd gasps, the bass drop, a fanfare, a sad trombone and a chiptune "Here Comes the Bride" loop that speeds up in the final 10 seconds.
The feel was tuned with headless parameter sweeps against controllers with simulated human reaction delays:
| Constant | Value | Why |
|---|---|---|
G / L_SCALE |
135 / 2.4 | Full cake L ≈ 61 px, G/L ≈ 2.2: an unbalanced full-throttle start topples in ~2.3 s |
DAMP |
1.0 | Low on purpose - high damping makes slow tray corrections useless |
GLUE / GLUE_ZONE |
9 / 8.5° | Frosting "catch net": small wobbles self-correct, big ones don't |
ARM_COUPLING |
0.5 | Half the walking jolt reaches the tray |
TRAY_SPEED / RESPONSE |
60 px/s / 7 | Smooth tray = forgiving of late, jerky corrections |
SLIDE_GAIN |
4.1 | Top tier slides off after a sustained ~22° lean |
TOPPLE_ANGLE |
40° | Whole cake falls |
A naive "hold walk and ignore the tray" run always topples; a tray controller with a 0.3 s reaction delay still wins. The perfect bot wins all 30 test seeds with 7 tiers in 35.5-37.8 s (22+ s to spare).
- Unit (Vitest): pendulum stability at rest, toppling past the limit, tiers above a fallen tier are removed with it, seeded RNG determinism, outcomes (win/topple/cupcake/timeout), scoring parts and grades, scoreboard sanity checks against real rounds, the scoreboard API (against an in-memory Redis), the IndexedDB profile, share text, timer formatting.
- Bot: the autopilot runs the full simulation headlessly on 30 seeds and must win each with ≥ 5 tiers in under 55 s. If it can't, re-tune the level, not the bot.
- Smoke (Playwright): loads the built site, starts a game, holds walk for 3 s, and asserts no console errors and a decreasing timer.
- CI: GitHub Actions runs install, lint, typecheck, test and build on every push and PR.
The repo deploys with zero configuration beyond importing it. vercel.json pins the Vite preset,
npm ci, npm run build, output dist, SPA-safe rewrites and immutable long-cache headers for
/assets/*.
- Go to https://vercel.com/new and import the
cake-walkGitHub repository. - Vercel detects Vite; leave the defaults (build
npm run build, outputdist). - Click Deploy. Every push to
mainthen deploys to production, and PRs get previews.
Or from the CLI:
npx vercel@latest # first run links the project
npx vercel@latest --prodapi/scores.ts is a Vercel Function backed by a free Upstash Redis
database. Until the database is connected it answers 503 and the game simply hides the
scoreboard; personal bests always work offline.
One-time setup: in the Vercel project open Storage → Create Database → Upstash for Redis
(free plan) and connect it to the project. That adds KV_REST_API_URL and KV_REST_API_TOKEN
(plain UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN work too). Redeploy.
- Only the top 100 per board are kept (
cw:all:<world>andcw:week:<week>:<world>sorted sets, plus acw:nameshash). Weekly boards expire 40 days after their last write. - Players are stored by a SHA-256 tag of a secret id that stays in their browser.
- The server range-checks the round stats, recomputes the score from them (a hand-edited score is
refused), ignores runs faster than the fastest plausible win, and limits each IP to 20 writes
a minute. It can't stop a determined cheater; remove a bad entry with
ZREMin the Upstash console. - Autopilot (
?bot=1) and debug (?debug=1) rounds are never recorded.
index.html carries the Open Graph and Twitter tags that WhatsApp, iMessage, Slack, Discord and
X read for link previews, plus a canonical URL and VideoGame structured data for search engines.
public/ holds the share image (og-image.png, 1200×630), icons, site.webmanifest,
robots.txt and sitemap.xml. These use the absolute production URL
https://cake-walk-two.vercel.app; if the domain changes, update it in index.html,
robots.txt and sitemap.xml.
