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:
parent
1adc590312
commit
d352192c9e
5 changed files with 322 additions and 0 deletions
|
|
@ -53,6 +53,7 @@ import RulesOfInferencePlaygroundPage from './pages/RulesOfInferencePlaygroundPa
|
||||||
import PebblePlacementGamePage from './pages/PebblePlacementGamePage';
|
import PebblePlacementGamePage from './pages/PebblePlacementGamePage';
|
||||||
import BulgarianSolitairePage from './pages/BulgarianSolitairePage';
|
import BulgarianSolitairePage from './pages/BulgarianSolitairePage';
|
||||||
import SubtractionGamePage from './pages/SubtractionGamePage';
|
import SubtractionGamePage from './pages/SubtractionGamePage';
|
||||||
|
import StackingBlocksPage from './pages/StackingBlocksPage';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|
@ -109,6 +110,7 @@ const App = () => (
|
||||||
<Route path="/puzzles/pebble-placement" element={<PebblePlacementGamePage />} />
|
<Route path="/puzzles/pebble-placement" element={<PebblePlacementGamePage />} />
|
||||||
<Route path="/puzzles/bulgarian-solitaire" element={<BulgarianSolitairePage />} />
|
<Route path="/puzzles/bulgarian-solitaire" element={<BulgarianSolitairePage />} />
|
||||||
<Route path="/subtraction-game" element={<SubtractionGamePage />} />
|
<Route path="/subtraction-game" element={<SubtractionGamePage />} />
|
||||||
|
<Route path="/puzzles/stacking-blocks" element={<StackingBlocksPage />} />
|
||||||
<Route path="/puzzles/sikinia-parliament" element={<SikiniaParliamentPuzzlePage />} />
|
<Route path="/puzzles/sikinia-parliament" element={<SikiniaParliamentPuzzlePage />} />
|
||||||
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
||||||
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
|
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,15 @@ const allInteractives: Interactive[] = [
|
||||||
path: '/discrete-math/foundations/rules-of-inference',
|
path: '/discrete-math/foundations/rules-of-inference',
|
||||||
theme: 'Discrete Math',
|
theme: 'Discrete Math',
|
||||||
dateAdded: '2024-12-23'
|
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'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
272
src/components/StackingBlocks.tsx
Normal file
272
src/components/StackingBlocks.tsx
Normal 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;
|
||||||
29
src/pages/StackingBlocksPage.tsx
Normal file
29
src/pages/StackingBlocksPage.tsx
Normal 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;
|
||||||
|
|
@ -75,6 +75,16 @@ const Puzzles = () => {
|
||||||
difficulty: "Intermediate" as const,
|
difficulty: "Intermediate" as const,
|
||||||
duration: "5-15 min",
|
duration: "5-15 min",
|
||||||
participants: "1 player"
|
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"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue