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:
parent
eccdabf64d
commit
d250cfbca9
5 changed files with 480 additions and 6 deletions
|
|
@ -270,6 +270,15 @@ const allInteractives: Interactive[] = [
|
|||
path: '/themes/discrete-math/structures/cube-coloring',
|
||||
theme: 'Discrete Math',
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
349
src/components/PresentsPuzzle.tsx
Normal file
349
src/components/PresentsPuzzle.tsx
Normal 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 equivalent—they 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue