Files
cs249r_book/site/games/lander.qmd
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

144 lines
6.8 KiB
Plaintext

---
title: "Gradient Lander"
subtitle: "Balance batch size and learning rate to converge safely."
description: "A browser mini-game from MLSysBook Playground. Land your model in the global minimum without running out of VRAM."
page-layout: article
format:
html:
include-in-header:
- text: |
<link rel="stylesheet" href="/assets/games/common.css">
<script type="module">
import "../assets/games/runtime.mjs";
import "../assets/games/lander.mjs";
</script>
---
```{=html}
<div class="mlsp-game-container" role="region" aria-label="Gradient Lander mini-game">
<canvas id="mlsp-canvas" class="mlsp-game-canvas" width="680" height="460"></canvas>
<div class="mlsp-game-hud">
<span class="mlsp-controls-line">
<kbd>↑</kbd> thrust (batch size) ·
<kbd>←</kbd> <kbd>→</kbd> steer learning rate ·
<kbd>R</kbd> retry
</span>
<span class="mlsp-sr-only" aria-live="polite">VRAM <span id="mlsp-vram">100</span>%, descent speed <span id="mlsp-speed">0</span></span>
<span class="mlsp-sr-only" id="mlsp-announce" aria-live="assertive" role="status"></span>
<button type="button" class="mlsp-fullscreen-btn" onclick="this.closest('.mlsp-game-container').requestFullscreen()" title="Full Screen" aria-label="Full Screen">⛶</button>
</div>
</div>
<div id="mlsp-aha-slot"></div>
<script>
(function bootLander(){
function tryBoot() {
if (!window.MLSP || !MLSP.games || !MLSP.games.lander) return setTimeout(tryBoot, 30);
var canvas = document.getElementById("mlsp-canvas");
var $vram = document.getElementById("mlsp-vram");
var $speed = document.getElementById("mlsp-speed");
var ahaSlot = document.getElementById("mlsp-aha-slot");
var pendingResult = null;
var resolvedApi = null;
Promise.resolve(MLSP.games.lander(canvas, {
onScoreChange: function(s) {
$vram.textContent = s.vram;
$speed.textContent = s.speed;
if (s.speed > 40) { $speed.style.color = "#c44444"; } else { $speed.style.color = "inherit"; }
},
onGameOver: function(result) {
var ann = document.getElementById("mlsp-announce");
if (ann) {
var labels = {
"win": "Converged on the global minimum.",
"diverged": "Diverged. Learning rate too high.",
"local-min": "Settled in a local minimum. Suboptimal.",
"off-course": "Off course. Weights diverged.",
"missed-basin": "Missed the basin. No clear gradient.",
"oom": "Out of memory. VRAM exhausted."
};
ann.textContent = (labels[result && result.reason] || "Run ended.") + " Press R or click Try Again.";
}
if (resolvedApi) attachAha(resolvedApi, result);
else pendingResult = result;
},
onRetry: function() { window.location.reload(); }
})).then(function(api) {
resolvedApi = api;
if (pendingResult) attachAha(api, pendingResult);
});
function attachAha(api, result) {
// Per-failure aha: each loss type maps to a distinct ML systems lesson.
// Falls back to the legacy single-message API for safety.
var aha = (api.aha && result && result.reason) ? api.aha(result.reason) : null;
var label = aha ? aha.label : api.ahaLabel;
var text = aha ? aha.text : api.ahaText;
var link = aha && aha.link ? aha.link : api.ahaLink;
MLSP.showAhaCard(ahaSlot, label, text, link);
attachShareRow(result);
}
function attachShareRow(result) {
if (!result || !result.shareText) return;
var card = ahaSlot.querySelector(".mlsp-aha-card");
if (!card || card.querySelector(".mlsp-share-row")) return;
var row = document.createElement("div");
row.className = "mlsp-share-row";
var pre = document.createElement("pre");
pre.className = "mlsp-share-text";
pre.textContent = result.shareText;
var btn = document.createElement("button");
btn.type = "button";
btn.className = "mlsp-share-btn";
btn.textContent = "📋 copy result";
btn.addEventListener("click", function() {
navigator.clipboard.writeText(result.shareText).then(function() {
btn.textContent = "✅ copied";
setTimeout(function() { btn.textContent = "📋 copy result"; }, 1500);
}).catch(function() {
btn.textContent = "(copy failed)";
});
});
row.appendChild(pre);
row.appendChild(btn);
card.appendChild(row);
}
}
tryBoot();
})();
</script>
```
## How to play
The game starts paused so you can read the controls. Press <kbd>Enter</kbd> (or <kbd>Space</kbd>, or <kbd>↑</kbd>) to launch when you're ready.
- <kbd>↑</kbd> hold to thrust — increases batch size, stabilizes descent, burns VRAM.
- <kbd>←</kbd> <kbd>→</kbd> tilt — steer the learning-rate trajectory across the loss landscape.
- Watch the dashed line under your ship — that's your altitude over the ground beneath. Watch the small blue circle ahead of your ship — that's where you'll land if you do nothing.
- Touch down on the **green pad** (the global minimum) at low speed and near-vertical angle. The green pad pulses so you can spot it instantly.
- Hit the orange pads → local minimum (suboptimal). Hit anything else → divergence. Run out of VRAM → OOM.
- Click <kbd>↺ TRY AGAIN</kbd> in the canvas, or press <kbd>R</kbd>, to retry. The terrain is the same for everyone playing today.
- **On a phone or tablet:** the canvas is divided into three vertical zones — tap-and-hold the **left third** to steer left, the **right third** to steer right, the **center** to thrust (and to launch from the READY screen). The game also respects your OS *Reduce Motion* preference.
## The Systems Concept
Large batches make optimization feel smoother because gradient noise falls — but the memory bill rises. Lander turns that tradeoff into a landing problem: stability only helps if you still have enough VRAM left and the learning-rate trajectory actually reaches the basin you wanted.
Each failure mode in the game maps to a real one in training:
- **Diverged (LR too high)** — over-aggressive learning rate overshoots the minimum.
- **Local minimum** — optimizer settles in a sub-optimal basin without enough exploration.
- **Missed the basin** — landed in a flat or saddle region, no clear gradient signal.
- **Off course** — weights drift out of the parameter space the model can handle.
- **OOM** — VRAM exhausted; the run dies the way real training processes do.
A "soft landing" on the global minimum is the only outcome where every constraint held simultaneously. That is the dream of large-batch training.
---
*Part of the [MLSysBook Playground](/games/) — try the other 13 games.* Found a bug? [Report an issue](https://github.com/harvard-edge/cs249r_book/issues/new?labels=bug&title=Bug+in+Game).