diff --git a/src/App.tsx b/src/App.tsx index d51111c..7003d3b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -53,6 +53,7 @@ import RulesOfInferencePlaygroundPage from './pages/RulesOfInferencePlaygroundPa import PebblePlacementGamePage from './pages/PebblePlacementGamePage'; import BulgarianSolitairePage from './pages/BulgarianSolitairePage'; import SubtractionGamePage from './pages/SubtractionGamePage'; +import StackingBlocksPage from './pages/StackingBlocksPage'; const queryClient = new QueryClient(); @@ -109,6 +110,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index 2c8dbee..d790dc3 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -225,6 +225,15 @@ const allInteractives: Interactive[] = [ path: '/discrete-math/foundations/rules-of-inference', theme: 'Discrete Math', dateAdded: '2024-12-23' + }, + { + id: 'stacking-blocks', + title: 'Stacking Blocks Optimization', + description: 'Split stacks of blocks to maximize your score! An optimization puzzle about strategic decision-making.', + tags: ['optimization', 'strategy', 'mathematical', 'algorithm', 'puzzles'], + path: '/puzzles/stacking-blocks', + theme: 'Puzzles', + dateAdded: '2025-01-22' } ]; diff --git a/src/components/StackingBlocks.tsx b/src/components/StackingBlocks.tsx new file mode 100644 index 0000000..2a1af48 --- /dev/null +++ b/src/components/StackingBlocks.tsx @@ -0,0 +1,272 @@ +import React, { useState, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +import { Slider } from '@/components/ui/slider'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import SocialShare from './SocialShare'; +import confetti from 'canvas-confetti'; + +interface StackingBlocksProps { + showSocialShare?: boolean; +} + +const StackingBlocks: React.FC = ({ showSocialShare = false }) => { + const [n, setN] = useState(8); + const [stacks, setStacks] = useState([]); + const [score, setScore] = useState(0); + const [gameStarted, setGameStarted] = useState(false); + const [gameComplete, setGameComplete] = useState(false); + const [selectedStack, setSelectedStack] = useState(null); + const [splitSize1, setSplitSize1] = useState(''); + const [splitSize2, setSplitSize2] = useState(''); + const [dialogOpen, setDialogOpen] = useState(false); + + const startGame = useCallback(() => { + setStacks([n]); + setScore(0); + setGameStarted(true); + setGameComplete(false); + setSelectedStack(null); + }, [n]); + + const resetGame = useCallback(() => { + setStacks([]); + setScore(0); + setGameStarted(false); + setGameComplete(false); + setSelectedStack(null); + setSplitSize1(''); + setSplitSize2(''); + setDialogOpen(false); + }, []); + + const handleStackClick = useCallback((index: number, stackSize: number) => { + if (stackSize === 1) return; // Can't split stacks of size 1 + setSelectedStack(index); + setSplitSize1('1'); + setSplitSize2((stackSize - 1).toString()); + setDialogOpen(true); + }, []); + + const executeSplit = useCallback(() => { + if (selectedStack === null) return; + + const size1 = parseInt(splitSize1); + const size2 = parseInt(splitSize2); + const originalSize = stacks[selectedStack]; + + // Validate the split + if (size1 <= 0 || size2 <= 0 || size1 + size2 !== originalSize) { + return; + } + + // Calculate points gained + const pointsGained = size1 * size2; + + // Update stacks + const newStacks = [...stacks]; + newStacks.splice(selectedStack, 1); // Remove the original stack + newStacks.push(size1, size2); // Add the two new stacks + newStacks.sort((a, b) => b - a); // Sort in descending order for better visualization + + setStacks(newStacks); + setScore(prev => prev + pointsGained); + + // Check if game is complete (all stacks are size 1) + if (newStacks.every(stack => stack === 1)) { + setGameComplete(true); + confetti({ + particleCount: 150, + spread: 70, + origin: { y: 0.6 } + }); + } + + setDialogOpen(false); + setSelectedStack(null); + setSplitSize1(''); + setSplitSize2(''); + }, [selectedStack, splitSize1, splitSize2, stacks]); + + const handleSplit1Change = (value: string) => { + setSplitSize1(value); + const size1 = parseInt(value); + if (!isNaN(size1) && selectedStack !== null) { + const originalSize = stacks[selectedStack]; + setSplitSize2((originalSize - size1).toString()); + } + }; + + const getStackColor = (size: number) => { + if (size === 1) return 'bg-gray-200'; + if (size <= 3) return 'bg-blue-200'; + if (size <= 6) return 'bg-green-200'; + if (size <= 10) return 'bg-yellow-200'; + return 'bg-red-200'; + }; + + const renderStack = (size: number, index: number) => { + const canSplit = size > 1; + return ( +
canSplit && handleStackClick(index, size)} + > +
Stack {index + 1}
+
+ {Array.from({ length: size }).map((_, blockIndex) => ( +
+ ))} +
+
Size: {size}
+
+ ); + }; + + return ( +
+ + + Stacking Blocks Optimization +

+ Split stacks to maximize your score! Each split earns points equal to the product of the two new stack sizes. +

+
+ + {!gameStarted ? ( +
+
+ + setN(value[0])} + className="mt-2" + /> +
+ +
+ ) : ( +
+
+
+ Current Score: {score} +
+ +
+ + + + {gameComplete ? ( +
+
+ 🎉 Game Complete! 🎉 +
+
+ Final Score: {score} +
+
+ All stacks have been reduced to single blocks! +
+
+ ) : ( + <> +
+ Click on any stack with more than 1 block to split it +
+ +
+ {stacks.map((size, index) => renderStack(size, index))} +
+ + )} +
+ )} +
+
+ + + + + Split Stack + +
+ {selectedStack !== null && ( + <> +
+ Splitting stack of size {stacks[selectedStack]} into two stacks: +
+
+
+ + handleSplit1Change(e.target.value)} + /> +
+
+ + +
+
+ {splitSize1 && splitSize2 && ( +
+ Points gained: {parseInt(splitSize1) * parseInt(splitSize2)} +
+ )} +
+ + +
+ + )} +
+
+
+ + {showSocialShare && ( + + )} +
+ ); +}; + +export default StackingBlocks; \ No newline at end of file diff --git a/src/pages/StackingBlocksPage.tsx b/src/pages/StackingBlocksPage.tsx new file mode 100644 index 0000000..7239dfc --- /dev/null +++ b/src/pages/StackingBlocksPage.tsx @@ -0,0 +1,29 @@ +import StackingBlocks from "@/components/StackingBlocks"; +import { useSearchParams } from "react-router-dom"; + +const StackingBlocksPage = () => { + const [searchParams] = useSearchParams(); + const from = searchParams.get('from'); + + if (from === 'puzzles') { + return ( +
+
+
+ +
+ +
+
+ ); + } + + return ; +}; + +export default StackingBlocksPage; \ No newline at end of file diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index faf758a..aec26e1 100644 --- a/src/pages/theme-pages/Puzzles.tsx +++ b/src/pages/theme-pages/Puzzles.tsx @@ -75,6 +75,16 @@ const Puzzles = () => { difficulty: "Intermediate" as const, duration: "5-15 min", participants: "1 player" + }, + { + id: "stacking-blocks", + title: "Stacking Blocks Optimization", + description: "Split stacks of blocks to maximize your score! Each split earns points equal to the product of the new stack sizes.", + path: "/puzzles/stacking-blocks", + tags: ["Optimization", "Strategy", "Mathematical", "Algorithm"], + difficulty: "Intermediate" as const, + duration: "10-20 min", + participants: "1 player" } ];