Compare commits
3 Commits
149bee0c99
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
75da766724
|
|||
|
cf0142ea85
|
|||
|
6e97d27e49
|
95
AGENTS.md
95
AGENTS.md
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user