import React, { useState, useRef } from 'react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Slider } from '@/components/ui/slider'; import SocialShare from '@/components/SocialShare'; const SKY_BLUE = '#e0f2fe'; const MAROON = '#800000'; function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } const MIN_HEAPS = 2; const MAX_HEAPS = 10; const MIN_STONES = 1; const MAX_STONES = 31; // Extended to 31 to include power-of-16 box type Mode = 'random' | 'user'; type Heap = number; type Player = 0 | 1; const heapColors = [ '#fbbf24', '#60a5fa', '#34d399', '#f472b6', '#f87171', '#a78bfa', '#facc15', '#38bdf8', '#4ade80', '#f472b6', '#f87171', '#a78bfa', '#fbbf24', '#60a5fa', '#34d399', '#f472b6', '#f87171', '#a78bfa', '#facc15', '#38bdf8', ]; // Function to break down a number into its binary representation (powers of 2) const getBinaryRepresentation = (num: number): number[] => { const powers: number[] = []; let remaining = num; let power = 1; while (remaining > 0) { if (remaining & 1) { powers.push(power); } remaining >>= 1; power <<= 1; } return powers; }; // Function to calculate which powers of 2 appear an odd number of times across all heaps const getOddPowers = (heaps: number[]): Set => { const powerCounts = new Map(); heaps.forEach(heap => { const powers = getBinaryRepresentation(heap); powers.forEach(power => { powerCounts.set(power, (powerCounts.get(power) || 0) + 1); }); }); const oddPowers = new Set(); powerCounts.forEach((count, power) => { if (count % 2 === 1) { oddPowers.add(power); } }); return oddPowers; }; const AssistedNimGame: React.FC = () => { const [mode, setMode] = useState(null); const [showSetup, setShowSetup] = useState(false); const [userHeapCount, setUserHeapCount] = useState(3); const [userHeapSizes, setUserHeapSizes] = useState([3, 3, 3]); const [heaps, setHeaps] = useState([]); const [currentPlayer, setCurrentPlayer] = useState(0); const [winner, setWinner] = useState(null); const [heapsGroupWidth, setHeapsGroupWidth] = useState('0px'); const initialHeapsRef = useRef([]); // Calculate the maximum number of powers of 2 needed for any heap const getMaxPowers = (heaps: number[]): number => { if (heaps.length === 0) return 0; // Since we cap at 31, we need 5 powers: 1, 2, 4, 8, 16 return 5; }; // Calculate width based on maximum powers of 2 React.useEffect(() => { if (mode !== null && heaps.length > 0) { const maxPowers = getMaxPowers(heaps); const gapWidth = 0.5; // Gap between power groups const labelWidth = 2; // Width for heap size label // Calculate total width accounting for different box sizes let totalBoxWidth = 0; for (let i = 0; i < maxPowers; i++) { const power = 1 << i; // 1, 2, 4, 8, 16 if (power === 16) { totalBoxWidth += 12; // Power-of-16 box is 12rem wide } else if (power === 8) { totalBoxWidth += 8; // Power-of-8 box is 8rem wide } else { totalBoxWidth += 4; // Other boxes are 4rem wide } } const totalWidthRem = labelWidth + totalBoxWidth + (maxPowers - 1) * gapWidth; setHeapsGroupWidth(`${totalWidthRem}rem`); } }, [mode, heaps.length]); // Store initial heaps for reset React.useEffect(() => { if (mode !== null && heaps.length > 0) { initialHeapsRef.current = [...heaps]; } }, [mode, heaps.length]); // Start a new game const startGame = (selectedMode: Mode) => { setMode(selectedMode); setWinner(null); setCurrentPlayer(0); if (selectedMode === 'random') { const k = getRandomInt(MIN_HEAPS, MAX_HEAPS); const randomHeaps = Array.from({ length: k }, () => getRandomInt(MIN_STONES, MAX_STONES)); setHeaps(randomHeaps); } else { setShowSetup(true); } }; // Handle user-defined setup const handleUserSetupConfirm = () => { setHeaps([...userHeapSizes]); setShowSetup(false); setWinner(null); setCurrentPlayer(0); }; // Handle stone click: remove all stones from clicked index to the end in that heap const handleStoneClick = (heapIdx: number, stoneIdx: number) => { if (winner !== null) return; if (heaps[heapIdx] === 0) return; const newHeaps = [...heaps]; newHeaps[heapIdx] = stoneIdx; setHeaps(newHeaps); // Check for win if (newHeaps.every(heap => heap === 0)) { setWinner(currentPlayer); } else { setCurrentPlayer(currentPlayer === 0 ? 1 : 0); } }; const handlePlayAgain = () => { setWinner(null); setCurrentPlayer(0); setHeaps([...initialHeapsRef.current]); }; const handleResetGame = () => { setWinner(null); setCurrentPlayer(0); setHeaps([...initialHeapsRef.current]); }; const handleNewGame = () => { setMode(null); setHeaps([]); setWinner(null); setCurrentPlayer(0); }; const handleHeapCountChange = (val: number) => { setUserHeapCount(val); setUserHeapSizes(Array.from({ length: val }, () => 3)); }; const handleHeapSizeChange = (idx: number, val: number) => { const newSizes = [...userHeapSizes]; newSizes[idx] = val; setUserHeapSizes(newSizes); }; // Calculate which powers of 2 appear an odd number of times const oddPowers = getOddPowers(heaps); // Render a heap broken down into powers of 2 const renderHeap = (heap: number, idx: number) => { const powers = getBinaryRepresentation(heap); const maxPowers = getMaxPowers(heaps); return (
{/* Heap size label */} {heap} {/* Powers of 2 groups - horizontal layout with fixed widths */}
{Array.from({ length: maxPowers }, (_, powerIdx) => { const power = 1 << powerIdx; // 2^powerIdx (1, 2, 4, 8, 16) const hasPower = powers.includes(power); const isOdd = oddPowers.has(power); const isPower8 = power === 8; // Check if this is the power-of-8 box const isPower16 = power === 16; // Check if this is the power-of-16 box return (
{/* Stones for this power - single row layout */} {hasPower ? (
{Array.from({ length: power }, (_, stoneIdx) => ( { // Calculate the actual stone index in the heap const actualIndex = powers .filter(p => p < power) .reduce((sum, p) => sum + p, 0) + stoneIdx; handleStoneClick(idx, actualIndex); }} title={`Power of 2: ${power}, Stone ${stoneIdx + 1}`} /> ))}
) : (
// Fixed empty space )}
); })}
); }; // Dynamic backdrop color based on current player const backdropColor = winner === null ? (currentPlayer === 0 ? SKY_BLUE : MAROON) : '#f0f0f0'; return (
Assisted Game of Nim Visualize the XOR strategy! Heaps are broken down into powers of 2. Red borders show powers that appear an odd number of times - these are your winning moves! {/* Game Controls */} {mode === null && (
)} {mode !== null && ( <> {/* Current Turn Indicator and Reset Button */}
{winner === null ? ( <>

Current Turn:

Player {currentPlayer + 1}
) : (
Winner: Player {winner + 1}!
)}
{/* Heaps Display */}
{heaps.map((heap, idx) => renderHeap(heap, idx))}
{/* Strategy Explanation */}

XOR Strategy Visualization

Red borders = Powers of 2 that appear an odd number of times (your winning moves)
Green borders = Powers of 2 that appear an even number of times (balanced)

Strategy based on a well-known analysis, in particular the intuition explored by {' '} Joel David Hamkins

{/* Play Again Button */} {winner !== null && (
)} )} {/* Instructions */}

Click any stone to remove it and all stones to its right. The visual breakdown shows powers of 2 - red borders indicate winning moves in the XOR strategy!

{/* User-defined setup modal */} Define Your Game
handleHeapCountChange(value[0])} min={MIN_HEAPS} max={MAX_HEAPS} step={1} className="mt-2" />
{userHeapSizes.map((size, idx) => (
handleHeapSizeChange(idx, value[0])} min={MIN_STONES} max={MAX_STONES} step={1} className="mt-1" />
))}
{/* Social Share */}
); }; export default AssistedNimGame;