- TypeScript 89.7%
- CSS 9.8%
- HTML 0.5%
| src | ||
| .gitignore | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
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). AtM = 45features,K = 50candidates,N = 50window, 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(1−g)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.