diff --git a/src/components/interactives/ComputingPi.tsx b/src/components/interactives/ComputingPi.tsx index fabeefc..e1ea674 100644 --- a/src/components/interactives/ComputingPi.tsx +++ b/src/components/interactives/ComputingPi.tsx @@ -11,6 +11,7 @@ type Toss = 'H' | 'T'; interface Run { sequence: Toss[]; stopped: boolean; + discarded: boolean; heads: number; tails: number; fraction: number | null; @@ -20,10 +21,26 @@ const MIN_RUNS = 4; const MAX_RUNS = 100; const DEFAULT_RUNS = 36; const TICK_MS = 110; -const MAX_TOSSES_PER_RUN = 1000; // safety cap; first-passage time has infinite expectation +const MAX_TOSSES_PER_RUN = 5000; // safety cap; first-passage time has infinite expectation +const AMBER_STEP = 500; // tosses per amber level +const AMBER_MAX_LEVEL = 9; + +// Amber wash classes, pre-declared so Tailwind's JIT picks them up. +const AMBER_WASH: Record = { + 0: '', + 1: 'bg-amber-500/10 border-amber-500/30', + 2: 'bg-amber-500/20 border-amber-500/40', + 3: 'bg-amber-500/30 border-amber-500/50', + 4: 'bg-amber-500/40 border-amber-500/60', + 5: 'bg-amber-500/50 border-amber-500/70', + 6: 'bg-amber-500/60 border-amber-500/80', + 7: 'bg-amber-500/70 border-amber-500/90', + 8: 'bg-amber-500/80 border-amber-600', + 9: 'bg-amber-500/90 border-amber-600', +}; function emptyRun(): Run { - return { sequence: [], stopped: false, heads: 0, tails: 0, fraction: null }; + return { sequence: [], stopped: false, discarded: false, heads: 0, tails: 0, fraction: null }; } function stepRun(r: Run): Run { @@ -33,12 +50,25 @@ function stepRun(r: Run): Run { const tails = r.tails + (toss === 'T' ? 1 : 0); const sequence = [...r.sequence, toss]; if (heads > tails) { - return { sequence, stopped: true, heads, tails, fraction: heads / sequence.length }; + return { + sequence, + stopped: true, + discarded: false, + heads, + tails, + fraction: heads / sequence.length, + }; } if (sequence.length >= MAX_TOSSES_PER_RUN) { - return { sequence, stopped: true, heads, tails, fraction: heads / sequence.length }; + // Cap hit without the walk crossing above ½ — drop this run so it doesn't bias the average. + return { sequence, stopped: true, discarded: true, heads, tails, fraction: null }; } - return { sequence, stopped: false, heads, tails, fraction: null }; + return { sequence, stopped: false, discarded: false, heads, tails, fraction: null }; +} + +function amberLevel(run: Run): number { + if (run.stopped) return 0; + return Math.min(AMBER_MAX_LEVEL, Math.floor(run.sequence.length / AMBER_STEP)); } const ComputingPi: React.FC = () => { @@ -49,12 +79,13 @@ const ComputingPi: React.FC = () => { const started = runs.length > 0; const allStopped = started && runs.every((r) => r.stopped); + const discardedCount = runs.filter((r) => r.discarded).length; const avgTimes4 = useMemo(() => { - const stopped = runs.filter((r) => r.stopped && r.fraction !== null); - if (stopped.length === 0) return null; - const sum = stopped.reduce((acc, r) => acc + (r.fraction ?? 0), 0); - return (sum / stopped.length) * 4; + const usable = runs.filter((r) => r.stopped && !r.discarded && r.fraction !== null); + if (usable.length === 0) return null; + const sum = usable.reduce((acc, r) => acc + (r.fraction ?? 0), 0); + return (sum / usable.length) * 4; }, [runs]); // Simulate one toss per active run on every tick. @@ -163,7 +194,17 @@ const ComputingPi: React.FC = () => { {allStopped && (

- All {runs.length} runs complete. Click any tile to see its full toss sequence. + All {runs.length} runs complete + {discardedCount > 0 && ( + <> + {' '}( + + {discardedCount} discarded after hitting the {MAX_TOSSES_PER_RUN}-toss cap + + ) + + )} + . Click any tile to see its full toss sequence.

)} @@ -183,12 +224,21 @@ const ComputingPi: React.FC = () => { {selectedRun.heads} heads,{' '} {selectedRun.tails} tails in{' '} {selectedRun.sequence.length}{' '} - toss{selectedRun.sequence.length === 1 ? '' : 'es'}. Recorded fraction:{' '} - - {selectedRun.heads}/{selectedRun.sequence.length} ={' '} - {(selectedRun.fraction ?? 0).toFixed(4)} - - . + toss{selectedRun.sequence.length === 1 ? '' : 'es'}.{' '} + {selectedRun.discarded ? ( + + Discarded: hit the {MAX_TOSSES_PER_RUN}-toss cap without heads ever exceeding tails. + + ) : ( + <> + Recorded fraction:{' '} + + {selectedRun.heads}/{selectedRun.sequence.length} ={' '} + {(selectedRun.fraction ?? 0).toFixed(4)} + + . + + )} )} @@ -212,32 +262,55 @@ interface RunTileProps { const RunTile: React.FC = ({ index, run, clickable, onClick }) => { const seq = run.sequence.join(''); - const footer = run.stopped - ? `${run.heads}/${run.sequence.length}` - : `${run.sequence.length}`; + const settledClass = run.discarded + ? 'bg-red-50 dark:bg-red-950/30 border-red-500/60' + : run.stopped + ? 'bg-green-50 dark:bg-green-950/30 border-green-500/60' + : AMBER_WASH[amberLevel(run)] || 'bg-card border-border'; + const footer = run.discarded + ? 'discarded' + : run.stopped + ? `${run.heads}/${run.sequence.length}` + : `${run.sequence.length} toss${run.sequence.length === 1 ? '' : 'es'}`; + const aria = run.discarded + ? `Run ${index + 1}, discarded (cap)` + : run.stopped + ? `Run ${index + 1}, fraction ${run.heads}/${run.sequence.length}` + : `Run ${index + 1}, in progress (${run.sequence.length} tosses)`; return ( ); diff --git a/src/data/admin-data.json b/src/data/admin-data.json index 66cdbd5..bbc244d 100644 --- a/src/data/admin-data.json +++ b/src/data/admin-data.json @@ -384,26 +384,34 @@ { "id": "b7be0d32-057f-4902-8b0c-e9fcaa068d5d", "text": "change threshold of safety cap to 5000", - "done": false, - "refId": "T1" + "done": true, + "refId": "T1", + "remark": "MAX_TOSSES_PER_RUN bumped from 1000 to 5000.", + "doneDate": "2026-04-21" }, { "id": "addb1334-383f-46f4-bfdf-b5177088a694", "text": "green for completed, increasingly darker shades of amber for when we exceed 500 tosses and keep going (in units of 500)", - "done": false, - "refId": "T2" + "done": true, + "refId": "T2", + "remark": "Added amberLevel(run) = min(9, floor(tosses/500)) and an AMBER_WASH lookup from bg-amber-500/10 up to /90 (pre-declared so Tailwind's JIT picks up all 9 class strings). Tiles only wear amber while still running; stopped runs stay green.", + "doneDate": "2026-04-21" }, { "id": "fca36815-569a-4930-9ef6-9ce6ea140df4", "text": "when the safety cap is breached, discard those experiments from the average", - "done": false, - "refId": "T3" + "done": true, + "refId": "T3", + "remark": "Runs now carry a discarded flag. stepRun sets it when the cap is hit (fraction stays null). avgTimes4 filters on !discarded && fraction !== null. Discarded tiles get a red wash + italic 'discarded' footer, and the all-complete summary and detail dialog call out how many hit the cap.", + "doneDate": "2026-04-21" }, { "id": "cd4c937f-3f9d-4314-88fb-76ada39a3744", "text": "above each of the tiny billboards, add a small grey row, on the left track the number of heads and on the right track the number of tails", - "done": false, - "refId": "T4" + "done": true, + "refId": "T4", + "remark": "Added a bg-black/5 (dark: bg-white/5) header strip above the billboard showing 'H:{n}' left-aligned and 'T:{n}' right-aligned. Restructured RunTile so the wash class still applies to the whole tile but the header sits flush with the border.", + "doneDate": "2026-04-21" } ] }