MaestroFor agentsDesign systemSource

A worked example: a racing career

A career mode: drivers, cars you buy and upgrade, circuits, races, championships that run over several rounds, and licences that gate what you may enter. The transcript beside this page — genres/racing.json — is the example as an ordered list of calls, applied to a real game by a test on every build.

The vocabulary is not the product. The reasoning is. A career game with teams, sponsors and a repair budget will declare different types and answer the same four questions.

The entity types it declares:

driver car upgrade circuit race championship licence

And four relation types: contains, takes_place_in, requires, unlocks.

The hard problem: two edges that both sound like "belongs to"

A championship contains races. A race needs a licence. In English both are "belongs to", and an agent that models them with one edge type produces a game where neither question can be asked.

They are different because they are walked differently:

The direction rule decides the spelling. contains runs championship → race, requires runs race → licence, and both read source-to-target as written. A query walking contains outward from a championship gets its calendar; a query walking requires outward from a race gets its entry conditions. Neither walk can be recovered from the other, which is the practical reason they are two types rather than one with a flag.

The round number lives on the edge

Round three of the Club Series is not a property of the race and not a property of the championship. The same race can be round two of one season and round five of another, and a round field on the race would hold one of those numbers and lose the other.

So round is a field on contains, which is law 2 doing exactly what it is for. It is also why the same edge type joins a car to its fitted upgrades with no round at all: an edge field that does not apply to every edge of the type is left unset rather than invented.

requires and unlocks are one gate seen from two ends

A licence gates the races above it; finishing a championship grants the next licence. Those are the same progression read forwards and backwards, and it is tempting to declare one relation type that means both.

It cannot be one type. A single type declaring both behaviours gates in both directions at once, and that combination is refused by name when you declare it — which is the surface stopping you from writing a graph that walks in circles. Two types, one for each direction:

Edge type Runs Meaning Behaviour
requires race → licence prerequisite prerequisite_of
unlocks championship → licence unlock unlocks

Read that table as one progression. The player wins the Club Series, which unlocks Licence C; National GT requires Licence C. The chain crosses between the two edge types at every step, and that is what a progression is in this model — not one long edge type, but two that alternate.

Drivers hold licences too

requires also runs driver → licence, and that is the same edge type rather than a fifth one because it is the same question: what does this thing need before it can take part. A separate driver_licence type would have meant two walks for "who and what can enter this race".

The endpoint declaration keeps it honest: requires admits race, championship, driver and upgrade at the source end and licence and upgrade at the target end, so an edge from a circuit to a car is turned away at write time rather than sitting in the game as a shape nobody meant.

Upgrades chain to themselves

Stage two of the engine needs stage one. Source and target are both upgrade, which is ordinary: a relation type's endpoints are two lists of type keys and nothing stops the same key appearing in both.

The stage number is a field on the upgrade — it is a property of that part, the way a talent's tier is in genres/mmorpg.md — and the prerequisite is an edge, for the same reason it is there.

Where this differs deliberately

Against genres/mmorpg.md, on how a place is reached. That game's place graph is a graph: connects_to joins zones to zones, and the question it answers is what is adjacent to what. This game's places form no graph at all — a circuit is not next to another circuit, and there is nothing to walk. takes_place_in runs race → circuit and stops.

The consequence is worth stating, because it is the sort of thing a worked example teaches by accident: a game does not need a place graph just because it has places. Declaring connects_to here would have produced an edge type with no edges, which is vocabulary a reader has to learn and cannot use.

Against genres/metroidvania.md, on where progression lives. The metroidvania holds its whole progression in one relation type, with the gate as a field on the crossing. This game splits progression across requires and unlocks because its gates are objects the player holds — a licence is a row, with a grade and a test count, and it is listed in the career screen. A door's required ability is not an object; it is a condition on one crossing.

Where to look next