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.
This commit is contained in:
gpt-engineer-app[bot] 2025-08-22 06:42:56 +00:00
parent 1adc590312
commit d352192c9e
5 changed files with 322 additions and 0 deletions

View file

@ -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 = () => (
<Route path="/puzzles/pebble-placement" element={<PebblePlacementGamePage />} />
<Route path="/puzzles/bulgarian-solitaire" element={<BulgarianSolitairePage />} />
<Route path="/subtraction-game" element={<SubtractionGamePage />} />
<Route path="/puzzles/stacking-blocks" element={<StackingBlocksPage />} />
<Route path="/puzzles/sikinia-parliament" element={<SikiniaParliamentPuzzlePage />} />
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />

View file

@ -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'
}
];

View file

@ -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<StackingBlocksProps> = ({ showSocialShare = false }) => {
const [n, setN] = useState(8);
const [stacks, setStacks] = useState<number[]>([]);
const [score, setScore] = useState(0);
const [gameStarted, setGameStarted] = useState(false);
const [gameComplete, setGameComplete] = useState(false);
const [selectedStack, setSelectedStack] = useState<number | null>(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 (
<div
key={index}
className={`flex flex-col items-center ${canSplit ? 'cursor-pointer' : ''}`}
onClick={() => canSplit && handleStackClick(index, size)}
>
<div className="text-sm font-medium mb-2">Stack {index + 1}</div>
<div className="flex flex-col-reverse items-center">
{Array.from({ length: size }).map((_, blockIndex) => (
<div
key={blockIndex}
className={`w-12 h-6 border border-black ${getStackColor(size)} ${
canSplit ? 'hover:brightness-90' : ''
}`}
style={{ marginBottom: '-1px' }}
/>
))}
</div>
<div className="text-xs text-muted-foreground mt-2">Size: {size}</div>
</div>
);
};
return (
<div className="w-full max-w-4xl mx-auto p-6 space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-2xl font-bold text-center">Stacking Blocks Optimization</CardTitle>
<p className="text-muted-foreground text-center">
Split stacks to maximize your score! Each split earns points equal to the product of the two new stack sizes.
</p>
</CardHeader>
<CardContent className="space-y-6">
{!gameStarted ? (
<div className="space-y-4">
<div>
<Label htmlFor="blocks-slider">Number of blocks (n): {n}</Label>
<Slider
id="blocks-slider"
min={2}
max={15}
step={1}
value={[n]}
onValueChange={(value) => setN(value[0])}
className="mt-2"
/>
</div>
<Button onClick={startGame} size="lg" className="w-full">
Start Game
</Button>
</div>
) : (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div className="text-lg font-semibold">
Current Score: <span className="text-primary">{score}</span>
</div>
<Button onClick={resetGame} variant="outline">
Reset Game
</Button>
</div>
<Separator />
{gameComplete ? (
<div className="text-center space-y-4">
<div className="text-2xl font-bold text-green-600">
🎉 Game Complete! 🎉
</div>
<div className="text-lg">
Final Score: <span className="font-bold text-primary">{score}</span>
</div>
<div className="text-sm text-muted-foreground">
All stacks have been reduced to single blocks!
</div>
</div>
) : (
<>
<div className="text-center text-muted-foreground">
Click on any stack with more than 1 block to split it
</div>
<div className="flex flex-wrap justify-center gap-8 min-h-40">
{stacks.map((size, index) => renderStack(size, index))}
</div>
</>
)}
</div>
)}
</CardContent>
</Card>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Split Stack</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{selectedStack !== null && (
<>
<div className="text-sm text-muted-foreground">
Splitting stack of size {stacks[selectedStack]} into two stacks:
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="size1">First stack size:</Label>
<Input
id="size1"
type="number"
min="1"
max={selectedStack !== null ? stacks[selectedStack] - 1 : 1}
value={splitSize1}
onChange={(e) => handleSplit1Change(e.target.value)}
/>
</div>
<div>
<Label htmlFor="size2">Second stack size:</Label>
<Input
id="size2"
type="number"
value={splitSize2}
readOnly
className="bg-gray-50"
/>
</div>
</div>
{splitSize1 && splitSize2 && (
<div className="text-sm text-center p-2 bg-blue-50 rounded">
Points gained: {parseInt(splitSize1) * parseInt(splitSize2)}
</div>
)}
<div className="flex gap-2">
<Button onClick={executeSplit} className="flex-1">
Split Stack
</Button>
<Button
onClick={() => setDialogOpen(false)}
variant="outline"
className="flex-1"
>
Cancel
</Button>
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
{showSocialShare && (
<SocialShare
title="Stacking Blocks Optimization Puzzle"
description="Challenge yourself with this optimization puzzle! Split stacks to maximize your score."
/>
)}
</div>
);
};
export default StackingBlocks;

View file

@ -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 (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8 space-y-6">
<div className="flex items-center gap-4">
<button
onClick={() => window.location.href = '/themes/puzzles'}
className="text-primary hover:text-primary/80 font-medium"
>
Back to Puzzles
</button>
</div>
<StackingBlocks showSocialShare={true} />
</div>
</div>
);
}
return <StackingBlocks showSocialShare={true} />;
};
export default StackingBlocksPage;

View file

@ -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"
}
];