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…
- Define a record type and build values of it
- Read fields with dot notation
- Produce updated copies with
{ p with … }— the functional update - Explain why the original record never changes, and why games love that
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.
{ 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.
{ p with hp = … } — only hp changes.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.
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.
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
- A record bundles named fields into one typed value; literals must fill every field, in any order.
- Read with a dot:
p.hp. OCaml infers the record type from the field names. { p with … }is the key move: a new record, changed fields named, everything else copied. The original is untouched — always.mincaps,maxfloors — perfect for hp arithmetic.- Phase-2 law: the world is an immutable value; a tick computes a new world.
Next: values that are choices — directions, events, maybes —
and the match that tames them all.