diff --git a/src/App.tsx b/src/App.tsx index d73976c..ea227a0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -59,6 +59,7 @@ import CrapsGamePage from './pages/CrapsGamePage'; import NeighborSumAvoidancePage from './pages/NeighborSumAvoidancePage'; import BurnsidesLemmaPage from './pages/BurnsidesLemmaPage'; import CubeColoringPage from './pages/CubeColoringPage'; +import PresentsPuzzlePage from './pages/PresentsPuzzlePage'; const queryClient = new QueryClient(); @@ -87,6 +88,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index 7e75219..799694d 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -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' } ]; diff --git a/src/components/PresentsPuzzle.tsx b/src/components/PresentsPuzzle.tsx new file mode 100644 index 0000000..93da4e1 --- /dev/null +++ b/src/components/PresentsPuzzle.tsx @@ -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>(new Set()); + const [aliceOpened, setAliceOpened] = useState>(new Set()); + const [bobOpened, setBobOpened] = useState>(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({ alice: 0, bob: 0, ties: 0 }); + const [hasSimulated, setHasSimulated] = useState(false); + + // Generate random present positions + const generatePresents = (): Set => { + const presents = new Set(); + 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): '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, + found: number, + color: string + ) => { + return ( +
+
+

{title}

+ + + {found}/26 + +
+
+ {Array.from({ length: 100 }, (_, i) => i + 1).map((boxNum) => { + const hasPresent = presentBoxes.has(boxNum); + const isOpened = openedBoxes.has(boxNum); + const showPresent = isOpened && hasPresent; + + return ( +
+ {showPresent ? : boxNum} +
+ ); + })} +
+
+ {title === 'Alice' && 'Opens in order: 1, 2, 3, 4, 5, ...'} + {title === 'Bob' && 'Opens odds first, then evens: 1, 3, 5, ..., 2, 4, 6, ...'} +
+
+ ); + }; + + return ( +
+ + + + + The Presents Puzzle + + + A probability puzzle about search strategies + + + + {/* Puzzle Statement */} + + +

+ Charlie puts 26 presents in 100 boxes, 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, …). + Who is more likely to see all 26 presents first? +

+
+
+ + {/* Grids */} +
+ {renderGrid('Alice', aliceOpened, aliceFound, 'blue')} + {renderGrid('Bob', bobOpened, bobFound, 'green')} +
+ + {/* Winner Display */} + {winner && ( + + +
+
+ {winner === 'alice' ? '🎉 Alice' : '🎉 Bob'} found all 26 presents first! +
+
+
+
+ )} + + {/* Control Buttons */} +
+ + +
+ + {/* Simulate 100 */} +
+
+ +
+ + {/* Results */} + {hasSimulated && ( + + + Results from 100 Simulations + + +
+
+
+ {simulationResults.alice} +
+
Alice Wins
+
+ {((simulationResults.alice / 100) * 100).toFixed(1)}% +
+
+
+
+ {simulationResults.bob} +
+
Bob Wins
+
+ {((simulationResults.bob / 100) * 100).toFixed(1)}% +
+
+
+ +
+

+ {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!' + } +

+
+
+
+ )} +
+ + {/* Insight */} + + +

💭 Think About It

+

+ At first glance, both strategies seem equivalent—they both check all 100 boxes eventually. + But the order matters! Try running the simulation multiple times to see which strategy + performs better on average. Can you explain why? +

+
+
+
+
+ + {showSocialShare && ( + + )} +
+ ); +}; + +export default PresentsPuzzle; diff --git a/src/pages/PresentsPuzzlePage.tsx b/src/pages/PresentsPuzzlePage.tsx new file mode 100644 index 0000000..615eaa9 --- /dev/null +++ b/src/pages/PresentsPuzzlePage.tsx @@ -0,0 +1,7 @@ +import PresentsPuzzle from "@/components/PresentsPuzzle"; + +const PresentsPuzzlePage = () => { + return ; +}; + +export default PresentsPuzzlePage; diff --git a/src/pages/theme-pages/discrete-math/Uncertainty.tsx b/src/pages/theme-pages/discrete-math/Uncertainty.tsx index 10850d9..c17cf99 100644 --- a/src/pages/theme-pages/discrete-math/Uncertainty.tsx +++ b/src/pages/theme-pages/discrete-math/Uncertainty.tsx @@ -1,12 +1,119 @@ -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 [searchTerm, setSearchTerm] = useState(''); + const [selectedInteractive, setSelectedInteractive] = useState(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 ( - + +
+
+
+

Uncertainty

+

+ Study probability theory, random variables, and statistical reasoning in discrete contexts through simulations and interactive models. +

+
+ + {/* Search bar */} +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ + {/* Gallery */} +
+ {filteredInteractives.map(interactive => ( + setSelectedInteractive(interactive)} + > + + {interactive.title} + + {interactive.description} + + + +
+ {interactive.tags.map(tag => ( + + {tag} + + ))} +
+
+
+ ))} +
+ + {filteredInteractives.length === 0 && ( +
+

No interactives found matching your search.

+
+ )} +
+
+
); }; -export default Uncertainty; \ No newline at end of file +export default Uncertainty;