Add Presents Puzzle interactive to Discrete Math > Uncertainty

Created a new interactive puzzle that explores probability and search strategies:
- Alice opens boxes in sequential order (1, 2, 3, ...)
- Bob opens odds first, then evens (1, 3, 5, ..., 2, 4, 6, ...)
- 26 presents randomly distributed in 100 boxes
- Interactive visualization with two 10×10 grids
- Single simulation with animated opening process
- Batch simulation of 100 rounds to analyze win rates
- Added to Uncertainty theme page (first interactive in this category)
This commit is contained in:
Claude 2025-11-19 02:19:48 +00:00
parent eccdabf64d
commit d250cfbca9
No known key found for this signature in database
5 changed files with 480 additions and 6 deletions

View file

@ -59,6 +59,7 @@ import CrapsGamePage from './pages/CrapsGamePage';
import NeighborSumAvoidancePage from './pages/NeighborSumAvoidancePage'; import NeighborSumAvoidancePage from './pages/NeighborSumAvoidancePage';
import BurnsidesLemmaPage from './pages/BurnsidesLemmaPage'; import BurnsidesLemmaPage from './pages/BurnsidesLemmaPage';
import CubeColoringPage from './pages/CubeColoringPage'; import CubeColoringPage from './pages/CubeColoringPage';
import PresentsPuzzlePage from './pages/PresentsPuzzlePage';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -87,6 +88,7 @@ const App = () => (
<Route path="/themes/discrete-math/graphs/neighbor-sum-avoidance" element={<NeighborSumAvoidancePage />} /> <Route path="/themes/discrete-math/graphs/neighbor-sum-avoidance" element={<NeighborSumAvoidancePage />} />
<Route path="/themes/discrete-math/structures/burnsides-lemma" element={<BurnsidesLemmaPage />} /> <Route path="/themes/discrete-math/structures/burnsides-lemma" element={<BurnsidesLemmaPage />} />
<Route path="/themes/discrete-math/structures/cube-coloring" element={<CubeColoringPage />} /> <Route path="/themes/discrete-math/structures/cube-coloring" element={<CubeColoringPage />} />
<Route path="/themes/discrete-math/uncertainty/presents-puzzle" element={<PresentsPuzzlePage />} />
<Route path="/themes/social-choice" element={<SocialChoice />} /> <Route path="/themes/social-choice" element={<SocialChoice />} />
<Route path="/themes/advanced-algorithms" element={<AdvancedAlgorithms />} /> <Route path="/themes/advanced-algorithms" element={<AdvancedAlgorithms />} />
<Route path="/themes/data-structures" element={<DataStructures />} /> <Route path="/themes/data-structures" element={<DataStructures />} />

View file

@ -270,6 +270,15 @@ const allInteractives: Interactive[] = [
path: '/themes/discrete-math/structures/cube-coloring', path: '/themes/discrete-math/structures/cube-coloring',
theme: 'Discrete Math', theme: 'Discrete Math',
dateAdded: '2025-01-22' dateAdded: '2025-01-22'
},
{
id: 'presents-puzzle',
title: 'The Presents Puzzle',
description: 'Explore a probability puzzle about search strategies. Charlie puts 26 presents in 100 boxes. Alice opens them in order while Bob opens odds first, then evens. Who is more likely to see all 26 presents first?',
tags: ['probability', 'search', 'simulation', 'expected-value', 'strategy', 'uncertainty'],
path: '/themes/discrete-math/uncertainty/presents-puzzle',
theme: 'Discrete Math',
dateAdded: '2025-11-19'
} }
]; ];

View file

@ -0,0 +1,349 @@
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import SocialShare from './SocialShare';
import { Gift, Play, RotateCw, Zap } from 'lucide-react';
interface PresentsPuzzleProps {
showSocialShare?: boolean;
shareUrl?: string;
}
interface SimulationResult {
alice: number;
bob: number;
ties: number;
}
const PresentsPuzzle = ({ showSocialShare = false, shareUrl }: PresentsPuzzleProps) => {
const [presentBoxes, setPresentBoxes] = useState<Set<number>>(new Set());
const [aliceOpened, setAliceOpened] = useState<Set<number>>(new Set());
const [bobOpened, setBobOpened] = useState<Set<number>>(new Set());
const [aliceFound, setAliceFound] = useState(0);
const [bobFound, setBobFound] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const [winner, setWinner] = useState<'alice' | 'bob' | null>(null);
const [simulationResults, setSimulationResults] = useState<SimulationResult>({ alice: 0, bob: 0, ties: 0 });
const [hasSimulated, setHasSimulated] = useState(false);
// Generate random present positions
const generatePresents = (): Set<number> => {
const presents = new Set<number>();
while (presents.size < 26) {
presents.add(Math.floor(Math.random() * 100) + 1);
}
return presents;
};
// Get Alice's opening order: 1, 2, 3, ..., 100
const getAliceOrder = (): number[] => {
return Array.from({ length: 100 }, (_, i) => i + 1);
};
// Get Bob's opening order: 1, 3, 5, ..., 99, 2, 4, 6, ..., 100
const getBobOrder = (): number[] => {
const odds = Array.from({ length: 50 }, (_, i) => i * 2 + 1);
const evens = Array.from({ length: 50 }, (_, i) => (i + 1) * 2);
return [...odds, ...evens];
};
// Simulate one round
const simulateRound = (presents: Set<number>): 'alice' | 'bob' => {
const aliceOrder = getAliceOrder();
const bobOrder = getBobOrder();
let aliceCount = 0;
let bobCount = 0;
for (let i = 0; i < 100; i++) {
if (presents.has(aliceOrder[i])) {
aliceCount++;
if (aliceCount === 26) return 'alice';
}
if (presents.has(bobOrder[i])) {
bobCount++;
if (bobCount === 26) return 'bob';
}
}
return 'alice'; // Default, though this should never happen
};
// Animate a single simulation
const startSimulation = async () => {
setIsRunning(true);
setWinner(null);
setAliceOpened(new Set());
setBobOpened(new Set());
setAliceFound(0);
setBobFound(0);
const presents = generatePresents();
setPresentBoxes(presents);
const aliceOrder = getAliceOrder();
const bobOrder = getBobOrder();
let aliceCount = 0;
let bobCount = 0;
let currentWinner: 'alice' | 'bob' | null = null;
for (let i = 0; i < 100 && !currentWinner; i++) {
await new Promise(resolve => setTimeout(resolve, 50)); // Animation delay
const aliceBox = aliceOrder[i];
const bobBox = bobOrder[i];
setAliceOpened(prev => new Set(prev).add(aliceBox));
setBobOpened(prev => new Set(prev).add(bobBox));
if (presents.has(aliceBox)) {
aliceCount++;
setAliceFound(aliceCount);
if (aliceCount === 26) {
currentWinner = 'alice';
}
}
if (presents.has(bobBox)) {
bobCount++;
setBobFound(bobCount);
if (bobCount === 26 && !currentWinner) {
currentWinner = 'bob';
}
}
}
setWinner(currentWinner);
setIsRunning(false);
};
// Reset simulation
const resetSimulation = () => {
setPresentBoxes(new Set());
setAliceOpened(new Set());
setBobOpened(new Set());
setAliceFound(0);
setBobFound(0);
setWinner(null);
setIsRunning(false);
};
// Run 100 simulations
const runMultipleSimulations = () => {
const results: SimulationResult = { alice: 0, bob: 0, ties: 0 };
for (let i = 0; i < 100; i++) {
const presents = generatePresents();
const winner = simulateRound(presents);
if (winner === 'alice') {
results.alice++;
} else {
results.bob++;
}
}
setSimulationResults(results);
setHasSimulated(true);
};
// Render a grid of boxes
const renderGrid = (
title: string,
openedBoxes: Set<number>,
found: number,
color: string
) => {
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-lg">{title}</h3>
<Badge variant="secondary" className="text-sm">
<Gift className="w-3 h-3 mr-1" />
{found}/26
</Badge>
</div>
<div className="grid grid-cols-10 gap-1 p-2 bg-muted/30 rounded-lg">
{Array.from({ length: 100 }, (_, i) => i + 1).map((boxNum) => {
const hasPresent = presentBoxes.has(boxNum);
const isOpened = openedBoxes.has(boxNum);
const showPresent = isOpened && hasPresent;
return (
<div
key={boxNum}
className={`
aspect-square rounded border flex items-center justify-center text-xs font-medium
transition-all duration-200
${isOpened
? showPresent
? 'bg-[#808000] text-white border-[#606000] shadow-sm'
: 'bg-muted border-border text-muted-foreground'
: 'bg-gray-400 border-gray-500 text-gray-700'
}
`}
title={`Box ${boxNum}${hasPresent ? ' (Present!)' : ''}`}
>
{showPresent ? <Gift className="w-3 h-3" /> : boxNum}
</div>
);
})}
</div>
<div className="text-xs text-muted-foreground">
{title === 'Alice' && 'Opens in order: 1, 2, 3, 4, 5, ...'}
{title === 'Bob' && 'Opens odds first, then evens: 1, 3, 5, ..., 2, 4, 6, ...'}
</div>
</div>
);
};
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Gift className="w-6 h-6" />
The Presents Puzzle
</CardTitle>
<CardDescription>
A probability puzzle about search strategies
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Puzzle Statement */}
<Card className="bg-muted/50">
<CardContent className="pt-6">
<p className="text-sm leading-relaxed">
Charlie puts <strong>26 presents</strong> in <strong>100 boxes</strong>, labeled 1 to 100.
Each second, Alice and Bob look in one box. Alice opens them in order (1, 2, 3, ),
while Bob opens the odds first, then the evens (1, 3, 5, , 2, 4, 6, ).
<strong> Who is more likely to see all 26 presents first?</strong>
</p>
</CardContent>
</Card>
{/* Grids */}
<div className="grid md:grid-cols-2 gap-6">
{renderGrid('Alice', aliceOpened, aliceFound, 'blue')}
{renderGrid('Bob', bobOpened, bobFound, 'green')}
</div>
{/* Winner Display */}
{winner && (
<Card className="bg-primary/10 border-primary/20">
<CardContent className="pt-6">
<div className="text-center">
<div className="text-2xl font-bold text-primary">
{winner === 'alice' ? '🎉 Alice' : '🎉 Bob'} found all 26 presents first!
</div>
</div>
</CardContent>
</Card>
)}
{/* Control Buttons */}
<div className="flex gap-3 justify-center flex-wrap">
<Button
onClick={startSimulation}
disabled={isRunning}
className="gap-2"
>
<Play className="w-4 h-4" />
Start
</Button>
<Button
onClick={resetSimulation}
variant="outline"
disabled={isRunning}
className="gap-2"
>
<RotateCw className="w-4 h-4" />
Reset
</Button>
</div>
{/* Simulate 100 */}
<div className="space-y-4">
<div className="flex justify-center">
<Button
onClick={runMultipleSimulations}
variant="secondary"
className="gap-2"
size="lg"
>
<Zap className="w-4 h-4" />
Simulate 100 Rounds
</Button>
</div>
{/* Results */}
{hasSimulated && (
<Card className="bg-accent/10 border-accent/20">
<CardHeader>
<CardTitle className="text-lg">Results from 100 Simulations</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="text-center space-y-2">
<div className="text-4xl font-bold text-blue-600 dark:text-blue-400">
{simulationResults.alice}
</div>
<div className="text-sm font-medium">Alice Wins</div>
<div className="text-xs text-muted-foreground">
{((simulationResults.alice / 100) * 100).toFixed(1)}%
</div>
</div>
<div className="text-center space-y-2">
<div className="text-4xl font-bold text-green-600 dark:text-green-400">
{simulationResults.bob}
</div>
<div className="text-sm font-medium">Bob Wins</div>
<div className="text-xs text-muted-foreground">
{((simulationResults.bob / 100) * 100).toFixed(1)}%
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-border">
<p className="text-sm text-muted-foreground text-center">
{simulationResults.alice > simulationResults.bob
? '💡 Alice tends to win more often!'
: simulationResults.bob > simulationResults.alice
? '💡 Bob tends to win more often!'
: '💡 They appear equally likely to win!'
}
</p>
</div>
</CardContent>
</Card>
)}
</div>
{/* Insight */}
<Card className="bg-muted/50">
<CardContent className="pt-6">
<h4 className="font-semibold mb-2 text-sm">💭 Think About It</h4>
<p className="text-xs text-muted-foreground">
At first glance, both strategies seem equivalentthey both check all 100 boxes eventually.
But the <em>order</em> matters! Try running the simulation multiple times to see which strategy
performs better on average. Can you explain why?
</p>
</CardContent>
</Card>
</CardContent>
</Card>
{showSocialShare && (
<SocialShare
url={shareUrl || window.location.href}
title="The Presents Puzzle"
description="A probability puzzle about search strategies and expected value"
/>
)}
</div>
);
};
export default PresentsPuzzle;

View file

@ -0,0 +1,7 @@
import PresentsPuzzle from "@/components/PresentsPuzzle";
const PresentsPuzzlePage = () => {
return <PresentsPuzzle />;
};
export default PresentsPuzzlePage;

View file

@ -1,11 +1,118 @@
import ComingSoon from "@/components/ComingSoon"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Search } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import Layout from '@/components/Layout';
import { useState } from 'react';
import PresentsPuzzle from '@/components/PresentsPuzzle';
interface Interactive {
id: string;
title: string;
description: string;
tags: string[];
path: string;
component: React.ComponentType<{ showSocialShare?: boolean; shareUrl?: string }>;
}
const interactives: Interactive[] = [
{
id: 'presents-puzzle',
title: "The Presents Puzzle",
description: 'Explore a probability puzzle about search strategies. Charlie puts 26 presents in 100 boxes. Alice opens them in order while Bob opens odds first, then evens. Who is more likely to see all 26 presents first?',
tags: ['probability', 'search', 'simulation', 'expected-value', 'strategy'],
path: '/themes/discrete-math/uncertainty/presents-puzzle',
component: PresentsPuzzle
},
];
const Uncertainty = () => { const Uncertainty = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedInteractive, setSelectedInteractive] = useState<Interactive | null>(null);
const filteredInteractives = interactives.filter(interactive =>
interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
);
if (selectedInteractive) {
const InteractiveComponent = selectedInteractive.component;
const shareUrl = `${window.location.origin}${selectedInteractive.path}`;
return ( return (
<ComingSoon <div className="min-h-screen bg-background">
title="Uncertainty" <div className="container mx-auto px-4 py-8 space-y-6">
description="Study probability theory, random variables, and statistical reasoning in discrete contexts through simulations and interactive models." <div className="flex items-center gap-4">
<button
onClick={() => setSelectedInteractive(null)}
className="text-primary hover:text-primary/80 font-medium"
>
Back to Uncertainty
</button>
</div>
<InteractiveComponent showSocialShare={true} shareUrl={shareUrl} />
</div>
</div>
);
}
return (
<Layout>
<div className="max-w-7xl mx-auto p-6">
<div className="space-y-6">
<div className="space-y-4">
<h1 className="text-3xl font-bold text-foreground">Uncertainty</h1>
<p className="text-muted-foreground text-lg">
Study probability theory, random variables, and statistical reasoning in discrete contexts through simulations and interactive models.
</p>
</div>
{/* Search bar */}
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
placeholder="Search interactives..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="pl-10"
/> />
</div>
{/* Gallery */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredInteractives.map(interactive => (
<Card
key={interactive.id}
className="cursor-pointer hover:shadow-lg transition-all duration-200 hover:scale-105"
onClick={() => setSelectedInteractive(interactive)}
>
<CardHeader className="space-y-3">
<CardTitle className="text-xl text-foreground">{interactive.title}</CardTitle>
<CardDescription className="text-muted-foreground line-clamp-3">
{interactive.description}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{interactive.tags.map(tag => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
</CardContent>
</Card>
))}
</div>
{filteredInteractives.length === 0 && (
<div className="text-center py-12">
<p className="text-muted-foreground text-lg">No interactives found matching your search.</p>
</div>
)}
</div>
</div>
</Layout>
); );
}; };