No description
  • TypeScript 89.7%
  • CSS 9.8%
  • HTML 0.5%
Find a file
2026-07-16 13:40:34 +00:00
src add backlink 2026-07-14 19:46:46 +02:00
.gitignore first commit 2026-07-14 19:40:55 +02:00
index.html first commit 2026-07-14 19:40:55 +02:00
package-lock.json first commit 2026-07-14 19:40:55 +02:00
package.json first commit 2026-07-14 19:40:55 +02:00
README.md Update README.md 2026-07-16 13:40:34 +00:00
tsconfig.json first commit 2026-07-14 19:40:55 +02:00
vite.config.ts first commit 2026-07-14 19:40:55 +02:00

Bandit Fisher

A single-page web app: a contextual bandit (GLM-UCB) learns where the fish are by casting a hook and observing binary catch / no-catch feedback. Toggle the reward between maximise catch and sustain the fishery and watch the same algorithm behave completely differently — the demo of the essay's thesis that the math is neutral; the ethics is in the reward.

Stack

No framework. Vite + TypeScript + vanilla DOM + HTML5 canvas. The renderer is an imperative requestAnimationFrame loop; the side panel is a handful of nodes updated once per bandit round (~1 Hz). React would fight the animation loop and buy nothing — there's no component tree, no routing, no fetching.

  • Bandit math: typed arrays + a hand-rolled Cholesky solver (no mathjs). At M = 45 features, K = 50 candidates, N = 50 window, a round is ~500K ops — well under a frame at 1 Hz cycling.
  • Output: single minified bundle, ~7.6 KB gzipped JS + 1.2 KB CSS.

Run

cd scratchbooks/apps/fishing-demo
npm install
npm run dev        # http://localhost:5173

Build a static bundle for deployment:

npm run build      # outputs to dist/
npm run preview    # serve the production build locally

Deploy dist/ to any static host (GitHub Pages, Netlify, Cloudflare Pages). No backend, no API keys, no model download.

Project layout

src/
├── main.ts          # orchestration: rAF loop + bandit timer + candidate sampler
├── bandit.ts        # GLMUCB class: warm Newton + sliding window + UCB bonus
├── featurize.ts     # RBF anchors (marginal grids), featurize(), grid1d()
├── environment.ts   # lake: hidden preference, p_catch, drift, population model
├── render.ts        # canvas: lake background, flock, hook, heat-map overlay
├── ui.ts            # side panel: belief bars, regret chart, sliders, mode toggle
├── linalg.ts        # typed-array linalg: Cholesky solve, sigmoid, quadratic form
└── style.css        # layout, palette

What you see

  • Lake (left). A drifting fish flock (the agent must learn where they are from catches, not from seeing them), a heat-map overlay showing the agent's believed catch probability, and the hook easing toward each new cast.
  • Side panel (right). The reward toggle, the agent's current best-guess bait colour (swatch), per-feature belief bars (R / G / B / size), a rolling catch-rate chart, the cumulative-regret chart, and live stats. Behind "Configure fish behaviour" the demo's three knobs are exposed: the sliding-window size N, the exploration scale β, and the drift rate δ.

Algorithm notes (one-liners)

  • Why RBF features: a linear bandit on raw (x, y, r, g, b, size) fits a tilted plane through a peak. RBF features let it represent any smooth function — the standard kernel-approximation trick.
  • Why binary reward: Bernoulli noise → logistic link → GLM-UCB, whose Hessian weights w = g(1g) focus learning on the decision boundary.
  • Why a sliding window: the simplest robust approach to drifting preferences — "the agent remembers the last 50 casts".
  • Why two reward modes: the population model makes the flock depletable. Switching the reward the agent sees (not the algorithm) is the whole demo.