Inspiration

I found Ice Cold Beer by accident, and I Love to play it.

It's a Taito cabinet from 1983, mechanical, no screen. Where the monitor should be there's a wooden playfield full of holes, and two joysticks that tilt a metal bar. A ball bearing rolls on the bar. You tip it left and right to walk the ball up to a lit hole without dropping it into a wrong one. That's the whole game. Keith Egging designed it.

That's what got me. Everyone knows Tetris. Nobody knows this thing. And the mechanic is genuinely good, it's all wrist and patience, and you feel every mistake.

Taito re-themed it themselves in 1984 as Zeke's Peak, a mountain climbing version. So I figured re-theming was fair game. Mine is a penguin frozen inside a ball of ice, and you're rolling it up a cave to a glowing hollow before it thaws.

What it does

One board a day. Everyone on the sub gets the same one.

The board is generated from the date, so the holes and the ice are identical for every player on a given day. Five glowing caves, bottom to top. Sink the penguin in the lit one and you move up. Miss and drop it in a fire vent and you're back at the bottom, minus five seconds, clock still running.

Leftover time gets banked. Scoring is two keys: rescues first, banked time breaks ties. So 3/5 always beats 2/5, no matter how fast the 2/5 was. That mattered to me, I didn't want speedrunning to beat actually solving it.

The daily part is the hook. Your 4/5 and my 4/5 are the same 4/5. There's a leaderboard, a countdown to tomorrow's board, and you can drop your result in the comments (one per day, replaying edits the same comment instead of spamming).

The ice

Here's the twist I'm most happy with. The bar isn't a fixed surface, it's ice, and the ice changes daily. Five slipperiness tiers, seeded from the date.

A stiff day, you can place the penguin. A greased day, it slides off the bar if you breathe on it. Same holes, completely different game. Each target gets more seconds on slicker ice and as you climb higher, 30 seconds at the bottom of an easy day, 70 at the top of a nasty one, so a brutal board is still winnable.

How I built it

Devvit Web, canvas 2D, fixed-timestep physics loop. No game engine.

That was on purpose. The whole thing rests on determinism, if the physics drift between two phones, the shared leaderboard is a lie. A fixed timestep with the board generator in a plain TypeScript module that both client and server import means the client draws the board and the server re-derives it to check your score. Neither can drift from the other.

The server owns the clock and the ice tier. A tampered client can't hand itself an easier day or extra banked time.

Leaderboard is one Redis sorted set. I encoded both score keys into one number (rescues × 10,000,000 + bankedMs), so "most rescues, fastest as tiebreak" comes out of a single range query instead of two.

Challenges

The wall climb. For a while you could zig-zag the ball up the rails and skip half the board. I assumed it was tunneling through holes at speed. It wasn't, the ball moves about 4 pixels a frame and the holes are 9 wide. It was geometry: the edge lanes were sparse and the bounces were too short to escape them. Fixing it meant making the bounce distance tilt-aware and going from a 6×7 to a 7×7 grid. I ran 60 days of boards to confirm no open corridors were left.

Mobile controls, twice. First version was drag-to-position: your finger's Y became the bar end's Y. It felt terrible. Jumpy, and laggy for a reason that took me too long to find, I was calling getBoundingClientRect() on every touchmove, which is a hundred-plus layout recalcs a second on a phone. Cached the rect, and then threw the whole scheme out anyway. It's analog sticks now: touch a side, and how far you drag from that point sets the speed. Small drag for placement, big drag for speed. That's the version that actually feels like the two joysticks on the original cabinet.

Frame one. Three separate crashes, all the same shape: the render loop started before the async board fetch finished, so it drew with undefined state. Gradient with a NaN coordinate. toFixed on nothing. I kept patching the symptom until I gave up and just initialized every piece of state at its declaration.

What I'm proud of

That it's actually deterministic. Same day, same board, same physics, any device, and I can prove it, not hope it.That it's a 43-year-old mechanic almost nobody has touched, rather than a clone of something you've already played. And that two thumbs on a phone feels close to two joysticks on a cabinet.

Known limitations

I'd rather say these than have someone find them.

Anti-cheat is sanity checks, not replay. It rejects impossible runs, scores that can't exist, time that couldn't have been banked, runs longer than their own clock, all server-side. But someone with a modified client could submit a plausible lie. The deterministic physics makes full server-side replay the obvious next step; I just didn't have the days for it.

Board solvability is a corridor check, not a real pathfind. Sixty simulated days never produced an unsolvable board. That's evidence, not proof.

What's next

There's a list, and most of it points at the same refactor.

The physics still live inside the render file, measured in pixels, wired to the canvas. That one fact blocks three separate things. Server-side replay validation needs the physics to run without a canvas. A resolution-independent canvas needs them measured in fractions instead of pixels. And the @ts-nocheck on the game loop only exists because the ported code fights the type checker while it's tangled up with rendering. Pull update() out into a pure, resolution-free module and all three open at once. That's the first job.

Replay validation is the one I actually want. Right now the server bounds-checks a submitted score: it rejects runs that couldn't have happened, but it would accept a plausible lie. The fix is to stop sending scores at all. Send the inputs, let the server run the same deterministic loop, and have it compute the score itself. Faking a score becomes identical to actually playing. The architecture already points at it, I just ran out of days.

Then there's the daily loop, which doesn't fully exist yet. Every post is a live window to today's board rather than an archive of its own day. Posts never go stale, which is nice, but it means they can't be honestly numbered and result comments pile up on whichever post people happen to use. Archived per-post boards and a scheduled daily post have to ship together, one without the other just makes a post titled #14 that quietly serves board #15. Together they turn it into a real daily ritual instead of one post that changes underneath you.

Smaller things: a start token so the server measures how long a run took instead of believing the client. Streaks. Weekly boards. Difficulty stats, "you cleared a greased day, only 8% did" is the kind of thing I'd want to see about my own run.

Built with

Devvit Web · TypeScript · Canvas 2D · Redis · a physics loop I wrote by hand and regret nothing about

Built With

Share this project:

Updates

Submission history