diff --git a/overlays/pure-snake/stcourse.yaml b/overlays/pure-snake/stcourse.yaml new file mode 100644 index 0000000..9f22b99 --- /dev/null +++ b/overlays/pure-snake/stcourse.yaml @@ -0,0 +1,558 @@ +schema: 1 +id: pure-snake +title: Pure Snake +pitch: Build Snake as pure functions, then wire it to the world with IO and event streams, in JavaScript, Scala 3 or F#. +visibility: public +root: /home/user/functional-programming-course +tracks: + - { id: js, label: JavaScript } + - { id: scala, label: Scala 3 } + - { id: fsharp, label: F# } +mentor: MENTOR.md +source: { kind: authored } +modules: + - id: m00 + title: "Setup and how to use this course" + lessons: + - { id: m00-text, title: "Setup and how to use this course", type: text, file: "course/00-setup.md" } + - id: m00-ex-green-lights + title: "Exercise 0.1 — Green lights" + type: exercise + file: "course/00-setup.md" + anchor: exercise-01--green-lights + hints: { file: "course/00-setup.md", match: "Exercise 0.1" } + localCommand: + js: "cd js && node --test \"test/m00-*.test.js\"" + scala: "cd scala && ./test.sh M00" + fsharp: "cd fsharp && ./test.sh M00" + - id: m00-ex-read-a-red-test + title: "Exercise 0.2 — Read a red test" + type: exercise + file: "course/00-setup.md" + anchor: exercise-02--read-a-red-test + hints: { file: "course/00-setup.md", match: "Exercise 0.2" } + localCommand: + js: "cd js && node --test \"test/m00-*.test.js\"" + scala: "cd scala && ./test.sh M00" + fsharp: "cd fsharp && ./test.sh M00" + - id: m00-ex-break-the-purity-audit-on-purpose + title: "Exercise 0.3 — Break the purity audit on purpose" + type: exercise + file: "course/00-setup.md" + anchor: exercise-03--break-the-purity-audit-on-purpose + localCommand: + js: "cd js && node --test \"test/m00-*.test.js\"" + scala: "cd scala && ./test.sh M00" + fsharp: "cd fsharp && ./test.sh M00" + - id: m01 + title: "Purity and immutability" + lessons: + - { id: m01-text, title: "Purity and immutability", type: text, file: "course/01-purity-and-immutability.md" } + - id: m01-ex-range + title: "Exercise 1.1 — `range`" + type: exercise + file: "course/01-purity-and-immutability.md" + anchor: exercise-11--range + hints: { file: "course/01-purity-and-immutability.md", match: "Exercise 1.1 (range)" } + localCommand: + js: "cd js && node --test \"test/m01-*.test.js\"" + scala: "cd scala && ./test.sh M01" + fsharp: "cd fsharp && ./test.sh M01" + - id: m01-ex-droplast + title: "Exercise 1.2 — `dropLast`" + type: exercise + file: "course/01-purity-and-immutability.md" + anchor: exercise-12--droplast + hints: { file: "course/01-purity-and-immutability.md", match: "Exercise 1.2 (dropLast)" } + localCommand: + js: "cd js && node --test \"test/m01-*.test.js\"" + scala: "cd scala && ./test.sh M01" + fsharp: "cd fsharp && ./test.sh M01" + - id: m01-ex-contains + title: "Exercise 1.3 — `contains`" + type: exercise + file: "course/01-purity-and-immutability.md" + anchor: exercise-13--contains + hints: { file: "course/01-purity-and-immutability.md", match: "Exercise 1.3 (contains)" } + localCommand: + js: "cd js && node --test \"test/m01-*.test.js\"" + scala: "cd scala && ./test.sh M01" + fsharp: "cd fsharp && ./test.sh M01" + - id: m01-ex-pipe + title: "Exercise 1.4 — `pipe` (JS only)" + type: exercise + file: "course/01-purity-and-immutability.md" + anchor: exercise-14--pipe-js-only + hints: { file: "course/01-purity-and-immutability.md", match: "Exercise 1.4 (pipe)" } + localCommand: + js: "cd js && node --test \"test/m01-*.test.js\"" + scala: "cd scala && ./test.sh M01" + fsharp: "cd fsharp && ./test.sh M01" + - id: m01-ex-compose + title: "Exercise 1.5 — `compose` (JS only)" + type: exercise + file: "course/01-purity-and-immutability.md" + anchor: exercise-15--compose-js-only + hints: { file: "course/01-purity-and-immutability.md", match: "Exercise 1.5 (compose)" } + localCommand: + js: "cd js && node --test \"test/m01-*.test.js\"" + scala: "cd scala && ./test.sh M01" + fsharp: "cd fsharp && ./test.sh M01" + - id: m01-ex-deepfreeze + title: "Exercise 1.6 — `deepFreeze` (JS only)" + type: exercise + file: "course/01-purity-and-immutability.md" + anchor: exercise-16--deepfreeze-js-only + hints: { file: "course/01-purity-and-immutability.md", match: "Exercise 1.6 (deepFreeze)" } + localCommand: + js: "cd js && node --test \"test/m01-*.test.js\"" + scala: "cd scala && ./test.sh M01" + fsharp: "cd fsharp && ./test.sh M01" + - id: m02 + title: "The game as data" + lessons: + - { id: m02-text, title: "The game as data", type: text, file: "course/02-the-game-as-data.md" } + - id: m02-ex-your-own-design + title: "Exercise 2.0 — Your own design (paper)" + type: exercise + file: "course/02-the-game-as-data.md" + anchor: exercise-20--your-own-design-paper + localCommand: + js: "cd js && node --test \"test/m02-*.test.js\"" + scala: "cd scala && ./test.sh M02" + fsharp: "cd fsharp && ./test.sh M02" + - id: m02-ex-point + title: "Exercise 2.1 — `point` and `pointEquals` (JS)" + type: exercise + file: "course/02-the-game-as-data.md" + anchor: exercise-21--point-and-pointequals-js + hints: { file: "course/02-the-game-as-data.md", match: "Exercise 2.1 (pointEquals)" } + localCommand: + js: "cd js && node --test \"test/m02-*.test.js\"" + scala: "cd scala && ./test.sh M02" + fsharp: "cd fsharp && ./test.sh M02" + - id: m02-ex-delta + title: "Exercise 2.2 — `delta`" + type: exercise + file: "course/02-the-game-as-data.md" + anchor: exercise-22--delta + hints: { file: "course/02-the-game-as-data.md", match: "Exercises 2.2 and 2.3 (delta, opposite)" } + localCommand: + js: "cd js && node --test \"test/m02-*.test.js\"" + scala: "cd scala && ./test.sh M02" + fsharp: "cd fsharp && ./test.sh M02" + - id: m02-ex-opposite + title: "Exercise 2.3 — `opposite`" + type: exercise + file: "course/02-the-game-as-data.md" + anchor: exercise-23--opposite + hints: { file: "course/02-the-game-as-data.md", match: "Exercises 2.2 and 2.3 (delta, opposite)" } + localCommand: + js: "cd js && node --test \"test/m02-*.test.js\"" + scala: "cd scala && ./test.sh M02" + fsharp: "cd fsharp && ./test.sh M02" + - id: m02-ex-action + title: "Exercise 2.4 — `Action` (JS)" + type: exercise + file: "course/02-the-game-as-data.md" + anchor: exercise-24--action-js + hints: { file: "course/02-the-game-as-data.md", match: "Exercise 2.4 (Action)" } + localCommand: + js: "cd js && node --test \"test/m02-*.test.js\"" + scala: "cd scala && ./test.sh M02" + fsharp: "cd fsharp && ./test.sh M02" + - id: m02-ex-initialstate + title: "Exercise 2.5 — `initialState`" + type: exercise + file: "course/02-the-game-as-data.md" + anchor: exercise-25--initialstate + hints: { file: "course/02-the-game-as-data.md", match: "Exercise 2.5 (initialState)" } + localCommand: + js: "cd js && node --test \"test/m02-*.test.js\"" + scala: "cd scala && ./test.sh M02" + fsharp: "cd fsharp && ./test.sh M02" + - id: m03 + title: "Pure game rules" + lessons: + - { id: m03-text, title: "Pure game rules", type: text, file: "course/03-pure-game-rules.md" } + - id: m03-ex-move + title: "Exercise 3.1 — `move`" + type: exercise + file: "course/03-pure-game-rules.md" + anchor: exercise-31--move + hints: { file: "course/03-pure-game-rules.md", match: "Exercise 3.1 (move)" } + localCommand: + js: "cd js && node --test \"test/m03-*.test.js\"" + scala: "cd scala && ./test.sh M03" + fsharp: "cd fsharp && ./test.sh M03" + - id: m03-ex-inbounds + title: "Exercise 3.2 — `inBounds`" + type: exercise + file: "course/03-pure-game-rules.md" + anchor: exercise-32--inbounds + hints: { file: "course/03-pure-game-rules.md", match: "Exercise 3.2 (inBounds)" } + localCommand: + js: "cd js && node --test \"test/m03-*.test.js\"" + scala: "cd scala && ./test.sh M03" + fsharp: "cd fsharp && ./test.sh M03" + - id: m03-ex-turn + title: "Exercise 3.3 — `turn`" + type: exercise + file: "course/03-pure-game-rules.md" + anchor: exercise-33--turn + hints: { file: "course/03-pure-game-rules.md", match: "Exercise 3.3 (turn)" } + localCommand: + js: "cd js && node --test \"test/m03-*.test.js\"" + scala: "cd scala && ./test.sh M03" + fsharp: "cd fsharp && ./test.sh M03" + - id: m03-ex-advance + title: "Exercise 3.4 — `advance`" + type: exercise + file: "course/03-pure-game-rules.md" + anchor: exercise-34--advance + hints: { file: "course/03-pure-game-rules.md", match: "Exercise 3.4 (advance)" } + localCommand: + js: "cd js && node --test \"test/m03-*.test.js\"" + scala: "cd scala && ./test.sh M03" + fsharp: "cd fsharp && ./test.sh M03" + - id: m04 + title: "Randomness without side effects" + lessons: + - { id: m04-text, title: "Randomness without side effects", type: text, file: "course/04-randomness-without-side-effects.md" } + - id: m04-ex-nextuint32 + title: "Exercise 4.1 — `nextUint32`" + type: exercise + file: "course/04-randomness-without-side-effects.md" + anchor: exercise-41--nextuint32 + hints: { file: "course/04-randomness-without-side-effects.md", match: "Exercise 4.1 (nextUint32)" } + localCommand: + js: "cd js && node --test \"test/m04-*.test.js\"" + scala: "cd scala && ./test.sh M04" + fsharp: "cd fsharp && ./test.sh M04" + - id: m04-ex-pure + title: "Exercise 4.2 — `pure`, `map`, `flatMap`, `run`" + type: exercise + file: "course/04-randomness-without-side-effects.md" + anchor: exercise-42--pure-map-flatmap-run + hints: { file: "course/04-randomness-without-side-effects.md", match: "Exercise 4.2 (pure, map, flatMap, run)" } + localCommand: + js: "cd js && node --test \"test/m04-*.test.js\"" + scala: "cd scala && ./test.sh M04" + fsharp: "cd fsharp && ./test.sh M04" + - id: m04-ex-intbelow + title: "Exercise 4.3 — `intBelow`" + type: exercise + file: "course/04-randomness-without-side-effects.md" + anchor: exercise-43--intbelow + hints: { file: "course/04-randomness-without-side-effects.md", match: "Exercise 4.3 (intBelow)" } + localCommand: + js: "cd js && node --test \"test/m04-*.test.js\"" + scala: "cd scala && ./test.sh M04" + fsharp: "cd fsharp && ./test.sh M04" + - id: m04-ex-freecells + title: "Exercise 4.4 — `freeCells`" + type: exercise + file: "course/04-randomness-without-side-effects.md" + anchor: exercise-44--freecells + hints: { file: "course/04-randomness-without-side-effects.md", match: "Exercise 4.4 (freeCells)" } + localCommand: + js: "cd js && node --test \"test/m04-*.test.js\"" + scala: "cd scala && ./test.sh M04" + fsharp: "cd fsharp && ./test.sh M04" + - id: m04-ex-spawnapple + title: "Exercise 4.5 — `spawnApple`" + type: exercise + file: "course/04-randomness-without-side-effects.md" + anchor: exercise-45--spawnapple + hints: { file: "course/04-randomness-without-side-effects.md", match: "Exercise 4.5 (spawnApple)" } + localCommand: + js: "cd js && node --test \"test/m04-*.test.js\"" + scala: "cd scala && ./test.sh M04" + fsharp: "cd fsharp && ./test.sh M04" + - id: m04-ex-tick + title: "Exercise 4.6 — `tick` and `newGame`" + type: exercise + file: "course/04-randomness-without-side-effects.md" + anchor: exercise-46--tick-and-newgame + hints: { file: "course/04-randomness-without-side-effects.md", match: "Exercise 4.6 (tick, newGame)" } + localCommand: + js: "cd js && node --test \"test/m04-*.test.js\"" + scala: "cd scala && ./test.sh M04" + fsharp: "cd fsharp && ./test.sh M04" + - id: m05 + title: "Folding time" + lessons: + - { id: m05-text, title: "Folding time", type: text, file: "course/05-folding-time.md" } + - id: m05-ex-update + title: "Exercise 5.1 — `update`" + type: exercise + file: "course/05-folding-time.md" + anchor: exercise-51--update + hints: { file: "course/05-folding-time.md", match: "Exercise 5.1 (update)" } + localCommand: + js: "cd js && node --test \"test/m05-*.test.js\"" + scala: "cd scala && ./test.sh M05" + fsharp: "cd fsharp && ./test.sh M05" + - id: m05-ex-replay + title: "Exercise 5.2 — `replay`" + type: exercise + file: "course/05-folding-time.md" + anchor: exercise-52--replay + hints: { file: "course/05-folding-time.md", match: "Exercise 5.2 (replay)" } + localCommand: + js: "cd js && node --test \"test/m05-*.test.js\"" + scala: "cd scala && ./test.sh M05" + fsharp: "cd fsharp && ./test.sh M05" + - id: m05-ex-history + title: "Exercise 5.3 — `history`" + type: exercise + file: "course/05-folding-time.md" + anchor: exercise-53--history + hints: { file: "course/05-folding-time.md", match: "Exercise 5.3 (history)" } + localCommand: + js: "cd js && node --test \"test/m05-*.test.js\"" + scala: "cd scala && ./test.sh M05" + fsharp: "cd fsharp && ./test.sh M05" + - id: m06 + title: "Rendering as data" + lessons: + - { id: m06-text, title: "Rendering as data", type: text, file: "course/06-rendering-as-data.md" } + - id: m06-ex-view + title: "Exercise 6.1 — `view`" + type: exercise + file: "course/06-rendering-as-data.md" + anchor: exercise-61--view + hints: { file: "course/06-rendering-as-data.md", match: "Exercise 6.1" } + localCommand: + js: "cd js && node --test \"test/m06-*.test.js\"" + scala: "cd scala && ./test.sh M06" + fsharp: "cd fsharp && ./test.sh M06" + - id: m06-ex-a-second-interpreter + title: "Exercise 6.2 (optional, untested) — a second interpreter" + type: exercise + file: "course/06-rendering-as-data.md" + anchor: exercise-62-optional-untested--a-second-interpreter + localCommand: + js: "cd js && node --test \"test/m06-*.test.js\"" + scala: "cd scala && ./test.sh M06" + fsharp: "cd fsharp && ./test.sh M06" + - id: m07 + title: "The IO type" + lessons: + - { id: m07-text, title: "The IO type", type: text, file: "course/07-the-io-type.md" } + - id: m07-ex-pure + title: "Exercise 7.1 — `pure` and `effect`" + type: exercise + file: "course/07-the-io-type.md" + anchor: exercise-71--pure-and-effect + hints: { file: "course/07-the-io-type.md", match: "Exercise 7.1" } + localCommand: + js: "cd js && node --test \"test/m07-*.test.js\"" + scala: "cd scala && ./test.sh M07" + fsharp: "cd fsharp && ./test.sh M07" + - id: m07-ex-map + title: "Exercise 7.2 — `map` and `flatMap`" + type: exercise + file: "course/07-the-io-type.md" + anchor: exercise-72--map-and-flatmap + hints: { file: "course/07-the-io-type.md", match: "Exercise 7.2" } + localCommand: + js: "cd js && node --test \"test/m07-*.test.js\"" + scala: "cd scala && ./test.sh M07" + fsharp: "cd fsharp && ./test.sh M07" + - id: m07-ex-sequence + title: "Exercise 7.3 — `sequence`, `draw`, `randomSeed`, `drawScene`" + type: exercise + file: "course/07-the-io-type.md" + anchor: exercise-73--sequence-draw-randomseed-drawscene + hints: { file: "course/07-the-io-type.md", match: "Exercise 7.3" } + localCommand: + js: "cd js && node --test \"test/m07-*.test.js\"" + scala: "cd scala && ./test.sh M07" + fsharp: "cd fsharp && ./test.sh M07" + - id: m07-ex-trace + title: "Exercise 7.4 — `trace`, the pure interpreter" + type: exercise + file: "course/07-the-io-type.md" + anchor: exercise-74--trace-the-pure-interpreter + hints: { file: "course/07-the-io-type.md", match: "Exercise 7.4" } + localCommand: + js: "cd js && node --test \"test/m07-*.test.js\"" + scala: "cd scala && ./test.sh M07" + fsharp: "cd fsharp && ./test.sh M07" + - id: m08 + title: "Event streams" + lessons: + - { id: m08-text, title: "Event streams", type: text, file: "course/08-event-streams.md" } + - id: m08-ex-run + title: "Exercise 8.1 — `run`" + type: exercise + file: "course/08-event-streams.md" + anchor: exercise-81--run + hints: { file: "course/08-event-streams.md", match: "Exercise 8.1" } + localCommand: + js: "cd js && node --test \"test/m08-*.test.js\"" + scala: "cd scala && ./test.sh M08" + fsharp: "cd fsharp && ./test.sh M08" + - id: m08-ex-keydowns + title: "Exercise 8.2 — sources: `keyDowns`, `keyUps`, `interval`" + type: exercise + file: "course/08-event-streams.md" + anchor: exercise-82--sources-keydowns-keyups-interval + hints: { file: "course/08-event-streams.md", match: "Exercise 8.2" } + localCommand: + js: "cd js && node --test \"test/m08-*.test.js\"" + scala: "cd scala && ./test.sh M08" + fsharp: "cd fsharp && ./test.sh M08" + - id: m08-ex-map + title: "Exercise 8.3 — `map`, `filter`, `filterMap`" + type: exercise + file: "course/08-event-streams.md" + anchor: exercise-83--map-filter-filtermap + hints: { file: "course/08-event-streams.md", match: "Exercise 8.3 / 8.4" } + localCommand: + js: "cd js && node --test \"test/m08-*.test.js\"" + scala: "cd scala && ./test.sh M08" + fsharp: "cd fsharp && ./test.sh M08" + - id: m08-ex-merge + title: "Exercise 8.4 — `merge`, `scan`, `startWith`" + type: exercise + file: "course/08-event-streams.md" + anchor: exercise-84--merge-scan-startwith + hints: { file: "course/08-event-streams.md", match: "Exercise 8.4" } + localCommand: + js: "cd js && node --test \"test/m08-*.test.js\"" + scala: "cd scala && ./test.sh M08" + fsharp: "cd fsharp && ./test.sh M08" + - id: m09 + title: "Wiring it together" + lessons: + - { id: m09-text, title: "Wiring it together", type: text, file: "course/09-wiring-it-together.md" } + - id: m09-ex-keytoaction + title: "Exercise 9.1 — `keyToAction`" + type: exercise + file: "course/09-wiring-it-together.md" + anchor: exercise-91--keytoaction + hints: { file: "course/09-wiring-it-together.md", match: "Exercise 9.1" } + localCommand: + js: "cd js && node --test \"test/m09-*.test.js\"" + scala: "cd scala && ./test.sh M09" + fsharp: "cd fsharp && ./test.sh M09" + - id: m09-ex-classicactions + title: "Exercise 9.2 — the app: `classicActions`, `states`, `render`, `main`" + type: exercise + file: "course/09-wiring-it-together.md" + anchor: exercise-92--the-app-classicactions-states-render-main + hints: { file: "course/09-wiring-it-together.md", match: "Exercise 9.2" } + localCommand: + js: "cd js && node --test \"test/m09-*.test.js\"" + scala: "cd scala && ./test.sh M09" + fsharp: "cd fsharp && ./test.sh M09" + - id: m09-ex-runio + title: "Exercise 9.3 — the runtime: `runIO` and `runApp`" + type: exercise + file: "course/09-wiring-it-together.md" + anchor: exercise-93--the-runtime-runio-and-runapp + hints: { file: "course/09-wiring-it-together.md", match: "Exercise 9.3" } + localCommand: + js: "cd js && node --test \"test/m09-*.test.js\"" + scala: "cd scala && ./test.sh M09" + fsharp: "cd fsharp && ./test.sh M09" + - id: m09-ex-browserplatform + title: "Exercise 9.4 (JS only) — `browserPlatform`" + type: exercise + file: "course/09-wiring-it-together.md" + anchor: exercise-94-js-only--browserplatform + localCommand: + js: "cd js && node --test \"test/m09-*.test.js\"" + scala: "cd scala && ./test.sh M09" + fsharp: "cd fsharp && ./test.sh M09" + - id: m10 + title: "Extensions, all pure" + lessons: + - { id: m10-text, title: "Extensions, all pure", type: text, file: "course/10-extensions.md" } + - id: m10-ex-movesevery + title: "Exercise 10.1 — `movesEvery`" + type: exercise + file: "course/10-extensions.md" + anchor: exercise-101--movesevery + localCommand: + js: "cd js && node --test \"test/m10-*.test.js\"" + scala: "cd scala && ./test.sh M10" + fsharp: "cd fsharp && ./test.sh M10" + - id: m10-ex-update + title: "Exercise 10.2 — `update`: `TogglePause`, `Restart`, `Frame`" + type: exercise + file: "course/10-extensions.md" + anchor: exercise-102--update-togglepause-restart-frame + hints: { file: "course/10-extensions.md", match: "Exercise 10.2" } + localCommand: + js: "cd js && node --test \"test/m10-*.test.js\"" + scala: "cd scala && ./test.sh M10" + fsharp: "cd fsharp && ./test.sh M10" + - id: m10-ex-two-more-keys + title: "Exercise 10.3 — two more keys" + type: exercise + file: "course/10-extensions.md" + anchor: exercise-103--two-more-keys + localCommand: + js: "cd js && node --test \"test/m10-*.test.js\"" + scala: "cd scala && ./test.sh M10" + fsharp: "cd fsharp && ./test.sh M10" + - id: m10-ex-the-frame-clock + title: "Exercise 10.4 — the frame clock" + type: exercise + file: "course/10-extensions.md" + anchor: exercise-104--the-frame-clock + hints: { file: "course/10-extensions.md", match: "Exercise 10.4" } + localCommand: + js: "cd js && node --test \"test/m10-*.test.js\"" + scala: "cd scala && ./test.sh M10" + fsharp: "cd fsharp && ./test.sh M10" + - id: m11 + title: "Capstone: Pong, by the method" + lessons: + - { id: m11-text, title: "Capstone: Pong, by the method", type: text, file: "course/11-capstone-pong.md" } + - id: m11-ex-model + title: "Exercise 11.1 — model" + type: exercise + file: "course/11-capstone-pong.md" + anchor: exercise-111--model + localCommand: + js: "cd js && node --test \"test/m11-*.test.js\"" + scala: "cd scala && ./test.sh M11" + fsharp: "cd fsharp && ./test.sh M11" + - id: m11-ex-rules + title: "Exercise 11.2 — rules" + type: exercise + file: "course/11-capstone-pong.md" + anchor: exercise-112--rules + hints: { file: "course/11-capstone-pong.md", match: "Exercise 11.2" } + localCommand: + js: "cd js && node --test \"test/m11-*.test.js\"" + scala: "cd scala && ./test.sh M11" + fsharp: "cd fsharp && ./test.sh M11" + - id: m11-ex-view + title: "Exercise 11.3 — view" + type: exercise + file: "course/11-capstone-pong.md" + anchor: exercise-113--view + localCommand: + js: "cd js && node --test \"test/m11-*.test.js\"" + scala: "cd scala && ./test.sh M11" + fsharp: "cd fsharp && ./test.sh M11" + - id: m11-ex-keys-and-wiring + title: "Exercise 11.4 — keys and wiring" + type: exercise + file: "course/11-capstone-pong.md" + anchor: exercise-114--keys-and-wiring + hints: { file: "course/11-capstone-pong.md", match: "Exercise 11.4" } + localCommand: + js: "cd js && node --test \"test/m11-*.test.js\"" + scala: "cd scala && ./test.sh M11" + fsharp: "cd fsharp && ./test.sh M11" + - id: ref + title: "Method and reference" + lessons: + - { id: ref-method, title: "The method", type: text, file: course/method.md } + - { id: ref-reference, title: "Reference", type: text, file: course/reference.md } diff --git a/overlays/systemic-design/stcourse.yaml b/overlays/systemic-design/stcourse.yaml new file mode 100644 index 0000000..8b88222 --- /dev/null +++ b/overlays/systemic-design/stcourse.yaml @@ -0,0 +1,110 @@ +schema: 1 +id: systemic-design +title: Systemic Design +pitch: Learn to build a rule-driven game engine by writing the systems that make its rules compose. +visibility: public +gating: sequential +root: /home/user/systemic-design-course +tracks: + - { id: rust, label: Rust } + - { id: ts, label: TypeScript } +mentor: mentor/SKILL.md +source: { kind: authored } +modules: + - id: m00 + title: "The Rewrite Trap" + lessons: + - id: m00-lesson + title: "The Rewrite Trap" + type: text + file: "course/00-rewrite-trap/lesson.md" + - id: m01 + title: "Handles and order of play" + lessons: + - id: m01-lesson + title: "Handles and order of play" + type: text + file: "course/01-handles/lesson.md" + localCommand: "node tools/start-module.mjs 01 " + - { id: m01-scenarios, title: Scenarios, type: text, file: "course/01-handles/scenarios.md" } + - id: m02 + title: "The action queue, depth-first" + lessons: + - id: m02-lesson + title: "The action queue, depth-first" + type: text + file: "course/02-action-queue/lesson.md" + localCommand: "node tools/start-module.mjs 02 " + - { id: m02-scenarios, title: Scenarios, type: text, file: "course/02-action-queue/scenarios.md" } + - id: m03 + title: "Deferred death" + lessons: + - id: m03-lesson + title: "Deferred death" + type: text + file: "course/03-death-step/lesson.md" + localCommand: "node tools/start-module.mjs 03 " + - { id: m03-scenarios, title: Scenarios, type: text, file: "course/03-death-step/scenarios.md" } + - id: m04 + title: "Interceptors and \"trigger twice\"" + lessons: + - id: m04-lesson + title: "Interceptors and \"trigger twice\"" + type: text + file: "course/04-interceptors/lesson.md" + localCommand: "node tools/start-module.mjs 04 " + - { id: m04-scenarios, title: Scenarios, type: text, file: "course/04-interceptors/scenarios.md" } + - id: m05 + title: "Behavior as data and Assemble" + lessons: + - id: m05-lesson + title: "Behavior as data and Assemble" + type: text + file: "course/05-assemble/lesson.md" + localCommand: "node tools/start-module.mjs 05 " + - { id: m05-scenarios, title: Scenarios, type: text, file: "course/05-assemble/scenarios.md" } + - id: m06 + title: "Grid, lexer and parser" + lessons: + - id: m06-lesson + title: "Grid, lexer and parser" + type: text + file: "course/06-parser/lesson.md" + localCommand: "node tools/start-module.mjs 06 " + - { id: m06-scenarios, title: Scenarios, type: text, file: "course/06-parser/scenarios.md" } + - id: m07 + title: "Resolving rules: NOT, identity, meta-nouns" + lessons: + - id: m07-lesson + title: "Resolving rules: NOT, identity, meta-nouns" + type: text + file: "course/07-rule-resolution/lesson.md" + localCommand: "node tools/start-module.mjs 07 " + - { id: m07-scenarios, title: Scenarios, type: text, file: "course/07-rule-resolution/scenarios.md" } + - id: m08 + title: "Push chains: validate, then commit" + lessons: + - id: m08-lesson + title: "Push chains: validate, then commit" + type: text + file: "course/08-push-chains/lesson.md" + localCommand: "node tools/start-module.mjs 08 " + - { id: m08-scenarios, title: Scenarios, type: text, file: "course/08-push-chains/scenarios.md" } + - id: m09 + title: "The turn pipeline" + lessons: + - id: m09-lesson + title: "The turn pipeline" + type: text + file: "course/09-turn-pipeline/lesson.md" + localCommand: "node tools/start-module.mjs 09 " + - { id: m09-scenarios, title: Scenarios, type: text, file: "course/09-turn-pipeline/scenarios.md" } + - id: m10 + title: "SpellGrid" + lessons: + - id: m10-lesson + title: "SpellGrid" + type: text + file: "course/10-spellgrid/lesson.md" + localCommand: "node tools/start-module.mjs 10 " + - { id: m10-scenarios, title: Scenarios, type: text, file: "course/10-spellgrid/scenarios.md" } diff --git a/packages/course-format/test/reference.test.ts b/packages/course-format/test/reference.test.ts new file mode 100644 index 0000000..61d9f33 --- /dev/null +++ b/packages/course-format/test/reference.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { createCourseRepository, extractHints } from "../src/index"; + +const overlays = resolve(import.meta.dir, "../../../overlays"); +const snakeRoot = "/home/user/functional-programming-course"; +const systemicRoot = "/home/user/systemic-design-course"; + +describe("reference courses", () => { + test.skipIf(!existsSync(snakeRoot))("pure-snake loads cleanly", async () => { + const repo = await createCourseRepository([overlays]); + const c = repo.get("pure-snake")!; + expect(c.diagnostics.filter((d) => d.level === "error")).toEqual([]); + expect(c.status).toBe("ok"); + expect(c.manifest!.modules).toHaveLength(13); + expect(c.lessonIndex.has("m03-ex-move")).toBe(true); + const hints = c.lessonIndex.get("m03-ex-move")!.lesson.hints!; + const md = await Bun.file(repo.resolvePath("pure-snake", hints.file)!).text(); + expect(extractHints(md, hints.match)).toHaveLength(3); + expect(await repo.readLessonMarkdown("pure-snake", "m03-ex-move")).toStartWith("### Exercise 3.1"); + }); + + test.skipIf(!existsSync(systemicRoot))("systemic-design loads cleanly", async () => { + const repo = await createCourseRepository([overlays]); + const c = repo.get("systemic-design")!; + expect(c.diagnostics.filter((d) => d.level === "error")).toEqual([]); + expect(c.status).toBe("ok"); + expect(c.manifest!.modules).toHaveLength(11); + expect(c.manifest!.gating).toBe("sequential"); + }); +});