TinyGames · how it works
A Breakout. Bounce the ball off the paddle, clear the bricks, do not let it past you. Where you hit the paddle decides where the ball goes, so the paddle is an aiming device rather than a wall.
Eight hand-drawn level shapes, and lettered capsules that fall out of broken bricks — the way Popcorn (1988) did it, including the one that takes your power-ups away.
Open index.html. No build step, no dependencies.
breakout/
index.html markup
style.css the frame around the canvas
js/engine.js copied unchanged from Overdrive
js/bricks.js the field, the level pictures and the collision maths
- no pixels, no engine, no DOM
js/breakout.js the playable layer
test-bricks.js node test-bricks.js
bump-version.py stamps version.json, the meta tag and the cache busters
assets/ Kenney Puzzle Pack II, CC0
The dangerous thing in a Breakout is not the bricks, it is the ball. At 430 px/s and 60 Hz it covers about 7 pixels a frame, and bricks are thin. Test collision by asking is the ball inside a brick right now? once a frame and a fast ball will step straight over one. The bug that produces is intermittent, speed-dependent and nearly impossible to reproduce on purpose — the worst kind to find by playing.
So the ball is never tested at a point. Every frame asks a different question:
travelling from A to B, what is the first thing I would touch, and which way do I bounce?
That is Bricks.sweep(). Each brick is grown by the ball's radius so the ball can be
treated as a point, then the movement segment is clipped against that box with the standard
slab test. The axis entered last is the face actually struck, which is what decides
whether vx or vy flips.
The test fires 3,000 fast rays across a full field and cross-checks every one against a deliberately stupid marcher that walks the segment in 20,000 steps:
$ node test-bricks.js
46 passed, 0 failed
Zero tunnelled, and every impact point agreed. A separate case fires a single 200,000 pixel step through the field and still finds the front row.
The shape is the level. A diamond leaves corridors down both sides, an arch traps the ball in the roof, and a solid slab plays nothing like either. So levels are not generated, they are drawn — one string per row, straight in the source, editable by anyone without reading a line of code:
[ "....CC....",
"...1111...",
"..112211..",
".11322311.",
"..112211..",
"...1111...",
"....CC...." ],
. is a gap, 1–9 pick a colour and take one hit, A–Z are the same colours at two
hits. Eight shapes ship: slab, pyramid, X, arch, checkerboard, crown, box, gem. After the
eighth it wraps round and starts hardening one brick in three.
The drawings are tested, not trusted. The court sizes a brick by dividing its width by
Config.COLS, and never looks at the pattern — so a level typed eleven characters wide
would lay its last column outside the right-hand wall, look very nearly right, and be
unclearable. The tests read the level table straight out of breakout.js and assert every
row is exactly ten wide, no level is empty, every character is legal, every colour exists
in the palette, and no two levels are the same shape.
They were then checked against what actually reaches the screen: render each level, sample the canvas at each brick's centre, and rebuild the picture from the pixels. All eight came back identical to what is written above.
That readback also found two HUD elements sitting on top of the field — the launch hint was pinned to 0.63 of the screen height and the level-eight X is eight rows deep, so the hint was printed across the bottom row of bricks. Both are now positioned relative to the paddle, which is below every possible field.
Broken bricks drop a lettered capsule about a fifth of the time. Catch it with the paddle.
| E | Extend | the paddle gets 70% wider |
| L | Laser | hold to fire; each bolt kills one brick |
| C | Catch | the ball sticks to the paddle so you can aim it |
| R | Reset | takes your power-ups away |
| S | Slow | the ball drops to 72% speed |
| D | Disrupt | splits every ball in play into three |
E, L, C and R are Popcorn's own four. S and D are not — they come from the wider Arkanoid family Popcorn was copying — and they are flagged as such in the source rather than quietly passed off as original.
R is the joke that makes the whole system work: because one of the falling capsules is bad, a capsule is never automatically worth chasing, and going for one is a decision rather than a reflex.
E and L run out after 18 seconds. C and the extra balls last until you lose a ball.
Catch was a pause button. C glued the ball to the paddle and never let go, so a
player could hold the ball indefinitely and the game could not end. It is not the kind of
thing you notice while playing — you launch out of habit. It showed up when a six-minute
auto-play soak broke eleven bricks: the ball had spent almost the whole run stuck to
the paddle, because the harness only launched after losing a life. The glue is now on a
2.5-second timer with a bar under the paddle showing it run down, which keeps the aiming
and removes the stalemate.
The court could not use its own edge names. Court extends Entity, and Entity
already defines left, top, right, bottom, width and height as read-only getters
for its hitbox. Assigning this.left throws. Inheriting an engine means inheriting its
property namespace — the fields are now wallL, wallR, ceilY, floor.
The ball locked into a vertical column. Launching straight up off a centred paddle
sends the ball straight back to the middle of the paddle, which sends it straight up again.
It clears one channel of bricks and loops there forever. A 90-second run with a
ball-tracking paddle broke twelve bricks. The launch now always leans a little off the
vertical, and deLoop() enforces a minimum horizontal and vertical component after every
bounce, because both degenerate paths are reachable through ordinary play.
Zero-time collisions stalled the frame. After a bounce the resolver continues with the
time left in the frame: left *= (1 - t). When t is 0 — which happens whenever the ball
is still overlapping the brick it just hit — that leaves left unchanged and the ball
advances nothing, spinning the loop until the iteration guard trips. The fix is two-part:
place the ball clear of the struck brick rather than nudging it a hundredth of a pixel, and
always retire a little time so a frame cannot fail to progress. Stalled frames went from 7
in 600 to 0.
A paddle parked in the corner now loses in 1.3 seconds. An eight-minute soak with a tracking paddle clears eleven levels with no stalled frames, no non-finite positions and no leaked entities.
Adding capsules turned one ball into a list, and every place that assumed the ball became a bug. Reading the code back caught all of these before playing did:
view.paddleW, the base width — so Extend made its own
new ends dead, and the widened paddle could not catch anything the old one would have missed;tick returns early on stuck, so
the two new balls hung motionless until the player launched. It now launches first;deLoop() immediately handed it one back,
walking the ball off the paddle it had just stuck to;hitBrick replaces both this.bricks and this.balls
underneath the loop that is iterating them, so the surviving balls chewed into the level
that had only just been built.Losing a life is now tied to the ball list emptying, not to a ball leaving the court — which is what makes Disrupt worth catching. Three balls are three chances, not three risks.
Bricks.paddleBounce(offset) returns a direction, not a reflection — the incoming angle
is thrown away on purpose. The middle sends the ball straight up, the tips send it out at
1.05 radians. That single decision is what makes a Breakout a game of skill rather than a
game of waiting.
The tests hold it to four properties: it is monotonic from tip to tip so aiming is predictable, always a unit vector so speed belongs to the game and not to where you were hit, always upward, and never so flat that the ball creeps sideways forever.
| Move the pointer | the paddle follows directly |
| Click, or Space | launch |
| Hold the pointer, Space or F | fire the laser |
| ← → or A D | move the paddle |
| Esc | pause |
| R | new game |
The paddle tracks the pointer without needing a press, so a touch screen works by dragging anywhere on the court — and holding fires, because otherwise the laser capsule is keyboard-only and therefore decoration on a phone.
60 × level a brick, +100 a capsule, +1000 for clearing the field. Three balls. Best
score is kept in localStorage.
The same version.json convention as the other three games and the Angular apps, with the
same update check and the same bump-version.py.
Advertisement