import React, { useEffect, useMemo, useRef, useState } from "react"; /** * Exploding Dots Addition (2→1 machine, i.e., base-2) * - Two input rows (top addend, bottom addend), each starting with all dots in the 1s box. * - User selects values (0–31). "Go" is enabled only when both are chosen AND a+b ≤ 31. * - On Go: rows combine into a working row, then we animate 2→1 explosions leftward until stable. * - Boxes: [16, 8, 4, 2, 1] (5 boxes to represent up to 31) */ export default function ExplodingDotsAddition() { const MAX = 31; const BOX_VALUES = [16, 8, 4, 2, 1]; // left→right const BOX_COUNT = BOX_VALUES.length; const [top, setTop] = useState(null); const [bottom, setBottom] = useState(null); const [stage, setStage] = useState< | "setup" // choosing numbers | "combine" // brief merge animation | "exploding" // step-by-step 2→1 explosions | "done" >("setup"); // Working cells for the animation (left→right: 16..1) const [cells, setCells] = useState([0, 0, 0, 0, 0]); // For a tiny pulse animation each time an explosion occurs const [explodeTick, setExplodeTick] = useState(0); const total = (top ?? -1) >= 0 && (bottom ?? -1) >= 0 ? (top as number) + (bottom as number) : null; const goEnabled = total !== null && total! <= MAX && stage === "setup"; // Reset everything function resetAll() { setStage("setup"); setCells([0, 0, 0, 0, 0]); setExplodeTick(0); } function handleGo() { if (!goEnabled || total === null) return; // Start with all dots in the ones place of the working row const start = [0, 0, 0, 0, 0]; start[BOX_COUNT - 1] = total; // put all dots into the 1s box setCells(start); setStage("combine"); // Brief pause to show the merge, then begin explosions setTimeout(() => setStage("exploding"), 600); } // Perform a single explosion step; returns [changed, newCells] function explodeStep(arr: number[]): [boolean, number[]] { const next = [...arr]; // Scan from rightmost to leftmost (1s→16s) for (let i = BOX_COUNT - 1; i >= 1; i--) { if (next[i] >= 2) { next[i] -= 2; next[i - 1] += 1; // 2 in a box explode to 1 in the box to the left return [true, next]; } } return [false, next]; } // Run the explosion animation when stage === "exploding" useEffect(() => { if (stage !== "exploding") return; let cancelled = false; const tick = () => { if (cancelled) return; const [changed, next] = explodeStep(cells); if (changed) { setCells(next); setExplodeTick((t) => t + 1); setTimeout(tick, 500); } else { setStage("done"); } }; const t = setTimeout(tick, 500); return () => { cancelled = true; clearTimeout(t); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [stage, cells]); // Helper: render a row of boxes with dots function Boxes({ counts, highlightRight = -1 }: { counts: number[]; highlightRight?: number }) { return (
{counts.map((count, idx) => ( ))}
); } function Box({ value, count, pulse }: { value: number; count: number; pulse?: boolean }) { const dots = Array.from({ length: count }); return (
{value}
{dots.map((_, i) => (
))}
); } // Build pre-Go displays (each addend lives entirely in the ones box) const topRow = useMemo(() => { const counts = [0, 0, 0, 0, 0]; if (top && top > 0) counts[BOX_COUNT - 1] = top; return counts; }, [top]); const bottomRow = useMemo(() => { const counts = [0, 0, 0, 0, 0]; if (bottom && bottom > 0) counts[BOX_COUNT - 1] = bottom; return counts; }, [bottom]); const errorText = useMemo(() => { if (top === null || bottom === null) return ""; if ((top as number) + (bottom as number) > MAX) return `Max total is ${MAX}. Reduce one of the addends.`; return ""; }, [top, bottom]); return (

Exploding Dots: 2→1 (Base 2) Addition

Pick two numbers (0–31). Each dot is a “1.” In a 2→1 machine, any 2 dots in a box explode into 1 dot in the box to the left.

{/* Selection Row */}
+
{/* Error / hint */} {errorText && (
{errorText}
)} {/* Action Buttons */}
{/* Visualization */}
{stage === "setup" && (
Top row
=
Bottom row
)} {stage !== "setup" && (
Working row {stage === "combine" && (combining dots…)} {stage === "exploding" && (exploding 2→1…)} {stage === "done" && (finished)}
{stage === "done" && ( )}
)}
{/* Footer note */}

Max total is 31, which fits in five boxes: 16s, 8s, 4s, 2s, and 1s.

); } function NumberPicker({ label, value, onChange, max }: { label: string; value: number | null; onChange: (n: number | null) => void; max: number }) { const safe = (n: number) => Math.max(0, Math.min(max, Math.floor(n))); return (
{label}
onChange(safe(parseInt(e.target.value)))} className="w-full" /> { const raw = e.target.value; if (raw === "") onChange(null); else onChange(safe(parseInt(raw))); }} placeholder="?" className="w-20 px-3 py-2 border border-black/10 rounded-xl bg-white shadow-sm" />
); } function RowLabel({ children }: { children: React.ReactNode }) { return
{children}
; } function ResultReadout({ cells }: { cells: number[] }) { // Convert binary counts to a number // cells left→right: [16,8,4,2,1] const value = cells.reduce((sum, count, i) => sum + (count > 0 ? [16, 8, 4, 2, 1][i] * count : 0), 0); const binary = cells.map((c) => (c > 0 ? 1 : 0)).join(""); return (
Result: {value} (binary {binary})
); } function highlightIndex(cells: number[]) { // Highlight the rightmost box that still has ≥2 dots (will explode next) for (let i = cells.length - 1; i >= 1; i--) { if (cells[i] >= 2) return i; } return -1; }