Records: The Shape of Game State

Your hero is more than a number — a name, hit points, a position. Records give that bundle one shape, and one tiny keyword teaches you how game worlds change… without changing anything.

You will be able to…

One hero, one value

In Phase 1 your game state was a couple of loose numbers — an x here, a vx there, each in its own let. That works for one bouncing ball. It collapses for a hero who carries a name, hit points and a position that must travel together. OCaml's answer is the record: a fixed set of named fields, declared once as a type. Same drill as Phase 1 — Run to play, Check to earn XP.

The type line is the blueprint: every player has exactly these four fields, each with its own type. The { … } literal builds one, and it must fill in every field — though the order is free, because fields go by name, not by position. Reading is the dot you know from other languages. In the toplevel the conversation looks like this:

Notice you never told OCaml that p is a player — the field names alone identified the type. Your first job: a spawner, so the game can mint fresh heroes on demand.

Write make_player : string -> player. A freshly spawned hero has the given name, a full 100 hp, and stands at the origin: x = 0, y = 0. Keep the type player definition — the tests use it too.

Only one number in the starter is wrong. Which field describes a hero at full health?
{ name = n; hp = 100; x = 0; y = 0 } — the function's argument fills the name field.

Break it on purpose: the missing field

What if you forget a field? Don't guess — run it and read the answer like a detective:

The compiler refuses: Some record fields are undefined: strength. It even names the field you forgot. No half-brewed potions leave the workshop — this bug is caught while you type, not at 2 a.m. inside a boss fight. Fix it by giving the potion a strength, then Run again.

The key move: { p with … }

Now the move this whole phase stands on. Your hero drinks a potion, takes a hit, walks east — surely we change p.hp? No. In our game worlds, records never change. Instead you describe a new record that differs only in the fields you name:

{ p with hp = 62 } reads "a player like p, except hp is 62". Every field you don't mention is copied over verbatim. Both records exist afterwards: after_battle at 62 hp, and p — untouched — at 100. This is the law of Phase 2: the game world is an immutable value, and every tick computes a new world from the old one. No global mutable state, no "wait, who changed my hp?" mysteries. When something is wrong, you can hold the before-world in one hand and the after-world in the other and compare them.

Write heal : int -> player -> player. It returns a new player with amount more hp — but never above 100. min is your ceiling.

The shape is { p with hp = … } — only hp changes.
"Add, but never past 100" is min 100 (p.hp + amount).

Write hit : int -> player -> player: a new player with amount less hp, floored at 0 — heroes don't have negative blood. max is your floor. The tests also verify that the player you were given is left exactly as it was: that part is automatic if you use with — and impossible to get wrong, because nothing here mutates.

Mirror of heal: subtract instead of add, floor instead of ceiling.
max 0 (p.hp - amount) picks whichever is larger — the floor or the result.

One with can update several fields at once — separate them with semicolons: { p with x = 7; y = 3 }. That's all you need to make the hero walk:

Write move_by : int -> int -> player -> player. move_by dx dy p is a new player shifted by dx and dy; name and hp ride along unchanged.

New x is p.x + dx; new y is p.y + dy.
{ p with x = p.x + dx; y = p.y + dy } — both fields in one update.

Records or tuples?

You met tuples like (3, 4) in Phase 1, and next lesson uses them for tiny throwaway pairs. The rule of thumb: a tuple is fine for a value that lives for one expression — a coordinate delta, a pair returned and immediately taken apart. The moment data survives across function calls, give it a record: named fields document themselves, can't be mixed up by position (was slot 3 the x or the y?), and with-updates need names to work their magic.

Checkpoint quiz

Loot collected

Next: values that are choices — directions, events, maybes — and the match that tames them all.