tiny.game
Overview · API · Code relationships · Verification · Audit
Overview
The package gives the program that hosts a game what it needs to run the game's simulation in fixed time steps. The package supplies a clock that paces the steps, input for each step, recording and replay of a run, exact comparison of a run's output, control of a headless game from an outside tool, and reloading of the game's code after a rebuild. The game's own code supplies the simulation update. The host calls that update once for each step the clock plans, with input the host reads or drives itself, and a replay calls it with the recorded input.
A host wants each step to cover the same span of simulated time however often it draws the screen. A developer wants to record the input a run received and replay it later, so the same run happens again step for step. A test wants to compare what a run produced with a saved reference and learn the offset of the first byte at which the two differ. A tool or test wants to drive the game headless, advancing it a chosen number of steps at a time and asking for its status in between. A developer who rebuilds the game's code wants the host to pick up the change while it keeps running.
The time between two draws varies, so it rarely comes to a whole number of steps, and the time left over has to count toward a later step. A long pause between draws owes the simulation many steps at once: 100 milliseconds at 60 steps a second owes six. A game can also keep time at a second, slower rate that divides the step rate, for example 20 slower frames a second inside 60 steps a second, and each step then needs to know which slower frame it falls in and its position inside that frame. For a replay to repeat a run, every step has to receive the exact input it received live. Input held as an in-memory struct carries padding bytes, pointers and the machine's byte order, and all three can differ from one run or machine to the next. A tool that drives the game may wait as long as it likes between commands, so a simulation paced by the wall clock would keep running while the tool waits. A compiler writes a library file over a stretch of time, so a host that loads the file at its first change can load a half-written library. Rebuilt code loaded into a running host takes over the game state the old code left in memory, so the rebuilt code has to fit that memory and agree with the host on how the two call each other.
The caller offers the clock (clock.Clock) the nanoseconds since its last draw each time it could draw again, and the clock turns them into whole steps and carries the leftover fraction into the next offer. The clock leaves both the choice of when to draw and the choice of whether to blend between steps to the caller. With the default cap, one offer plans at most four steps, and the caller can set another cap. Each offer returns a plan (clock.Plan) that holds the number of steps to run and counts the whole steps dropped past the cap, so for the 100-millisecond pause the plan holds four steps and counts two dropped. The caller runs every planned step, one call to Clock.next each, before it offers time again, and the clock asserts that order, so a caller that breaks that order panics in Debug and ReleaseSafe builds, and its behavior is undefined in ReleaseFast and ReleaseSmall builds. Each step (clock.Step) carries its index and its length as a fraction, one over the step rate. Each step also carries the slower frame it falls in (source_frame) and its position inside that frame, counted from zero (phase), so at 60 steps and 20 slower frames a second the positions run 0, 1, 2.
The game encodes each step's input as a fixed-size array of bytes (input.Snapshot), and the package records and compares those bytes. A helper for button edges (input.Button) derives whether a button was newly pressed or newly released from whether it was down on the previous step and whether it is down now. A recorder for a run (session.Session) stores each step with its snapshot in storage the caller provides. Recording starts at step zero and takes the steps in order, so for a step out of order append returns NonSequentialStep. Once the storage is full, append returns SessionCapacityExceeded. Session.replay calls the caller's update with each recorded step and snapshot, in order. A replay hands the update only the context the caller passes and the recorded step and snapshot, and it reads no clock of its own. A reader for input scripts (session.inputAt) gives the input for one step from a list of input changes sorted by step: each entry starts at the step in its tick field and holds until the next entry. The fields a caller lists in pulse_fields, such as a reset, are true only on their own entry's step.
An exact byte comparison (golden) reports whether an output matches a saved reference, the offset of the first difference and both lengths. When a shorter output matches the start of a longer one, the first difference is at the shorter output's length. A caller can run the byte comparison first and parse both outputs only when they differ.
For a host driven by commands, Clock.commandStep advances exactly one step and reads no elapsed time. A command parser (dev) reads three commands, each one JSON object per line: step with a count of steps from one to a maximum the caller sets, status and quit. For any other command name, dev.parse returns UnknownCommand and dev.common returns null, so a host can add commands of its own. Only a step command advances the simulation, so a status command leaves it at the step it had reached. A line server (transport) carries those lines over standard input or a Unix socket: it hands each line to an object the caller supplies, with a writer that carries the object's reply back to the sender. When the object asks to quit, the line server stops. A command line holds at most 1 MiB, and the scratch memory the object gets for one line is reset before the next line. Once its socket is listening, the line server prints a JSON listening event with the socket's path on standard output.
A change detector for files (settle) takes a file's size, its modification time and the current time from the caller at each look. After the size and modification time have held still for a window of time since the last change the detector saw, the detector reports that change a single time and then waits for the next change. The detector's first look at a file records a baseline and reports nothing. A reloader (reload.Loader) copies the game's shared library to a new numbered file beside it and opens the copy. The reloader looks at the original file each time the caller polls it, and once the file has held still for 120 milliseconds, the reloader loads a fresh copy. Before a swap, the reloader calls a function the library exports and reads back a record that describes the library (contract), and the reloader checks three fields of that record: the major interface version and the size and alignment of the state the library needs. The caller sets the largest state alignment and a size bound for the library file, and the largest file the reloader accepts is one byte smaller than that bound. The reloader's state capacity (state_capacity) starts at the first library's state size, and a caller can set it, for example to the size of the state memory the host allocated. The reloader refuses a library that exports no contract function, whose contract is newer than the host's or of another major interface version, whose state alignment fails to be a power of two or exceeds the caller's largest, or whose state size is larger than the reloader's state capacity. After a refusal, the old library stays loaded, and the poll's result carries the error. After a swap, the reloader deletes the previous copy, and the poll's result carries the new contract.
The package root re-exports these eight namespaces, so a caller reaches each one by name through the package's import, such as game.clock.
The example creates a clock at 60 steps and 20 slower frames a second, offers it the time since the last draw, and runs each planned step.
const game = @import("game");var clock = try game.clock.Clock.init(.{ .steps_per_second = 60, .source_frames_per_second = 20,});const plan = clock.offerElapsed(elapsed_ns);for (0..plan.steps) |_| { simulation.tick(clock.next());}Definitions
Namespaces
Public namespaces.
clock: A clock that turns the time a host measures between draws into fixed simulation steps and numbers each step it hands out.input: The input a game receives for one step: the edges of a button, and a fixed-size array of bytes that holds the step's input for recording.settle: A detector that reports a change to a file once the file has held still for a window of time.dev: A parser for the commands that drive a headless game: advance some steps, report status, or quit.golden: An exact byte comparison between a saved reference output and a new one.transport: Two servers carry lines holding one command each from an outside tool to a host, over standard input or a Unix socket, and carry each reply back.session: Recording and replay of the input each step of a run received, and a reader that gives the input for any step from a short list of input changes.reload: A loader that runs a game from a shared library and swaps in the rebuilt library while the host keeps running.
Code relationships
Direct static dependencies extracted from parsed source by semantic graph analysis.
Uses: tiny.smg, tiny.sys
Used by: tiny.sdfii
Verification
No verification records are cataloged for this module in this build.
Audit
| Evidence | Value |
|---|---|
| Source | fun/game/src/root.zig |
| Definitions | 8 of 8 documented |
| Members | 0 of 0 documented |
| Public names | 8 API, 54 indexed |
| Version | 26.7.0 |
| Revision | daab053ee433 |