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
This commit is contained in:
2026-08-02 09:34:16 -04:00
parent 149bee0c99
commit 6e97d27e49
2 changed files with 141 additions and 23 deletions

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 |
|---|---|---|
@@ -1072,6 +1077,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 +1094,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 +1106,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 +1164,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