Add two tutorial games that build, run in CI, and cannot drift

examples/sidescroller and examples/jrpg are complete programs, built with the
library and exercised headless by ctest. The chapters quote them with
`c excerpt=examples/...` blocks rather than restating the code, so a chapter
cannot drift from a program that compiles -- the excerpt check fails the moment
the source moves. 34 excerpts in one chapter, 21 in the other.

The two are complementary. The sidescroller is the physics tutorial: gravity, a
jump, coins, hazards. The JRPG is the content-pipeline tutorial: a town map,
NPCs spawned from map objects, four-way per-facing animation, a text box, a
follower.

Both smoke tests drive real SDL_Events through akgl_controller_handle_event and
step the physics clock at a fixed 1/60s rather than sleeping, so a scripted run
is deterministic and finishes in under five seconds.

Writing them is what turned up most of the defects recorded in the next commit,
because a game exercises paths a unit test does not. Each workaround says in the
chapter which library gap forced it:

- collision is written in a custom movementlogicfunc, because
  akgl_physics_arcade_collide raises AKERR_API and akgl_physics_simulate never
  calls collide at all;
- the sidescroller cancels the step's own gravity when it blocks downward,
  because otherwise a quarter-pixel of penetration makes the *horizontal* sweep
  report blocked and the character walks backwards a tile at a time;
- both clear movement_controls_face on every map-spawned actor, because the
  default facefunc leaves a stopped actor with no facing bit, no sprite, and no
  draw;
- the JRPG's follower gets a renderfunc that nulls obj->parent for the duration
  of the draw, because a child's offset is counted twice.

Assets are CC0 from three Kenney packs, vendored with per-pack licence text,
per-file provenance, and the geometry contract in
docs/tutorials/assets/README.md. CC0 specifically rather than merely free: a
reader who copies a tutorial into their own game inherits no obligation.
scripts/fetch_tutorial_assets.sh refreshes them in the shape
mkcontrollermappings.sh was fixed into for 0.5.0 -- it checks curl's status,
refuses a pack page that does not say CC0, verifies the archive and the staged
dimensions, and leaves the tracked bytes untouched on any failure. Both failure
paths were tested, and a no-op refresh is byte-identical.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 20:59:00 -04:00
parent b938460127
commit 88aaa184e4
72 changed files with 8946 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
{
"name": "ss_coin",
"speedtime": 200,
"speed_x": 0.0,
"speed_y": 0.0,
"acceleration_x": 0.0,
"acceleration_y": 0.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE"
],
"sprite": "ss_coin"
}
]
}

View File

@@ -0,0 +1,46 @@
{
"name": "ss_hazard_blob",
"speedtime": 180,
"speed_x": 24.0,
"speed_y": 0.0,
"acceleration_x": 200.0,
"acceleration_y": 0.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE"
],
"sprite": "ss_hazard_blob"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_LEFT"
],
"sprite": "ss_hazard_blob"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_LEFT",
"AKGL_ACTOR_STATE_MOVING_LEFT"
],
"sprite": "ss_hazard_blob"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_RIGHT"
],
"sprite": "ss_hazard_blob"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_RIGHT",
"AKGL_ACTOR_STATE_MOVING_RIGHT"
],
"sprite": "ss_hazard_blob"
}
]
}

View File

@@ -0,0 +1,46 @@
{
"name": "ss_hazard_moth",
"speedtime": 120,
"speed_x": 40.0,
"speed_y": 40.0,
"acceleration_x": 300.0,
"acceleration_y": 300.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE"
],
"sprite": "ss_hazard_moth"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_LEFT"
],
"sprite": "ss_hazard_moth"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_LEFT",
"AKGL_ACTOR_STATE_MOVING_LEFT"
],
"sprite": "ss_hazard_moth"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_RIGHT"
],
"sprite": "ss_hazard_moth"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_RIGHT",
"AKGL_ACTOR_STATE_MOVING_RIGHT"
],
"sprite": "ss_hazard_moth"
}
]
}

View File

@@ -0,0 +1,89 @@
{
"name": "ss_player",
"speedtime": 120,
"speed_x": 90.0,
"speed_y": 0.0,
"acceleration_x": 600.0,
"acceleration_y": 0.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_RIGHT"
],
"sprite": "ss_player_idle_right"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_RIGHT",
"AKGL_ACTOR_STATE_MOVING_RIGHT"
],
"sprite": "ss_player_run_right"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_LEFT"
],
"sprite": "ss_player_idle_left"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_LEFT",
"AKGL_ACTOR_STATE_MOVING_LEFT"
],
"sprite": "ss_player_run_left"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_DOWN"
],
"sprite": "ss_player_idle_right"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_UP",
"AKGL_ACTOR_STATE_MOVING_UP"
],
"sprite": "ss_player_jump_right"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_RIGHT",
"AKGL_ACTOR_STATE_MOVING_UP"
],
"sprite": "ss_player_jump_right"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_LEFT",
"AKGL_ACTOR_STATE_MOVING_UP"
],
"sprite": "ss_player_jump_left"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_RIGHT",
"AKGL_ACTOR_STATE_MOVING_RIGHT",
"AKGL_ACTOR_STATE_MOVING_UP"
],
"sprite": "ss_player_jump_right"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_LEFT",
"AKGL_ACTOR_STATE_MOVING_LEFT",
"AKGL_ACTOR_STATE_MOVING_UP"
],
"sprite": "ss_player_jump_left"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "coin.png",
"frame_width": 32,
"frame_height": 32
},
"name": "ss_coin",
"width": 32,
"height": 32,
"speed": 200,
"loop": true,
"loopReverse": false,
"frames": [
0
]
}

View File

@@ -0,0 +1,17 @@
{
"spritesheet": {
"filename": "hazard.png",
"frame_width": 32,
"frame_height": 32
},
"name": "ss_hazard_blob",
"width": 32,
"height": 32,
"speed": 180,
"loop": true,
"loopReverse": false,
"frames": [
0,
1
]
}

View File

@@ -0,0 +1,17 @@
{
"spritesheet": {
"filename": "hazard.png",
"frame_width": 32,
"frame_height": 32
},
"name": "ss_hazard_moth",
"width": 32,
"height": 32,
"speed": 120,
"loop": true,
"loopReverse": false,
"frames": [
2,
3
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "player.png",
"frame_width": 32,
"frame_height": 32
},
"name": "ss_player_idle_left",
"width": 32,
"height": 32,
"speed": 250,
"loop": true,
"loopReverse": false,
"frames": [
3
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "player.png",
"frame_width": 32,
"frame_height": 32
},
"name": "ss_player_idle_right",
"width": 32,
"height": 32,
"speed": 250,
"loop": true,
"loopReverse": false,
"frames": [
0
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "player.png",
"frame_width": 32,
"frame_height": 32
},
"name": "ss_player_jump_left",
"width": 32,
"height": 32,
"speed": 250,
"loop": true,
"loopReverse": false,
"frames": [
4
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "player.png",
"frame_width": 32,
"frame_height": 32
},
"name": "ss_player_jump_right",
"width": 32,
"height": 32,
"speed": 250,
"loop": true,
"loopReverse": false,
"frames": [
1
]
}

View File

@@ -0,0 +1,19 @@
{
"spritesheet": {
"filename": "player.png",
"frame_width": 32,
"frame_height": 32
},
"name": "ss_player_run_left",
"width": 32,
"height": 32,
"speed": 90,
"loop": true,
"loopReverse": false,
"frames": [
3,
4,
5,
4
]
}

View File

@@ -0,0 +1,19 @@
{
"spritesheet": {
"filename": "player.png",
"frame_width": 32,
"frame_height": 32
},
"name": "ss_player_run_right",
"width": 32,
"height": 32,
"speed": 90,
"loop": true,
"loopReverse": false,
"frames": [
0,
1,
2,
1
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB