TinyGames · how it works

Deflector

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 part that matters: tunnelling

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 levels are drawings

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, 19 pick a colour and take one hit, AZ 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.


Capsules

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.


Four bugs worth recording

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.

And five the power-ups brought with them

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:

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.


The paddle

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.


Controls

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.


Scoring

60 × level a brick, +100 a capsule, +1000 for clearing the field. Three balls. Best score is kept in localStorage.


Versioning

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.


Things deliberately left undone