diff --git a/presentations/making-games/assets/budgets-legibility.png b/presentations/making-games/assets/budgets-legibility.png index 1a18554..2b16923 100644 Binary files a/presentations/making-games/assets/budgets-legibility.png and b/presentations/making-games/assets/budgets-legibility.png differ diff --git a/presentations/making-games/assets/captures.json b/presentations/making-games/assets/captures.json new file mode 100644 index 0000000..b49ecf4 --- /dev/null +++ b/presentations/making-games/assets/captures.json @@ -0,0 +1,95 @@ +{ + "sourceSha256": "c2ed572c63a5cf8d4935104a1df49251239306182b9686eb08e66ae6ca688373", + "captures": [ + { + "file": "garden-start.png", + "description": "Finished game starts with no planted flowers.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "7de9abb9d67b6ff1f99b83d8d5d25c6bc2dd04ad5ebde726ff8f81973cc53533" + }, + { + "file": "dream-garden.png", + "description": "Populated final game, grown through its bloom handler.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "30a7e554d2869f491aa9b78408e05299ab9af2b8dcaf4e43ee860708b8245014" + }, + { + "file": "shared-rest.png", + "description": "Player one holds A: RELAX, sleeping pose and slower seeds.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "2eef293f519d23a66ae245ad5fbdaf5bd24b296342df534eba3702a904216ee6" + }, + { + "file": "dream-rest.png", + "description": "Actual final Rest scene preserves the planted flowers.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "f078d873fd75b85fc9d360f68c66177c521d115ab76fb539ef4601a6db90b4bf" + }, + { + "file": "first-game.png", + "description": "Actual first steering checkpoint, with only one dreamer.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "dbc86956d2ed20a8f6f493bb3e7b22153a4ac9d8ae44c6c65f9b0690e594cfd2" + }, + { + "file": "tilemaps.png", + "description": "Actual cloud tilemap checkpoint before scrolling.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "7de9abb9d67b6ff1f99b83d8d5d25c6bc2dd04ad5ebde726ff8f81973cc53533" + }, + { + "file": "display-angles.png", + "description": "Geometry example using real Dream Garden assets.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "a64ed83ee165d56ec6a8af4b020a78f451cbed6eedc49f26d9142bb014d9cf85" + }, + { + "file": "display-projections.png", + "description": "Geometry example using real Dream Garden assets.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "bb3f47930b9b34347ef15f0b65a969d8d0da6bd44f452033388b5b88857a08b8" + }, + { + "file": "budgets-legibility.png", + "description": "Geometry example using real Dream Garden assets.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "4bba014d74818400a154f16fb488c697ef7a779e634aa203c8c172021f4638f1" + } + ] +} diff --git a/presentations/making-games/assets/cloud-atlas.png b/presentations/making-games/assets/cloud-atlas.png new file mode 100644 index 0000000..b0c8a7d Binary files /dev/null and b/presentations/making-games/assets/cloud-atlas.png differ diff --git a/presentations/making-games/assets/display-angles.png b/presentations/making-games/assets/display-angles.png index be2a0d9..143c399 100644 Binary files a/presentations/making-games/assets/display-angles.png and b/presentations/making-games/assets/display-angles.png differ diff --git a/presentations/making-games/assets/display-projections.png b/presentations/making-games/assets/display-projections.png index 23370c1..61f1145 100644 Binary files a/presentations/making-games/assets/display-projections.png and b/presentations/making-games/assets/display-projections.png differ diff --git a/presentations/making-games/assets/dream-garden.png b/presentations/making-games/assets/dream-garden.png index 545a158..b0d3130 100644 Binary files a/presentations/making-games/assets/dream-garden.png and b/presentations/making-games/assets/dream-garden.png differ diff --git a/presentations/making-games/assets/dream-rest.png b/presentations/making-games/assets/dream-rest.png index 6daf1b5..97dd2d5 100644 Binary files a/presentations/making-games/assets/dream-rest.png and b/presentations/making-games/assets/dream-rest.png differ diff --git a/presentations/making-games/assets/first-game.png b/presentations/making-games/assets/first-game.png index 49007f7..a1df37d 100644 Binary files a/presentations/making-games/assets/first-game.png and b/presentations/making-games/assets/first-game.png differ diff --git a/presentations/making-games/assets/garden-start.png b/presentations/making-games/assets/garden-start.png new file mode 100644 index 0000000..8aee9ad Binary files /dev/null and b/presentations/making-games/assets/garden-start.png differ diff --git a/presentations/making-games/assets/shared-rest.png b/presentations/making-games/assets/shared-rest.png index a20d12a..85780dc 100644 Binary files a/presentations/making-games/assets/shared-rest.png and b/presentations/making-games/assets/shared-rest.png differ diff --git a/presentations/making-games/assets/tilemaps.png b/presentations/making-games/assets/tilemaps.png index beba69f..8aee9ad 100644 Binary files a/presentations/making-games/assets/tilemaps.png and b/presentations/making-games/assets/tilemaps.png differ diff --git a/presentations/making-games/examples/artwork.md b/presentations/making-games/examples/artwork.md index 12edd40..60b79a4 100644 --- a/presentations/making-games/examples/artwork.md +++ b/presentations/making-games/examples/artwork.md @@ -20,9 +20,8 @@ Neither Node nor sharp is a game runtime dependency. The generated sheet contains two dream creatures, golden seeds, four flower species and sparse garden accents. Dreamers have four poses each. Flowers have -two frames each. Seeds are packed at 9 × 9 pixels: a subdued idle frame and -three brighter glow frames. Flying accents are shrunk inside their 16-pixel -tiles and darkened so the planted flowers remain distinct. `images/steel8x8.png` is copied unchanged from the SDK's +two frames each. The original fourth row is unused in this cloud experiment. Seeds are packed at 9 × 9 pixels: a subdued idle frame and +three brighter glow frames. Flying accents now use the separate cloud atlas described below. `images/steel8x8.png` is copied unchanged from the SDK's `games/demos/tutorial_game/images/steel8x8.png`. `make_audio.py` synthesizes the original bloom chime and a 32-second soft loop @@ -34,3 +33,15 @@ used. ## Generation prompt Create a production-ready transparent pixel-art SPRITE ATLAS for an experimental circular arcade game called Dream Garden. Exact layout: EIGHT equal columns and FOUR equal rows, a 2:1 horizontal image. Each cell contains one separate centered sprite with generous transparent padding. No visible grid lines, letters, numbers, labels, background, shadow beneath objects, or scene. Strict low resolution pixel art: chunky crisp square pixels, limited pastel mint/lavender/pink/amber palette with deep navy outlines, no antialiasing style or gradients. Intended to read at 19x19 pixels. ROW ONE all 8 cells: a tiny rounded dream creature with two eyes and soft leafy antennae. Cells 1-4 pink creature, neutral face, leaning left, leaning right, sleeping with closed eyes. Cells 5-8 same creature in mint blue, same four poses. ROW TWO cells 1-4: one floating golden seed/wisp in four subtle animation phases, simple bright core and a little tail; cells 5-8 entirely empty transparent. ROW THREE all 8 cells: four distinct dream flowers, TWO frames per flower with petals open slightly differently. Flower species: pink star blossom, mint fern blossom, lavender moon flower, amber glowing mushroom flower. ROW FOUR all 8 cells: eight tiny sparse dream garden accents on transparency: a mint root curl, a pink petal pair, a lavender crescent, three tiny amber stars, a blue sprout, a mint leaf, lavender bubbles, a single pink curl. All objects completely contained within their individual cells, exact consistent spacing. Cozy strange playful dream imagery for a festival about dreaming and rest. No spacecraft, weapons, robots, metal, UI, logos, or text. Export on genuine transparent background. + +## Fluffy cloud experiment + +The built-in imagegen tool generated a [separate six-cloud atlas](../assets/cloud-atlas.png). +The exact prompt is included as `sources/cloud-prompt.txt` in the game download. +`node games/demos/dream_garden/sources/prepare-clouds.cjs` packs two variants each +at 16 × 16, 32 × 16 and 48 × 32 pixels, cuts them into adjoining 16-pixel tiles +and produces an 18-frame strip. Binary alpha removes faint surrounding glow; +70% brightness keeps clouds subdued. Clouds use their own palette group. +Larger shapes occupy two cells or a 3 × 2 block, without gaps at cell boundaries. +The packer reverses source cell order to match the polar renderer’s tile +orientation; the map places the resulting frames row by row. diff --git a/presentations/making-games/examples/dream-garden-guide.md b/presentations/making-games/examples/dream-garden-guide.md index 58f2aa1..43cebe9 100644 --- a/presentations/making-games/examples/dream-garden-guide.md +++ b/presentations/making-games/examples/dream-garden-guide.md @@ -9,7 +9,7 @@ The festival's [Dream No Return theme](https://www.sickhouse.nl/festival/dream-n inspired a garden whose players can choose a slower pace. This is an original sample game, not a festival commission or an official festival game. -[Download the complete game](dream-garden.zip). While these changes are under review, use the SDK branch `feat/dream-garden-layers`. Extract the archive at the root of that SDK checkout to create `games/demos/dream_garden/`. The source, packed assets, audio and rebuilding recipes are included, along with `tests/test_dream_garden.py` so the checks below also work when installing into a current SDK checkout. After merging, the same archive works in an updated main checkout. +[Download the complete game](dream-garden.zip). While these changes are under review, use the SDK branch `feat/dream-garden-clouds`. Extract the archive at the root of that SDK checkout to create `games/demos/dream_garden/`. The source, packed assets, audio and rebuilding recipes are included, along with `tests/test_dream_garden.py` so the checks below also work when installing into a current SDK checkout. After merging, the same archive works in an updated main checkout. [Small first-game starter](first_game.py) · [Back to the slides](../) @@ -29,22 +29,23 @@ Windows uses `vs-emu.bat --game demos.dream_garden`. | --- | --- | --- | --- | | Begin / join | A | Space | Numpad 0 | | Drift around the rim | Left / Right | A / D | Left / Right arrows | -| Slow everyone's garden | Hold A | Hold Space | Hold Numpad 0 | +| Slow the seeds | Hold A | Hold Space | Hold Numpad 0 | | Rest and keep the flowers on screen | B | B | Numpad 1 | | Toggle the quiet lullaby and bloom sound | X | N | Numpad 2 | | Return to the launcher | Y / Back / Home | M / 1 / Escape | Numpad 3 / + / Escape | The garden starts empty. Catch a golden seed to plant a flower at the catching -player’s depth, on the side they last moved toward, with a centre offset of -75% of the player’s width. Each player remembers their own direction while -stopped; opposite inputs cancel without changing it. Before the first movement, -planting defaults to the left. -Flowers drift inward at 8% of seed speed and disappear at the centre. +player’s depth, displaced by 75% of the player’s width. While moving, catches +plant in the current movement direction. After stopping, the first catch uses +that direction, then later catches alternate sides until movement resumes. +Each player has an independent sequence; opposite inputs count as stopping. +Before the first movement, the first catch defaults to the left. +Flowers drift inward at 8% of normal seed speed and disappear at the centre. Smaller, subdued golden seeds occasionally pulse with a randomly triggered glow. Missing a seed has no penalty. A second player joins by steering or pressing A, using an already reserved mint dreamer. -Either player can slow seed movement, the drifting background and flower -animation. New spawn intervals also lengthen, after the pending seed deadline. +Either player can slow the seeds. Clouds, planted flower drift and flower +animation keep their normal pace. New spawn intervals also lengthen, after the pending seed deadline. Flowers gradually replace the oldest ones when the garden fills up. There is no death, score, countdown or difficulty ramp. Watching is welcome. Music and bloom sounds start on. X toggles both, showing NOISY or QUIET @@ -52,6 +53,8 @@ for two seconds before the text slides outward past the rim over 450 ms. Another toggle resets the position and restarts the notice. B stops the music and keeps the current flowers in a still garden. A starts a new garden from the Rest scene. +![The finished garden begins empty, with a pink dreamer below the clouds](../assets/garden-start.png) + ## How the example works `code/dream_garden.py` contains the whole game. `main()` returns Title, which @@ -63,7 +66,11 @@ The second player costs one reserved sprite even before joining. The Garden graph uses **42 sprites, 3 tilemaps, 3 layers and 5 image strips**. The flower pool counts toward that cost even when its slots are hidden. The lower layer draws flowers first and players above them. The middle layer draws -smaller, darker flying accents in 16-pixel tiles, followed by 9-pixel seeds. +six subdued fluffy cloud shapes, followed by 9-pixel golden seeds. Clouds +occupy 1 × 1, 2 × 1 or 3 × 2 tile cells, packed at 16 × 16, 32 × 16 +and 48 × 32 pixels. Clouds keep scrolling at 0.45 depth units per tick. Seeds move 15% slower +by default (0.3825), and slow to 0.102 while relaxing. Seeds always paint +above clouds, including when their shapes overlap. The top HUD shows RELAX while A is held, then slides it out on release, plus temporary audio feedback. Both flips keep the top text upright. @@ -74,6 +81,30 @@ base and never go into the rotor ROM. The sample follows the repository's GPLv3 license. [Artwork sources and rebuilding](artwork.html) record the new art and original sound synthesis. The general SDK tutorial remains a separate example. +## Build this garden step by step + +Each stage is a complete, cumulative source file inside the download at +`code/checkpoints/`. The slides explain the changes and launch the matching +version in the web emulator. The last stage matches `code/dream_garden.py` exactly. + +| Stage | Adds | +| --- | --- | +| 01-steering | One pink dreamer moving around the rim | +| 02-poses | Leaning and sleeping poses | +| 03-pools | Reserved seed and flower slots | +| 04-timer | Periodic random seed spawning | +| 05-flowers | Collisions, directional planting and inward flower drift | +| 06-tiles | Fluffy clouds in the middle layer, behind seeds | +| 07-scroll | Steady scrolling clouds, slower outward seeds | +| 08-hud | Seed-only RELAX and a label that slides out | +| 09-scenes | Title and voluntary rest, preserving flowers | +| 10-shared | A second player, independent planting sequences and random seed glow | +| 11-audio | Default-on lullaby and bloom sounds, timed NOISY/QUIET notices | + +A new garden starts without flowers. The planting direction follows movement, +then alternates after stopping. Every stage keeps the same packed artwork, +with unused strips available for the later steps. + ## Checks From the SDK root: diff --git a/presentations/making-games/examples/dream-garden.zip b/presentations/making-games/examples/dream-garden.zip index 194475b..b402f14 100644 Binary files a/presentations/making-games/examples/dream-garden.zip and b/presentations/making-games/examples/dream-garden.zip differ diff --git a/presentations/making-games/examples/dream_garden.py b/presentations/making-games/examples/dream_garden.py index 94ce62c..05b4510 100644 --- a/presentations/making-games/examples/dream_garden.py +++ b/presentations/making-games/examples/dream_garden.py @@ -1,6 +1,6 @@ """Dream Garden: a shared, unhurried circular garden for Overkill 2026. -LEFT/RIGHT drift, hold A to slow the whole garden, B rests, X toggles audio. +LEFT/RIGHT drift, hold A to slow the seeds, B rests, X toggles audio. A second controller can join at any time. Watching is also a way to play. """ @@ -13,11 +13,16 @@ AWAKE, LEFT_POSE, RIGHT_POSE, ASLEEP = range(4) DRIFT_SPEED = 0.45 SLOW_SPEED = 0.12 +CLOUD_SPEED = DRIFT_SPEED DRIFT_SPAWN_MS = 1800 SLOW_SPAWN_MS = 4000 SEED_DEPTH = 160 FLOWER_COUNT = 24 -PATTERN_ROWS = 6 +PATTERN_ROWS = 12 +SEED_SPEED_RATIO = 0.85 +# (column, row, first tile, width in cells, height in cells) +CLOUD_PATCHES = ((1, 0, 0, 1, 1), (7, 1, 1, 2, 1), (11, 4, 3, 3, 2), + (5, 6, 9, 1, 1), (13, 8, 10, 2, 1), (1, 9, 12, 3, 2)) FLOWER_DRIFT_RATIO = 0.08 FLOWER_DEPTH = 255 GLOW_FRAMES = (1, 2, 3, 2, 1, 0) @@ -34,15 +39,16 @@ def slide_text_out(label, start_y, started): return started -def garden_tiles(world): - ground = world.tilemap("garden.png", columns=16, rows=16, - view_width=256, view_height=160) - for row in range(16): - for col in range(16): - band = row % PATTERN_ROWS - ground[col, row] = ((col + band) % 8 - if (col + band * 3) % 5 == 0 - else vs2.EMPTY_TILE) +def cloud_tiles(air): + ground = air.tilemap("clouds.png", columns=16, rows=22, + view_width=256, view_height=160) + for base_row in range(0, ground.rows, PATTERN_ROWS): + for col, row, first, width, height in CLOUD_PATCHES: + for dy in range(height): + target_row = base_row + row + dy + if target_row < ground.rows: + for dx in range(width): + ground[col + dx, target_row] = first + dy * width + dx return ground @@ -63,7 +69,7 @@ def build(self): self.world = self.layer("world", projection=vs2.TUNNEL) self.air = self.layer("air", projection=vs2.TUNNEL) self.hud = self.layer("hud", projection=vs2.HUD) - self.ground = garden_tiles(self.air) + self.ground = cloud_tiles(self.air) self.flowers = self.world.sprite_pool( "flowers.png", count=FLOWER_COUNT, on_empty=vs2.RECYCLE) self.seeds = self.air.sprite_pool("seed.png", count=16) @@ -85,9 +91,13 @@ def build(self): self.slow = False self.sound_on = True vs2.audio.music("lullaby", loop=True) - # Start toward LEFT; stopping keeps each player’s last steering direction. + # Idle catches alternate; movement resets the first idle planting side. self.last_direction1 = 1 self.last_direction2 = 1 + self.plant_side1 = 1 + self.plant_side2 = 1 + self.moving1 = False + self.moving2 = False self.seed_glows = {} self.speed = DRIFT_SPEED self.spawn_ms = DRIFT_SPAWN_MS @@ -96,11 +106,16 @@ def build(self): def steer(self, dreamer, controller, base_frame, sleeping): direction = controller.held(LEFT) - controller.held(RIGHT) dreamer.x = (dreamer.x + direction) % vs2.display.width - if direction: - if dreamer is self.friend: + if dreamer is self.friend: + self.moving2 = bool(direction) + if direction: self.last_direction2 = direction - else: + self.plant_side2 = direction + else: + self.moving1 = bool(direction) + if direction: self.last_direction1 = direction + self.plant_side1 = direction pose = (ASLEEP if sleeping else LEFT_POSE if direction > 0 else RIGHT_POSE if direction < 0 else AWAKE) @@ -139,8 +154,8 @@ def update(self): self.mood_exit = slide_text_out(self.mood, 1, self.mood_exit) self.speed = SLOW_SPEED if slow else DRIFT_SPEED self.spawn_ms = SLOW_SPAWN_MS if slow else DRIFT_SPAWN_MS - self.dream_time = (self.dream_time + self.speed) % 96 - self.scroll = (self.scroll + self.speed) % ( + self.dream_time = (self.dream_time + CLOUD_SPEED) % 96 + self.scroll = (self.scroll + CLOUD_SPEED) % ( PATTERN_ROWS * self.ground.tile_height) self.ground.view_y = self.scroll phase = int(self.dream_time // 6) % 2 @@ -149,7 +164,7 @@ def update(self): for flower in self.flowers: flower.frame = (flower.frame // 2) * 2 + phase for flower in self.flowers: - flower.y += self.speed * FLOWER_DRIFT_RATIO + flower.y += CLOUD_SPEED * SEED_SPEED_RATIO * FLOWER_DRIFT_RATIO if flower.y >= FLOWER_DEPTH: self.flowers.despawn(flower) self.move_seeds() @@ -174,18 +189,23 @@ def update(self): self.sound_notice_until = ticks_add(ticks_ms(), 2000) def bloom(self, seed, player): - side = self.last_direction2 if player is self.friend else self.last_direction1 + side = self.plant_side2 if player is self.friend else self.plant_side1 centre = player.x + player.width // 2 self.flowers.spawn(x=(centre + side * player.width * 0.75 - self.flower_half) % vs2.display.width, y=player.y, frame=randrange(4) * 2 + self.flower_phase) + if player is self.friend: + if not self.moving2: + self.plant_side2 = -side + elif not self.moving1: + self.plant_side1 = -side self.retire_seed(seed) if self.sound_on: vs2.audio.sound("bloom") def move_seeds(self): for seed in self.seeds: - seed.y -= self.speed + seed.y -= self.speed * SEED_SPEED_RATIO self.glow_seed(seed) if seed.overlaps(self.dreamer): self.bloom(seed, self.dreamer) diff --git a/presentations/making-games/presenter-guide.md b/presentations/making-games/presenter-guide.md index 991ff93..3fb6c4b 100644 --- a/presentations/making-games/presenter-guide.md +++ b/presentations/making-games/presenter-guide.md @@ -25,7 +25,7 @@ links provide references rather than dependencies for the presentation. ## Before the talk Follow the [desktop setup]({{ '/docs/guides/desktop.html' | relative_url }}) in a -current SDK checkout. During branch review, use `feat/dream-garden-layers` or +current SDK checkout. During branch review, use `feat/dream-garden-clouds` or extract the [complete game download](examples/dream-garden.zip) at the SDK root. The download creates `games/demos/dream_garden/` and includes the source, packed PNGs, YAML, menu icon, MP3s, artwork provenance and rebuild recipes. @@ -42,7 +42,7 @@ tracebacks. Test two gamepads if available. One keyboard also works: | Action | Player 1 | Player 2 | | --- | --- | --- | | Steer | A / D | Left / Right arrows | -| Begin / join / slow time | Space | Numpad 0 | +| Begin / join / slow seeds | Space | Numpad 0 | | Rest | B | Numpad 1 | | Toggle audio | N | Numpad 2 | | Exit | 1 or M | Numpad + or 3 | @@ -76,15 +76,15 @@ capture if you need to present without the emulator. Run `demos.dream_garden` again. Start with A, catch a seed to grow a flower, and let another pass harmlessly. Invite a second person to join. Either player can -hold A to slow the shared flow. Point out RELAX, slower flying seeds and the much slower inward flower drift. -Plants appear on the side each dreamer last moved toward, even after stopping. Seeds pulse independently at random. +hold A to slow the seeds while clouds and planted flowers keep their normal pace. Point out RELAX, fluffy clouds in varied sizes, seeds normally moving 15% slower than clouds and slowing further under A, and the much slower inward flower drift. +Plants follow current movement; after stopping, the first follows that direction, then later catches alternate sides until movement resumes. Seeds pulse independently at random. Toggle the quiet audio and show NOISY/QUIET holding for two seconds before sliding out. RELAX slides out when A is released. B chooses a still rest scene that keeps the flowers. A begins a new garden. Y/Back returns to the launcher. Keep [the finished source](examples/dream_garden.py) open at `build`, `update` and `move_seeds`. `#/finished-demo` shows the resting scene if a live demo is unavailable. The display geometry slides use the same game's actual sprites -and font through the SDK desktop renderer. The general API tutorial remains a +and font through the emulator’s WebGL display backend. The general API tutorial remains a separate Trench Run walkthrough. ## Editing, hosting and printing @@ -108,7 +108,7 @@ Sickhouse's theme announcement informed the new game direction. Cover and console photographs come from this website. Circular-coordinate and scene-lifecycle diagrams come from the existing SDK documentation. New game -captures come from the SDK desktop renderer. The [artwork and sound record](examples/artwork.html) +captures come from the emulator’s WebGL display backend. The [artwork and sound record](examples/artwork.html) contains the built-in image generation prompt and the original tone recipes. The source and download accompany the slides. Reveal.js 6.0.2 is vendored with its MIT license and version record in `vendor/reveal/`. @@ -126,3 +126,21 @@ Use the browser’s Back action to return to the same slide, or open the link in another tab using your browser’s usual modifier key. Reload the checkpoint URL to start fresh. Keep the desktop setup ready as a backup for poor network conditions. + +## Rebuilding the WebGL captures + +All nine game and display captures use the SDK's WebGL scene compositor and +LED display backend, including its glow. The geometry examples use the same +packed assets. Photographs, source atlases and explanatory diagrams retain +their original files. The capture script requires Playwright and Chromium, +and fails if WebGL2 is unavailable rather than switching to Canvas 2D. + +```sh +python3 tools/presentation-scenes.py --sdk vsdk --out /tmp/garden-scenes.json +node tools/capture-presentation.mjs vsdk /tmp/garden-scenes.json +``` + +Run from the website repository after `make publish`. Set `CHROMIUM` to an +installed Chrome executable if necessary. `assets/captures.json` records the +backend and image hashes. The populated garden grows flowers through the real +game's bloom handler; the starter and tilemap images run their actual checkpoints. diff --git a/presentations/making-games/slides.md b/presentations/making-games/slides.md index aa3b909..d7e60e1 100644 --- a/presentations/making-games/slides.md +++ b/presentations/making-games/slides.md @@ -14,7 +14,7 @@ Sources: [Sickhouse's 2026 theme](https://www.sickhouse.nl/festival/dream-no-ret --- - + ## A console built around a fan @@ -29,14 +29,14 @@ Sources: [Sickhouse's 2026 theme](https://www.sickhouse.nl/festival/dream-no-ret Notes: -**0:30–1:45.** Keep the hardware introduction short. Persistence of vision turns a rotating LED arm into a disc. The current game target is an ESP32-S3, with roughly 8 MB of RAM shared by the interpreter, heap and image strips. One core handles the game and another handles display work. Audio plays on the base station, not on the rotor. The photo shows a Ventilastation build, not a board identification guide. Game developers can work on a computer without owning or building the console. +**0:30–1:30.** Keep the hardware introduction short. Persistence of vision turns a rotating LED arm into a disc. The current game target is an ESP32-S3, with roughly 8 MB of RAM shared by the interpreter, heap and image strips. One core handles the game and another handles display work. Audio plays on the base station, not on the rotor. The photo shows a Ventilastation build, not a board identification guide. Game developers can work on a computer without owning or building the console. Sources: [Tutorial introduction](../../docs/vs2/tutorial/index.html), [website](../../en/). Photograph: this website's `images/pic01.jpg`. --- - + ## The screen is drawn over time @@ -51,13 +51,13 @@ Sources: [Tutorial introduction](../../docs/vs2/tutorial/index.html), [website]( Notes: -**1:45–3:15.** A conventional panel has physical pixels distributed over a surface. Ventilastation moves a line of LEDs through that surface and changes their colours at each angle. Your eye integrates the emitted light over a rotation. The output serves 256 angular columns, each with 54 radial LED samples. The renderer prepares polar framebuffers, and the output serves ready columns independently of the 30 ms game tick. Motor speed does not set your update rate. This is why physical brightness, motion and display deadlines matter even when a desktop image looks correct. The clip shows the real display. If video cannot play, the poster still gives the physical context. +**1:30–2:45.** A conventional panel has physical pixels distributed over a surface. Ventilastation moves a line of LEDs through that surface and changes their colours at each angle. Your eye integrates the emitted light over a rotation. The output serves 256 angular columns, each with 54 radial LED samples. The renderer prepares polar framebuffers, and the output serves ready columns independently of the 30 ms game tick. Motor speed does not set your update rate. This is why physical brightness, motion and display deadlines matter even when a desktop image looks correct. The clip shows the real display. If video cannot play, the poster still gives the physical context. Sources: [Physical display and renderer](../../docs/vs2/tutorial/index.html), [geometry](../../docs/vs2/tutorial/display.html). Video: this website's `images/banner.mp4`. --- - + ## The pixels form wedges @@ -71,9 +71,9 @@ Sources: [Physical display and renderer](../../docs/vs2/tutorial/index.html), [g Notes: -**3:15–4:45.** Here are four copies of our dream creature at the same depth. Their orientation follows the disc. Their antennae point inward rather than staying upright on a rectangular screen. Angular cells get physically narrower near the centre, while the LED bar determines radial spacing. A constant angular velocity covers different physical distances at different radii. Put critical text near the rim and decide whether movement means angle or depth. A garden arranged around the disc lets people tend the whole circumference. +**2:45–4:00.** Here are four copies of our dream creature at the same depth. Their orientation follows the disc. Their antennae point inward rather than staying upright on a rectangular screen. Angular cells get physically narrower near the centre, while the LED bar determines radial spacing. A constant angular velocity covers different physical distances at different radii. Put critical text near the rim and decide whether movement means angle or depth. A garden arranged around the disc lets people tend the whole circumference. -Source: [Circular geometry and sprite orientation](../../docs/vs2/tutorial/display.html). Image: the SDK desktop renderer with Dream Garden's actual assets. +Source: [Circular geometry and sprite orientation](../../docs/vs2/tutorial/display.html). Image: the emulator’s WebGL display backend with Dream Garden's actual assets. --- @@ -84,21 +84,21 @@ Source: [Circular geometry and sprite orientation](../../docs/vs2/tutorial/displ

Drift around the rim and catch seeds to grow flowers.

-

Either player can hold A to slow the shared flow.

+

Either player can hold A to slow the shared seed flow.

Passing seeds do no harm. Watching is welcome.

Inspired by Dream No Return

-Two dream creatures on the rim, golden seeds drifting outward and a ring of pink, mint and lavender flowers +Two dream creatures on the rim, golden seeds above blue-gray clouds and pink, mint and lavender flowers drifting inward
Notes: -**4:45–6:00.** Overkill's 2026 theme gives us a design prompt about dreaming and rest, with chapters including Time, Shape Shifting, Escape and Lullabies. This game is our response. A pink dreamer catches seeds. A mint friend can join. A catch creates a flower and a miss simply passes. Holding A slows both players' world. There is no score, death, countdown or difficulty ramp. B chooses a still resting scene. Ask the audience what happens when time becomes something players can share rather than a resource they must beat. Then move straight into making it. +**4:00–5:15.** Overkill's 2026 theme gives us a design prompt about dreaming and rest, with chapters including Time, Shape Shifting, Escape and Lullabies. This game is our response. A pink dreamer catches seeds. A mint friend can join. A catch creates a flower and a miss simply passes. Holding A slows the shared seed flow. Clouds keep moving at their normal pace. The garden starts empty, and flowers drift slowly inward after planting. There is no score, death, countdown or difficulty ramp. B chooses a still resting scene. Ask the audience what happens when time becomes something players can share rather than a resource they must beat. Then move straight into making it. -Sources: [Sickhouse's theme](https://www.sickhouse.nl/festival/dream-no-return-2026), [finished game source](examples/dream_garden.py). Screenshot: a populated game state rendered by the SDK desktop renderer. +Sources: [Sickhouse's theme](https://www.sickhouse.nl/festival/dream-no-return-2026), [finished game source](examples/dream_garden.py). Screenshot: a populated game state rendered by the emulator’s WebGL display backend. --- - + ## The desktop development loop @@ -119,7 +119,7 @@ Edit local files, rerun, watch the terminal. Windows uses `vs-emu.bat`. Notes: -**6:00–7:30.** Prepare dependencies before the live demonstration. The setup guide covers Linux, macOS and Windows, including MicroPython and FFmpeg. The desktop host uses CPython, while game code runs in MicroPython. Do not initialize the Retro-Go firmware submodule just to make a game. The terminal shows game prints and tracebacks. The emulator rebuilds changed image ROMs at launch. Dream Garden is included in the SDK checkout, or can be installed from the complete game download. WASD are the directions, Space is A, B is B, N is X and 1 or M exits. F1 shows the keyboard layout. Two gamepads or the second keyboard controls support shared play. The Run this checkpoint button opens the finished game in the browser. +**5:15–6:30.** Prepare dependencies before the live demonstration. The setup guide covers Linux, macOS and Windows, including MicroPython and FFmpeg. The desktop host uses CPython, while game code runs in MicroPython. Do not initialize the Retro-Go firmware submodule just to make a game. The terminal shows game prints and tracebacks. The emulator rebuilds changed image ROMs at launch. Dream Garden is included in the SDK checkout, or can be installed from the complete game download. WASD are the directions, Space is A, B is B, N toggles audio and 1 or M exits. F1 shows the keyboard layout. Two gamepads or the second keyboard controls support shared play. The Run this checkpoint button opens the finished game in the browser. Sources: [Desktop setup](../../docs/guides/desktop.html), [complete Dream Garden download](examples/dream-garden.zip), [game instructions](examples/dream-garden-guide.html). @@ -147,7 +147,7 @@ games/myname/mygame/ `main()` returns a scene. The menu icon is 64 × 30 pixels. Notes: -**7:30–8:45.** The code module has the same name as its game folder. Group and folder form the launch name, myname.mygame. The metadata explicitly declares the current API. The launcher discovers the folder without a central application list. Copy dreamer.png and menu.png from the downloaded sample, list only dreamer.png with frames: 8 in the starter YAML, and copy the starter source to code/mygame.py. The finished sample uses demos/dream_garden and code/dream_garden.py. The game download includes its code, images, YAML, icon, sound and source credits. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**6:30–7:45.** The code module has the same name as its game folder. Group and folder form the launch name, myname.mygame. The metadata explicitly declares the current API. The launcher discovers the folder without a central application list. Copy dreamer.png and menu.png from the downloaded sample, list only dreamer.png with frames: 8 in the starter YAML, and copy the starter source to code/mygame.py. The finished sample uses demos/dream_garden and code/dream_garden.py. The game download includes its code, images, YAML, icon, sound and source credits. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. Sources: [Your first game](../../docs/vs2/tutorial/first-game.html), [starter source](examples/first_game.py), [complete game](examples/dream-garden.zip). @@ -155,16 +155,19 @@ Sources: [Your first game](../../docs/vs2/tutorial/first-game.html), [starter so -## Dream Garden sprite atlas +## Dreamers, seeds, flowers and clouds -The complete Dream Garden atlas: pink and mint creatures in four poses each, golden seed animation frames, four flowers with two frames each, and eight garden details +
+Pink and mint dreamers, golden seed frames and four flower species on the original source atlas +Six fluffy cloud shapes, packed at three sizes for the flying layer +
-

Dreamers, seeds, flowers and garden details.

+

Dreamers and flowers below. Clouds and seeds above.

Notes: -**8:45–9:30.** Show the original artwork sheet before explaining its runtime format. The top row has four pink poses and the same four poses in mint for player two. Below are four seed frames, four flower species with two frames each, and eight sparse garden details. The source atlas was generated for this game and is included in the download. The preparation script crops its cells, corrects the leaning pose order, resizes them with nearest-neighbour sampling, makes alpha binary and packs horizontal strips. The dreamers become 19 × 19 pixel frames. Keep the artwork legible at that final size, especially on the moving physical display. +**7:45–8:30.** Show the original artwork sheet before explaining its runtime format. The top row has four pink poses and the same four poses in mint for player two. Below are four seed frames, four flower species with two frames each, and eight sparse garden details. The source atlas was generated for this game and is included in the download. The preparation script crops its cells, corrects the leaning pose order, resizes them with nearest-neighbour sampling, makes alpha binary and packs horizontal strips. The fourth row is unused in this experiment; a separate six-cloud atlas supplies the flying layer. The dreamers become 19 × 19 pixel frames. Keep the artwork legible at that final size, especially on the moving physical display. -Source: [Artwork provenance and preparation](examples/artwork.html). Image: Dream Garden's original `sources/source-atlas.png`, included unchanged. +Source: [Artwork provenance and preparation](examples/artwork.html). Images: Dream Garden's original source and cloud atlases, included unchanged. --- @@ -187,7 +190,7 @@ Source: [Artwork provenance and preparation](examples/artwork.html). Image: Drea Notes: -**9:30–10:30.** The console uses compiled strips rather than decoding PNGs during play. The full dreamer strip has eight 19-pixel frames, so it is 152 pixels wide. The picture shows its first four poses. The next four repeat those poses in mint for player two. YAML declares all eight frames. Related art shares a palette group. The full game adds seed, flower and garden strips plus the existing CP437 font in a separate text palette. You can replace the packed PNGs with your own pixel art. Rerun the emulator after editing assets. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**8:30–9:30.** The console uses compiled strips rather than decoding PNGs during play. The full dreamer strip has eight 19-pixel frames, so it is 152 pixels wide. The picture shows its first four poses. The next four repeat those poses in mint for player two. YAML declares all eight frames. Related art shares a palette group. The full game adds seed.png (4 frames), flowers.png (8), clouds.png (18) in its own palette group, and steel8x8.png (256) in a text palette. Add these entries to __images__.yaml as you introduce each asset. The download includes the complete YAML. You can replace the packed PNGs with your own pixel art. Rerun the emulator after editing assets. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. Sources: [Image setup](../../docs/vs2/tutorial/first-game.html), [artwork provenance and preparation](examples/artwork.html). Image: the actual packed dreamer strip, cropped to player one's poses. @@ -206,7 +209,7 @@ Sources: [Image setup](../../docs/vs2/tutorial/first-game.html), [artwork proven Notes: -**10:30–12:15.** This is the main architectural rule. Create layers, sprites, pools, tilemaps and labels in `build()`. Returning from build seals the scene. Update can move, animate, show and hide them, but creating a new sprite there raises `SceneSealedError`. Structural budgets fail at scene entry rather than in the middle of a run. Build runs again whenever the scene is re-entered, and previous drawable handles are no longer valid. State that should survive belongs in `__init__`. A nominal tick is 30 milliseconds, about 33 updates per second. Slow updates delay the next tick rather than catching up lost time. +**9:30–11:15.** This is the main architectural rule. Create layers, sprites, pools, tilemaps and labels in `build()`. Returning from build seals the scene. Update can move, animate, show and hide them, but creating a new sprite there raises `SceneSealedError`. Structural budgets fail at scene entry rather than in the middle of a run. Build runs again whenever the scene is re-entered, and previous drawable handles are no longer valid. State that should survive belongs in `__init__`. A nominal tick is 30 milliseconds, about 33 updates per second. Slow updates delay the next tick rather than catching up lost time. Sources and diagram: [Scene lifecycle](../../docs/vs2/reference/scene.html), [runtime constraints](../../docs/vs2/tutorial/index.html). @@ -237,7 +240,7 @@ def main(): ``` Notes: -**12:15–13:45.** Read this as an engine interface rather than a Python lesson. Main returns the first scene. Build creates a TUNNEL layer and a sprite by image name. X is the sprite's edge, so subtracting half its width centres the dreamer on angle zero. Update reads stable controller levels. Left minus right gives a direction and cancels opposite inputs. Wrapping keeps the stored X bounded, and the renderer also handles images crossing the seam. This is complete code once the folder, assets and metadata exist. The download has a short docstring with installation instructions. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**11:15–12:45.** Read this as an engine interface rather than a Python lesson. Main returns the first scene. Build creates a TUNNEL layer and a sprite by image name. X is the sprite's edge, so subtracting half its width centres the dreamer on angle zero. Update reads stable controller levels. Left minus right gives a direction and cancels opposite inputs. Wrapping keeps the stored X bounded, and the renderer also handles images crossing the seam. This is complete code once the folder, assets and metadata exist. The download has a short docstring with installation instructions. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. Sources: [First game](../../docs/vs2/tutorial/first-game.html), [circular display](../../docs/vs2/tutorial/display.html), [starter code](examples/first_game.py). @@ -260,9 +263,9 @@ Sources: [First game](../../docs/vs2/tutorial/first-game.html), [circular displa Notes: -**13:45–15:00.** Switch to the prepared desktop starter. Hold left, cross the seam, then hold right. Show that opposite buttons cancel. Point out the terminal and source file, then return to the deck. The goal is to make the circular coordinate model tangible before adding rules. If the emulator is unavailable, use this real renderer capture and trace the position change aloud. It shows only the starter's one sprite. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**12:45–14:00.** Switch to the prepared desktop starter. Hold left, cross the seam, then hold right. Show that opposite buttons cancel. Point out the terminal and source file, then return to the deck. The goal is to make the circular coordinate model tangible before adding rules. If the emulator is unavailable, use this real renderer capture and trace the position change aloud. It shows only the starter's one sprite. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. -Sources: [Starter source](examples/first_game.py), [first-game instructions](../../docs/vs2/tutorial/first-game.html). Screenshot: actual game assets through the SDK renderer, with all other game drawables hidden. +Sources: [Starter source](examples/first_game.py), [first-game instructions](../../docs/vs2/tutorial/first-game.html). Screenshot: actual game assets through the emulator’s WebGL display backend, running the actual steering checkpoint. --- @@ -275,7 +278,7 @@ Sources: [Starter source](examples/first_game.py), [first-game instructions](../ Notes: -**15:00–16:15.** This is the main mental adjustment for developers used to rectangular screens. The logical display has 256 angular columns and 54 radial LEDs. X zero is the bottom, then 64 is left, 128 top, and 192 right. X wraps. Y zero is the outer rim, and positive Y goes inward. A sprite crossing the angular seam stays continuous. The physical width of an angular column decreases near the centre, so a rectangular image gets squeezed there. Use `vs2.display.width` and `.height` to read the geometry, but distinguish physical LED height from tunnel depth on the next slide. +**14:00–15:15.** This is the main mental adjustment for developers used to rectangular screens. The logical display has 256 angular columns and 54 radial LEDs. X zero is the bottom, then 64 is left, 128 top, and 192 right. X wraps. Y zero is the outer rim, and positive Y goes inward. A sprite crossing the angular seam stays continuous. The physical width of an angular column decreases near the centre, so a rectangular image gets squeezed there. Use `vs2.display.width` and `.height` to read the geometry, but distinguish physical LED height from tunnel depth on the next slide. Source and diagram: [The circular display](../../docs/vs2/tutorial/display.html). @@ -295,9 +298,9 @@ Source and diagram: [The circular display](../../docs/vs2/tutorial/display.html) Notes: -**16:15–17:45.** Choose a projection per layer. The finished Dream Garden uses two TUNNEL layers for the planted garden and flying objects, plus one HUD layer for text. These dreamers use Y values 0, 24 and 48 on TUNNEL above and HUD below. Decreasing tunnel depth moves a seed outward toward the players. HUD removes perspective scaling, though angular cells still narrow near the centre. FULLSCREEN draws a centred image whose X rotates it and whose Y follows the tunnel depth curve. It accepts sprites only. Layers paint in creation order, then drawables in creation order inside each layer. The lower layer draws flowers then dreamers; the middle draws flying accents then seeds; the HUD draws last. Physical height 54 is not a tunnel off-screen threshold. +**15:15–16:45.** Choose a projection per layer. The finished Dream Garden uses two TUNNEL layers for the planted garden and flying objects, plus one HUD layer for text. These dreamers use Y values 0, 24 and 48 on TUNNEL above and HUD below. Decreasing tunnel depth moves a seed outward toward the players. HUD removes perspective scaling, though angular cells still narrow near the centre. FULLSCREEN draws a centred image whose X rotates it and whose Y follows the tunnel depth curve. It accepts sprites only. Layers paint in creation order, then drawables in creation order inside each layer. The lower layer draws flowers then dreamers; the middle draws clouds then seeds; the HUD draws last. Physical height 54 is not a tunnel off-screen threshold. -Source: [Projections and draw order](../../docs/vs2/tutorial/display.html). Image: actual dreamer assets in two SDK scene layers. +Source: [Projections and draw order](../../docs/vs2/tutorial/display.html). Image: actual dreamer assets in two scene layers through the emulator’s WebGL display backend. --- @@ -307,24 +310,26 @@ Source: [Projections and draw order](../../docs/vs2/tutorial/display.html). Imag Run this checkpoint -```python -direction = joy1.held(LEFT) - joy1.held(RIGHT) -self.dreamer.x = (self.dreamer.x + direction) % vs2.display.width - -if joy1.held(A): - self.dreamer.frame = ASLEEP +
+
+
if sleeping:
+    dreamer.frame = ASLEEP
 elif direction > 0:
-    self.dreamer.frame = LEFT_POSE
+    dreamer.frame = LEFT_POSE
 elif direction < 0:
-    self.dreamer.frame = RIGHT_POSE
+    dreamer.frame = RIGHT_POSE
 else:
-    self.dreamer.frame = AWAKE
-```
-
-Player two uses the same poses, starting at frame 4.
+    dreamer.frame = AWAKE
+
+
+

Frame changes reuse the same sprite.

+

Flowers alternate between paired frames.

+

Golden seeds wait a random interval, then run a short glow sequence.

+
+
Notes: -**17:45–19:15.** Property writes update existing renderer records. Frame picks artwork from the strip without creating a new sprite. Dreamers lean when moving and close their eyes while holding A. The finished helper takes a controller and a base frame, so the second player reuses the same behaviour. Flower pairs pulse using the garden's slow clock. Read available frame counts from sprite.image.frames. Frame and visibility are independent, useful for preparing the second player before joining. Invalid frames fail immediately. Think of the display as a retained graph whose properties change during play. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**16:45–18:15.** Property writes update existing renderer records. Frame picks artwork from the strip without creating a new sprite. Dreamers lean when moving and close their eyes while holding A. The finished helper takes a controller and a base frame, so the second player reuses the same behaviour. Flower pairs pulse using a steady clock. Early checkpoints cycle the seed frames together. The completed game gives every live seed its own random 90–240 tick wait, then plays frames 1, 2, 3, 2, 1, 0 for six ticks each. Reset the wait after a pulse and remove its state when the seed retires. This makes the seeds glow occasionally rather than flashing together. The final checkpoint includes that helper. Read available frame counts from sprite.image.frames. Frame and visibility are independent, useful for preparing the second player before joining. Invalid frames fail immediately. Think of the display as a retained graph whose properties change during play. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. Sources: [Sprites](../../docs/vs2/tutorial/sprites.html), [Dream Garden's steer helper](examples/dream_garden.py). @@ -337,11 +342,11 @@ Sources: [Sprites](../../docs/vs2/tutorial/sprites.html), [Dream Garden's steer Run this checkpoint ```python -# In build(): -self.seeds = self.world.sprite_pool("seed.png", count=16) +# In build(), before creating the dreamer: self.flowers = self.world.sprite_pool( "flowers.png", count=24, on_empty=vs2.RECYCLE ) +self.seeds = self.world.sprite_pool("seed.png", count=16) # During play: seed = self.seeds.spawn(x=100, y=160) @@ -352,7 +357,7 @@ if seed is None: A full flower pool reuses its oldest live flower. Notes: -**19:15–20:45.** Reservation happens once in build. Spawn reveals and positions a reserved sprite. A full seed pool returns None and drops that particular spawn. Flowers instead recycle their oldest live slot, so new growth continues without an ever-growing graph. Both choices are part of the design: the garden can keep changing, yet nothing demands a perfect catch rate. Pools count at their full reserved size, including hidden slots. Iteration visits live sprites and supports retiring the current one. Pooling renderer objects does not remove all unrelated Python allocations. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**18:15–19:45.** Reservation happens once in build. Flowers begin hidden, so the garden starts empty. Create the dreamer after the flowers to draw the player above the planted garden. The flying layer comes at the next step. Spawn reveals and positions a reserved sprite. A full seed pool returns None and drops that particular spawn. Flowers instead recycle their oldest live slot, so new growth continues without an ever-growing graph. Both choices are part of the design: the garden can keep changing, yet nothing demands a perfect catch rate. Pools count at their full reserved size, including hidden slots. Iteration visits live sprites and supports retiring the current one. Pooling renderer objects does not remove all unrelated Python allocations. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. Source: [Sprite pools](../../docs/vs2/tutorial/pools.html), [sample source](examples/dream_garden.py). @@ -377,10 +382,10 @@ def spawn_seed(self): self.call_later(self.spawn_ms, self.spawn_seed) ``` -1.8 seconds drifting. 4 seconds while holding A. +A seed every 1.8 seconds. RELAX changes the gap later. Notes: -**20:45–22:00.** The callback rearms itself with the current gap. A held A changes spawn_ms, but a seed that is already scheduled keeps its deadline. The next gap uses the slower rhythm. Seed movement reacts immediately in update. This avoids rebuilding or continually replacing timer records while a button is held. A full pool simply skips a spawn. Scene-owned timers disappear when the scene closes or suspends, so an old callback cannot plant into the rest screen. Timers suit occasional events, not the animation loop. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**19:45–21:00.** The callback rearms itself with the current gap. This checkpoint uses 1800 ms. At the HUD checkpoint, held A changes spawn_ms to 4000 ms, but a seed that is already scheduled keeps its deadline. The next gap uses the slower rhythm. Seed movement reacts immediately in update. This avoids rebuilding or continually replacing timer records while a button is held. A full pool simply skips a spawn. Scene-owned timers disappear when the scene closes or suspends, so an old callback cannot plant into the rest screen. Timers suit occasional events, not the animation loop. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. Sources: [Scene timers](../../docs/vs2/reference/scene.html), [sample spawn_seed](examples/dream_garden.py). @@ -393,47 +398,88 @@ Sources: [Scene timers](../../docs/vs2/reference/scene.html), [sample spawn_seed Run this checkpoint ```python +# In build(), in this order: +self.world = self.layer("world", projection=vs2.TUNNEL) +self.air = self.layer("air", projection=vs2.TUNNEL) +self.flowers = self.world.sprite_pool( + "flowers.png", count=24, on_empty=vs2.RECYCLE +) +self.dreamer = self.world.sprite("dreamer.png", y=0) +self.seeds = self.air.sprite_pool("seed.png", count=16) + +# In the seed movement loop: if seed.overlaps(self.dreamer): self.bloom(seed) -elif seed.y < -seed.image.height: - self.seeds.despawn(seed) ``` -X wraps across column zero. Y does not wrap. - -The full game checks both active dreamers. +Flowers below players. Flying seeds above both. Notes: -**22:00–23:30.** Collision tests use axis-aligned bounds in a layer's circular coordinate system. They understand the angular seam, so a dreamer crossing zero can still catch a seed without a duplicate actor. They are not pixel-perfect physical tests on the fan. The actual loop first decreases seed depth, then checks either active dreamer. Bloom retires the seed and plants a flower at the dreamer’s depth. Its centre is displaced by 75% of the dreamer’s width in the direction they last moved toward. Each player remembers their own direction after stopping; opposite inputs cancel without changing it. Before moving, the fallback is left. Flowers drift toward the centre at only 8% of seed speed. A missed seed retires after passing the rim. The response changes the garden without ending play or recording failure. This is a small code change with a large effect on the experience. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**21:00–22:30.** Collision tests use axis-aligned bounds in a layer's circular coordinate system. They understand the angular seam, so a dreamer crossing zero can still catch a seed without a duplicate actor. They are not pixel-perfect physical tests on the fan. The actual loop first decreases seed depth, then checks either active dreamer. Bloom retires the seed and plants a flower at the dreamer’s depth. Its centre is displaced by 75% of the dreamer’s width. Moving catches plant in the current direction. After stopping, the first catch uses that direction, then later catches alternate sides. Moving again resets the sequence, separately for each player. Opposite inputs count as stopping. Before moving, the first catch defaults to left. The later planting slide shows the helper that implements this rule. Flowers drift toward the centre at only 8% of normal seed speed. A missed seed retires after passing the rim. The response changes the garden without ending play or recording failure. This is a small code change with a large effect on the experience. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. Sources: [Collision API](../../docs/vs2/tutorial/sprites.html), [Dream Garden's move_seeds and bloom](examples/dream_garden.py). +--- + + +## Planting follows the player's movement + +Run this checkpoint + +```python +# Movement resets the next planting side. +self.moving = bool(steer) +if steer: + self.plant_side = steer + +# In bloom(): +centre = self.dreamer.x + self.dreamer.width // 2 +centre += self.plant_side * self.dreamer.width * 0.75 +self.flowers.spawn( + x=(centre - self.image("flowers.png").width // 2) % 256, + y=self.dreamer.y, frame=randrange(4) * 2 +) +if not self.moving: + self.plant_side = -self.plant_side +``` + +Stopped catches alternate sides until movement resumes. + +Notes: +**22:30–23:45.** This checkpoint has one player. Movement resets plant_side every tick. Once stationary, bloom flips it after each catch, so the first follows the last movement and subsequent catches alternate. When we add the second player, the finished source keeps separate plant_side1 and plant_side2 values. Catching plants at the same Y as that player, displaced by 75% of its width. The checkpoint starts without flowers. The 24-slot flower pool recycles the oldest when full, and missed seeds disappear harmlessly beyond the rim. Test catches across the angular seam as well as in the middle of a sprite. The player stays above the flowers because its sprite was created after the flower pool. + +Sources: [Allocation and target checks](../../docs/vs2/tutorial/budgets.html), [sample source](examples/dream_garden.py). + --- -## The drifting garden is one tilemap +## Fluffy clouds can span several tiles Run this checkpoint
-
self.ground = world.tilemap(
-    "garden.png",
-    columns=16, rows=16,
+
self.ground = self.air.tilemap(
+    "clouds.png",
+    columns=16, rows=22,
     view_width=256,
     view_height=160
 )
-self.ground[1, 2] = vs2.EMPTY_TILE
-

Sparse accents leave room for the seeds.

+# A large cloud uses frames 3–8 in a 3×2 block. +self.ground[11, 4] = 3 +self.ground[12, 4] = 4 +# Reserve seeds after the clouds: +self.seeds = self.air.sprite_pool("seed.png", count=16)
+

1×1, 2×1 or 3×2 cells. Seeds draw above clouds.

-Sparse mint roots, pink petals and lavender moons on a dark circular garden, with a pink dreamer at the rim +Muted fluffy clouds in different sizes drift over a dark circular garden, with a pink dreamer at the rim
Notes: -**23:30–24:45.** The garden tiles are frames in another strip. One byte per cell chooses a frame or EMPTY_TILE. Dimensions describe the stored grid and its viewport, not a conventional screen resolution. Build fills a sparse repeating pattern of roots, petals and moons. Transparent cells preserve dark space so seeds remain visible. The map comes first in the middle layer, followed by seeds; flowers and players sit below it. Smaller, slightly darker flying details distinguish this flow from the planted garden. A tilemap fits repeated terrain better than dozens of separate sprites, although its rendering cost still depends on the actual viewport and content. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**23:45–25:00.** Cloud tiles are frames in another strip, with a separate palette group. One byte per cell chooses a frame or EMPTY_TILE. Dimensions describe the stored grid and its viewport, not a conventional screen resolution. Build places six fluffy cloud shapes in a sparse repeating pattern. Source artwork is packed at 16×16, 32×16 and 48×32 pixels, then cut into adjoining 16-pixel tiles. Medium clouds use two cells; large clouds use a 3×2 block whose frames are placed row by row. There is no padding between the tiles, so the puffs stay continuous across cell boundaries. Transparent cells preserve dark space so seeds remain visible. The map comes first in the middle layer, followed by seeds; flowers and players sit below it. Subdued blue-gray clouds distinguish this flow from the planted garden, while golden seeds remain bright and catchable. A tilemap fits repeated terrain better than dozens of separate sprites, although its rendering cost still depends on the actual viewport and content. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. -Source: [Tilemaps](../../docs/vs2/tutorial/tilemaps-and-text.html). Image: the sample's ground tilemap and dreamer through the SDK renderer. +Source: [Tilemaps](../../docs/vs2/tutorial/tilemaps-and-text.html). Image: the actual tilemap checkpoint through the emulator’s WebGL display backend. --- @@ -444,26 +490,28 @@ Source: [Tilemaps](../../docs/vs2/tutorial/tilemaps-and-text.html). Image: the s Run this checkpoint ```python -pattern_height = 6 * self.ground.tile_height -self.scroll = (self.scroll + self.speed) % pattern_height +pattern_height = 12 * self.ground.tile_height +self.scroll = (self.scroll + 0.45) % pattern_height self.ground.view_y = self.scroll for seed in self.seeds: - seed.y -= self.speed + seed.y -= self.speed * 0.85 +for flower in self.flowers: + flower.y += 0.45 * 0.85 * 0.08 ``` -The garden repeats every six tile rows. +The clouds repeat every twelve tile rows. -The seeds share its pace. +Seeds move outward. Flowers drift inward at 8% of normal seed speed. Notes: -**24:45–26:00.** The map is stored data plus a movable view. Wrapping a six-row pattern avoids rewriting cells every tick. Drift speed is 0.45 depth units per tick, and holding A reduces it to 0.12. Seeds decrease Y by the same speed, sharing the background's movement. This ties the visible world's rhythm to the player's choice. A non-repeating world would refill a row when a tile passes. Flower animation also uses dream_time, advanced by the same speed and wrapped to keep it bounded. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**25:00–26:15.** The map is stored data plus a movable view. Wrapping a twelve-row pattern avoids rewriting cells every tick. Clouds keep scrolling at 0.45 depth units per tick. Seeds decrease Y at 0.3825 normally (85% of cloud speed), or 0.102 while holding A. Only the seeds slow down; clouds and planted flower motion stay steady. Seed sprites are created after the cloud tilemap, so they always paint on top. The stored map has 22 rows: ten for the viewport plus twelve for a complete scroll period, so wrapping cannot expose an empty tail. This ties the visible world's rhythm to the player's choice. A non-repeating world would refill a row when a tile passes. Flower animation uses dream_time, advanced by the constant cloud speed and wrapped to keep it bounded. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. Sources: [Tilemap scrolling](../../docs/vs2/tutorial/tilemaps-and-text.html), [Dream Garden update](examples/dream_garden.py). --- - + ## The HUD sets the mood @@ -484,7 +532,7 @@ Low Y keeps text legible. Both flips make top text upright. Notes: -**26:00–27:30.** A label uses a tilemap record rather than a sprite per character. Five eight-column glyphs span forty columns, so x 108 centres the label on top angle 128. At the top, the native inward-facing orientation makes text upside down unless both axes flip. Y 1 keeps it near the rim. RELAX appears while A is held, then slides outward past the rim over 450 ms when released. A new request resets its position and cancels the exit animation. The final game adds a second HUD label for two-second sound feedback. This uses the SDK's existing full CP437 font. A short word also avoids making a large dashboard on a small disc. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**26:15–27:30.** A label uses a tilemap record rather than a sprite per character. Five eight-column glyphs span forty columns, so x 108 centres the label on top angle 128. At the top, the native inward-facing orientation makes text upside down unless both axes flip. Y 1 keeps it near the rim. RELAX appears while A is held, then slides outward past the rim over 450 ms when released. A new request resets its position and cancels the exit animation. The final game adds a second HUD label for two-second sound feedback. This uses the SDK's existing full CP437 font. A short word also avoids making a large dashboard on a small disc. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. Source: [Labels and flips](../../docs/vs2/tutorial/tilemaps-and-text.html), [sample source](examples/dream_garden.py). @@ -524,7 +572,7 @@ Sources: [Scene reference](../../docs/vs2/reference/scene.html), [Title, Garden ```python joy1.held(LEFT) # continuous steering -joy2.held(A) # the friend asks for slower time +joy2.held(A) # the friend asks for slower seeds joy1.just_pressed(B) # choose the rest scene ``` @@ -533,7 +581,7 @@ The second dreamer joins an already reserved sprite. Y or Back returns to the launcher on either controller. Notes: -**29:00–30:00.** Both controllers expose stable level and edge reads for the tick. The second player's first steering input or A press reveals the mint dreamer reserved in build. No new sprite allocation is needed. Either active player can hold A to affect the whole garden. Y and Back remain the shared arcade's exit route. Keyboard player two uses Left/Right arrows to steer, numpad 0 for A, numpad 1 for B and numpad 2 for the X action, while two gamepads use their normal mappings. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**29:00–30:00.** Both controllers expose stable level and edge reads for the tick. The second player's first steering input or A press reveals the mint dreamer reserved in build. No new sprite allocation is needed. Either active player can hold A to slow the seed flow for both players. Y and Back remain the shared arcade's exit route. Keyboard player two uses Left/Right arrows to steer, numpad 0 for A, numpad 1 for B and numpad 2 for the X action, while two gamepads use their normal mappings. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. Sources: [Controls reference](../../docs/vs2/reference/services.html#vs2-controls), [game controls](examples/dream-garden-guide.html). @@ -564,6 +612,34 @@ Sources: [Audio API](../../docs/vs2/reference/services.html#vs2-audio), [origina --- + + +## Each seed has its own glow rhythm + +Run this checkpoint + +```python +# Each live seed owns [waiting_ticks, glow_tick]. +state = self.seed_glows[seed] +if state[0] > 0: + state[0] -= 1 + seed.frame = 0 +else: + seed.frame = GLOW_FRAMES[state[1] // 6] + state[1] += 1 +``` + +Wait 90–240 ticks, then pulse through frames 1, 2, 3, 2, 1, 0. + +Retiring a seed also removes its glow state. + +Notes: +**31:00–32:30.** The first seed checkpoints cycle frames together to demonstrate animation. The finished game replaces that common clock with glow_seed, called once per live seed from move_seeds. A new seed gets [randrange(90, 241), 0]. The excerpt shows its waiting and active phases. Each frame lasts six ticks, so a pulse lasts 36 ticks. At the end, reset the wait to another random 90–240 ticks and the glow tick to zero. Otherwise the next frame lookup would exceed the six-element sequence. retire_seed removes the dictionary entry before returning the sprite to its pool, ensuring a reused slot gets a fresh wait. The wait and animation use game ticks, so holding A slows travel without synchronizing the glows. Flowers use a separate steady paired-frame clock. Seeds sit above clouds in the middle layer, so the small golden pulse stays visible. The final browser checkpoint runs exactly the completed source. + +Source: [Finished glow_seed and retire_seed helpers](examples/dream_garden.py). + +--- + ## Players can change the pace @@ -574,21 +650,21 @@ Sources: [Audio API](../../docs/vs2/reference/services.html#vs2-audio), [origina
slow = slow1 or slow2  # either active player
 # Arm held-A after releasing the start press.
-self.speed = 0.12 if slow else 0.45
+self.speed = 0.12 if slow else 0.45  # seeds only
 self.spawn_ms = 4000 if slow else 1800
-

One request changes the shared world.

+

Either player can relax the shared seed flow.

The populated garden with the pink dreamer asleep and RELAX visible at the top Notes: -**31:00–31:45.** The final mechanic is deliberately small. Either player's request slows every seed, the background and flower animation. Spawn intervals adapt at the next scheduled callback. Steering stays responsive. Nothing raises the pace because of a score or elapsed time. For the festival, the interesting discussion is what this asks people to do together. They can catch seeds, make room for a friend, hold time slower or simply watch. Invite the audience to imagine another interaction with shared time. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. +**32:30–33:15.** The final mechanic is deliberately small. Either player's request slows every seed and lengthens future seed spawn gaps. Clouds and planted flowers retain their normal drift and animation; seeds remain above the clouds. Spawn intervals adapt at the next scheduled callback. Steering stays responsive. Nothing raises the pace because of a score or elapsed time. For the festival, the interesting discussion is what this asks people to do together. They can catch seeds, make room for a friend, hold time slower or simply watch. Invite the audience to imagine another interaction with shared time. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. -Source: [Garden update](examples/dream_garden.py). Screenshot: an actual held-A game state through the SDK renderer. +Source: [Garden update](examples/dream_garden.py). Screenshot: an actual held-A game state through the emulator’s WebGL display backend. --- - + ## The display graph has explicit budgets @@ -602,35 +678,12 @@ Source: [Garden update](examples/dream_garden.py). Screenshot: an actual held-A Read the target limits from `vs2.limits`. Notes: -**31:45–33:15.** Forty-two sprites means two reserved dreamers, sixteen seeds and twenty-four flowers. Hidden player two and empty pool slots still count. The flying accents, RELAX label and audio notice use three tilemap records. The lower layer draws planted flowers then dreamers; the middle draws flying accents then seeds; the HUD paints above both. Five strips include the full font shared by the other scenes. Layers and records are separate resources. VS2 checks the graph at entry and reports the layers contributing to an exceeded budget. Tilemaps can be costly even when their record count is low. Random seed glow uses a small state entry per live seed, removed on retirement. Our bounded garden can run indefinitely without growing its renderer graph. Target timing and heap still need measurement on the console. +**33:15–34:30.** Forty-two sprites means two reserved dreamers, sixteen seeds and twenty-four flowers. Hidden player two and empty pool slots still count. The cloud tilemap, RELAX label and audio notice use three tilemap records. The lower layer draws planted flowers then dreamers; the middle draws clouds then seeds; the HUD paints above both. Five strips include the full font shared by the other scenes. Layers and records are separate resources. VS2 checks the graph at entry and reports the layers contributing to an exceeded budget. Tilemaps can be costly even when their record count is low. Random seed glow uses a small state entry per live seed, removed on retirement. Our bounded garden can run indefinitely without growing its renderer graph. Target timing and heap still need measurement on the console. Sources: [Resource limits](../../docs/vs2/reference/services.html#vs2-limits), [sample build](examples/dream_garden.py). --- - - -## Heap stability is part of game correctness - -Run this checkpoint - -```python -seed.y -= self.speed -self.flowers.spawn(x=angle, y=player.y, frame=kind * 2) -self.seeds.despawn(seed) -``` - -Sealing controls render-object creation, not every Python allocation. - -Long play still needs a target check. - -Notes: -**33:15–34:30.** A garden that welcomes watching should survive a long session. Pools bound the renderer graph, but Python temporaries can still accumulate on the target. Garbage collection happens around scene boundaries, while the desktop emulator has more headroom and collects automatically. Keep the update loop short and reuse handles. The sample bounds scroll and dream_time and rewrites its mood label only on a state change. Each live seed has a bounded glow timer state, removed when it retires. It snapshots flower angles, depths and frames only when leaving for Rest. Those choices do not prove zero heap growth or sufficient hardware frame timing. Measure sustained play and the full garden on the rotor before an exhibition. The Run this checkpoint button opens the finished game in the browser. - -Sources: [Allocation and target checks](../../docs/vs2/tutorial/budgets.html), [sample source](examples/dream_garden.py). - ---- - ## The disc decides what is readable @@ -645,9 +698,9 @@ Sources: [Allocation and target checks](../../docs/vs2/tutorial/budgets.html), [ Notes: -**34:30–36:00.** The emulator preserves renderer semantics, but cannot judge physical brightness or enforce the real output's column deadline. These copies of RELAX use HUD positions 1 and 35. The inner word compresses because angular cells have less circumference. Keep critical instructions near the rim and let the centre convey depth or drifting effects. Our leaves, blossoms and seeds are deliberately sparse. Test their contrast and recognizability on the real LEDs, along with a crowded garden and a sustained session. This is how the display geometry feeds back into the game rather than remaining an introduction slide. +**34:30–36:00.** The emulator preserves renderer semantics, but cannot judge physical brightness or enforce the real output's column deadline. These copies of RELAX use HUD positions 1 and 35. The inner word compresses because angular cells have less circumference. Keep critical instructions near the rim and let the centre convey depth or drifting effects. Our clouds, blossoms and seeds are deliberately sparse. Test their contrast and recognizability on the real LEDs, along with a crowded garden and a sustained session. This is how the display geometry feeds back into the game rather than remaining an introduction slide. -Source: [Hardware and legibility](../../docs/vs2/tutorial/budgets.html). Image: actual font labels through the SDK renderer. +Source: [Hardware and legibility](../../docs/vs2/tutorial/budgets.html). Image: actual font labels through the emulator’s WebGL display backend. --- @@ -668,9 +721,9 @@ Source: [Hardware and legibility](../../docs/vs2/tutorial/budgets.html). Image: Notes: -**36:00–38:00.** Run the finished game. A begins. Start with no flowers. Show the dreamer moving and deliberately catch seeds to grow flowers at the same depth, on the side the player last moved toward. Stop and catch another to show that the direction is remembered. Point out their slow inward drift and the flying seeds’ occasional glow. Let another seed pass to show that it causes no harm. Invite a second person or use Left/Right arrows and numpad 0 to reveal the mint friend. Have either player hold A and point out the RELAX label and slower background. The lullaby starts on; use X or keyboard N to toggle it and the bloom sounds. Show the two-second NOISY/QUIET notice. B chooses Rest, preserving the flowers while stopping audio. A begins a fresh dream. M or 1 returns to the launcher. If hardware is available, show the same game without spending the demo on flashing. Otherwise use the screenshots. Keep source open at build, move_seeds and update. The Run this checkpoint button opens the finished game in the browser. +**36:00–38:00.** Run the finished game. A begins. Start with no flowers. Show the dreamer moving and deliberately catch seeds to grow flowers at the same depth, on the side the player last moved toward. Stop and catch several: the first follows the last movement direction, then plants alternate sides. Move again to reset the sequence. Point out their slow inward drift and the flying seeds’ occasional glow. Let another seed pass to show that it causes no harm. Invite a second person or use Left/Right arrows and numpad 0 to reveal the mint friend. Have either player hold A and point out the RELAX label and slower seeds against steadily moving clouds. The lullaby starts on; use X or keyboard N to toggle it and the bloom sounds. Show the two-second NOISY/QUIET notice. B chooses Rest, preserving the flowers while stopping audio. A begins a fresh dream. M or 1 returns to the launcher. If hardware is available, show the same game without spending the demo on flashing. Otherwise use the screenshots. Keep source open at build, move_seeds and update. The Run this checkpoint button opens the finished game in the browser. -Sources: [Finished source](examples/dream_garden.py), [game instructions](examples/dream-garden-guide.html), [complete download](examples/dream-garden.zip). Screenshot: the actual Rest scene through the SDK renderer. +Sources: [Finished source](examples/dream_garden.py), [game instructions](examples/dream-garden-guide.html), [complete download](examples/dream-garden.zip). Screenshot: the actual Rest scene through the emulator’s WebGL display backend. --- diff --git a/tools/capture-presentation.mjs b/tools/capture-presentation.mjs new file mode 100644 index 0000000..2c0b060 --- /dev/null +++ b/tools/capture-presentation.mjs @@ -0,0 +1,78 @@ +// Capture slide images with the SDK's actual WebGL scene and LED renderers. +// Install playwright, then: +// python3 tools/presentation-scenes.py --sdk vsdk --out /tmp/garden-scenes.json +// node tools/capture-presentation.mjs vsdk /tmp/garden-scenes.json +// CHROMIUM can select an installed Chrome executable. +import fs from 'node:fs'; +import http from 'node:http'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +const { chromium } = createRequire(import.meta.url)('playwright'); +const sdk = path.resolve(process.argv[2] || 'vsdk'); +const frames = JSON.parse(fs.readFileSync(process.argv[3], 'utf8')); +const out = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../presentations/making-games/assets'); +const html = ``; +const server = http.createServer((req, res) => { + const pathname = new URL(req.url, 'http://localhost').pathname; + if (pathname === '/') { res.setHeader('Content-Type', 'text/html'); res.end(html); return; } + const file = path.resolve(sdk, '.' + pathname); + if (!file.startsWith(path.join(sdk, 'web') + path.sep) || !fs.existsSync(file)) { + res.writeHead(404); res.end(); return; + } + res.setHeader('Content-Type', 'text/javascript'); + res.end(fs.readFileSync(file)); +}); +await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); +let browser; +try { + browser = await chromium.launch({executablePath:process.env.CHROMIUM || undefined, + headless:true, args:['--no-sandbox']}); + const page = await browser.newPage({viewport:{width:900,height:900}, deviceScaleFactor:1}); + page.on('pageerror', error => console.error(error)); + await page.goto(`http://127.0.0.1:${server.address().port}/`); + await page.waitForFunction(() => typeof window.captureScene === 'function'); + const records = []; + for (const frame of frames) { + const renderer = await page.evaluate(frame => window.captureScene(frame), frame); + await page.locator('canvas').screenshot({path:path.join(out, frame.name)}); + const sha256 = crypto.createHash('sha256').update(fs.readFileSync(path.join(out, frame.name))).digest('hex'); + records.push({file:frame.name, description:frame.description, ...renderer, sha256}); + console.log(`Captured ${frame.name} with ${renderer.displayBackend} and ${renderer.sceneBackend}`); + } + const sourceSha256 = crypto.createHash('sha256').update(fs.readFileSync(path.join(sdk, + 'games/demos/dream_garden/code/dream_garden.py'))).digest('hex'); + fs.writeFileSync(path.join(out, 'captures.json'), JSON.stringify({sourceSha256,captures:records},null,2)+'\n'); +} finally { + if (browser) await browser.close(); + await new Promise(resolve => server.close(resolve)); +} diff --git a/tools/presentation-scenes.py b/tools/presentation-scenes.py new file mode 100644 index 0000000..4a83b8a --- /dev/null +++ b/tools/presentation-scenes.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Export real SDK scenes and packed assets for the WebGL slide captures. + +No image drawing happens here. capture-presentation.mjs renders these payloads +with the SDK's WebGL scene compositor and LED display backend. +""" +import argparse +import base64 +import importlib.util +import json +import os +import random +import sys +from pathlib import Path + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--sdk', type=Path, required=True) +parser.add_argument('--out', type=Path, required=True) +options = parser.parse_args() +SDK = options.sdk.resolve() +options.out = options.out.resolve() +os.chdir(SDK) +sys.path[:0] = [str(SDK / 'tests'), str(SDK)] +import test_dream_garden as fixture +import vs2 +from ventilastation import api_guard +from ventilastation.app_loader import load_app +from ventilastation.director import configure_runtime, director, reset_runtime + +frames = [] + + +def step(one=0, two=0, count=1): + for _ in range(count): + comms = director.platform.comms + comms.next_joy2 = lambda: two + comms.next_extra = lambda: 0 + comms.push_input(bytes([one])) + director.step_once() + + +def start(checkpoint=None): + reset_runtime() + api_guard.reset() + random.seed(2026) + runtime = configure_runtime('headless') + load_rom = runtime.load_rom + runtime.load_rom = lambda name: load_rom(str(SDK / 'apps/micropython' / name)) + load_app('demos.dream_garden') + if checkpoint: + file = SDK / 'games/demos/dream_garden/code/checkpoints' / (checkpoint + '.py') + spec = importlib.util.spec_from_file_location('capture_checkpoint', file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + director.scene_stack[-1].switch(module.main()) + step() + else: + step(fixture.A) + step() + scene = director.scene_stack[-1] + scene.pending_calls.clear() + return scene + + +def capture(scene, name, description): + runtime = director + encode = lambda data: base64.b64encode(data).decode('ascii') + frames.append(dict(name=name, description=description, + scene=encode(vs2.export_scene_payload(scene)), + palette=encode(runtime.palette_data), + strips=[dict(slot=slot, data=encode(data)) + for slot, data in runtime._stripe_buffers.items()])) + + +garden = start() +capture(garden, 'garden-start.png', 'Finished game starts with no planted flowers.') +step(0, fixture.LEFT) +for index in range(12): + player = garden.dreamer if index % 2 == 0 else garden.friend + player.x = (index * 21 - player.width // 2) % 256 + garden.bloom(garden.seeds.spawn(x=player.x, y=10), player) + step(count=90) +garden.dreamer.x = 247 +garden.friend.x = 55 +for index, depth in enumerate((20, 40, 60, 80, 110, 130, 150)): + garden.seeds.spawn(x=(index * 37 + 20) % 256, y=depth, frame=index % 4) +capture(garden, 'dream-garden.png', 'Populated final game, grown through its bloom handler.') +step(fixture.A) +capture(garden, 'shared-rest.png', 'Player one holds A: RELAX, sleeping pose and slower seeds.') +step(fixture.B) +capture(director.scene_stack[-1], 'dream-rest.png', 'Actual final Rest scene preserves the planted flowers.') +capture(start('01-steering'), 'first-game.png', 'Actual first steering checkpoint, with only one dreamer.') +capture(start('06-tiles'), 'tilemaps.png', 'Actual cloud tilemap checkpoint before scrolling.') + + +class Angles(vs2.Scene): + def build(self): + world = self.layer('world', projection=vs2.TUNNEL) + for x in (0, 64, 128, 192): + world.sprite('dreamer.png', x=x - 9, y=0) + + +class Projections(vs2.Scene): + def build(self): + for name, projection, centre in (('tunnel', vs2.TUNNEL, 128), ('hud', vs2.HUD, 0)): + layer = self.layer(name, projection=projection) + for index, y in enumerate((0, 24, 48)): + layer.sprite('dreamer.png', x=centre + index * 32 - 41, y=y) + + +class Legibility(vs2.Scene): + def build(self): + hud = self.layer('hud', projection=vs2.HUD) + for y in (1, 35): + hud.label('steel8x8.png', columns=5, x=108, y=y, + flip_x=True, flip_y=True, text='RELAX') + + +for cls, filename in ((Angles, 'display-angles.png'), + (Projections, 'display-projections.png'), + (Legibility, 'budgets-legibility.png')): + start().switch(cls()) + step() + capture(director.scene_stack[-1], filename, 'Geometry example using real Dream Garden assets.') +options.out.write_text(json.dumps(frames)) +reset_runtime() +print(f'Exported {len(frames)} scenes to {options.out}') diff --git a/tools/test-presentation.py b/tools/test-presentation.py index 8aa4a5e..1ca8a31 100644 --- a/tools/test-presentation.py +++ b/tools/test-presentation.py @@ -2,6 +2,8 @@ """Check talk pacing, hosted assets, and the runnable first-game example.""" import argparse import importlib.util +import hashlib +import json from html.parser import HTMLParser from pathlib import Path import re @@ -124,6 +126,23 @@ def check_checkpoints(site, sdk): print('All Python slides launch a published cumulative checkpoint; 11 stages match the SDK') +def check_webgl_captures(sdk): + assets = TALK / 'assets' + record = json.loads((assets / 'captures.json').read_text()) + source = sdk / 'games/demos/dream_garden/code/dream_garden.py' + assert record['sourceSha256'] == hashlib.sha256(source.read_bytes()).hexdigest() + captures = {item['file']: item for item in record['captures']} + expected = {'dream-garden.png', 'shared-rest.png', 'dream-rest.png', + 'first-game.png', 'tilemaps.png', 'display-angles.png', + 'display-projections.png', 'budgets-legibility.png', 'garden-start.png'} + assert set(captures) == expected + for filename, capture in captures.items(): + assert capture['displayBackend'] == 'WebGL2', filename + assert capture['sceneBackend'] == 'WebGL shader', filename + assert capture['sha256'] == hashlib.sha256((assets / filename).read_bytes()).hexdigest(), filename + print('All game and display captures match the recorded WebGL output and completed game source') + + if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--site', type=Path, required=True) @@ -134,3 +153,4 @@ def check_checkpoints(site, sdk): check_site(options.site.resolve(), options.baseurl.rstrip('/'), options.sdk.resolve()) check_starter(options.sdk.resolve()) check_checkpoints(options.site.resolve(), options.sdk.resolve()) + check_webgl_captures(options.sdk.resolve()) diff --git a/vsdk b/vsdk index b921eee..446e540 160000 --- a/vsdk +++ b/vsdk @@ -1 +1 @@ -Subproject commit b921eee8270219acc30efa65da577d89e23b5296 +Subproject commit 446e5404c39d33d44aaacb9cc556f6ec2f01e2d1