The canonical Laige game shape in its smallest complete form (PRD §13,
NFR-13.5): one component (PlayerPos), one system (MovePlayer), one
entity (the player), deterministic by default. It is the reference
pattern for AI agents and new developers: every mark, declaration,
registration, and tick primitive a Laige game uses appears here, in
order.
| File | Role |
|---|---|
hello.laige |
The project manifest (provisional M1 format: laige.project v1 JSON. The asset-pipeline format it replaces is owned by M3-ASSET-01). |
config.json |
The canonical sample config in the engine's declarative JSON surface (the version 1 schema, M1-CFG-01 — laige-run's format). The values are identical to the config embedded in hello.cpp (see "Config" below). |
hello.cpp |
The game source. |
hello-baseline.cpp |
The --expect baseline check (M1-DET-04) — separate TU so hello.cpp stays inside the PRD §9.4 line budget. |
hello-fp32.cpp |
The float_pinned_32 build variant (M1-DET-04, ADR 0002): defines the backend macros and includes hello.cpp — same game source, backend swapped by the build. |
baselines/ |
The committed per-tick hash-stream baselines, one per backend (M1-DET-04; baselines/README.md). |
LICENSE |
Per-sample license (ADR 0001: each sample ships its own license; MIT, matching the repository). |
The sample builds with the repository (no separate configure):
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -jThe binaries land at the source-tree path samples/hello/bin/
(a RUNTIME_OUTPUT_DIRECTORY override — the CI detcheck job invokes
them from the repository root without arguments). Run them from
anywhere. Note the path is shared across build trees: building a second
tree (e.g. build-asan/) overwrites it, so rebuild the tree you intend
to run before executing the binary.
./samples/hello/bin/hello # 300 ticks, no replay (fixed_point_16_16)
./samples/hello/bin/hello --replay LOG # 300 ticks + record LOG (debug builds)
./samples/hello/bin/hello --log LOG # replay LOG, print its hash stream
./samples/hello/bin/hello --expect BASELINE # compare the stream against a baseline
./samples/hello/bin/hello-fp32 # the same game on float_pinned_32- stdout carries exactly the hash stream — nothing else:
N+1lines of<tick> <hash>(tick0= the initial state, then one line per completed tick; the hash is 16 lowercase hex digits, theWorld::stateHashvalue — the detcheck scenario contract, docs/api/detcheck.md).Nis 300 (the M1 CI scenario length) or, in--logmode, the log's frame count. - stderr carries the one-line summary (
hello headless ticks=300 status=ok/hello replay ticks=300 status=ok) and diagnostics. - Exit codes:
0ok;1a run failure (a tick failed, a replay write failed, the log finalization failed, or — with--expect— a per-tick identity mismatch with the baseline, with the first-divergence report on stderr);2usage, IO, or replay-identity error (nothing was run), including a baseline read/contract failure (hello: baseline: ...). --replayand--logare mutually exclusive;--expectcomposes with either run form; an unknown option or a missing option value is a usage error (2).
The player starts at the box center (0, 0) and moves 1 unit per tick
along the diagonal, wrapping in a 32-unit box (−16..16 on each axis): a
full crossing is 32 ticks (~0.53 s at 60 Hz). The step is exact in
fpx16_16 (ADR 0002): one step crosses at most one wrap boundary.
The file reads top to bottom in the order a game is built:
- The component —
PlayerPos(the player's 2D position), marked withLAIGE_COMPONENTandLAIGE_DETERMINISM_SAFE(FR-1.2, G-R8). - The system —
MovePlayer, declared withLAIGE_SYSTEM(MovePlayer, 1)(1 ms budget, FR-1.3) and declaredWriteaccess onPlayerPos. - The run —
mainshows the setup phase (world creation, component and system registration, entity creation + component placement, the pre-run system schedule), the tick loop (onebeginFrame()+ onerunSystems()per tick — therunReplayshape), and the ordered teardown (CONC-006: finalize the replay log only on success, clear the world, shut down the logging facade).
The tick loop uses the engine's own tick primitives rather than
laige::Engine::run_headless: the detcheck contract needs a
per-completed-tick state hash on stdout, and the M1 Engine owns its own
loop and exposes no per-tick hook. The Engine remains the runner for
the built-in (no-game) scenario (laige-run); this template is the
runner for a game scenario, and laige-replay's scope note
(docs/api/replay.md) documents the split.
The canonical sample config is embedded in hello.cpp (the
aggregate at the top of main): 60 Hz, scene budget 8, the engine
churn default (256), the house seed 0x1F055EED
(docs/testing.md), deterministic — the
backend field is fixed_point_16_16 in hello and
float_pinned_32 in the hello-fp32 build variant (the
LAIGE_HELLO_BACKEND_ID macro, ADR 0002: the backend is part of the
replay identity, so each binary's stream belongs to exactly one
backend). config.json is the declarative record of the same values
in the engine's JSON surface (the version 1 schema: "version": 1
plus the same keys — docs/api/config.md)
— it is what laige-run --headless samples/hello/config.json
consumes and what the M1-DET-04 baseline tooling reads; the template
binary keeps its CLI minimal (CORE-004) and does not re-read it.
--replay LOG records the run's replay log (zero-length M1 frames —
no input system exists yet) through laige::ReplayRecorder
(docs/api/replay.md); the log's identity is
the world's component schema + the config (seed, tick rate, math
backend, config hash — ADR 0002). Recording is a debug-build
feature (matching the engine's startReplayRecording policy): a
Release (NDEBUG) binary rejects --replay with exit 2.
--log LOG replays the log on a freshly set-up world: it loads the
log, checks the replay identity against this (world, config) — a
mismatch is a rejected replay (2), never a silent divergence — and
re-runs the log's frame count through the same tick loop, printing the
replayed hash stream. The replayed stream is bit-identical to the
original (determinism, S-7): the hello_replay CTest test asserts
byte-identity between the recorded and replayed runs.
Note: laige-replay (the engine tool) replays only logs recorded by
laige-run (the engine's built-in registrations); a game scenario's
log is replayed by the scenario's own binary — which is why this
template carries the --log mode.
--expect BASELINE compares the run's per-tick hash stream against a
committed baseline with the laige-replay --expect contract (the
scenario's own binary carries the check because a game log cannot be
replayed by laige-replay — the replay identity, ADR 0002):
exit 0 identity, exit 1 first-divergence report (or stream-
length mismatch) on stderr, exit 2 baseline read/contract error
before any tick is run. The committed baselines
(baselines/) are the 301-line streams of the
reference build (canonical Debug g++), one per backend;
fixed_point_16_16 must be reproduced by every conforming build,
float_pinned_32 is same-build/same-ISA (a desynced build is declared
unsupported — never re-baselined).
In CI: every P0 OS job's ctest runs hello_baseline_fpx /
hello_baseline_fp32 (this job's own build vs the baselines, both
backends) plus the failure fixtures, and the merge detcheck job adds
the two-configuration pairs (g++ vs clang++ Debug, Debug+ASan vs
Release, both backends, via laige-detcheck --run-a/--run-b). Scope
and results:
docs/benchmarks/determinism-matrix.md.
ctest --test-dir build -R '^hello' (registered in tests/sample):
| Test | Checks |
|---|---|
hello_scenario |
The CI scenario: no-arg run, exit 0, exactly 301 hash lines on stdout, ticks=300 status=ok on stderr. |
hello_replay |
Record (--replay) then replay (--log): exit 0, 301 hash lines, and the replayed stdout byte-identical to the original. |
hello_replay_missing |
--log on a missing log: exit 2, the replay error on stderr. |
hello_replay_malformed |
--log on a corrupt log (fixture): exit 2, the replay error on stderr. |
hello_mode_exclusive |
--replay + --log together: exit 2. |
hello_usage_unknown |
Unknown option: exit 2. |
hello_usage_missing_value |
A dangling option: exit 2. |
hello_config_valid |
config.json parses cleanly on the engine's config surface (laige-run --headless consumes it). |
hello_line_budget |
The game-code budget (PRD §9.4): non-comment, non-blank lines of hello.cpp < 100. |
hello_baseline_fpx |
M1-DET-04: this build's hello --expect the committed fixed_point_16_16 baseline: exit 0, 301 hash lines — per-tick identity vs the reference build on every P0 OS job. |
hello_baseline_fp32 |
M1-DET-04: the same assertion for hello-fp32 vs the float_pinned_32 baseline. |
hello_baseline_mismatch |
M1-DET-04: a derived baseline with one flipped hash: exit 1, the laige-replay --expect first-divergence report. |
hello_baseline_truncated |
M1-DET-04: a derived baseline cut to 160 lines: exit 1, the stream-length-mismatch report. |
hello_baseline_malformed |
M1-DET-04: a derived baseline with a bad hash token: exit 2 before any tick is run (the load-time contract, CORE-008). |
hello_baseline_missing |
M1-DET-04: a missing baseline file: exit 2, the actionable read error. |
The game source is budgeted at < 100 lines of code (counted as
non-blank, non-comment lines of hello.cpp; the hello_line_budget
test enforces it). The file's comments carry the NFR-13.5
"heavily commented" requirement and do not count against the budget.