computing-pi: raise cap to 5000, amber levels, discard, H/T counters
T1: MAX_TOSSES_PER_RUN is now 5000. T2: Tiles in progress past 500 tosses wear a progressively darker amber wash (units of 500, up to 9 levels). Completed runs stay green. T3: Runs that hit the cap are now flagged discarded. They carry a red wash and an italic 'discarded' footer, are excluded from avg × 4, and the summary / detail dialog note how many were dropped. T4: Added a grey H/T header strip to each tile showing the running heads count on the left and tails count on the right.
This commit is contained in:
parent
58b10a6d99
commit
1efdec5e44
2 changed files with 124 additions and 43 deletions
|
|
@ -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<number, string> = {
|
||||
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 && (
|
||||
<p className="text-sm text-center text-muted-foreground">
|
||||
All {runs.length} runs complete. Click any tile to see its full toss sequence.
|
||||
All {runs.length} runs complete
|
||||
{discardedCount > 0 && (
|
||||
<>
|
||||
{' '}(
|
||||
<span className="text-destructive">
|
||||
{discardedCount} discarded after hitting the {MAX_TOSSES_PER_RUN}-toss cap
|
||||
</span>
|
||||
)
|
||||
</>
|
||||
)}
|
||||
. Click any tile to see its full toss sequence.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
@ -183,12 +224,21 @@ const ComputingPi: React.FC = () => {
|
|||
<span className="font-mono">{selectedRun.heads}</span> heads,{' '}
|
||||
<span className="font-mono">{selectedRun.tails}</span> tails in{' '}
|
||||
<span className="font-mono">{selectedRun.sequence.length}</span>{' '}
|
||||
toss{selectedRun.sequence.length === 1 ? '' : 'es'}. Recorded fraction:{' '}
|
||||
toss{selectedRun.sequence.length === 1 ? '' : 'es'}.{' '}
|
||||
{selectedRun.discarded ? (
|
||||
<span className="text-destructive">
|
||||
Discarded: hit the {MAX_TOSSES_PER_RUN}-toss cap without heads ever exceeding tails.
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
Recorded fraction:{' '}
|
||||
<span className="font-mono font-semibold">
|
||||
{selectedRun.heads}/{selectedRun.sequence.length} ={' '}
|
||||
{(selectedRun.fraction ?? 0).toFixed(4)}
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
|
@ -212,21 +262,39 @@ interface RunTileProps {
|
|||
|
||||
const RunTile: React.FC<RunTileProps> = ({ index, run, clickable, onClick }) => {
|
||||
const seq = run.sequence.join('');
|
||||
const footer = run.stopped
|
||||
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}`;
|
||||
: `${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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={!clickable}
|
||||
aria-label={`Run ${index + 1}${run.stopped ? `, fraction ${footer}` : ''}`}
|
||||
className={`rounded-lg border px-2 py-1.5 flex flex-col gap-1 text-left transition-colors ${
|
||||
run.stopped
|
||||
? 'bg-green-50 dark:bg-green-950/30 border-green-500/60'
|
||||
: 'bg-card border-border'
|
||||
} ${clickable ? 'cursor-pointer hover:border-primary' : 'cursor-default'}`}
|
||||
aria-label={aria}
|
||||
className={`rounded-lg border overflow-hidden flex flex-col text-left transition-colors ${settledClass} ${
|
||||
clickable ? 'cursor-pointer hover:border-primary' : 'cursor-default'
|
||||
}`}
|
||||
>
|
||||
{/* H / T counters */}
|
||||
<div className="flex justify-between items-center text-[10px] font-mono tabular-nums bg-black/5 dark:bg-white/5 px-2 py-0.5 text-muted-foreground">
|
||||
<span>H:{run.heads}</span>
|
||||
<span>T:{run.tails}</span>
|
||||
</div>
|
||||
|
||||
{/* Billboard + footer */}
|
||||
<div className="flex flex-col gap-1 px-2 py-1.5 flex-1">
|
||||
<div
|
||||
className="overflow-hidden whitespace-nowrap text-right font-mono text-xs h-4 tracking-wider"
|
||||
style={{
|
||||
|
|
@ -234,10 +302,15 @@ const RunTile: React.FC<RunTileProps> = ({ index, run, clickable, onClick }) =>
|
|||
WebkitMaskImage: 'linear-gradient(to right, transparent 0, black 25%)',
|
||||
}}
|
||||
>
|
||||
{seq || ' '}
|
||||
{seq || ' '}
|
||||
</div>
|
||||
<div
|
||||
className={`font-mono text-[11px] tabular-nums ${
|
||||
run.discarded ? 'text-destructive italic' : 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{footer}
|
||||
</div>
|
||||
<div className="font-mono text-[11px] text-muted-foreground tabular-nums">
|
||||
{run.stopped ? footer : `${footer} toss${run.sequence.length === 1 ? '' : 'es'}`}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue