docs: course design spec and implementation plan

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-28 23:31:16 +01:00
co-authored by Claude Opus 5.5
commit 6dc5bdde9e
3 changed files with 380 additions and 0 deletions
@@ -0,0 +1,171 @@
# Pure Snake — a functional programming course (design)
## Intent
A self-paced course that teaches functional programming by building a complete,
playable Snake game in which **everything is pure**: the game rules, randomness,
rendering, time, keyboard input, and the program itself. Side effects exist only
as *values* (an `IO` type and event streams) that the student builds from scratch;
a single `run` call at the very edge of the program is the only place anything
actually happens.
**Why:** students who've had one Scala course know `map`/`filter`/`fold` and case
classes, but rarely know how to apply them to an interactive, stateful system. The
outcome we want is practical: by the end, a student can take a game written the
usual way (`snake.currentPos = ...`, `apple.pos = ...`) and convert it into a purely
functional system — and explain each step of how and why — because they have
actually done it, not just read about it.
## Audience
- Bachelor's/master's CS students with one prior Scala course.
- Working **alone at home**. Two supported modes:
1. **Solo** — the text, tiered hints and tests carry them.
2. **With an AI as a Socratic mentor** — the course ships instructions that
make an AI assistant guide by questioning rather than handing over answers.
- Assumed: basic JS syntax familiarity is *not* required to be strong; Module 1
bridges from Scala.
## Goals
- A fully functioning Snake game at the end, built by the student.
- Teaching and understanding over speed: each module explains *why*, not just *what*.
- Test suites the student runs against their own code at every step.
- Links to online sources for every idea used (papers, docs, blog posts, talks).
- Three tracks: **JavaScript (primary), Scala 3, F#** — each with its own runnable
test suites, so the course is fully doable in any of the three.
- A transferable method: a capstone where the student converts a different game.
## Non-goals
- Not a "before" stateful version for the student to start from — the student can
reason about stateful code themselves. (Stateful snippets may appear briefly as
illustration in prose, but there is no stateful codebase to refactor.)
- No FP libraries in the student's code (no Ramda, RxJS, cats-effect, fs2, FSharp
async/observables as the implementation). Production libraries are *linked* as
"what this looks like in the real world".
- No TypeScript, no bundlers, no frameworks.
- Not a course on game design or graphics.
## Anti-goals (failure even if it "works")
- Course reads as a fill-in-the-blanks exercise that can be completed by
pattern-matching tests without understanding.
- Impurity smuggled in "just for convenience" (a `let` that's reassigned, a
`Math.random()` in game logic, `addEventListener` outside the edge).
- The Scala/F# tracks feel like second-class afterthoughts with broken or
missing tests.
- AI mentor mode that just dumps solutions.
## Constraints and decided tooling
- **JS:** modern JavaScript, ES modules, zero dependencies. Tests with Node's
built-in runner (`node --test`). Game renders to an HTML `<canvas>`; a thin
`index.html` only loads the module.
- **Scala 3:** tests with munit, run via `scala-cli` (no sbt project). Game runs
in the browser via Scala.js (scala-cli supports it) on the same canvas page.
- **F#:** tests with Expecto via `dotnet test` (or `dotnet run` for Expecto).
Game runs in the browser via Fable on the same canvas page.
- Students hand-build `IO` and streams in **all three** languages; the APIs have
the same shape and names across tracks (adjusted for idiom, e.g. `flatMap` vs
`bind`), documented side-by-side.
- Where possible, pure-logic test cases live as **shared language-neutral data
files** (JSON fixtures: given state + input → expected state) consumed by all
three suites, so behaviour cannot drift between tracks. Tests for IO/streams
are written natively per language but mirror each other one-to-one.
- Reference solutions ship in a separate `solutions/` folder per track; students
are told to try first. All test suites must pass against the solutions
(this is how the course itself is verified).
## Course structure (decisions marked *[call]* were made by the designer, not the requester)
- **One shared text, side-by-side code.** *[call]* Each module's prose is written
once and language-neutral where possible; every code listing, signature and
exercise appears in all three languages (JS / Scala / F#) next to each other.
Where a concept is language-specific (Module 1, tagged unions in Module 2), the
module has a short per-track section. Bridges go from Scala (the shared
background) to each track — for F#, "the Scala thing you know is spelled like
this in F#".
- **Canonical data model, but designed first by the student.** *[call]* Module 2
has the student sketch their own model and answer design questions *before*
revealing the course's canonical model, which they then adopt and compare with
theirs. The canonical model is fixed so that the shared JSON fixtures work
identically in every track.
- **One growing codebase per student, per track.** *[call]* The student builds a
single project across modules. Each module states which files it touches. If
stuck, a student may copy the previous module's checkpoint from `solutions/` to
continue — tests are organised per module so earlier ones keep passing.
- **All three tracks ship together, complete.** *[call]* Following "not
second-class": every module is finished in all three tracks, with tests passing
against reference solutions, before the course is called done. No hour budget;
depth is whatever the concept needs.
- **Cross-language determinism.** The PRNG must give bit-identical sequences in JS,
Scala and F# (32-bit integer arithmetic) so fixtures involving apples are shared.
## Module sequence
Each module contains: concept text with Scala bridges, learning objectives,
exercises with signatures in all three languages, tiered hints (nudge → stronger
hint → near-solution), Socratic discussion questions, test suites per track,
"further reading" links, and a short "in production" note (cats-effect, fs2,
Elm, Elmish, RxJS, etc.).
0. **Setup & how to use the course** — tracks, running tests, solo vs. mentor mode.
1. **Purity & immutability in JS for Scala people** — referential transparency,
`const` vs. immutable data, `Object.freeze`, spread copies, functions as values,
currying/composition (`pipe`, `compose`).
2. **The game as data** — `Point`, `Direction`, `Snake`, `GameState`; algebraic data
types as tagged unions in JS (mapping from Scala `enum`/case classes and F#
discriminated unions); smart constructors.
3. **Pure game rules** — `step : State → State`: movement, growth, wall/self
collision, illegal 180° turns, eating.
4. **Randomness without side effects** — seeded PRNG as `seed → [value, seed]`;
apple placement avoiding the snake; first encounter with the state-passing
pattern (naming the State monad).
5. **Folding time** — the game as `reduce` over a list of inputs; deterministic
replays; why this makes testing trivial.
6. **Rendering as data** — `view : State → Scene` returning a description of what
to draw (rectangles, text), not drawing it.
7. **The IO type** — effects as values; `of`/`map`/`flatMap`; monad laws (tested);
interpreting a `Scene` into an `IO` of canvas operations; a test interpreter so
IO is verifiable without a screen.
8. **Event streams** — ticks and key presses as values; `map`/`filter`/`merge`/`scan`
built from scratch; streams as descriptions that do nothing until run.
9. **Wiring it together** — `main` is a single `IO` value: `scan(update)` over merged
input + tick streams, rendered each frame; exactly one `run` at the edge. Game
is playable.
10. **Extensions, all pure** — score, game over, restart, pause, speed-up.
11. **Capstone: the method** — an explicit recipe for converting a stateful game
into a functional one, then a guided conversion of a second game (Pong or
Tetris) with lighter scaffolding and its own tests.
## Socratic mentor mode
- A `MENTOR.md` (with thin `AGENTS.md` / `CLAUDE.md` pointers) that instructs an AI
assistant to: ask questions before explaining, never write solution code unless
the student explicitly gives up on an exercise, use the module's discussion
questions and hint tiers in order, check understanding by asking the student to
explain/predict, and consult `solutions/` only to evaluate the student's work.
- Each module lists the discussion questions the mentor (or the solo student)
should be able to answer before moving on.
## Sources
Each module ends with links to the ideas used. Expected anchors include: Hughes
"Why Functional Programming Matters"; the Elm Architecture guide; Haskell/Scala
`IO` writings (cats-effect docs, "Functional Programming in Scala"); Conal Elliott
on FRP; "Mostly Adequate Guide to FP" (JS); Expecto/munit/Node test docs; Scala.js
and Fable docs; PCG/xorshift/mulberry32 PRNG references; Gary Bernhardt
"Functional Core, Imperative Shell" (as the contrast to what we're doing). Links
must be checked as live at build time.
## Left to the builder
- Exact repository layout, file names, and module file format (Markdown assumed).
- Exact API names for `IO`/stream operations, as long as they're consistent
across tracks and documented side-by-side.
- Stream implementation strategy (push vs. pull), with the choice explained to
the student.
- Choice of PRNG algorithm, grid size, tick rate.
- Which capstone game (Pong or Tetris) — pick whichever teaches the method best.
- Visual style of the game page (keep it clean and simple).