The Tick: Pure Game Loops

Sixty times a second, a game computes the next world from the current one. Make that computation a pure function and you get testing, replays and multiplayer almost for free.

You will be able to…

A game is a film

A running game is a flip-book: still frames, shown fast. Between two frames, something must answer one question — given the world as it was, what is the world now? In Camel Quest that answer is always a pure function we call the tick: step : float -> world -> world. The float is dt, the time since the last frame in seconds — about 0.016 at 60 fps, exactly the delta time you met with Anim.start in lesson 1.6. Multiply speeds by dt and they become "pixels per second": frames may come fast or slow, your ball flies at the same speed. Here's a whole world and its tick — stepped by hand, no animation yet:

A quarter-second per tick for easy mental maths: 100 pixels per second × 0.25 s = 25 pixels per tick. And look at what didn't happen: w0 still reads {x = 0.; …}. Three photographs, not one repainted canvas. The film strip of your game is a chain of immutable worlds — step just makes the next photo.

Break it on purpose: ints vs floats, again

Positions and speeds are floats (motion between pixels must be smooth), so the dotted operators are back. Swap one out and read the complaint:

The compiler aims straight at it: the field access w.x has type float but an expression was expected of type int — because bare + only adds ints. Since lesson 1.1 this rule hasn't changed; by now it's catching your physics bugs. The fix is one dot: +.

Bounce: a whole game rule in one function

Time to write a real tick. A ball drifts along x; the court runs from wall 0. to wall 380. (that's the canvas width minus the ball's 20-pixel width). The rule, precisely:

Implement step_ball : float -> world -> world following the spec above, exactly. -. w.vx is the flipped speed. The tests probe mid-flight, both walls, and a frozen dt = 0.

Compute x' once with let x' = … in, then decide with if/else if/else.
The wall cases build full records; the glide case is a with update of x only.
Mind the dots: +. and *. everywhere — and 0., not 0.

Watch it fly: Game.run

Your tick is tested — now let's see it. The Gamekit's game loop wants three things: init, the first world; step, your pure tick; and draw, which turns a world into pixels. Game.run ~init ~step ~draw wires them into the flip-book: draw the world, compute the next one, repeat. All the impurity — the actual painting — is quarantined in draw; the rules of your game never touch a pixel. Press Run and tweak: the speed, the colours, the ball size…

Keep the paddle on the table

Next ingredient for any pong-like: a paddle that follows input but can't leave the court. "Pull a value back inside an interval" is called clamping, and it's two old friends from lesson 2.1 stacked:

Write clamp : float -> float -> float -> float so that clamp lo hi v is v pulled inside [lo, hi]: too small snaps to lo, too big snaps to hi, inside passes through. (A 60-wide paddle on our 400-wide canvas lives at clamp 0. 340. x.)

min hi v handles "too big". What handles "too small"?
Stack them: max lo (min hi v).

Determinism: the replay superpower

Why the fuss about pure? A pure function computes its result from its arguments and does nothing else — no reading globals, no Random, no peeking at the clock, no printing. So step dt w is deterministic: same arguments, same new world. Every time. On every machine. Feel what that buys a game:

A global mutable score would wreck all four: step's output would depend on hidden state, and "same inputs" would no longer promise "same game". That's the real reason this phase banned globals — not ideology, engineering. Prove yours behaves:

A cloud scrolls forever across the sky. Write scroll : float -> world -> world: move x' = w.x +. w.vx *. dt; if x' >= 400. wrap around to x' -. 400. (one wrap is plenty — callers keep vx *. dt small); vx never changes. The tests call your function twice with identical arguments and demand identical worlds — the determinism check.

Same skeleton as step_ball, but the wall teleports instead of bouncing.
Watch the boundary: the spec says >=, so landing exactly on 400. wraps to 0.

Checkpoint quiz

Loot collected

Everything is on the workbench: records, variants, lists, the tick. Next lesson we bolt them together into a real, playable snake.