3 Commits

Author SHA1 Message Date
75da766724 Record how to find out whether a tutorial teaches
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 21s
libakgl CI Build / performance (push) Failing after 20s
libakgl CI Build / memory_check (push) Failing after 17s
libakgl CI Build / mutation_test (push) Failing after 19s
docs_examples proves every listing in docs/ compiles and still matches the file
it was quoted from. It cannot prove a chapter teaches anything -- a document can
be made entirely of verified excerpts and still be unfollowable, because what a
reader needs is the glue between them.

So AGENTS.md gains "Testing a Tutorial": hand the chapter to a subagent on a
weaker model with the library, its headers and the art but not the finished
program, and make them build and run the game. The weaker model is the point; it
will not paper over a gap with knowledge the document did not give it.

The section carries the rules that make the result mean something -- copy the
tree with examples/ and docs/ removed rather than asking them not to look, let
them read the public headers because a real reader has them, do not make them
hand-author a .tmj, give them a screenshot helper, and verify their binary
yourself -- plus the sandbox recipe, and the warning to prove the build path
works before handing it over.

It also records how to read the report, because a weaker model states its own
mistakes as documentation defects with total confidence. Reproduce before
believing; and a rejected finding usually still leaves something behind.

The evidence for doing any of this is in the table at the end: four read-only
reviews of chapters 20 and 21 found real gaps and missed all three of the
defects the first build-and-run found, including a published example output the
chapter could not produce.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 09:45:28 -04:00
cf0142ea85 Say how close an NPC has to be placed to be talked to
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 21s
libakgl CI Build / performance (push) Failing after 22s
libakgl CI Build / memory_check (push) Failing after 17s
libakgl CI Build / mutation_test (push) Failing after 18s
A reader drew their own town, put the elder 115 pixels from anywhere the player
could stand, and spent the rest of their time looking for the bug in
jrpg_cmhf_talk. There is no bug: the handler finds nobody within TALK_RANGE and
returns success, which is indistinguishable from the game not being wired up.

The chapter stated the constant and not what it means in the editor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 09:36:25 -04:00
6e97d27e49 Fix what showed up when the tutorials were actually built and run
Two readers were given the library, the art, the chapter, and no reference
program, and told to compile and run. Both got a working game. These are the
defects that only came out because a compiler and a frame loop were involved --
four earlier read-only reviews had missed every one.

libakstdlib's header was never named. The chapters used aksl_strncpy,
aksl_snprintf and aksl_atoi throughout and said only "libakstdlib is documented
in deps/libakstdlib"; the obvious guess from the function prefix is <aksl.h> and
it is wrong. That was the only compile error the sidescroller reader hit, and it
was the chapter's fault. Both chapters now carry a per-file include table and say
which header it is.

The sidescroller's asset directory defaulted to ".". The chapter said the code
"falls back to `.`" without showing the #ifndef or that assetdir is initialised
from the macro, so a reader gets a game that only finds its art when launched
from exactly the right directory.

The sidescroller's summary line reported 0.0,0.0 airborne while the screenshot
showed the player standing on the ground. Teardown runs in main's CLEANUP block
and releases the actor pool before the summary prints. The chapter published an
example output a reader could not reproduce; the capture that makes it true is
now shown beside it.

The JRPG's CMake block interpolated ${JRPG_REPO_ROOT}, which is this repository's
own variable and undefined for anybody else -- so the font path came out wrong
and akgl_text_loadfont failed long after everything else had loaded. It is
${CMAKE_CURRENT_SOURCE_DIR} now, and the chapter says why both definitions are
needed rather than one.

Chapter 21 described --demo and a summary line in its build section that no step
wrote. Both are now a section: the script table, playback through
akgl_controller_handle_event rather than the handlers, the fixed clock step, and
printing the position before teardown. It also now says libakgl keeps no frame
counter, which a reader assumed it did.

Both map sections now say to draw it in Tiled and point at chapter 13, naming the
fields the loader needs that Tiled fills in automatically -- both readers
hand-wrote a .tmj before being given one and hit exactly those.

Excerpts across docs/ go from 190 to 198.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 09:34:16 -04:00
4 changed files with 250 additions and 23 deletions

View File

@@ -682,6 +682,101 @@ of a hundred thousand times, and the timings a checked run prints are labelled
as meaningless. **Do not add memory-check suites**: a new path worth checking
belongs in a benchmark, where it gets both.
## Testing a Tutorial
`ctest -R docs_examples` proves every listing in `docs/` still compiles and
still matches the file it was quoted from. It cannot prove the chapter *teaches*
anything: a document can be composed entirely of verified excerpts and still be
unfollowable, because what a reader needs is the glue between them.
**So test a tutorial by making somebody follow it.** Hand it to a subagent on a
weaker model with no context beyond the chapter, and have them build the thing it
describes. The weaker model is the point -- it will not paper over a gap with
knowledge the document did not give it, which is exactly the failure mode a
capable reviewer has.
### The rules that make the result mean something
- **Give them what a real reader has, and nothing more.** The chapter, the
library source, its public headers, and the art. **Not the finished program.**
Copy the tree with `examples/` and `docs/` removed rather than telling them not
to look -- a rule they can break is not a control.
- **Headers are fair game.** The manual defers signatures to Doxygen and a real
user has `include/akgl/*.h` in front of them. Forbidding those tests a reader
who does not exist.
- **They must compile it and run it.** This is the whole thing. A design nobody
built proves nothing, and the defects that matter most do not fail to compile.
- **Verify their work yourself.** Run the binary they produced. Look at the
screenshot they took. Do not take a report's word for what it built --
see "Reading the report" below.
- **Do not make them author assets by hand.** A real reader draws a map in Tiled;
hand-writing a `.tmj` tests nothing about the chapter and eats the whole
session. Give them `docs/tutorials/assets/`. The asset *formats* are already
covered by `docs_examples`, which runs the chapter's own JSON blocks through
`akgl_sprite_load_json` and `akgl_character_load_json`.
- **Give them a screenshot helper**, marked as scaffolding and not part of the
tutorial, so there is a picture to check. Screen output is evidence; an exit
status of 0 is not.
### Setting the sandbox up
One directory per reader, because two of them building at once in the same tree
collide:
```sh
rsync -a --exclude='.git' --exclude='build' --exclude='examples' --exclude='docs' \
. "$SB/reader/libakgl/"
cp docs/20-tutorial-sidescroller.md "$SB/reader/TUTORIAL.md"
cp -r docs/tutorials/assets/sidescroller "$SB/reader/art"
mkdir -p "$SB/reader/game"
```
A consumer using the CMake the chapter itself teaches --
`add_subdirectory(../libakgl libakgl)` plus the documented
`target_link_libraries` line -- configures and builds in about a minute from
cold, and seconds after that. Tell them to use
`cmake --build build --target <theirs> --parallel`, or they will also build
every test suite in the tree.
**Prove the path works before you hand it over.** Write a throwaway consumer
that opens a window and takes a screenshot, build it, run it, and delete it. A
reader who cannot build is a reader who finds nothing, and the fault will be
yours -- the screenshot helper's first draft included `akgl/renderer.h` for
`akgl_renderer`, which is declared in `akgl/game.h`, and would have cost them
the session.
### Reading the report
**Every finding is a hypothesis until you check it.** A weaker model reports its
own mistakes as documentation defects with total confidence, and both kinds are
worth having -- but only one is worth acting on.
- Reproduce the failure before believing it. One reader reported the dialogue
freeze broken; it had drawn its own map with the NPC out of reach and never
used the one it was given. Running the reference showed the player moving zero
pixels through the whole freeze window.
- A rejected finding usually still leaves something. That same map failure was
not a code defect, but it did show the chapter stated `TALK_RANGE` without
saying what it means when you are placing NPCs -- and that a too-distant NPC
produces *success*, not an error.
- Fix the document, not the reader. If they guessed a header name, the chapter
never named it.
### What this catches that review does not
Four read-only passes over the two tutorials in `docs/` found real gaps and
missed all three of these, which the first build-and-run found immediately:
| Defect | Why reading missed it |
|---|---|
| libakstdlib's header was never named -- `aksl_*` functions, `<akstdlib.h>` file | Every reviewer already knew, or did not have to write the `#include` |
| The asset directory defaulted to `"."` instead of the compiled-in macro | Prose said "falls back", and nobody had to run it from another directory |
| A published example output the chapter could not produce | The capture happened after teardown released the pool. Nothing fails to compile |
The third is the shape to remember: **a tutorial that prints an expected result
is making a claim, and a claim nobody executed is a claim that is probably
wrong.**
## Commit & Pull Request Guidelines
Recent commits use concise, imperative summaries such as `Fix a scale bug...` and often explain related changes in one sentence. Keep each commit scoped and describe user-visible behavior. Pull requests should summarize the change, identify affected modules, list the CMake/CTest commands run, and link relevant issues. Include screenshots only for rendering, map, or `charviewer` changes.

View File

@@ -134,19 +134,36 @@ target_compile_definitions(sidescroller
The parts that matter:
Each `.c` file includes `sidescroller.h` plus whatever it calls directly. `main.c` adds
`akgl/character.h`, `akgl/controller.h`, `akgl/heap.h`, `akgl/registry.h`, `akgl/renderer.h`,
`akgl/sprite.h` and `akgl/text.h`; `player.c` adds `akgl/controller.h`, `akgl/heap.h`,
`akgl/registry.h`, `akgl/util.h` and `<math.h>`; `actors.c` adds `akgl/heap.h`,
`akgl/registry.h` and `<math.h>`. libakgl's headers are self-contained, so including the one
that declares what you are calling is always enough.
Each `.c` file includes `sidescroller.h` plus whatever it calls directly:
| File | Adds |
|---|---|
| `main.c` | `<string.h>`, `akstdlib.h`, `SDL3_image/SDL_image.h`, `SDL3_mixer/SDL_mixer.h`, `SDL3_ttf/SDL_ttf.h`, `akgl/character.h`, `akgl/controller.h`, `akgl/heap.h`, `akgl/registry.h`, `akgl/renderer.h`, `akgl/sprite.h`, `akgl/text.h`, `akgl/tilemap.h` |
| `player.c` | `<math.h>`, `akstdlib.h`, `akgl/controller.h`, `akgl/heap.h`, `akgl/physics.h`, `akgl/registry.h`, `akgl/util.h` |
| `actors.c` | `<math.h>`, `akstdlib.h`, `akgl/heap.h`, `akgl/registry.h` |
**libakstdlib's header is `<akstdlib.h>`**, not `<aksl.h>` — its functions are prefixed
`aksl_` but the file is not. libakerror's is `<akerror.h>`. libakgl's own headers are
self-contained, so including the one that declares what you are calling is always enough.
The build file:
- You link all four SDL libraries even though this game has no text and no sound, because
libakgl itself is built against them.
- `SS_ASSET_DIR` is baked in at compile time, so the program can be run from any working
directory. The code falls back to `"."` if it is not defined.
directory. Give it a fallback so the file still compiles without CMake, and **use it as the
default**: a program that defaults to `"."` only finds its assets when it happens to be
launched from the right directory.
```c excerpt=examples/sidescroller/main.c
#ifndef SS_ASSET_DIR
#define SS_ASSET_DIR "."
#endif
```
```c excerpt=examples/sidescroller/main.c
char *assetdir = SS_ASSET_DIR;
```
### The header
@@ -649,7 +666,14 @@ reads. Load a character before its sprites and it fails on the first name it can
## 5. Draw a level
Open Tiled, make a new map, and set it up like this:
**Draw the map in [Tiled](https://mapeditor.org) and save it as `.tmj`.** The rest of this
section is what to set in the editor; Tiled writes the JSON. [Chapter 13](13-tilemaps.md) is
the reference for the format itself, and for what libakgl reads out of it and what it
ignores — read that before hand-editing a `.tmj`, because the loader needs several fields
Tiled fills in automatically (a root-level `width` and `height`, an `id` on every layer, and
`tilecount`, `columns`, `imagewidth` and `imageheight` on every tileset).
Make a new map and set it up like this:
| Setting | Value |
|---|---|
@@ -1625,6 +1649,19 @@ It prints where the player finished:
sidescroller: 240 frames, 0 of 4 coins, 0 deaths, player at 136.0,160.0 grounded
```
**Capture that position at the end of `run()`, not in `main`.** Teardown happens in `main`'s
`CLEANUP` block and releases the actor pool, so by the time the summary is printed
`ss_game.player` points at a slot that has been given back — and the line reports `0.0,0.0`:
```c excerpt=examples/sidescroller/main.c
if ( ss_game.player != NULL ) {
ss_game.final_x = ss_game.player->x;
ss_game.final_y = ss_game.player->y;
}
```
That is what `final_x` and `final_y` on `ss_Game` are for.
`grounded` and a sensible `y` are how you know collision ran. If the player is hundreds of
pixels below the level, revisit step 7.

View File

@@ -80,11 +80,16 @@ TrueType font at runtime. Bake both paths in at compile time:
```cmake
target_compile_definitions(jrpg PRIVATE
JRPG_ASSET_DIR="${JRPG_REPO_ROOT}/docs/tutorials/assets/jrpg"
JRPG_FONT_FILE="${JRPG_REPO_ROOT}/tests/assets/akgl_test_mono.ttf"
JRPG_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets"
JRPG_FONT_FILE="${CMAKE_CURRENT_SOURCE_DIR}/assets/font.ttf"
)
```
**Two definitions, not one.** The font is a separate path from the assets, and a game that
defines only `JRPG_ASSET_DIR` fails at `akgl_text_loadfont` with "Couldn't open" — after
everything else has loaded, which makes it look like a font problem rather than a build
one.
Give them defaults in the header so the file still compiles on its own:
```c excerpt=examples/jrpg/jrpg.h
@@ -425,8 +430,8 @@ already works.
## 5. Draw the town
Set the map up as chapter 20 describes — orthogonal, CSV layer format, 16×16 tiles — at
30 × 20 tiles, with three layers:
Draw it in Tiled and save it as `.tmj`, set up as chapter 20 describes — orthogonal, CSV
layer format, 16×16 tiles — at 30 × 20 tiles, with three layers:
| Layer | Type | What it is |
|---|---|---|
@@ -957,6 +962,13 @@ range, otherwise do nothing.
#define TALK_RANGE 64.0f
```
**Place your NPCs with that number in mind.** The distance is measured between the two
actors' positions, and both sprites are 32 pixels, so 64 is two sprite widths — close, by
design. An NPC parked behind a building or across a wall from anywhere the player can stand
is an NPC the player can see and never talk to, and nothing reports that: the handler finds
nobody in range and returns success. If a conversation will not open, print the distance
before you go looking at the code.
```c excerpt=examples/jrpg/world.c
#define TOWNSFOLK_COUNT (sizeof(TOWNSFOLK) / sizeof(TOWNSFOLK[0]))
```
@@ -1072,6 +1084,15 @@ The `+ 16.0f` centres on the middle of a 32-pixel sprite rather than its top-lef
The loop is chapter 20's, with two additions. Drain the events first:
```c excerpt=examples/jrpg/jrpg.c
static akerr_ErrorContext *frame(long frameno)
{
PREPARE_ERROR(errctx);
SDL_Event event;
akgl_Iterator opflags = {
.flags = AKGL_ITERATOR_OP_UPDATE,
.layerid = 0
};
while ( SDL_PollEvent(&event) ) {
if ( event.type == SDL_EVENT_QUIT ) {
running = false;
@@ -1080,6 +1101,10 @@ The loop is chapter 20's, with two additions. Drain the events first:
}
```
`frameno` is the outer loop's own counter, incremented once per call. **libakgl does not keep
a frame number** — `akgl_game` has an fps figure but no counter, so a game that wants one
keeps it.
Then the camera, the world, and the panel on top of it:
```c excerpt=examples/jrpg/jrpg.c
@@ -1088,17 +1113,9 @@ Then the camera, the world, and the panel on top of it:
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
```
This game passes an iterator to `akgl_game_update` rather than `NULL`:
```c excerpt=examples/jrpg/jrpg.c
akgl_Iterator opflags = {
.flags = AKGL_ITERATOR_OP_UPDATE,
.layerid = 0
};
```
`AKGL_ITERATOR_OP_UPDATE` asks for the update pass. [Chapter 7](07-the-game-and-the-frame.md)
covers the other flags.
This game passes the `opflags` declared at the top of `frame()` to `akgl_game_update` rather
than the `NULL` chapter 20 passes. `AKGL_ITERATOR_OP_UPDATE` asks for the update pass;
[Chapter 7](07-the-game-and-the-frame.md) covers the other flags.
### Teardown
@@ -1154,6 +1171,77 @@ where the player finished:
jrpg: 320 frames, player at (280, 130)
```
### Writing the scripted run
Neither the demo nor that summary line is part of the game — both exist so the program can be
checked without a person at the keyboard, and both are worth having for exactly that reason.
The script is a table of frame numbers and keys:
```c excerpt=examples/jrpg/jrpg.h
typedef struct {
long frame; /**< Frame number this step fires on. */
SDL_Keycode key; /**< Key to synthesize. */
bool down; /**< True for a press, false for a release. */
} jrpg_ScriptStep;
```
```c excerpt=examples/jrpg/jrpg.c
static const jrpg_ScriptStep JRPG_DEMO_SCRIPT[] = {
{ 10, SDLK_RIGHT, true }, /* the per-facing walk animation */
{ 70, SDLK_RIGHT, false },
{ 75, SDLK_UP, true }, /* up the map, past the buildings */
{ 205, SDLK_UP, false },
{ 215, SDLK_SPACE, true }, /* the elder is in range: open the box */
{ 216, SDLK_SPACE, false },
{ 225, SDLK_LEFT, true }, /* frozen: AKGL_ERR_LOGICINTERRUPT eats this */
{ 245, SDLK_LEFT, false },
{ 255, SDLK_SPACE, true }, /* dismiss */
{ 256, SDLK_SPACE, false },
{ 265, SDLK_DOWN, true }, /* and walk away */
{ 285, SDLK_DOWN, false }
};
```
Playing it back is building an `SDL_Event` and handing it to the same function the real event
loop uses:
```c excerpt=examples/jrpg/jrpg.c
PASS(errctx, aksl_memset((void *)&event, 0x00, sizeof(event)));
if ( JRPG_DEMO_SCRIPT[i].down ) {
event.type = SDL_EVENT_KEY_DOWN;
} else {
event.type = SDL_EVENT_KEY_UP;
}
event.key.which = 0;
event.key.key = JRPG_DEMO_SCRIPT[i].key;
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
```
**Go through `akgl_controller_handle_event`, not straight to the handlers.** A script that
calls the handlers directly still passes when the control map matches nothing at all, which
is the failure it most needs to catch.
Two more things make a headless run reproducible. Drive the clock rather than sleeping on it,
so a simulated second does not cost a real one:
```c excerpt=examples/jrpg/jrpg.c
akgl_physics->gravity_time = SDL_GetTicksNS() - JRPG_FIXED_STEP_NS;
```
```c excerpt=examples/jrpg/jrpg.c
#define JRPG_FIXED_STEP_NS (AKGL_TIME_ONESEC_NS / 60)
```
And print the position while the player still exists — before teardown releases the actor
pool:
```c excerpt=examples/jrpg/jrpg.c
printf("jrpg: %ld frames, player at (%.0f, %.0f)\n", frameno, player->x, player->y);
```
The frame number is the loop's own counter, passed down. libakgl does not keep one.
## Where to look next
- [Chapter 20](20-tutorial-sidescroller.md) — the same shape of program with gravity, a jump

View File

@@ -4,6 +4,13 @@ This file documents the harness that keeps `docs/` honest. It is not a chapter
a reader learning libakgl never needs it — and it is checked by the same harness
it describes, so the file that publishes the convention is held to it.
**The harness checks that every listing is true. It cannot check that a chapter
teaches.** For that, the tutorials get a second test: the chapter is handed to a
subagent on a weaker model with the library, the headers and the art but *not*
the finished program, and that reader has to build and run the game. See
`AGENTS.md`, "Testing a Tutorial". Four read-only reviews of chapters 20 and 21
missed three defects the first build-and-run found in minutes.
## Documentation examples
**Every fenced block in `docs/*.md` is compiled, linked, run, or matched against