From d352192c9e40d2476771dcbacec32d0482616dca Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 06:42:56 +0000 Subject: [PATCH] Add Stacking Blocks interactive Add "Stacking Blocks" interactive to the Puzzles page. This new interactive is based on the description provided in the attached screenshots. --- src/App.tsx | 2 + src/components/InteractiveIndex.tsx | 9 + src/components/StackingBlocks.tsx | 272 ++++++++++++++++++++++++++++ src/pages/StackingBlocksPage.tsx | 29 +++ src/pages/theme-pages/Puzzles.tsx | 10 + 5 files changed, 322 insertions(+) create mode 100644 src/components/StackingBlocks.tsx create mode 100644 src/pages/StackingBlocksPage.tsx 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 ? ( + + + Number of blocks (n): {n} + setN(value[0])} + className="mt-2" + /> + + + Start Game + + + ) : ( + + + + Current Score: {score} + + + Reset Game + + + + + + {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: + + + + First stack size: + handleSplit1Change(e.target.value)} + /> + + + Second stack size: + + + + {splitSize1 && splitSize2 && ( + + Points gained: {parseInt(splitSize1) * parseInt(splitSize2)} + + )} + + + Split Stack + + setDialogOpen(false)} + variant="outline" + className="flex-1" + > + Cancel + + + > + )} + + + + + {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 ( + + + + window.location.href = '/themes/puzzles'} + className="text-primary hover:text-primary/80 font-medium" + > + ← Back to Puzzles + + + + + + ); + } + + 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" } ];
+ Split stacks to maximize your score! Each split earns points equal to the product of the two new stack sizes. +