diff --git a/presentations/making-games/assets/captures.json b/presentations/making-games/assets/captures.json index b49ecf4..b97beac 100644 --- a/presentations/making-games/assets/captures.json +++ b/presentations/making-games/assets/captures.json @@ -1,6 +1,16 @@ { "sourceSha256": "c2ed572c63a5cf8d4935104a1df49251239306182b9686eb08e66ae6ca688373", "captures": [ + { + "file": "garden-title.png", + "description": "Actual title scene of the finished game.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "87b278c70c12d432aa3e8930d2c7113b428b9f069317d56f5c3267d3e1562e52" + }, { "file": "garden-start.png", "description": "Finished game starts with no planted flowers.", @@ -61,6 +71,16 @@ "height": 900, "sha256": "7de9abb9d67b6ff1f99b83d8d5d25c6bc2dd04ad5ebde726ff8f81973cc53533" }, + { + "file": "garden-planting.png", + "description": "Actual seed-catching and planting checkpoint, before clouds and HUD.", + "displayBackend": "WebGL2", + "sceneBackend": "WebGL shader", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "width": 900, + "height": 900, + "sha256": "267fdee5f98168cb959c8b00e45ced23bf2e086b81632d0fbfec27920b903737" + }, { "file": "display-angles.png", "description": "Geometry example using real Dream Garden assets.", @@ -90,6 +110,16 @@ "width": 900, "height": 900, "sha256": "4bba014d74818400a154f16fb488c697ef7a779e634aa203c8c172021f4638f1" + }, + { + "file": "display-arxels.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": "80790b1752dbaaec7f5c44f150d333db422bd725b6214316542ab7e6e45f28d7" } ] } diff --git a/presentations/making-games/assets/catalog-captures.json b/presentations/making-games/assets/catalog-captures.json new file mode 100644 index 0000000..850178a --- /dev/null +++ b/presentations/making-games/assets/catalog-captures.json @@ -0,0 +1,122 @@ +[ + { + "file": "catalog-vyruss.png", + "slug": "alecu.vyruss_vs2", + "title": "Vyruss VS2", + "displayBackend": "WebGL2", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "runtime": { + "adapterName": "MicroPython WASM", + "adapterSource": "wasm", + "editorOpen": false, + "pollingHalted": false, + "currentButtons": 0, + "currentInput": { + "joy1": 0, + "joy2": 0, + "extra": 0 + } + }, + "sha256": "dc3980d6624726b3a6596b5c9b2b9f698e36a79e03c7328649c51fb6542e19af" + }, + { + "file": "catalog-ventap.png", + "slug": "alecu.ventap", + "title": "Ventap", + "displayBackend": "WebGL2", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "runtime": { + "adapterName": "MicroPython WASM", + "adapterSource": "wasm", + "editorOpen": false, + "pollingHalted": false, + "currentButtons": 0, + "currentInput": { + "joy1": 0, + "joy2": 0, + "extra": 0 + } + }, + "sha256": "c64c3e698922340c331ada397cc590a23211926fffb078904e05ddf8310296f8" + }, + { + "file": "catalog-vixeous.png", + "slug": "alecu.vixeous", + "title": "Vixeous", + "displayBackend": "WebGL2", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "runtime": { + "adapterName": "MicroPython WASM", + "adapterSource": "wasm", + "editorOpen": false, + "pollingHalted": false, + "currentButtons": 0, + "currentInput": { + "joy1": 0, + "joy2": 0, + "extra": 0 + } + }, + "sha256": "be0a150acc9a03495fd89bdd86b0e74871b34aeeb82b8d0b29276eab9bddef86" + }, + { + "file": "catalog-vortris.png", + "slug": "vsjam-may25.vortris", + "title": "Vortris", + "displayBackend": "WebGL2", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "runtime": { + "adapterName": "MicroPython WASM", + "adapterSource": "wasm", + "editorOpen": false, + "pollingHalted": false, + "currentButtons": 0, + "currentInput": { + "joy1": 0, + "joy2": 0, + "extra": 0 + } + }, + "sha256": "e0d64e2e764daebc0cfd81d7b2eac02acf2bf13d4b7c70f94e05e793dabb7095" + }, + { + "file": "catalog-fanphibious.png", + "slug": "vsjam-oct25.fanphibious_danger", + "title": "Fanphibious Danger", + "displayBackend": "WebGL2", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "runtime": { + "adapterName": "MicroPython WASM", + "adapterSource": "wasm", + "editorOpen": false, + "pollingHalted": false, + "currentButtons": 0, + "currentInput": { + "joy1": 0, + "joy2": 0, + "extra": 0 + } + }, + "sha256": "ca751a383212fc0524721c00581fe4caab3a10c0bab6755267180248fe885b8e" + }, + { + "file": "catalog-vzumaki.png", + "slug": "vsjam-may25.vzumaki", + "title": "Vzumaki", + "displayBackend": "WebGL2", + "gpu": "ANGLE (Intel, ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)", + "runtime": { + "adapterName": "MicroPython WASM", + "adapterSource": "wasm", + "editorOpen": false, + "pollingHalted": false, + "currentButtons": 0, + "currentInput": { + "joy1": 0, + "joy2": 0, + "extra": 0 + } + }, + "sha256": "408e91b13fdc53bfbd3471216b97926ca7114218c211817875073b359c486ec8" + } +] diff --git a/presentations/making-games/assets/catalog-fanphibious.png b/presentations/making-games/assets/catalog-fanphibious.png new file mode 100644 index 0000000..31141b5 Binary files /dev/null and b/presentations/making-games/assets/catalog-fanphibious.png differ diff --git a/presentations/making-games/assets/catalog-ventap.png b/presentations/making-games/assets/catalog-ventap.png new file mode 100644 index 0000000..2020c07 Binary files /dev/null and b/presentations/making-games/assets/catalog-ventap.png differ diff --git a/presentations/making-games/assets/catalog-vixeous.png b/presentations/making-games/assets/catalog-vixeous.png new file mode 100644 index 0000000..77dc56e Binary files /dev/null and b/presentations/making-games/assets/catalog-vixeous.png differ diff --git a/presentations/making-games/assets/catalog-vortris.png b/presentations/making-games/assets/catalog-vortris.png new file mode 100644 index 0000000..4d83c4b Binary files /dev/null and b/presentations/making-games/assets/catalog-vortris.png differ diff --git a/presentations/making-games/assets/catalog-vyruss.png b/presentations/making-games/assets/catalog-vyruss.png new file mode 100644 index 0000000..ed2b739 Binary files /dev/null and b/presentations/making-games/assets/catalog-vyruss.png differ diff --git a/presentations/making-games/assets/catalog-vzumaki.png b/presentations/making-games/assets/catalog-vzumaki.png new file mode 100644 index 0000000..28cb3cf Binary files /dev/null and b/presentations/making-games/assets/catalog-vzumaki.png differ diff --git a/presentations/making-games/assets/cybercirujas.jpg b/presentations/making-games/assets/cybercirujas.jpg new file mode 100644 index 0000000..def2fdf Binary files /dev/null and b/presentations/making-games/assets/cybercirujas.jpg differ diff --git a/presentations/making-games/assets/display-arxels.png b/presentations/making-games/assets/display-arxels.png new file mode 100644 index 0000000..dec7d4e Binary files /dev/null and b/presentations/making-games/assets/display-arxels.png differ diff --git a/presentations/making-games/assets/garden-planting.png b/presentations/making-games/assets/garden-planting.png new file mode 100644 index 0000000..ed49c80 Binary files /dev/null and b/presentations/making-games/assets/garden-planting.png differ diff --git a/presentations/making-games/assets/garden-title.png b/presentations/making-games/assets/garden-title.png new file mode 100644 index 0000000..820c1cf Binary files /dev/null and b/presentations/making-games/assets/garden-title.png differ diff --git a/presentations/making-games/deck.js b/presentations/making-games/deck.js index 940e7ff..7898739 100644 --- a/presentations/making-games/deck.js +++ b/presentations/making-games/deck.js @@ -16,7 +16,7 @@ Reveal.initialize({ controlsTutorial: false, progress: true, slideNumber: 'c/t', - totalTime: 45 * 60, + totalTime: 27 * 60, pdfSeparateFragments: false, pdfMaxPagesPerSlide: 1, highlight: { highlightOnLoad: true }, diff --git a/presentations/making-games/examples/dream-garden-guide.md b/presentations/making-games/examples/dream-garden-guide.md index 43cebe9..84c8cda 100644 --- a/presentations/making-games/examples/dream-garden-guide.md +++ b/presentations/making-games/examples/dream-garden-guide.md @@ -9,7 +9,10 @@ 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-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. +[Download the complete game](dream-garden.zip). Use an up-to-date SDK main +checkout. Extract the archive at its root to create `games/demos/dream_garden/`. +The source, packed assets, audio and rebuilding recipes are included, along with +`tests/test_dream_garden.py`. [Small first-game starter](first_game.py) · [Back to the slides](../) @@ -81,29 +84,25 @@ 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. +## Three stages for the short talk + +The presentation launches three complete, cumulative versions. Their sources +are inside the download at `code/checkpoints/`. + +| Stage | Source | Adds | +| --- | --- | --- | +| 1 | `01-steering.py` | One pink dreamer around the rim | +| 2 | `05-flowers.py` | Timed seeds, catching, directional planting and inward flower drift | +| 3 | `11-audio.py` | The completed shared cloud garden, HUD, scenes, glow and audio | + +The final stage matches `code/dream_garden.py` exactly. Additional fine-grained +checkpoints remain available in the SDK for independent study. The short talk +shows only these three launchable progressions. + +A new garden starts without flowers. Planting follows movement, then alternates +after stopping. All stages use the same packed artwork, with unused strips +available for later features. + ## Checks diff --git a/presentations/making-games/handout.html b/presentations/making-games/handout.html index cdfd8d7..dab3c33 100644 --- a/presentations/making-games/handout.html +++ b/presentations/making-games/handout.html @@ -1,8 +1,8 @@ --- layout: presentation-page -title: Making games for Ventilastation — reading view +title: Ventilastation — reading view ---

Download the Markdown source

-

A 45-minute English presentation for Overkill Festival 2026, building Dream Garden. Each slide includes its pacing and speaker notes below.

+

A 12-minute introduction for everyone, followed by 15 minutes of coding and three Dream Garden stages. Each slide includes pacing and speaker notes.

{% capture content %}{% include_relative slides.md %}{% endcapture %} {{ content | replace: 'Notes:', '### Speaker notes' | markdownify }} diff --git a/presentations/making-games/index.html b/presentations/making-games/index.html index 1b3cfbb..0fc3fd9 100644 --- a/presentations/making-games/index.html +++ b/presentations/making-games/index.html @@ -1,6 +1,6 @@ --- layout: null -title: Making games for Ventilastation +title: Ventilastation — Circular Arcades --- @@ -9,7 +9,7 @@ - + {{ page.title }} | Ventilastation diff --git a/presentations/making-games/presenter-guide.md b/presentations/making-games/presenter-guide.md index 3fb6c4b..d7a9095 100644 --- a/presentations/making-games/presenter-guide.md +++ b/presentations/making-games/presenter-guide.md @@ -1,45 +1,67 @@ --- layout: presentation-page -title: Presenting “Making games for Ventilastation” +title: Presenting Ventilastation at Circular Arcades lang: en --- [Open the slides](./) or use the [reading view with notes](handout.html). -This English talk is for game developers attending Overkill Festival 2026. It -builds **Dream Garden**, a small shared garden inspired by Sickhouse's -[Dream No Return theme](https://www.sickhouse.nl/festival/dream-no-return-2026). -The walkthrough lasts 40 minutes, including two desktop demonstrations. -Questions get five minutes. Every slide has a duration and cumulative time cue. +This English talk has two parts: a **12-minute introduction for everyone** and +**15 minutes of coding**. Questions and open play follow the timed 27 minutes. +The event is Sickhouse’s [Circular Arcades meetup on October 14, 2026](https://www.sickhouse.nl/events/colliders-meet-up-circular-arcades). -## Presenting +## The introduction -- Arrow keys or Space advance. Shift + Space goes back. -- `S` opens the speaker view with notes, the next slide and a pacing timer. Allow the speaker popup if your browser blocks it. -- `F` toggles fullscreen. `Esc` opens the overview. `?` shows keyboard help. -- Slide anchors let you bookmark demonstration checkpoints. +Introduce Cybercirujas and its reuse of technology, then explain the fan display +through motion, arxels and transformed artwork. Show existing games made by +alecu and by the community. Explain two-player controls and audio, then scenes +and the visual pieces of a game without showing code. Close with the +[Ventilastation × The Overkill Jam](https://itch.io/jam/ventilastation-overkill-jam). +It starts October 14, and its page lists November 11 as the submission end date. +The resulting games will appear at [The Overkill, November 26–29](https://www.sickhouse.nl/events/the-overkill-festival-2026). +Artists, musicians, writers and playtesters can contribute alongside programmers. -The slides, library, highlighting and images come from this website. External -links provide references rather than dependencies for the presentation. +For a ten-minute introduction, shorten the two game-gallery slides and the +scene examples. For fifteen minutes, allow questions or a brief hardware +interaction at the controller slide. The coding section stays fifteen minutes. -## Before the talk +## The coding section + +Keep the focus on the differences of this console: angle and depth, the angular +seam, a reserved display graph, curved indexed art, draw order and HUD text +orientation. Setup, timer details, glow implementation, budget tables and heap +analysis stay in the linked guides. The final game still includes those features. + +There are exactly three launch buttons, each opening a new tab: + +| Stage | Slide | Runnable source in the game download | +| --- | --- | --- | +| One creature around the rim | `#/first-game-code` | `code/checkpoints/01-steering.py` | +| Catching and planting | `#/planting` | `code/checkpoints/05-flowers.py` | +| Completed shared cloud garden | `#/finished-demo` | `code/checkpoints/11-audio.py` | -Follow the [desktop setup]({{ '/docs/guides/desktop.html' | relative_url }}) in a -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. +The last file matches `code/dream_garden.py` exactly. Earlier fine-grained +checkpoints remain in the SDK and complete download, but this talk uses only +these three versions. Reserve about one minute of live play for each of the +first two stages and two minutes for the completed game. Return to the +presentation tab after each launch. -Run the finished game: +## Before the talk + +Use a current SDK main checkout and follow the [desktop setup]({{ '/docs/guides/desktop.html' | relative_url }}). +Dream Garden is included, or extract the [complete game download](examples/dream-garden.zip) +at the SDK root. Prepare dependencies before presenting. ```sh ./vs-emu.sh --game demos.dream_garden ``` -Windows uses `vs-emu.bat --game demos.dream_garden`. Keep its terminal visible for -tracebacks. Test two gamepads if available. One keyboard also works: +Windows uses `vs-emu.bat --game demos.dream_garden`. Keep the terminal available +for tracebacks. The browser launches are quick demonstrations; the desktop +emulator is the local development loop. Test both physical controllers and the +room volume beforehand. Music and bloom sounds start on. -| Action | Player 1 | Player 2 | +| Action | Player 1 keyboard | Player 2 keyboard | | --- | --- | --- | | Steer | A / D | Left / Right arrows | | Begin / join / slow seeds | Space | Numpad 0 | @@ -47,100 +69,46 @@ tracebacks. Test two gamepads if available. One keyboard also works: | Toggle audio | N | Numpad 2 | | Exit | 1 or M | Numpad + or 3 | -Sound starts on. Check the room's volume before launching; N toggles music and bloom sounds together, with a two-second NOISY/QUIET notice that slides out past the rim. Use 1 or M to leave a demonstration. F1 shows the keyboard layout. +Release the start press before holding A to relax. Show that only seeds slow; +clouds and flower animation stay steady. Stop after moving and catch successive +seeds to demonstrate alternating planting sides. N shows NOISY or QUIET for two +seconds before the notice slides out. B keeps the flowers in a still Rest scene. +A begins another dream. F1 displays keyboard help. -## First demonstration +## Presenting, hosting and editing -Create `games/myname/mygame/`, put [the starter](examples/first_game.py) in -`code/mygame.py`, and copy `dreamer.png` and `menu.png` from Dream Garden. Use a -YAML file that lists only the dreamer: +Arrow keys or Space advance. Shift + Space goes back. `S` opens the speaker +view with notes and timing. `F` toggles fullscreen and `Esc` opens the overview. -```yaml -palettegroups: - garden: - - strip: dreamer.png - frames: 8 -``` +Edit `slides.md`. `---` separates slides and `Notes:` starts speaker notes. +`data-talk-part` separates introduction and coding, and `data-timing` records +seconds. Keep the note cues, 12-minute introduction and 15-minute coding totals +consistent. The website serves the dark deck, notes and reading view at +`/presentations/making-games/`, including project-site prefixes. -Save it as `images/__images__.yaml`. Add `meta.json`: +[Open the print layout](./?print-pdf) in Chromium and print landscape, without +margins and with background graphics enabled. The reading view is printable too. -```json -{"api": "vs2", "api_revision": 2, "title": "My Garden"} -``` +## Sources and images + +The Cybercirujas photo was supplied by Alejandro Cura from his local collection. +The [UNQ account](https://agencia.unq.edu.ar/?p=7125) supports the collective’s +repair, recirculation and knowledge-sharing work. The official jam and Sickhouse +pages support the event information. Individual game source folders are linked +in their slides’ notes. The earlier Ventilastation Jam 2025 deck inspired the +subject, while the current SDK supports the technical details. -Run `./vs-emu.sh --game myname.mygame`. Steer across the angular seam and show -that opposite inputs cancel. The slide at `#/first-demo` has a matching renderer -capture if you need to present without the emulator. - -## Finished demonstration - -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 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 emulator’s WebGL display backend. The general API tutorial remains a -separate Trench Run walkthrough. - -## Editing, hosting and printing - -Edit `slides.md`. `---` separates slides, `Notes:` starts speaker notes, and -`data-timing` gives seconds. Keep note ranges and the 45-minute total consistent. -The website's usual Jekyll and Pages build serves the deck at -`/presentations/making-games/`, including project-site prefixes. The deck keeps -its dark theme in slide, reading and speaker views. - -[Open the print layout](./?print-pdf) in Chrome or Chromium and print landscape, -with no margins and background graphics enabled. The reading view is also -printable and includes notes. - -## Sources and assets - -The previous [Ventilastation Jam 2025 deck](https://docs.google.com/presentation/d/1gfKrzKq1-QdnFjiBYQB6Z456wWDf1JxnY42-vLq9Bws/edit) -inspired the subject order. The current SDK documentation supports the API and -display explanations. The [festival site](https://2026.theoverkill.nl/) and -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 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/`. - -## Browser checkpoints - -Code examples have a **Run this checkpoint** button. It opens the browser -emulator with a cumulative version of the game at that slide, -using the same packed assets as the desktop game. The stages are steering, -poses, pools, timers, flowers, tiles, scrolling, HUD, scenes, shared play and -audio. The early stages start directly; from the scenes checkpoint onward, -press Space (A) at the title. A/D steer, Space relaxes time when that mechanic -appears, B rests after scenes are introduced, and N toggles the final audio. -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. +All game screenshots and display examples use the WebGL backend with glow. +Original photos, artwork atlases and the coordinate diagram keep their sources. ```sh python3 tools/presentation-scenes.py --sdk vsdk --out /tmp/garden-scenes.json node tools/capture-presentation.mjs vsdk /tmp/garden-scenes.json +node tools/capture-presentation-catalog.mjs ``` -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. +Run from the website repository after `make publish`, with Playwright installed. +Set `CHROMIUM` to an installed Chrome executable if necessary. Both capture +scripts require WebGL. `assets/captures.json` and `assets/catalog-captures.json` +record backend and image hashes. The Garden examples use actual packed assets; +the game gallery captures real games running in the web emulator. diff --git a/presentations/making-games/slides.md b/presentations/making-games/slides.md index d7e60e1..248b854 100644 --- a/presentations/making-games/slides.md +++ b/presentations/making-games/slides.md @@ -1,782 +1,431 @@ - + -# Making games for Ventilastation +# Ventilastation -Dream Garden at Overkill Festival 2026 +Games on a fan Alejandro Cura +Circular Arcades at Sickhouse, 14 October 2026 + Notes: -**0:00–0:30.** We will make a small shared garden for a display with no corners. The audience knows games, so spend the time on the unusual screen, a few API rules and the design choices that make this game worth playing. The theme for Overkill 2026 is Dream No Return. Our example lets people drift, grow flowers and choose a slower pace. Use the desktop emulator and the current Ventilastation API, VS2. +**0:00–0:20.** Introduce the console and the people around it before showing any code. The first twelve minutes are for everyone, including people who do not program. The following fifteen minutes focus on making a circular game. We will launch only three cumulative Garden versions. Questions and open play follow the timed talk. -Sources: [Sickhouse's 2026 theme](https://www.sickhouse.nl/festival/dream-no-return-2026), [the festival](https://2026.theoverkill.nl/), [Ventilastation](../../en/). Cover photograph: this website's `images/banner.jpg`. +Source: [Sickhouse’s Circular Arcades meetup](https://www.sickhouse.nl/events/colliders-meet-up-circular-arcades). Cover photo: this website’s images/banner.jpg. --- + - - -## A console built around a fan +## Cybercirujas
-

Spinning LEDs form the display.

-

An ESP32-S3 runs your MicroPython game.

-

The project is open source and open hardware.

+

An Argentine collective that repairs and recirculates technology.

+

Shared knowledge, free software and resistance to planned obsolescence.

+

Ventilastation is part of that community.

-Ventilastation's LED arm and electronics mounted inside a fan +The Cybercirujas community gathered among reused computers and electronics
- Notes: -**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. +**0:20–1:40.** Cirujeo means finding useful things among what others discard. Cybercirujas extend that practice to computers and electronics, and share the knowledge needed to keep them useful. People contribute repairs, parts, software, teaching and curiosity. Connect this to Ventilastation: an unexpected object becomes a place for shared play. Explain the relationship personally rather than presenting it as a commercial sponsor. Do not imply that this photo shows a specific event or identify people in it. -Sources: [Tutorial introduction](../../docs/vs2/tutorial/index.html), [website](../../en/). Photograph: this website's `images/pic01.jpg`. +Source: [UNQ’s account of Cybercirujas](https://agencia.unq.edu.ar/?p=7125), including its interviews with the collective. Photo supplied by Alejandro Cura as cybercirujas.jpg. --- + - - -## The screen is drawn over time +## A game console built around a fan
-

A moving LED arm paints each angular column at a different instant.

-

Persistence of vision joins those columns into a disc.

-

The game tick and physical rotation are separate clocks.

+

A rotating LED arm draws a circular screen.

+

An ESP32-S3 runs the game in MicroPython.

+

Built in Argentina. Open source and open hardware.

- +Ventilastation’s LED arm and electronics mounted inside a fan
- Notes: -**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. +**1:40–2:40.** Point to the moving arm, electronics and fan rather than listing components. It is a console people can write games for, not just a video loop. The code runs on the rotating board. A separate base handles sound and controllers. People can develop on a laptop without building their own hardware. -Sources: [Physical display and renderer](../../docs/vs2/tutorial/index.html), [geometry](../../docs/vs2/tutorial/display.html). Video: this website's `images/banner.mp4`. +Sources: [Ventilastation](../../en/), [current SDK introduction](../../docs/vs2/tutorial/index.html). Photo: this website’s images/pic01.jpg. --- - + -## The pixels form wedges +## Light becomes a screen through motion
-

Each column spans an angle, rather than a fixed horizontal distance.

-

The same angular step is wider at the rim and narrower near the centre.

-

Moving around the disc also rotates the artwork.

+

The arm paints a different slice at each angle.

+

Our eyes join the slices into a disc.

+

The image has a centre and a rim, with no corners.

-The same dream creature at four angles, with its leafy antennae pointing inward at every position +
Notes: -**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. +**2:40–3:50.** Play the physical display clip and trace the arm’s rotation with your hand. Each LED follows a circle. Changing its light as it rotates makes a coloured arc. Persistence of vision joins the successive angles into the image. Unlike a stationary rectangular panel, positions on the screen arrive at different instants. The game update and motor rotation are separate clocks. Keep this intuitive for the non-coding audience. -Source: [Circular geometry and sprite orientation](../../docs/vs2/tutorial/display.html). Image: the emulator’s WebGL display backend with Dream Garden's actual assets. +Sources: [Physical display](../../docs/vs2/tutorial/index.html), [display geometry](../../docs/vs2/tutorial/display.html). Video: this website’s images/banner.mp4. --- - + -## Dream Garden +## Arxels get smaller toward the centre
-

Drift around the rim and catch seeds to grow flowers.

-

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 above blue-gray clouds and pink, mint and lavender flowers drifting inward +

An arxel is a tiny arc of light.

+

The same angle covers a wider arc at the rim.

+

Near the centre, arxels get narrower and details crowd together.

- -Notes: -**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 emulator’s WebGL display backend. - ---- - - - -## The desktop development loop - -Run this checkpoint - -```sh -git clone https://github.com/ventilastation/vsdk.git -cd vsdk -python3 -m venv .venv -. .venv/bin/activate -python -m pip install -r requirements.txt - -./vs-emu.sh --game demos.dream_garden -``` - -Edit local files, rerun, watch the terminal. - -Windows uses `vs-emu.bat`. - -Notes: -**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). - ---- - - - -## A game is a folder - -Run this checkpoint - -```text -games/myname/mygame/ - code/mygame.py - images/dreamer.png - images/__images__.yaml - menu.png - meta.json -``` - -```json -{"api": "vs2", "api_revision": 2, "title": "My Garden"} -``` - -`main()` returns a scene. The menu icon is 64 × 30 pixels. - -Notes: -**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). - ---- - - - -## Dreamers, seeds, flowers and clouds - -
-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 +The same eight-by-eight block of LED cells appears broad at the rim and progressively narrower nearer the centre
-

Dreamers and flowers below. Clouds and seeds above.

- Notes: -**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. +**3:50–5:00.** Arxel combines arc and pixel. These three blocks use the same eight angular columns and eight radial LEDs. Their angular width stays constant, but their physical width shrinks at the smaller radii. Radial LED spacing stays fixed. Distinguish the size of a light cell from an actor shrinking through a tunnel projection. The image uses the HUD projection to isolate the radial geometry. Explain why small lettering and fine artwork become hard to read near the centre. No maths is needed here. -Source: [Artwork provenance and preparation](examples/artwork.html). Images: Dream Garden's original source and cloud atlases, included unchanged. +Source: [Current display geometry](../../docs/vs2/tutorial/display.html). Image: actual font block cells rendered through the WebGL display backend. --- - + -## PNGs become indexed image strips - -Run this checkpoint +## Ordinary artwork bends around the disc
-
palettegroups:
-  garden:
-    - strip: dreamer.png
-      frames: 8
-

Equally sized frames, arranged horizontally.

-

256 colours per palette group.

+The pink dreamer’s ordinary rectangular pixel-art poses +

PNG artwork becomes curved on the display.

+

It stretches along the rim, compresses inward and turns with its angle.

-Four pink dreamer poses: awake, leaning left, leaning right and asleep +The same dreamer sprite rotates with its position at the bottom, left, top and right of the disc
Notes: -**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. +**5:00–6:00.** Show the original flat art and then the four copies around the rim. The renderer maps its horizontal pixels to angular columns and its vertical pixels to radial positions. The result bends and changes proportions. Its local upright direction rotates around the disc. The dreamer’s antennae point inward at every angle. Critical details need to survive that transformation, and a conventional game screen pasted into a circle will not behave like its rectangular original. Invite artists to treat this as a design material. -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. +Source: [Sprite geometry](../../docs/vs2/tutorial/display.html). Images: the real dreamer strip and a WebGL rendering of those assets. --- + - - -## A scene builds once per entry - -![A scene builds its display graph, becomes sealed and closes on a transition](assets/scene-lifecycle.png) +## Two joysticks, music and sound -`build()` reserves the display graph. +Two joysticks provide **directions and action buttons**. -`update()` changes the existing objects, every 30 ms. +Games can compete, cooperate or share a space. +Speakers carry **music, effects and feedback** beyond the disc. Notes: -**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. +**6:00–7:00.** Hold up or point to the two physical controllers. Explain that the player input has the familiar joystick directions and buttons, with room for one- and two-player games. The screen is unusual, but those inputs do not require inventing new controllers. Music and effects play through the base station, not through a speaker on the spinning arm. Audio can carry mood, rhythm and events that the crowded centre cannot show clearly. Keyboard controls and the desktop emulator substitute for the hardware during development. -Sources and diagram: [Scene lifecycle](../../docs/vs2/reference/scene.html), [runtime constraints](../../docs/vs2/tutorial/index.html). +Sources: [Controllers and audio](../../docs/vs2/reference/services.html), [base station](../../en/). --- - + -## A complete first game +## Games made by alecu -Run this checkpoint - -```python -import vs2 -from vs2.controls import LEFT, RIGHT, joy1 - -class Game(vs2.Scene): - def build(self): - world = self.layer("world", projection=vs2.TUNNEL) - self.dreamer = world.sprite("dreamer.png", y=0) - self.dreamer.x = -(self.dreamer.width // 2) - - def update(self): - steer = joy1.held(LEFT) - joy1.held(RIGHT) - self.dreamer.x = (self.dreamer.x + steer) % vs2.display.width - -def main(): - return Game() -``` - -Notes: -**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). - ---- - - - -## First checkpoint: a creature you can steer - -Run this checkpoint - -
-
-

./vs-emu.sh --game myname.mygame

-

A / D drift around the rim.

-

Positive X turns left at the bottom.

-

Desktop demonstration

-
-A single pink dream creature at the bottom rim of the circular display + Notes: -**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. +**7:00–8:00.** Use these as examples of different ways to inhabit a disc: orbiting a target, moving through depth and flying over terrain. Mention Super Ventilagon as an earlier circular obstacle game and ancestor of this console. Keep game history brief. These are existing games, not promises about what the new jam will produce. The screenshots come from their runnable implementations in the SDK’s alecu group. -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. +Sources: [Vyruss VS2](https://github.com/ventilastation/vsdk/tree/main/games/alecu/vyruss_vs2), [Ventap](https://github.com/ventilastation/vsdk/tree/main/games/alecu/ventap), [Vixeous](https://github.com/ventilastation/vsdk/tree/main/games/alecu/vixeous), [Super Ventilagon](https://ventilagon.protocultura.net/). Images: real games captured with the WebGL display backend. --- + - - -## X is an angle. Y points inward. - -![Circular coordinates: zero at the bottom, 64 at the left, 128 at the top and 192 at the right, with y growing inward](assets/display-axes.png) +## The community expands what a disc can do + Notes: -**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. +**8:00–9:10.** These examples came from earlier community game jams. Vortris adapts falling blocks to a circular board. Fanphibious Danger makes the crossing lanes circular. Vzumaki uses text and motion for a poetic experience, broadening the idea of a game on this console. Credit the makers collectively and point to the original source folders for their work. Do not infer individual author names from commit history. There are also other community games in the SDK. The point is to invite new approaches, not prescribe a single genre. -Source and diagram: [The circular display](../../docs/vs2/tutorial/display.html). +Sources: [Vortris](https://github.com/ventilastation/vsdk/tree/main/games/vsjam-may25/vortris), [Fanphibious Danger](https://github.com/ventilastation/vsdk/tree/main/games/vsjam-oct25/fanphibious_danger), [Vzumaki](https://github.com/ventilastation/vsdk/tree/main/games/vsjam-may25/vzumaki). Images: actual games captured with the WebGL display backend. --- - + -## Layers choose the projection +## Scenes are moments in an experience -
-
-

TUNNEL
Y is depth, 0–255. Objects shrink inward.

-

HUD
Y is an LED index, 0–53.

-

FULLSCREEN
A centred image. X rotates it.

-
-Dreamers at depths 0, 24 and 48 on TUNNEL at the top and at radial LED positions 0, 24 and 48 on HUD at the bottom + Notes: -**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. +**9:10–10:00.** A scene is one active moment: a title, the playable garden, a pause, a result or a rest. It can have its own pictures, controls and sound decisions. This need not mean lives and a game-over screen. Garden voluntarily switches to Rest, preserving what players planted. Explain the concept before its Python interface so artists and designers can plan an experience too. -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. +Source: [Dream Garden’s Title, Garden and Rest](examples/dream_garden.py). Images: those actual scenes through the WebGL display backend. --- - + -## Movement and animation are property writes - -Run this checkpoint +## The pieces of a game image
-
-
if sleeping:
-    dreamer.frame = ASLEEP
-elif direction > 0:
-    dreamer.frame = LEFT_POSE
-elif direction < 0:
-    dreamer.frame = RIGHT_POSE
-else:
-    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.

+
+

Layers choose what draws on top.

+

Sprites are movable pictures.

+

Tilemaps assemble repeated picture cells.

+

Labels turn font cells into text.

+

Full-screen images cover the disc with one picture.

+Flowers and dreamers below clouds and seeds, with RELAX text drawn on top
Notes: -**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. +**10:00–11:10.** Point at the image while naming the pieces. Flowers and dreamers form the lower layer. Clouds and golden seeds occupy the middle layer. RELAX is a label on the top layer. The creatures are sprites, and a large cloud spans several tilemap cells. A full-screen image is another option for a backdrop or illustration covering the disc; this garden instead uses sparse clouds. These are tools for visual composition. Avoid explaining constructors, reserve sizes or projection constants in the non-coder section. -Sources: [Sprites](../../docs/vs2/tutorial/sprites.html), [Dream Garden's steer helper](examples/dream_garden.py). +Sources: [Drawables](../../docs/vs2/reference/drawables.html), [projections](../../docs/vs2/tutorial/display.html). Image: the completed garden while holding A, captured through WebGL. --- - + -## Seeds and flowers have bounded pools +## Ventilastation × The Overkill Jam -Run this checkpoint +

Starts 14 October 2026

-```python -# 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) -if seed is None: - return # this seed waits for another opportunity -``` +Games will meet players at The Overkill, **26–29 November**. + +Artists, musicians, writers, programmers and curious playtesters are welcome. -A full flower pool reuses its oldest live flower. +[itch.io/jam/ventilastation-overkill-jam](https://itch.io/jam/ventilastation-overkill-jam) Notes: -**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. +**11:10–12:00.** Close the introduction with the invitation, so people who leave before the code still know how to join. The jam page lists October 14 through November 11 for submissions. Link to it for the authoritative times and current instructions rather than adding a timezone guess. The resulting games are intended for the real console at The Overkill in late November. Find collaborators with different skills. The next fifteen minutes show enough code to begin experimenting. -Source: [Sprite pools](../../docs/vs2/tutorial/pools.html), [sample source](examples/dream_garden.py). +Sources: [Official jam page](https://itch.io/jam/ventilastation-overkill-jam), [Sickhouse’s November 26–29 festival announcement](https://www.sickhouse.nl/events/the-overkill-festival-2026). --- - + -## A timer controls the seed rhythm - -Run this checkpoint - -```python -from urandom import randrange +## A game is a folder on your laptop -# At the end of build(): -self.call_later(self.spawn_ms, self.spawn_seed) +```text +games/myname/mygame/ + code/mygame.py + images/ # PNG strips and __images__.yaml + sounds/ # optional MP3s + menu.png + meta.json +``` -def spawn_seed(self): - self.seeds.spawn( - x=randrange(vs2.display.width), y=160 - ) - self.call_later(self.spawn_ms, self.spawn_seed) +```sh +./vs-emu.sh --game myname.mygame ``` -A seed every 1.8 seconds. RELAX changes the gap later. +MicroPython runs your app or game in the desktop emulator. Notes: -**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. +**12:00–13:15.** Start the coding section at minute twelve. Use an up-to-date SDK main checkout, where Dream Garden is already included. Dependencies should be installed before the session; link to the desktop guide rather than spending the fifteen minutes on setup. Windows uses vs-emu.bat. Explain the folder name, matching Python module and images manifest. main() returns the starting scene. Metadata declares api: vs2 and api_revision: 2. The menu icon is 64×30. Artists can edit the PNGs without changing game logic, and audio lives as MP3s on the host or console base. The complete Garden download includes the assets and runnable code. -Sources: [Scene timers](../../docs/vs2/reference/scene.html), [sample spawn_seed](examples/dream_garden.py). +Sources: [Desktop setup](../../docs/guides/desktop.html), [complete Garden download](examples/dream-garden.zip). --- - + -## A collision grows a flower +## Stage 1: one creature around the rim -Run this checkpoint +Play stage 1 ```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) -``` - -Flowers below players. Flying seeds above both. - -Notes: -**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 +import vs2 +from vs2.controls import LEFT, RIGHT, joy1 -Run this checkpoint +class Game(vs2.Scene): + def build(self): + world = self.layer("world", projection=vs2.TUNNEL) + self.dreamer = world.sprite("dreamer.png", y=0, x=-9) + def update(self): + steer = joy1.held(LEFT) - joy1.held(RIGHT) + self.dreamer.x = (self.dreamer.x + steer) % 256 -```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 +def main(): + return Game() ``` -Stopped catches alternate sides until movement resumes. +A / D steer. Crossing column zero stays continuous. 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. +**13:15–15:45.** This is the whole starter’s behaviour, with x=-9 centring the 19-pixel dreamer at angle zero. The downloadable starter derives that offset from its width. The runnable stage uses the same result. Explain the unusual part: X is an angular position and wraps, while Y zero sits at the outer rim. Held directions are stable controller states; opposite directions cancel. Open stage one in its own tab, move across the seam and return to the deck. Reserve about one minute for this demonstration. The packed dreamer image and current metadata complete the folder. Everyone can replace its artwork; programmers can change the movement. -Sources: [Allocation and target checks](../../docs/vs2/tutorial/budgets.html), [sample source](examples/dream_garden.py). +Sources: [Starter source](examples/first_game.py), [display model](../../docs/vs2/tutorial/display.html). --- - - -## Fluffy clouds can span several tiles + -Run this checkpoint +## Angle and depth replace screen X and Y
-
self.ground = self.air.tilemap(
-    "clouds.png",
-    columns=16, rows=22,
-    view_width=256,
-    view_height=160
-)
-# 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.

+

X wraps through 256 angles.

+

TUNNEL Y is depth from 0 to 255. Increasing it moves inward and shrinks the picture.

+

HUD Y selects one of 54 radial LED positions.

-Muted fluffy clouds in different sizes drift over a dark circular garden, with a pink dreamer at the rim +Angular coordinates start at the bottom and wrap around the disc, while radial positions point inward
Notes: -**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 actual tilemap checkpoint through the emulator’s WebGL display backend. - ---- - - - -## Scrolling moves the view - -Run this checkpoint - -```python -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 * 0.85 -for flower in self.flowers: - flower.y += 0.45 * 0.85 * 0.08 -``` - -The clouds repeat every twelve tile rows. - -Seeds move outward. Flowers drift inward at 8% of normal seed speed. - -Notes: -**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 - -Run this checkpoint - -```python -self.mood = self.hud.label( - "steel8x8.png", columns=5, x=108, y=1, - flip_x=True, flip_y=True, text="RELAX", visible=False -) - -# On release, animate y outward until the text clears the rim. -slide_text_out(self.mood, 1, started) -``` - -Low Y keeps text legible. - -Both flips make top text upright. - -Notes: -**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). - ---- - - - -## Title, garden and rest scenes - -Run this checkpoint - -```python -self.switch(Garden()) # A on the title -self.switch(Rest(flowers)) # B in the garden -self.switch(Garden()) # A after resting - -self.push(PauseMenu()) # suspend, show another scene -self.pop() # resume below, or leave -``` - -Transitions commit at the end of the tick. - -B keeps the flowers in a still garden. - -Notes: -**27:30–29:00.** Rest is a voluntary transition. On B, the game snapshots flower angles, depths and frames at this scene boundary, stops music and switches to Rest. Its build recreates those flowers and a short invitation to begin another dream. There is no game-over scene. Durable state belongs in __init__, while drawable handles belong in build. Use push and pop if you want a modal overlay that resumes the previous scene. Suspension releases its graph and timers, so resumption rebuilds them. Return after queuing a transition. Only one transition is allowed per tick and later callbacks will not touch the outgoing scene. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. - -Sources: [Scene reference](../../docs/vs2/reference/scene.html), [Title, Garden and Rest source](examples/dream_garden.py). - ---- - - - -## Two people can share one garden - -Run this checkpoint - -```python -joy1.held(LEFT) # continuous steering -joy2.held(A) # the friend asks for slower seeds -joy1.just_pressed(B) # choose the rest scene -``` - -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 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. +**15:45–17:15.** Connect this to the arxels from the introduction. X zero is the bottom, 64 the left, 128 the top and 192 the right. Positive motion therefore turns left at the bottom. Use display.width in general code; the previous slide’s 256 states this console’s geometry plainly. TUNNEL adds a depth transformation on top of the radial cells, so moving inward also shrinks an actor. Its range is not the physical LED count. HUD uses LED rows directly and suits labels near the rim. Do not treat display.height as the point where a tunnel seed has left the world. Test both the angular seam and the centre with actual assets. -Sources: [Controls reference](../../docs/vs2/reference/services.html#vs2-controls), [game controls](examples/dream-garden-guide.html). +Source and diagram: [Circular coordinates and projections](../../docs/vs2/tutorial/display.html). --- - - -## A quiet lullaby is optional + -Run this checkpoint +## Reserve pictures, then move them ```python -vs2.audio.music("lullaby", loop=True) -vs2.audio.sound("bloom") -vs2.audio.stop_music() -``` - -Sound starts on. X toggles music and bloom sounds. - -NOISY / QUIET stays for two seconds, then slides past the rim. - -MP3s play on the base or desktop host. - -Notes: -**30:00–31:00.** The game includes an original soft tone loop and a short bloom chime. Music and bloom sounds start enabled. X stops both, and another X press enables them again. A top-layer NOISY or QUIET notice shows the new state for two seconds, then slides outward past the rim over 450 ms; rapid toggles reset its position and restart that duration. Bloom sound only plays while sound is on. B also stops music before resting. Names resolve to MP3s in this game's sounds folder. Audio does not go into image ROMs or rotor RAM. The ESP32 sends commands to the Raspberry Pi base or emulator host. Music can continue across scenes in the same application and stops when returning to the launcher, so we deliberately stop it sooner at rest. Check the room's volume before the demonstration. Use the Run this checkpoint button to open this stage in the browser; it contains only the features covered so far. - -Sources: [Audio API](../../docs/vs2/reference/services.html#vs2-audio), [original audio recipe](examples/artwork.html), [game source](examples/dream_garden.py). +def build(self): + self.seeds = self.air.sprite_pool("seed.png", count=16) ---- - - - -## 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 +def update(self): + for seed in self.seeds: + seed.y -= self.speed * 0.85 ``` -Wait 90–240 ticks, then pulse through frames 1, 2, 3, 2, 1, 0. +`build()` creates the display graph at scene entry. -Retiring a seed also removes its glow state. +`update()` changes its existing objects every 30 ms. 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. +**17:15–18:45.** This is the runtime rule worth retaining from the longer presentation. Build the layers and reserve sprites, pools, tilemaps and labels at scene entry. Returning from build seals that graph. A catch reuses a reserved flower slot rather than creating a fresh renderer object. Spawn reveals a slot and despawn retires it. The finished Garden reserves sixteen seed slots and twenty-four flowers, recycling the oldest flower when full. We do not need a budget table or Python allocation lecture on stage; those details remain in the reference. The excerpt uses the finished game’s air layer. Ordinary game state and drawable handles are separate when a scene is rebuilt. -Source: [Finished glow_seed and retire_seed helpers](examples/dream_garden.py). +Sources: [Scene lifecycle](../../docs/vs2/reference/scene.html), [pools](../../docs/vs2/tutorial/pools.html), [finished Garden](examples/dream_garden.py). --- - + -## Players can change the pace +## Stage 2: catching seeds grows flowers -Run this checkpoint +Play stage 2
-
slow = slow1 or slow2  # either active player
-# Arm held-A after releasing the start press.
-self.speed = 0.12 if slow else 0.45  # seeds only
-self.spawn_ms = 4000 if slow else 1800
-

Either player can relax the shared seed flow.

+
for seed in self.seeds:
+    seed.y -= self.speed * 0.85
+    if seed.overlaps(self.dreamer):
+        self.bloom(seed)
+

The overlap test understands the angular seam.

+

Flowers start at the player’s depth and drift inward.

-The populated garden with the pink dreamer asleep and RELAX visible at the top +Stage two’s pink dreamer catches golden seeds and grows flowers, before clouds or HUD text are introduced
Notes: -**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. +**18:45–21:15.** Open the second cumulative version. It adds timed seeds, collision and planting, but keeps one player and leaves clouds, HUD, scenes and sound for the final version. The overlap test uses angular and depth bounds, including wrap at column zero. Explain only the response needed to see the game’s shape: plant at the catching player’s Y, offset 75% of their width toward movement. After stopping, the first catch follows the last direction and later catches alternate. Flowers drift inward far more slowly than seeds move outward. Misses do no harm. Have the audience predict which side a flower will appear on, then demonstrate. The full helper is in the download; avoid reading its arithmetic line by line. Reserve roughly a minute for live play. -Source: [Garden update](examples/dream_garden.py). Screenshot: an actual held-A game state through the emulator’s WebGL display backend. +Sources: [Game instructions and source](examples/dream-garden-guide.html), [collision](../../docs/vs2/tutorial/sprites.html). Image: the actual planting checkpoint through WebGL. --- - + -## The display graph has explicit budgets +## Art stays separate from game logic -| Resource | Dream Garden | Limit | -|---|---:|---:| -| Layers | 3 | 8 | -| Sprites | 42 | 100 | -| Tilemaps, including labels | 3 | 16 | -| Image strips per asset pack | 5 | 100 | +
+The original atlas of dreamers, seed frames and flowers +The separate atlas of six fluffy clouds in three sizes +
-Read the target limits from `vs2.limits`. +

PNG strips hold frames. Palette groups hold up to 256 colours.

Notes: -**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. +**21:15–22:55.** Keep both atlases from the original presentation, but reduce the asset pipeline to what matters here. The runtime builds an indexed image ROM from the PNG strips and YAML manifest. Dreamers and flowers share one palette group; clouds have another so they can stay subdued. Animated frames have equal dimensions and sit side by side in a strip. The large clouds become adjacent 16-pixel cells, so one shape can span two cells or a three-by-two block. Original art stays separate from code and can be replaced. Point artists at the downloadable files. Test the result on the disc, because rectangular art changes proportions and details compress near the centre. Leave conversion scripts and frame-count minutiae to the guide. -Sources: [Resource limits](../../docs/vs2/reference/services.html#vs2-limits), [sample build](examples/dream_garden.py). +Source: [Artwork and rebuild recipes](examples/artwork.html). Images: the original source atlases, unchanged. --- - + -## The disc decides what is readable +## Depth, draw order and readable text -
-
-

Text belongs near the rim.

-

Dark space keeps the seeds visible.

-

Check timing and intensity on hardware.

-
-RELAX is readable at radial position 1 near the rim and compressed at radial position 35 toward the centre -
+```python +self.air = self.layer("air", projection=vs2.TUNNEL) +self.ground = cloud_tiles(self.air) +self.seeds = self.air.sprite_pool("seed.png", count=16) + +hud = self.layer("hud", projection=vs2.HUD) +hud.label("steel8x8.png", columns=5, x=108, y=1, + flip_x=True, flip_y=True, text="RELAX") +``` + +Clouds first, seeds above them. HUD text near the rim. 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 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. +**22:55–24:25.** This is an excerpt from the complete version, following its lower world layer. Layers draw in creation order, and drawables draw in creation order inside each layer. Reserve clouds before seeds, and flowers before players. A scrolling tilemap changes its view instead of creating new actors. The map and sprites share the tunnel projection, so both curve into the disc. HUD uses LED rows, keeping text near the wider outer cells. At the top, flip both axes so text reads upright. The full-screen projection is also available for a single centred picture, while the completed Garden uses lower world, middle air and top HUD. These are display-specific choices; the collision and input logic will feel familiar to experienced developers. -Source: [Hardware and legibility](../../docs/vs2/tutorial/budgets.html). Image: actual font labels through the emulator’s WebGL display backend. +Sources: [Draw order and projections](../../docs/vs2/tutorial/display.html), [tilemaps and text](../../docs/vs2/tutorial/tilemaps-and-text.html), [finished source](examples/dream_garden.py). --- - + -## Second checkpoint: a shared dream +## Stage 3: the completed shared garden -Run this checkpoint +Play the finished game
-

./vs-emu.sh --game demos.dream_garden

-

Catch a seed, invite a friend, hold A.

-

B rests with the flowers. A begins again.

-

Desktop demonstration

+

Invite a second player into the same garden.

+

Hold A to relax the seed flow. Clouds keep moving.

+

N toggles music and sounds. B keeps the flowers in a resting scene.

-The voluntary resting scene keeps the garden's flowers and offers A to begin a new dream +The completed Dream Garden with two players, clouds, golden seeds and flowers
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 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 emulator’s WebGL display backend. - ---- - - - - -## A game travels with its source and assets - -Run this checkpoint - -- Matching `code/.py` entry point -- PNG strips and `__images__.yaml` -- Menu icon and VS2 metadata -- Sound files and credits for borrowed assets - -[Assets, menu and sharing](../../docs/guides/assets-and-sharing.html) - - -Notes: -**38:00–39:00.** Share the source folder under games/group/name. Leave generated ROMs to the build pipeline. Keep the entry-point name and metadata consistent, include every referenced image, and check a fresh checkout rather than relying on files left on your laptop. The guide covers menu placement and contributions to the repository. Borrowed artwork and sounds need credits and compatible permissions. A small complete game is a good first contribution. A hardware-tested build lets the next person spend time playing rather than guessing how to assemble the assets. The Run this checkpoint button opens the finished game in the browser. +**24:25–26:35.** Run the completed game, exactly the final source. A begins. A/D steer player one, arrows steer player two, and Space or numpad zero asks for slower seeds. Release the start press before holding A to relax. Show the three visible layers and the way golden seeds remain above the clouds. Point out an occasional glow and the flowers’ much slower inward drift. Either player can slow seeds; clouds and flower animation keep their own steady pace. Planting direction is independent for each player. Music and bloom sounds start on. N switches audio, with a two-second NOISY or QUIET notice that slides out. B voluntarily rests with the planted flowers; A begins another dream. Keep the demonstration focused on the differences of this display and the shared interaction. The default desktop setup is the development path; the browser launch is a quick demonstration. No hardware assembly is required to join the jam. -Source: [Assets, menu and sharing](../../docs/guides/assets-and-sharing.html). +Sources: [Complete source](examples/dream_garden.py), [game guide and download](examples/dream-garden-guide.html). Image: the real populated game through WebGL. --- - + ## Your next circular game +[Join the jam on itch.io](https://itch.io/jam/ventilastation-overkill-jam) + [Desktop setup](../../docs/guides/desktop.html) [Dream Garden source and complete download](examples/dream-garden-guide.html) -[Ventilastation API reference](../../docs/vs2/reference/index.html) - -[First-game starter](examples/first_game.py) - -[General API walkthrough](../../docs/vs2/tutorial/index.html) +[Ventilastation API](../../docs/vs2/reference/index.html) Notes: -**39:00–40:00.** The game guide includes installation, both controller mappings, source and the complete asset folder. The small starter gets one dreamer moving. The general tutorial still teaches the same API using its separate Trench Run example, so do not describe it as a chapter-by-chapter Dream Garden tutorial. The reference gives the full interface after the audience has the circular mental model. Invite people to change a rule, a rhythm or an interaction in this garden, then test that choice on the desktop and physical disc. For a festival audience, ask what shared play on this unusual object could feel like. - -Sources: the linked current documentation and sample files. +**26:35–27:00.** Finish the timed coding section at minute twenty-seven. Ask people to form teams and choose one interaction that uses the centre, the rim, rotation or shared play. The linked guide and reference cover the material omitted from this shorter talk. Stay for questions and open play without assigning them part of the fifteen-minute coding slot. ---- - - - -## Questions - -What would you dream on a disc? - -[ventilastation.protocultura.net](https://ventilastation.protocultura.net/) - -Notes: -**40:00–45:00.** Leave five minutes for questions. Keep the desktop game and source available. Discuss display geometry, shared time, collision space, projection choice, reserve sizing, asset palettes and physical testing. If someone wants to build the console, point them to the repository after the session. The reading view contains these notes and the game download. Finish with a clear route to making a circular game and room for the audience's own ideas. +Sources: the linked official jam page and current documentation. diff --git a/presentations/making-games/theme.css b/presentations/making-games/theme.css index 611c4dc..b63e11c 100644 --- a/presentations/making-games/theme.css +++ b/presentations/making-games/theme.css @@ -101,3 +101,13 @@ html.reveal-print .deck-links { display: none; } .reveal .checkpoint-launch:hover, .reveal .checkpoint-launch:focus-visible { background: #234b41; color: #fff; outline: 2px solid #b7ffe5; } + +/* Flat photo rows and brief definitions for the public introduction. */ +.reveal .game-gallery { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:24px; align-items:start; } +.reveal .game-gallery figure { margin:0; } +.reveal .game-gallery img { display:block; width:100%; height:315px; object-fit:contain; } +.reveal .game-gallery figcaption { font-size:30px; line-height:1.3; margin-top:18px; text-align:center; } +.reveal .game-gallery figcaption span { color:var(--muted); font-size:25px; } +.reveal .glossary p { font-size:30px; line-height:1.25; margin-bottom:20px; } +.reveal .event-date { color:var(--accent); font-size:44px; } +.reveal .split .strip { width:100%; } diff --git a/tools/capture-presentation-catalog.mjs b/tools/capture-presentation-catalog.mjs new file mode 100644 index 0000000..cfe1013 --- /dev/null +++ b/tools/capture-presentation-catalog.mjs @@ -0,0 +1,86 @@ +// Real games played in the published emulator, captured with the WebGL display. +// Run after make publish: node tools/capture-presentation-catalog.mjs +// Optional game slugs limit the capture set. CHROMIUM selects installed Chrome. +import fs from 'node:fs'; +import path from 'node:path'; +import http from 'node:http'; +import crypto from 'node:crypto'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +const { chromium } = createRequire(import.meta.url)('playwright'); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const out = path.join(root, 'presentations/making-games/assets'); +const games = [ + {slug:'alecu.vyruss_vs2', file:'catalog-vyruss.png', title:'Vyruss VS2', keys:['Space'], wait:4500}, + {slug:'alecu.ventap', file:'catalog-ventap.png', title:'Ventap', keys:[], wait:1400}, + {slug:'alecu.vixeous', file:'catalog-vixeous.png', title:'Vixeous', keys:['Space'], wait:3500}, + {slug:'vsjam-may25.vortris', file:'catalog-vortris.png', title:'Vortris', keys:[], wait:1200}, + {slug:'vsjam-oct25.fanphibious_danger', file:'catalog-fanphibious.png', title:'Fanphibious Danger', keys:[], wait:1500}, + {slug:'vsjam-may25.vzumaki', file:'catalog-vzumaki.png', title:'Vzumaki', keys:[], wait:6000}, +]; +const server = http.createServer((req, res) => { + let file = path.resolve(root, '.' + new URL(req.url,'http://localhost').pathname); + if (!file.startsWith(root + path.sep)) {res.writeHead(404);res.end();return;} + if (fs.existsSync(file) && fs.statSync(file).isDirectory()) file = path.join(file,'index.html'); + if (!fs.existsSync(file)) {res.writeHead(404);res.end();return;} + const types = {'.html':'text/html','.js':'text/javascript','.mjs':'text/javascript', + '.css':'text/css','.json':'application/json','.wasm':'application/wasm','.png':'image/png'}; + res.setHeader('Content-Type',types[path.extname(file)] || 'application/octet-stream'); + 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 records = []; + for (const game of games.filter(g => !process.argv.slice(2).length || process.argv.slice(2).includes(g.slug))) { + const context = await browser.newContext({viewport:{width:1280,height:1050},deviceScaleFactor:1}); + const page = await context.newPage(); + page.on('pageerror', e => console.error(game.slug,e.message)); + await page.goto(`http://127.0.0.1:${server.address().port}/emulator/`); + await page.waitForFunction(() => window.VentilastationWebEmulator,null,{timeout:60000}); + // Historical jam folders contain hyphens. Use the runtime's slug API + // instead of the URL validator, which currently accepts identifiers only. + await page.evaluate(slug => window.VentilastationWebEmulator.restartRuntime( + {full:true,autostartSlug:slug}),game.slug); + await page.addStyleTag({content:'.stage-fullscreen-toggle,.base-preview{display:none!important}.stage-display::after{display:none!important}#frame-canvas-gl{width:900px!important;height:900px!important;max-width:none!important;max-height:none!important}.stage-display{width:900px!important;height:900px!important;background:#0b1016!important}'}); + await page.waitForTimeout(1800); + for (const key of game.keys) await page.keyboard.press(key,{delay:90}); + await page.waitForTimeout(game.wait); + const info = await page.evaluate(() => { + const canvas = document.querySelector('#frame-canvas-gl'); + const gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); + if (!gl || getComputedStyle(canvas).display === 'none') throw Error('WebGL display is unavailable'); + const error = document.querySelector('#scene-error-banner'); + if (error && !error.hidden) throw Error(error.textContent); + const runtime = window.VentilastationWebEmulator.getRuntimeInfo(); + if (runtime.adapterSource === 'error' || runtime.pollingHalted) { + throw Error('Game runtime failed: '+JSON.stringify(runtime)+' '+document.querySelector('#runtime-message')?.textContent); + } + const debug = gl.getExtension('WEBGL_debug_renderer_info'); + return {displayBackend:gl instanceof WebGL2RenderingContext?'WebGL2':'WebGL', + gpu:debug?gl.getParameter(debug.UNMASKED_RENDERER_WEBGL):'WebGL',runtime}; + }); + await page.evaluate(() => { + Object.defineProperty(document,'hidden',{configurable:true,get:()=>true}); + document.dispatchEvent(new Event('visibilitychange')); + }); + await page.waitForTimeout(100); + await page.locator('#frame-canvas-gl').screenshot({path:path.join(out,game.file)}); + const sha256 = crypto.createHash('sha256').update(fs.readFileSync(path.join(out,game.file))).digest('hex'); + records.push({file:game.file,slug:game.slug,title:game.title,...info,sha256}); + const previous = path.join(out,'catalog-captures.json'); + const existing = fs.existsSync(previous)?JSON.parse(fs.readFileSync(previous,'utf8')):[]; + fs.writeFileSync(previous,JSON.stringify([...existing.filter(e=>e.slug!==game.slug),records.at(-1)],null,2)+'\n'); + console.log(`Captured ${game.title} with ${info.displayBackend}`); + await context.close(); + } + const previous = path.join(out,'catalog-captures.json'); + const existing = fs.existsSync(previous)?JSON.parse(fs.readFileSync(previous,'utf8')):[]; + const updated = [...existing.filter(e=>!records.some(r=>r.slug===e.slug)),...records]; + fs.writeFileSync(previous,JSON.stringify(updated,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 index 4a83b8a..da2d026 100644 --- a/tools/presentation-scenes.py +++ b/tools/presentation-scenes.py @@ -72,6 +72,11 @@ def capture(scene, name, description): for slot, data in runtime._stripe_buffers.items()])) +garden = start() +garden.switch(__import__('games.demos.dream_garden.code.dream_garden', + fromlist=['Title']).Title()) +step() +capture(director.scene_stack[-1], 'garden-title.png', 'Actual title scene of the finished game.') garden = start() capture(garden, 'garden-start.png', 'Finished game starts with no planted flowers.') step(0, fixture.LEFT) @@ -91,6 +96,15 @@ def capture(scene, name, description): 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.') +garden = start('05-flowers') +for index in range(5): + garden.dreamer.x = index * 34 + garden.bloom(garden.seeds.spawn(x=garden.dreamer.x, y=10)) + step(count=90) +garden.dreamer.x = 247 +for index, y in enumerate((35, 70, 110, 150)): + garden.seeds.spawn(x=30 + index * 51, y=y) +capture(garden, 'garden-planting.png', 'Actual seed-catching and planting checkpoint, before clouds and HUD.') class Angles(vs2.Scene): @@ -116,9 +130,18 @@ def build(self): flip_x=True, flip_y=True, text='RELAX') +class Arxels(vs2.Scene): + def build(self): + hud = self.layer('hud', projection=vs2.HUD) + for y in (1, 21, 41): + # CP437's full block is the same 8×8 image at every radius. + hud.sprite('steel8x8.png', x=124, y=y, frame=219) + + for cls, filename in ((Angles, 'display-angles.png'), (Projections, 'display-projections.png'), - (Legibility, 'budgets-legibility.png')): + (Legibility, 'budgets-legibility.png'), + (Arxels, 'display-arxels.png')): start().switch(cls()) step() capture(director.scene_stack[-1], filename, 'Geometry example using real Dream Garden assets.') diff --git a/tools/test-presentation.py b/tools/test-presentation.py index 1ca8a31..92cf1f9 100644 --- a/tools/test-presentation.py +++ b/tools/test-presentation.py @@ -29,6 +29,7 @@ def handle_starttag(self, tag, attrs): def check_pacing(): slides = (TALK / 'slides.md').read_text().split('\n---\n') elapsed = 0 + parts = {'intro': 0, 'coding': 0} identifiers = set() for slide in slides: identifier = re.search(r'\.slide: id="([^"]+)"', slide)[1] @@ -36,15 +37,19 @@ def check_pacing(): identifiers.add(identifier) duration = int(re.search(r'data-timing="(\d+)"', slide)[1]) assert duration > 0, identifier + part = re.search(r'data-talk-part="([^"]+)"', slide)[1] + parts[part] += duration + if part == 'intro': + assert '```python' not in slide.split('\nNotes:\n')[0], identifier notes = slide.split('\nNotes:\n', 1)[1] cue = re.search(r'\*\*(\d+):(\d+)–(\d+):(\d+)\.\*\*', notes) start = int(cue[1]) * 60 + int(cue[2]) end = int(cue[3]) * 60 + int(cue[4]) assert (start, end) == (elapsed, elapsed + duration), identifier elapsed = end - assert elapsed == 45 * 60, elapsed - assert int(re.search(r'data-timing="(\d+)"', slides[-1])[1]) == 5 * 60 - print(f'{len(slides)} slides: 40-minute talk and 5 minutes of questions') + assert parts == {'intro': 12 * 60, 'coding': 15 * 60}, parts + assert elapsed == 27 * 60, elapsed + print(f'{len(slides)} slides: 12-minute introduction and 15-minute coding section') def check_site(site, baseurl, sdk): @@ -111,19 +116,23 @@ def check_starter(sdk): def check_checkpoints(site, sdk): stages = set() + launches = 0 for slide in (TALK / 'slides.md').read_text().split('\n---\n'): content = slide.split('\nNotes:\n')[0] if '```python' in content or 'language-python' in content: - assert 'class="checkpoint-launch"' in content, content[:160] + progression = re.search(r'data-progression="([^"]+)"', content)[1] + assert progression in {'01-steering', '05-flowers', '11-audio'}, progression for checkpoint in re.findall(r'data-checkpoint="([^"]+)"', content): + launches += 1 path = Path('games/demos/dream_garden/code/checkpoints') / (checkpoint + '.py') assert (sdk / path).is_file(), checkpoint assert (site / 'emulator' / path).read_bytes() == (sdk / path).read_bytes(), checkpoint stages.add(checkpoint) - assert len(stages) == 11, stages + assert stages == {'01-steering', '05-flowers', '11-audio'}, stages + assert launches == 3, launches assert (sdk / 'games/demos/dream_garden/code/checkpoints/11-audio.py').read_bytes() == (sdk / 'games/demos/dream_garden/code/dream_garden.py').read_bytes() assert 'idle_timeout' not in (TALK / 'slides.md').read_text() - print('All Python slides launch a published cumulative checkpoint; 11 stages match the SDK') + print('Exactly three cumulative launch stages match the SDK, including the completed game') def check_webgl_captures(sdk): @@ -134,7 +143,8 @@ def check_webgl_captures(sdk): 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'} + 'display-projections.png', 'budgets-legibility.png', 'garden-start.png', + 'garden-title.png', 'garden-planting.png', 'display-arxels.png'} assert set(captures) == expected for filename, capture in captures.items(): assert capture['displayBackend'] == 'WebGL2', filename @@ -143,6 +153,17 @@ def check_webgl_captures(sdk): print('All game and display captures match the recorded WebGL output and completed game source') +def check_catalog_captures(): + assets = TALK / 'assets' + captures = json.loads((assets / 'catalog-captures.json').read_text()) + assert len(captures) == 6 + for capture in captures: + assert capture['displayBackend'] in ('WebGL', 'WebGL2'), capture['file'] + assert capture['runtime']['adapterSource'] != 'error', capture['file'] + assert capture['sha256'] == hashlib.sha256((assets / capture['file']).read_bytes()).hexdigest() + print('Existing alecu and community game images match their recorded WebGL captures') + + if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--site', type=Path, required=True) @@ -154,3 +175,4 @@ def check_webgl_captures(sdk): check_starter(options.sdk.resolve()) check_checkpoints(options.site.resolve(), options.sdk.resolve()) check_webgl_captures(options.sdk.resolve()) + check_catalog_captures()