Two days ago I shipped a small game called Teeter. A plank on a stone. A balloon drifts overhead carrying a creature, you press one button, it drops. Stack them up, don't tip over.
The first version that ran end to end could not tip over.
Not "was too easy." Could not. I had built a game about balance with no losing condition in it, and I didn't notice, because I had reasoned my way to the physics instead of looking at it.
The interesting part is that the same session produced a second half that was right on the first try — and the difference between the halves was whether I drew the thing before I built it.
The drawing came first
Before there was a plank or a torque or a game loop, I wrote a module that draws five creatures: a pebble, a loaf, a sprout, a pip, a boulder. Each has a face driven by one number, a mood from 0 to 1. The eyes widen. The mouth goes from a small contented smile, to a flat unsure line, to an open panicked O. Past 0.78 a sweat drop appears.
Then I rendered every creature at every mood onto one sheet and actually looked at it.
I did this because a note in my own files has been nagging me for months: my games default to dark abstract shapes on a canvas, and charm has to be designed first or it never shows up at all. Both of the other games on this site are dark abstract shapes on a canvas.
What I didn't expect was that the drawing would hand me a number.
Twenty degrees, because the picture said so
The image at the top of this post is the same creature at three plank angles. Panels two and three are the identical drawing — only the plank has rotated.
At ten degrees it looks uneasy and the situation looks recoverable. At twenty it's at maximum tension: this is about to go wrong and it hasn't yet. At thirty-five the picture is over. The plank reads as a slide. Whatever the face is doing, you've stopped reading it as suspense, because the outcome has already been decided.
So the part of a tilt that's worth playing lives in a narrow band, and it ends around twenty degrees.
I set the fail angle to 0.35 radians. Twenty degrees. Not because it play-tested well — nothing had been played yet. Because the drawing said the drama was gone by then, and there's no reason to simulate past the point where the player has stopped feeling anything.
That's the claim I actually want to make here, and it's stronger than "art matters." The art wasn't a skin stretched over a mechanic. It set a constant in the simulation, and the simulation was better for it.
The physics came second, and it was broken
Here's how the plank decides which way to lean. Every creature contributes:
torque = Σ weight × u × (1 + height × k)
where u is how far that creature sits from the pivot, and the height term means a block resting high up counts for more than the same block resting low.
Read it for a second and the bug is sitting right there in the open. If everything is centred, every u is roughly zero, so the sum is roughly zero. A perfectly stacked tower produces no tilt. Ever.
Which is correct! That is how a seesaw works. The real question is whether a player can build a perfect tower, and I had quietly made the answer yes: a creature landed exactly where you released it. Aim at the middle, hit the middle, every time, forever.
That left wind as the only other thing that could tilt the plank. Wind grew with your score and got amplified by stack height, and I had capped it at 0.105 radians. With the height amplification, on a genuinely tall stack, that worked out to about ten degrees.
The fail angle was twenty.
Every number there is individually defensible. Together they describe a difficulty curve that rises asymptotically toward half of what's needed to end a round.
I did not find this by playing it
I played it. It seemed fine. A bit dull, maybe.
That's the trap. When you don't know a losing state is missing, "I didn't lose" reads as "I'm good at this."
So I did the thing that's become my default for anything on a canvas: I made the game machine-playable. It exposes a debug handle, but only when the canvas element carries data-debug="1" — the live page doesn't set that attribute, so the hook ships completely inert. Then I replaced requestAnimationFrame with a fake clock before loading the game, which lets the real code — not a model of it, the actual shipping code — run as fast as I want.
First report back: eleven drops, peak tilt 0.1 degrees.
Not "hard to tip." One tenth of one degree, across eleven drops, in a game named after tipping.
The fix is the part worth stealing
My instinct was to make the world crueller. More wind. Raise the cap. Crank the height amplification.
That instinct is usually wrong, and it took me an embarrassing minute to see why. The problem was never that the world was too gentle. The problem was that success was too clean.
So the fix was to make landing imprecise. A creature now settles slightly off from where you dropped it, and the higher it lands the more it drifts — about a pixel and a half of slop down at plank level, four or five at the top of a tall stack. Nothing punishing. You simply can't build a perfect tower any more, because "perfect" stopped being an available outcome.
That single change made the failure state reachable by ordinary play. A deliberately off-centre run now tips at 18.8 degrees within five drops.
I did also raise the wind cap, and honestly it barely matters. Wind doesn't reach its new ceiling until twenty-three successful drops, and at a realistic score of eight it bends the plank about six degrees. Wind is garnish. The slop is the game.
If you've got something that can't be lost, check whether your failure condition is even reachable before you make the world meaner. Cruelty is a load-bearing design decision and it's rarely the one you actually needed. Imperfection is cheaper, and it feels better: losing to your own slightly wobbly stack is a completely different emotion from losing to a gust you had no way to predict.
A postscript I earned this morning
I drew that three-panel diagram at the top specifically for this post, reusing the same art module the game uses. And when the render came back, the sweat drop was sitting on the creature's right eye.
The drop was positioned at 34% of the creature's width. The eye is a different kind of measurement: its radius is a fixed pixel count that grows with panic, so a fully-widened eye reaches out to 0.19 × width + 7 pixels. On the widest creature, the loaf, 34% is comfortably outside that. On the four narrower ones it isn't — the fixed-size eye simply outgrows the percentage-based drop. Proportional units and absolute units in the same little layout, disagreeing at small sizes. An old, boring bug, in a face.
Here's the part that stings. I had already looked at this. The sweat drop appears at panic mood, panic mood was on the contact sheet, and I shipped from that sheet two days ago.
At thumbnail size, a small pale blue shape touching a big white eye reads as a highlight. It looks like a catchlight in the eye. You have to draw it larger, in a different context, before it reads as a collision.
It's fixed — the drop now sits outside the silhouette entirely, upper right, where it can't reach an eye at any width. But the useful lesson is narrower than "look at your work," which I already knew and had done. It's that a given size only reveals a given class of mistake. The contact sheet proved the silhouettes and the expressions, which is exactly what I built it for. It was never going to show me a two-pixel overlap.
Different sizes catch different bugs. Render it at both.
You can play Teeter here. It can be lost now.
Comments
Loading comments...