From 6af3ec81ef1859d2511d61294927931a83a56e96 Mon Sep 17 00:00:00 2001 From: Neeldhara Misra Date: Tue, 22 Jul 2025 03:35:33 +0530 Subject: [PATCH] Add Assisted Nim Game and update components --- src/App.tsx | 2 + src/components/AssistedNimGame.tsx | 397 ++++++++++++++++++++++++++++ src/components/GamesGallery.tsx | 8 + src/components/InteractiveIndex.tsx | 9 + src/pages/AssistedNimGamePage.tsx | 12 + src/pages/Index.tsx | 7 + 6 files changed, 435 insertions(+) create mode 100644 src/components/AssistedNimGame.tsx create mode 100644 src/pages/AssistedNimGamePage.tsx diff --git a/src/App.tsx b/src/App.tsx index 69df7cb..60f17e7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,6 +30,7 @@ import TernarySearchTrickGreenScreen from "./pages/TernarySearchTrickGreenScreen import NorthcottsGamePage from "./pages/NorthcottsGamePage"; import KnightsPuzzlePage from "./pages/KnightsPuzzlePage"; import NimGamePage from "./pages/NimGamePage"; +import AssistedNimGamePage from "./pages/AssistedNimGamePage"; import Foundations from "./pages/theme-pages/discrete-math/Foundations"; import Proofs from "./pages/theme-pages/discrete-math/Proofs"; import Counting from "./pages/theme-pages/discrete-math/Counting"; @@ -86,6 +87,7 @@ const App = () => ( } /> } /> } /> + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/components/AssistedNimGame.tsx b/src/components/AssistedNimGame.tsx new file mode 100644 index 0000000..ee3fa28 --- /dev/null +++ b/src/components/AssistedNimGame.tsx @@ -0,0 +1,397 @@ +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; \ No newline at end of file diff --git a/src/components/GamesGallery.tsx b/src/components/GamesGallery.tsx index 83e2ca0..2aae6df 100644 --- a/src/components/GamesGallery.tsx +++ b/src/components/GamesGallery.tsx @@ -7,6 +7,7 @@ import Layout from '@/components/Layout'; import GameOfSim from '@/components/GameOfSim'; import NorthcottsGame from '@/components/NorthcottsGame'; import NimGame from '@/components/NimGame'; +import AssistedNimGame from '@/components/AssistedNimGame'; interface Game { id: string; @@ -17,6 +18,13 @@ interface Game { } const games: Game[] = [ + { + id: 'assisted-nim', + title: 'Assisted Game of Nim', + description: 'Learn the XOR strategy! Visualize how powers of 2 determine winning moves with color-coded borders.', + tags: ['strategy', 'two-player', 'math', 'logic', 'game-theory', 'xor-strategy', 'educational'], + component: AssistedNimGame + }, { id: 'nim', title: 'Game of Nim', diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index 2780529..d3e38da 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -91,6 +91,15 @@ const allInteractives: Interactive[] = [ theme: 'Puzzles', dateAdded: '2024-07-21' }, + { + id: 'assisted-nim', + title: 'Assisted Game of Nim', + description: 'Learn the XOR strategy! Visualize how powers of 2 determine winning moves with color-coded borders.', + tags: ['strategy', 'two-player', 'math', 'logic', 'game-theory', 'xor-strategy', 'educational'], + path: '/games/assisted-nim', + theme: 'Games', + dateAdded: '2024-07-21' + }, { id: 'nim', title: 'Game of Nim', diff --git a/src/pages/AssistedNimGamePage.tsx b/src/pages/AssistedNimGamePage.tsx new file mode 100644 index 0000000..233302a --- /dev/null +++ b/src/pages/AssistedNimGamePage.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import AssistedNimGame from '@/components/AssistedNimGame'; + +const AssistedNimGamePage = () => { + return ( +
+ +
+ ); +}; + +export default AssistedNimGamePage; \ No newline at end of file diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 2026e9b..5fbaff8 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -69,6 +69,13 @@ const allInteractives = [{ tags: ['strategy', 'two-player', 'math', 'logic'], path: '/games/nim', theme: 'Games' +}, { + id: 'assisted-nim', + title: 'Assisted Game of Nim', + description: 'Learn the XOR strategy! Visualize how powers of 2 determine winning moves with color-coded borders.', + tags: ['strategy', 'two-player', 'math', 'logic', 'xor-strategy', 'educational'], + path: '/games/assisted-nim', + theme: 'Games' }]; // Featured interactives (3 fixed ones)