Commit Graph

260 Commits

Author SHA1 Message Date
github-actions[bot]
b5098f6987 Update contributors list [skip ci] 2026-05-05 13:44:19 +00:00
github-actions[bot]
fe9fb5e32a Update contributors list [skip ci] 2026-05-05 02:21:50 +00:00
github-actions[bot]
5a10b3b02f Update contributors list [skip ci] 2026-05-04 14:44:55 +00:00
github-actions[bot]
19866a16bb Update contributors list [skip ci] 2026-05-04 00:04:39 +00:00
github-actions[bot]
74120942ed Update contributors list [skip ci] 2026-05-03 23:36:01 +00:00
github-actions[bot]
ca127de594 Update contributors list [skip ci] 2026-05-03 23:09:12 +00:00
github-actions[bot]
464f21f503 Update contributors list [skip ci] 2026-05-03 22:52:16 +00:00
github-actions[bot]
cbafd39669 Update contributors list [skip ci] 2026-05-03 22:30:52 +00:00
github-actions[bot]
287563d50a Update contributors list [skip ci] 2026-05-03 22:15:10 +00:00
Vijay Janapa Reddi
825d9571a6 chore: remove archived content and refresh contributor docs
- Remove retired _archive/ and scripts/archive/ trees (site, book filters, games, vault); vault CHANGELOG points to git history for old scripts.
- CONTRIBUTING: site project row, site/ in area map, root vs TinyTorch pre-commit, vault schema drift wording.
- Newsletter CLI: path-agnostic news alias; tinytorch pre-commit comments; add tools/ and staffml-vault-types READMEs for maintainers.
2026-05-02 10:48:00 -04:00
Farhan Asghar
81a17f7a11 fix(site/community): restore spotlight quote visibility in dark mode (#1605)
The textbook bootstrap theme paints `blockquote` with a near-white
background and dark muted text, which made the community Spotlight
quotes nearly invisible against the dark card in dark mode. Scope a
neutralization of background/padding/border to `.spotlight-quote` and
add explicit dark-mode color overrides so the rule still wins when the
theme toggles via either `.quarto-dark` or `[data-bs-theme="dark"]`.

Co-authored-by: Vijay Janapa Reddi <vj@eecs.harvard.edu>
2026-04-30 19:03:51 -04:00
github-actions[bot]
1c510e9c96 Update contributors list [skip ci] 2026-04-30 12:39:26 +00:00
Vijay Janapa Reddi
cede64264b Merge branch 'fix/site-mobile-snap' into dev — remove broken-on-mobile scroll-snap from landing 2026-04-28 19:40:07 -04:00
Vijay Janapa Reddi
00c10e4413 fix(site): remove scroll-snap from landing page
`scroll-snap-type: y mandatory` was set on <html> globally (landing.css)
and re-asserted with !important when the v3 landing class was present
(landing-v3.css). The intended mobile escape hatch — disabling
scroll-snap-align inside @media (max-width: 768px) — never worked:

  1. Specificity mismatch. Enable rules used `.mls-landing-v3
     .mls-section-richcards` (0,2,0); the disable used `.mls-section-
     richcards` (0,1,0). Media queries don't bump specificity, so the
     enable rule won and snap stayed active on phones for two of three
     sections.
  2. The mandatory `scroll-snap-type` on <html> was never disabled, so
     even with all snap-aligns removed, mandatory snap remained active
     on iOS — fighting Safari's momentum scroll.

Both bugs combined to produce the jittery, half-snap behavior on phones.
Even fixed, mandatory snap on a content-rich landing page locks readers
into one section at a time on small viewports — the polish wasn't worth
the friction. Removed entirely.

Kept: scroll-behavior: smooth and scroll-padding-top: 60px on <html> for
in-page anchor smoothness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:39:51 -04:00
Vijay Janapa Reddi
054e37477a fix(site): repair about page CSS + restructure newsletter sync
The about and community pages on mlsysbook.ai were rendering unstyled
because they referenced bootstrap-{HASH}.css filenames that were never
deployed to /site_libs/ on gh-pages. Two structural bugs in the deploy
workflows let this state happen:

site-publish-live.yml never copied site_libs/ to gh-pages — the
landing-page deploy loop explicitly skipped it. Now copied alongside
the subsite HTML so the bootstrap hashes referenced in about/community/
newsletter resolve.

sync-newsletter.yml ran a parallel partial deploy that overwrote
site_libs/ daily without updating about/community HTML, leaving them
pointing at hashes that no longer existed. Replaced with a content-only
workflow: pull Buttondown, commit posts to dev, cherry-pick the same
commit onto main (the stable release branch from which gh-pages is
published), dispatch site-publish-live on main. Single deploy path now
owns gh-pages, atomicity restored by construction.

Supporting changes to make the cherry-pick conflict-free:

- _stats.yml gitignored (auto-regenerated on every `news pull` run).
  The unused raw subscriber_count is no longer written to the file —
  only issue_count and the bucketed subscriber_display, both of which
  change at meaningful boundaries rather than drifting daily.
- pull.py preserves the previous bucketed display on Buttondown API
  failure rather than writing 0 → "0 subscribers" on the page.
- index.qmd static fallback updated from 11 issues / 900+ to 14 / 1000+
  to match current reality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:26:52 -04:00
github-actions[bot]
798a6b65bb chore: sync 1 newsletter post(s) from Buttondown [automated] 2026-04-28 06:57:57 +00:00
github-actions[bot]
83f618d1da Update contributors list [skip ci] 2026-04-27 19:21:26 +00:00
github-actions[bot]
87bb4f520a Update contributors list [skip ci] 2026-04-27 13:34:54 +00:00
github-actions[bot]
2a84a8bfda chore: sync 1 newsletter post(s) from Buttondown [automated] 2026-04-27 06:57:15 +00:00
github-actions[bot]
9ca5b076b5 Update contributors list [skip ci] 2026-04-26 20:42:21 +00:00
github-actions[bot]
0219692a58 Update contributors list [skip ci] 2026-04-26 19:50:27 +00:00
github-actions[bot]
61b77dd84d Update contributors list [skip ci] 2026-04-26 19:22:39 +00:00
github-actions[bot]
59e0ddc0e3 Update contributors list [skip ci] 2026-04-26 18:43:13 +00:00
github-actions[bot]
54fb82c4df Update contributors list [skip ci] 2026-04-26 18:14:30 +00:00
github-actions[bot]
36e9b0dd52 Update contributors list [skip ci] 2026-04-26 17:47:04 +00:00
github-actions[bot]
bfd2a1d8b2 Update contributors list [skip ci] 2026-04-26 16:27:17 +00:00
github-actions[bot]
30394a8bd0 Update contributors list [skip ci] 2026-04-26 16:00:40 +00:00
Vijay Janapa Reddi
ae7829ded1 feat(games): READY overlay rolled out to all 11 remaining games
User: 'whatever we did for the LLM and how you arrived at it, that's exactly
the same record that we should apply to these other ones.' — applying the
same press-ENTER-to-start pattern from Lander to every other game so the
READY-screen UX is consistent across the catalog.

Each game now:
- imports mountReadyOverlay from ./runtime.mjs
- declares a 'started' flag (false at init)
- mounts a per-game READY overlay with title, goal, controls, ENTER prompt
- gates ticker / onTick on started
- gates input handlers (keydown / pointerdown / cell handlers) on started

Per-game launch copy:
- batch: 'Push batch size up for throughput — but don't OOM.'
- allreduce: 'Tap each GPU on the beat. Keep gradients flowing.'
- moe: 'Route each colored token to the matching expert.'
- kvcache: 'Pack incoming requests. Beat fragmentation.'
- topology: 'Wire 8 GPUs. Maximize bandwidth, avoid bottlenecks.'
- checkpoint: 'Train fast. Checkpoint before a node failure strikes.'
- loader: 'Type each letter as it enters the zone — feed the GPU.'
- roofline: 'Stay under the cyan ceiling — that's your hardware roof.'
- prune: 'Cut faint weights to 60% sparsity. Keep accuracy above 50%.'
  (also overrides prune's existing first-click-to-start in favor of READY)
- oom: 'Pack tensors into HBM. Activations dominate — that's why you'll OOM.'
- quantization: 'Drop weights below the bit budget. Hit the red center 7+ of 10.'

Verified: 14/14 games pass Playwright sweep — canvas mounted, runtime ready,
no JS errors, no 4xx network requests. Each game shows the READY overlay on
load and dismisses on Enter / Space / Up / tap.
2026-04-26 08:04:57 -04:00
Vijay Janapa Reddi
aa6703e3dc fix(cluster): repair completely broken click-to-place + add READY overlay
User reported: 'cluster commander just doesn't do anything and the game is
broken, so it doesn't work. I click around, but nothing happens.'

Two layered bugs found via Playwright instrumentation:

1. Pixi v8 per-Graphics hit testing failed silently for the grid cells.
   Hover (pointerover) worked but pointerdown never fired despite
   eventMode='static' + explicit hitArea. handleGridClick was never reached.
   Fix: bypass Pixi events for cells. Route pointermove/pointerdown through
   canvas.addEventListener and translate (clientX, clientY) → grid (r, c).
   Robust against any Pixi event-system quirks.

2. The new shared mountReadyOverlay was swallowing every click after dismissal.
   In Pixi v8, visible=false does NOT stop event capture. The full-canvas
   overlay's eventMode='static' kept catching pointerdowns even when invisible.
   Fix in runtime.mjs: launch() now sets root.eventMode='none' AND removes
   root from its parent, ensuring no further event capture.

Cluster also gets the standard READY overlay (press ENTER to launch) so the
player can read the goal before the timer starts.

Verified locally: Playwright headless, 3 mid-canvas clicks → Scheduled: 8,
multiple Fine-tune 2x2 blocks placed on grid, 'scheduled Fine-tune' float
text visible. Pre-fix: Scheduled stayed at 0 forever.
2026-04-26 08:04:57 -04:00
Vijay Janapa Reddi
aa75852a80 polish(404): bigger SVG font, smaller joke text, tighter joke pool
Iterates on the post-merge 404 redesign across all 8 sub-sites:

- SVG roofline-plot fonts bumped for readability without changing
  layout: axis labels 9.5pt to 14pt, region labels 9.5pt to 14pt,
  title and "your page (404)" annotation 11pt to 16pt.

- Random-joke font shrunk from 1.6rem to 1.3rem (1.1rem on mobile)
  so the joke no longer dominates the SVG above it.

- Removed the static "Looks like this page slipped past our load
  balancer" subline from 6 sub-sites — it read as a competing static
  joke alongside the random rotation. Slides and instructors keep
  their informational sublines.

- Joke pool tightened 97 to 79 via a strict ML-systems-centric test:
  if you could swap "page" for any generic web/ops resource and the
  joke still works, cut. Cuts removed the H&P canonical material
  (Amdahl's Law, TLB miss, Dennard scaling, false sharing, fat-tree
  topology) that is general computer architecture rather than ML
  systems specifically. 19 borderline jokes were rewritten to anchor
  punchlines in concepts only ML practitioners decode (KV cache,
  gradient AllReduce, prefill, ZeRO, speculative-decode acceptance,
  1F1B schedule, ridge point, BPE merges).
2026-04-26 08:04:57 -04:00
Vijay Janapa Reddi
80e1bbb794 feat(pipeline): READY overlay + smooth blue→brown turnaround + wider stages
Three changes addressing user feedback on Pipeline Pacer:

1. Wider GPU stages (82px → 116px). Was leaving ~175px of dead space on the
   right of the canvas; now spans 464px wide with ~50px margins matching the
   left, filling the canvas evenly.

2. Smooth U-turn at GPU 3 instead of instant blue→brown color swap. New
   600ms 'turning' state on each block: outCubic-eased RGB lerp from
   compute-blue to routing-orange, plus a vertical dip (sin curve) that
   visually traces the forward→backward U-turn. Stroke color also lerps
   from $2f6e9d to $9a6620.

3. Pre-game READY overlay using the new shared mountReadyOverlay helper.
   Game starts paused; player presses ENTER (or Space, or ↑, or taps) to
   launch. Spawn function and ticker now gate on `started` so nothing fires
   before the player commits.

Also: new mountReadyOverlay() helper added to runtime.mjs. Drop-in for any
game that wants the READY-press-ENTER pattern. Same dim overlay + title +
goal + controls + pulsing CTA + 'take your time' hint as Lander, with all
fields optional. Exported on window.MLSP.runtime.mountReadyOverlay.
2026-04-26 08:04:57 -04:00
Vijay Janapa Reddi
82d72f6dc7 feat(lander): wider terrain — global pad anywhere + curvier surface + level system
User: 'the surface can be a bit more curved or more challenging - maybe levels?
and make hte global minium not always appear right in the middle'

Three changes to createTerrain + lossY:

1. Global pad spawns anywhere across the playable width (was: rand(0.42*W,
   0.58*W) — always near center). Now: rand(0.18*W, 0.82*W). Forces actual
   steering instead of drop-and-tap.

2. Local pads always flank global with a 110px center-to-center gap. If only
   one half of the canvas has room (global near edge), both locals go there
   separated within that half. Wells never bleed together.

3. New third sine harmonic in lossY (period ~Pi*7.3) adds finer surface
   ripples that weren't in the original. Plus all three harmonic amplitudes
   scale with day-based level (Day 1 = level 0 mild, Day 11+ = level 8 max).

4. dayChip now shows 'LVL N' so the player can read why today's terrain
   feels harder than yesterday's.

Implicit progression: each day's puzzle is harder than the previous, capping
at level 8 so it never becomes impossible. The daily seed still produces
identical terrain for everyone playing on the same day.
2026-04-26 08:04:57 -04:00
Vijay Janapa Reddi
35c8fac713 fix(games): use 'all-time' in display strings to satisfy codespell
The legacy .js game files (oom, prune, quantization, _archive/roofline)
displayed "alltime best" in their HUD text. Codespell flags this as a
misspelling of "all-time". The newer .mjs rewrites already use
"all-time best"; this aligns the older files with that convention.

Variable names (alltimeBest) and onGameOver/onScoreChange payload keys
({ alltimeBest: ... }) are unchanged — they're camelCase and already
ignored by the regex in pyproject.toml. Only display string literals
were touched, so no consumer breaks.

Unblocks codespell CI on dev, which has been red since the games-polish
loop landed.
2026-04-26 07:34:24 -04:00
github-actions[bot]
304d7cc93f chore: sync 1 newsletter post(s) from Buttondown [automated] 2026-04-26 06:40:47 +00:00
Vijay Janapa Reddi
3e05393c80 chore(games): force vendor rebuild — previous deploy may not have copied vendor file
The pixi-filters.min.mjs fix from 51ee2baf9 didn't propagate to the dev
preview deploy (deployed file still has the absolute path). This appends
a one-line comment to force git to track a content change so the next
deploy commits the file.

If this still doesn't propagate, the issue is in Quarto's resource-copy
step, not in the deploy script.
2026-04-25 19:34:40 -04:00
Vijay Janapa Reddi
8135dda4c8 feat(site): redesign 404 pages with curated ML systems jokes
Replace the inline-game 404 across all 8 sub-sites with a unified
design: roofline-plot illustration, random pick from a 97-joke
editorial-curated pool, dual CTA (home + playground), and a
GitHub-issue-backed contribute link for community submissions.

The joke pool was curated through a multi-agent editorial process:
4 generators (each with a different ML systems lens) produced 240
candidates; 5 reviewer personas (architect, NLP researcher, production
engineer, undergrad, copy editor) scored each; the synthesizer kept 92
surviving jokes plus 5 new canonical-architecture additions (Amdahl,
TLB, Dennard, false sharing, fat-tree topology) the architect flagged
as missing from the H&P-tradition canon.

Self-contained CSS with prefers-color-scheme: dark adapts to host
themes without coupling to each sub-site's bespoke styling. SVG
follows the book's semantic palette (blue memory-bound slope, green
compute roof, MIT red error annotation). Per-site navigation
preserved across all sub-sites.

Adds .github/ISSUE_TEMPLATE/404_joke.yml so contributions arrive
structured (joke text, theme dropdown, license checkbox) and
auto-tagged (site, 404-joke, good-first-issue) for triage.
2026-04-25 19:34:40 -04:00
Vijay Janapa Reddi
51ee2baf92 fix(games): vendor pixi-filters had absolute import baked in
Post-merge Playwright sweep against the live dev preview revealed 4 games
still 404'ing on /assets/games/vendor/pixi.min.mjs (no path prefix). Trace:
the vendored pixi-filters.min.mjs ships with the absolute path

  import {...} from "/assets/games/vendor/pixi.min.mjs"

baked into its bundle as the peer-dependency reference. This was hidden
behind the lazy import (only oom/prune/quantization/roofline call into
filters), which is why Lander and 9 other games passed.

Fix: sed-replace the absolute string with a sibling-relative path
'./pixi.min.mjs', which resolves correctly regardless of deploy base
since both vendor files live in the same directory.

Verified with full Playwright sweep against locally-rendered _build:
14/14 games now pass — canvas mounted, runtime ready, no errors, no
404s, including the four previously-broken filter-using games.
2026-04-25 19:28:53 -04:00
Vijay Janapa Reddi
0307796d97 Merge refactor/top-level-volume-paths: move volumes to /vol1/ and /vol2/
Aligns the actual deploy path with the project's source-code URL
convention. Volumes now deploy to top-level /vol1/ and /vol2/ on both
dev preview and live publish, matching navbar/footer/announcement
references that have always pointed there. The /book/vol1/ nesting and
its dev-rewriter special case are removed.

The legacy /book/ single-volume textbook on live stays untouched —
that's the user-facing front door until an explicit Cloudflare-redirect
cutover.

Old BOOK_DEPLOY_PATH variable will be deleted after the dev preview
rebuild confirms the rename is live.
2026-04-25 19:21:16 -04:00
Vijay Janapa Reddi
1499c0d58f refactor: move volumes to top-level /vol1/ and /vol2/ URLs
The project's source convention (navbar, footer, announcements,
instructor course-map, etc.) already treats mlsysbook.ai/vol1/ and
mlsysbook.ai/vol2/ as the canonical URLs. The /book/vol1/ nesting was
an artifact of the legacy single-volume textbook still occupying /book/
on live, kept alive by a special-case mapping in the dev URL rewriter.
This refactor aligns the actual deploy paths with the source convention
before Monday's release locks in citation-grade URLs.

Public URL change:
  was:   https://mlsysbook.ai/book/vol1/  + .../book/vol2/
  is:    https://mlsysbook.ai/vol1/       + .../vol2/

Variable change (set on harvard-edge/cs249r_book):
  VOL1_DEPLOY_PATH=vol1   (new)
  VOL2_DEPLOY_PATH=vol2   (new)
  BOOK_DEPLOY_PATH=book   (will be deleted post-merge)

Workflow changes:

  book-publish-live.yml
    - Reads VOL1_DEPLOY_PATH + VOL2_DEPLOY_PATH (fail-fast on empty).
    - Each volume deploys to its own top-level path on gh-pages.
    - Skip-list regex now includes both top-level paths plus 'book' so
      the legacy single-volume textbook at /book/ stays untouched
      (the user-facing front door remains the OLD textbook until an
      explicit cutover via the Cloudflare redirect).
    - Root index.html redirect now targets /$VOL1_PATH/ instead of
      /$BOOK_PATH/vol1/. The CF redirect is what users see; this is
      a fallback only.
    - Validation, commit message, and step summary lines log both
      volume paths separately rather than a single book parent.

  book-preview-dev.yml
    - Same VOL1/VOL2 read with fail-fast guard.
    - Cleans /vol1/, /vol2/, AND legacy /book/ on the dev preview repo
      to avoid zombie content from the previous nested deploy.
    - Copies preview-site/vol1 → /vol1/ and preview-site/vol2 → /vol2/
      separately (no longer wraps both under /book/).
    - Drops the /book/ chooser page on dev (the dev landing page
      already has volume cards). The chooser file stays in the repo
      for the eventual live cutover.

  publish-all-live.yml
    - Step summary now lists Volume I and Volume II as separate links.

  rewrite-dev-urls.sh
    - vol1 → vol1, vol2 → vol2 (identity mapping; matches every other
      subsite). The PREFIX depth math stays generic for any future
      nested subsite.

  site/index.qmd
    - Volume cards link to vol1/ and vol2/ (top-level relative). On
      live this resolves to mlsysbook.ai/vol1/ and mlsysbook.ai/vol2/.

Result: zero asymmetry between the volume URL convention used in source
and the actual deploy paths. The dev URL rewriter no longer needs a
special case. Citations made against Monday's release URLs will be
permanent and aligned with the project's everyday URL vocabulary.
2026-04-25 19:20:02 -04:00
Vijay Janapa Reddi
dfe51d98f7 Merge feat/games-polish-loop: 10-iter Lander polish + 14-game path-prefix fix
Brings the games subsite to working order on dev preview and ships the
10-iteration polish loop on Gradient Lander (the 404-headliner game).

Lander (10 iterations + playtest hotfix):
  iter 1  pre-game READY card with ENTER prompt + goal-pad pulse
  iter 2  thrust puffs, impact-velocity-scaled shake, win celebration
  iter 3  difficulty curve — slower rotation, capped slope, trajectory marker
  iter 4  in-canvas VRAM + descent-speed bars (replaces DOM HUD)
  iter 5  per-failure aha cards (diverged / local-min / OOM / etc.)
  iter 6  layered ship, altitude line, symmetric pad labels
  iter 7  daily seed, best-score, retry pill, emoji-grid share + Copy
  iter 8  refreshed landing page copy + screen-reader announce
  iter 9  mobile touch zones, prefers-reduced-motion, aria-live
  iter 10 final tone + win-pill recolor
  hotfix  module-relative imports + ENTER-to-launch UX feedback

Catalog (all 14 games):
  Module-relative imports across .qmd + .mjs files. Same path-prefix bug
  that broke Lander on dev preview affected every game; one batched fix.

Verification: all 14 games swept with Playwright (headless Chromium) on
locally-rendered _build/. Pass: canvas mounted, MLSP runtime ready, game
registered, no JS errors, no 4xx network requests.
2026-04-25 19:16:30 -04:00
Vijay Janapa Reddi
9d4732f4ff fix(games): module-relative imports across all 13 remaining games
The same path-prefix bug that broke Lander on dev preview affected the other
13 games too. Fixing all of them in one batch so the entire catalog works
on /cs249r_book_dev/, mlsysbook.ai/, and localhost equally.

Pattern applied:
  .qmd  include-in-header script:
    import "/assets/games/X.mjs"  →  import "../assets/games/X.mjs"
  .mjs  ES imports:
    from "/assets/games/runtime.mjs"          →  from "./runtime.mjs"
    from "/assets/games/vendor/pixi.min.mjs"  →  from "./vendor/pixi.min.mjs"

Files touched (10 .mjs + 13 .qmd):
  .mjs: allreduce, batch, cluster, kvcache, moe, oom, pipeline, prune,
        quantization, topology
  .qmd: allreduce, batch, checkpoint, cluster, kvcache, loader, moe, oom,
        pipeline, prune, quantization, roofline, topology
  (checkpoint, loader, roofline .mjs already used 'import * as runtime from
   ./runtime.mjs' — only their qmd files needed updating)

Verification: all 14 games rendered locally (quarto render games/), served
via python3 -m http.server, swept with Playwright headless Chromium.
Result: 14/14 pass — canvas mounted, MLSP runtime ready, game registered,
no JS errors, no 4xx network requests. Visual screenshots confirm each
game's HUD/title/content paints correctly.
2026-04-25 19:16:00 -04:00
Vijay Janapa Reddi
021feb5875 fix(lander): playtest hotfix — module-relative imports + ENTER-to-launch
User reported the live dev preview was broken (blank canvas, 'doesn't do anything').
Playwright probe confirmed all .mjs imports 404'd:

  [http 404] https://harvard-edge.github.io/assets/games/runtime.mjs
  [http 404] https://harvard-edge.github.io/assets/games/lander.mjs

Root cause: dev preview lives at /cs249r_book_dev/ but every game imported
its modules via root-absolute paths (/assets/games/...). The dev-URL rewrite
script only handles https://mlsysbook.ai/... — not root-relative paths.
All 14 games have this bug; Lander is fixed here.

Path-prefix fix:
- lander.qmd: /assets/games/X.mjs → ../assets/games/X.mjs
- lander.mjs: /assets/games/runtime.mjs → ./runtime.mjs (sibling)
- lander.mjs: /assets/games/vendor/pixi.min.mjs → ./vendor/pixi.min.mjs
- runtime.mjs: /assets/games/vendor/pixi.min.mjs → ./vendor/pixi.min.mjs
- runtime.mjs: pixi-filters dynamic import → ./vendor/pixi-filters.min.mjs

UX feedback (bundled): user asked 'say hit enter to start so people don't
feel rushed and then they can read what's expected':
- READY CTA 'press UP to launch' → 'press ENTER to launch'
- Added italic 'Take your time — read the controls.' hint above the CTA
- Keydown accepts Enter, Space, OR ↑ as launch — any of the three works
- Center touch zone calls new shared launch() helper
- 'How to play' instructions updated to match

Verification: rendered locally (quarto render games/lander.qmd), served via
python3 -m http.server, probed with Playwright (headless Chromium). Page
loads, READY shows new CTA, Enter dismisses overlay, ↑ thrusts, crash
triggers per-failure aha card with correct share text. Zero console errors.

Outstanding: other 13 games still have the same path-prefix bug. Either
apply the same per-file fix, or extend rewrite-dev-urls.sh to also rewrite
/assets/... paths.
2026-04-25 19:06:17 -04:00
Vijay Janapa Reddi
23e098688d feat(lander): iter 10 — final pass — drop misleading m/s, win-pill recolor
Cold re-read surfaced three small but real issues:

- dayChip claimed 'softest landing today: X m/s' — but X is screen-velocity
  in pixels-per-frame, not m/s. Misleading scientific units.
- Retry button stayed MIT-red regardless of outcome — wins missed a small
  visual reward.
- A few stale 'fuel' comments left over from the Lunar Lander origin.

Changes:
- dayChip: 'softest landing today: v=1.42' (dimensionless, matches share format).
  Empty-state: 'land softer than yesterday' (implies cross-day comparison).
- Retry pill recolors green ('↺ PLAY AGAIN') on win, stays MIT-red ('↺ TRY AGAIN')
  on any loss
- Comment sweep

Loop complete. 10/10 iterations on feat/games-polish-loop. See
.claude/_reviews/games-polish-loop-lander.md for the full log + reusable
template for the other 13 games.
2026-04-25 18:42:57 -04:00
Vijay Janapa Reddi
25016a9923 feat(lander): iter 9 — mobile / touch / a11y — three touch zones, reduce-motion, aria-live
The game was unplayable on a phone (keyboard-only controls). Animations
ignored OS reduce-motion preference. No aria announcement for game-over.

- Three invisible Pixi touch zones: left ⅓ steer-left, right ⅓ steer-right,
  center ⅓ thrust + tap-to-launch from READY screen
- Pointer fallbacks (pointerupoutside, pointercancel) prevent stuck-key state
- Z-order re-pinned after touch-zone creation so retry pill stays clickable
- reduceMotion flag from matchMedia('(prefers-reduced-motion: reduce)')
- safeShake + safeBurst wrappers gate all 5 shakes and 4 big crash bursts
- Goal-pad pulse and CTA pulse held static when reduce-motion is set
- New aria-live='assertive' span; onGameOver writes per-reason announcement
- 'How to play' gains a mobile/tablet bullet

Lens-bounded change. Iter 10 (final ship-readiness pass) is next.
2026-04-25 18:40:39 -04:00
Vijay Janapa Reddi
1e97ae6da9 feat(lander): iter 8 — landing page — trim duplicate HUD, refresh copy, a11y
The landing page lagged the in-canvas state machine. DOM HUD duplicated VRAM
and Speed values now drawn in-canvas. 'How to play' didn't mention READY
screen, RETRY button, daily seed, trajectory marker, or altitude line.
'The Systems Concept' only framed the win path.

- DOM HUD trimmed to a key-cap controls row; VRAM/Speed values moved to
  .mlsp-sr-only aria-live span (kept for AT, hidden visually)
- 'How to play' rewritten to match current state (READY-to-launch, all six
  affordances called out)
- 'The Systems Concept' lists all five failure modes mapped to real training:
  diverged, local-min, missed-basin, off-course, OOM
- Tail line invites the other 13 games
- New shared CSS: .mlsp-controls-line (key-cap row), .mlsp-sr-only (standard
  visually-hidden pattern); reusable across the catalog

Lens-bounded change. Iter 9 (mobile/touch/a11y) is next.
2026-04-25 18:26:28 -04:00
Vijay Janapa Reddi
eb98094d53 feat(lander): iter 7 — replay loop — daily seed, best score, retry button, share row
Lander was missing every replay-loop affordance the other 13 games have:
no daily seed, no best-score persistence, no visible retry button, no share
artifact, dead-static game-over state.

- dailySeed('lander') → terrain RNG; same loss surface worldwide today
- bestScore integration: lowest impact speed stored per-day; top-center chip
  shows 'Day #N · your softest landing today: X m/s'
- In-canvas RETRY pill (Pixi Container, eventMode=static), MIT-red background,
  visible only after gameOverFired
- buildShareText(state) per outcome: emoji-grid lines for win + 5 failure modes,
  with  new personal best marker
- attachShareRow in lander.qmd appends share text + 📋 copy button to aha card,
  with success feedback ( copied → reverts)
- New shared CSS in common.css: .mlsp-share-row, .mlsp-share-text, .mlsp-share-btn
  (reusable by every other game)

Lens-bounded change. Iter 8 (landing page) is next.
2026-04-25 18:21:51 -04:00
Vijay Janapa Reddi
7b6ce66886 feat(lander): iter 6 — visual polish — layered ship, altitude line, label symmetry
The game functioned but didn't feel presentation-bar. Ship was a tiny unstroked
triangle (protagonist of the screen, visually forgettable). Only the left
local pad was labeled. No altitude cue. The 'stochastic gradient noise' label
floated orphaned in the top-left.

- Ship rebuilt as layered Graphics: halo + body + crisp stroke + interior highlight
- Flame layered into outer glow + brighter core ($0xffd28a)
- Right local pad now labeled symmetrically with the left
- New altitude-reference dashed line from ship to the surface beneath
  (only drawn when headroom > 18px so it doesn't crowd touchdown)
- 'stochastic gradient noise' → 'loss landscape', repositioned at the basin
  wash so it reads as chart annotation, not free-floating decoration

Lens-bounded change. Iter 7 (replay loop & shareability) is next.
2026-04-25 18:17:41 -04:00
Vijay Janapa Reddi
50fd1ade50 feat(lander): iter 5 — pedagogy — per-failure aha messages, real OOM
Six possible outcomes, but all produced the identical aha card. The OOM event
didn't even end the game — flashed text once and let play continue. And
'GRADIENT EXPLOSION' misuses ML terminology (gradient explosion is NaN
propagation, not landing in the wrong place).

- state.reason tracks which failure occurred ('win'|'diverged'|'local-min'|
  'off-course'|'missed-basin'|'oom')
- 'GRADIENT EXPLOSION' → 'MISSED THE BASIN' (accurate to loss-surface metaphor)
- OOM now properly ends the game with full juice (matches real training)
- New AHA[reason] map: six distinct messages, each mapping the failure to its
  real ML systems counterpart
- api.aha(reason) returns the right card; lander.qmd attachAha consumes it
- Bug fix: state.gameOverFired guard so onGameOver fires once, not every frame

Lens-bounded change. Iter 6 (visual polish) is next.
2026-04-25 18:15:56 -04:00
Vijay Janapa Reddi
67ebc7d833 feat(lander): iter 4 — HUD legibility — in-canvas VRAM and speed bars
The HUD lived in a DOM strip below the canvas. Player couldn't read VRAM
and watch the ship simultaneously without an eye-flick worth ~200ms — long
enough to crash. Both VRAM and speed were text-only.

- VRAM vertical bar (top-right), color shifts blue → orange → red as memory depletes
- Descent-speed horizontal bar (bottom-left) with explicit green safe-zone,
  red danger-zone, and 'soft-landing limit ↑' threshold marker
- Both bars drawn every frame at top of ticker (never stale during pre-game / post-crash)
- DOM HUD retained for a11y / screen readers; iter 8 will trim its copy

Lens-bounded change. Iter 5 (failure-state pedagogy) is next.
2026-04-25 18:13:06 -04:00
Vijay Janapa Reddi
5659ccddf0 feat(lander): iter 3 — difficulty curve — slower rotation, capped slope, trajectory marker
Brutal first-experience. Rotation was 0.08 rad/frame (~4.8 rad/s — a 0.3s tap
rotates ~80°). Terrain slope randomized over [-8, +8] producing run-to-run
variance unrelated to player skill. No predictive feedback whatsoever.

- rotSpeed 0.08 → 0.055 (precision without sluggishness)
- terrain.slope range [-8, +8] → [-4, +4] (less luck, same lesson)
- Soft-landing thresholds: speed 2.0 → 2.4, angle 0.5 → 0.6 rad
- New translucent trajectory marker (30 frames coast-ahead) + faint trail line
- Promoted maxSafeSpeed and maxSafeAngle to named constants for future tuning

Lens-bounded change. Iter 4 (HUD legibility) is next.
2026-04-25 18:11:25 -04:00