From 4e2e099094e2228741847d1ac388facf2a7ae4f8 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:45:45 +0000 Subject: [PATCH] Changes --- src/App.tsx | 2 + src/components/InteractiveIndex.tsx | 9 + src/components/LadybugClockPuzzle.tsx | 704 ++++++++++++++++++++++++++ src/pages/LadybugClockPuzzlePage.tsx | 30 ++ src/pages/theme-pages/Puzzles.tsx | 10 + 5 files changed, 755 insertions(+) create mode 100644 src/components/LadybugClockPuzzle.tsx create mode 100644 src/pages/LadybugClockPuzzlePage.tsx diff --git a/src/App.tsx b/src/App.tsx index 37d4718..55ab15d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -66,6 +66,7 @@ import RentDivisionPuzzlePage from './pages/RentDivisionPuzzlePage'; import BagchalGamePage from './pages/BagchalGamePage'; import AscDescGridPuzzlePage from './pages/AscDescGridPuzzlePage'; import EternalDominationGamePage from './pages/EternalDominationGamePage'; +import LadybugClockPuzzlePage from './pages/LadybugClockPuzzlePage'; const queryClient = new QueryClient(); @@ -142,6 +143,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index 799694d..a9ce43c 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -279,6 +279,15 @@ const allInteractives: Interactive[] = [ path: '/themes/discrete-math/uncertainty/presents-puzzle', theme: 'Discrete Math', dateAdded: '2025-11-19' + }, + { + id: 'ladybug-clock', + title: 'The Ladybug Clock Puzzle', + description: 'A ladybug walks randomly on a clock face. Discover the surprising probability that any given number is painted last!', + tags: ['probability', 'random-walk', 'simulation', 'statistics', 'cover-time', 'puzzle'], + path: '/puzzles/ladybug-clock', + theme: 'Puzzles', + dateAdded: '2025-01-21' } ]; diff --git a/src/components/LadybugClockPuzzle.tsx b/src/components/LadybugClockPuzzle.tsx new file mode 100644 index 0000000..67e28b6 --- /dev/null +++ b/src/components/LadybugClockPuzzle.tsx @@ -0,0 +1,704 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +import { Slider } from '@/components/ui/slider'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Play, Pause, RotateCcw, SkipForward, FastForward, Info, ExternalLink, BarChart3 } from 'lucide-react'; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Cell } from 'recharts'; +import SocialShare from '@/components/SocialShare'; + +interface LadybugClockPuzzleProps { + showSocialShare?: boolean; +} + +interface SimulationState { + position: number; // 0-11 (0 = 12 o'clock, 1 = 1 o'clock, etc.) + visited: Set; + path: number[]; + lastPainted: number | null; + isComplete: boolean; + moveCount: number; +} + +const CLOCK_NUMBERS = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + +const LadybugClockPuzzle: React.FC = ({ showSocialShare = true }) => { + // Simulation state + const [simulation, setSimulation] = useState({ + position: 0, + visited: new Set([0]), + path: [0], + lastPainted: null, + isComplete: false, + moveCount: 0 + }); + + // Controls + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(50); // 1-100, maps to delay + const [numPositions, setNumPositions] = useState(12); + + // Statistics + const [lastPaintedCounts, setLastPaintedCounts] = useState>({}); + const [totalSimulations, setTotalSimulations] = useState(0); + const [averageMoves, setAverageMoves] = useState(0); + const [totalMoves, setTotalMoves] = useState(0); + + // User prediction + const [userPrediction, setUserPrediction] = useState(null); + const [showPredictionResult, setShowPredictionResult] = useState(false); + + // Refs + const animationRef = useRef(null); + const isPlayingRef = useRef(isPlaying); + + useEffect(() => { + isPlayingRef.current = isPlaying; + }, [isPlaying]); + + // Get delay from speed (inverse relationship) + const getDelay = useCallback(() => { + return Math.max(50, 1000 - (speed * 9)); + }, [speed]); + + // Reset simulation + const resetSimulation = useCallback(() => { + setIsPlaying(false); + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + setSimulation({ + position: 0, + visited: new Set([0]), + path: [0], + lastPainted: null, + isComplete: false, + moveCount: 0 + }); + }, []); + + // Perform one step + const step = useCallback(() => { + setSimulation(prev => { + if (prev.isComplete) return prev; + + // Random move: clockwise or counterclockwise + const direction = Math.random() < 0.5 ? 1 : -1; + const newPosition = ((prev.position + direction) % numPositions + numPositions) % numPositions; + + const newVisited = new Set(prev.visited); + const wasNewlyPainted = !newVisited.has(newPosition); + newVisited.add(newPosition); + + const isNowComplete = newVisited.size === numPositions; + const lastPainted = isNowComplete && wasNewlyPainted ? newPosition : prev.lastPainted; + + return { + position: newPosition, + visited: newVisited, + path: [...prev.path, newPosition], + lastPainted: lastPainted, + isComplete: isNowComplete, + moveCount: prev.moveCount + 1 + }; + }); + }, [numPositions]); + + // Auto-play loop + useEffect(() => { + if (!isPlaying || simulation.isComplete) { + return; + } + + const timeoutId = setTimeout(() => { + if (isPlayingRef.current && !simulation.isComplete) { + step(); + } + }, getDelay()); + + return () => clearTimeout(timeoutId); + }, [isPlaying, simulation, step, getDelay]); + + // Handle simulation completion + useEffect(() => { + if (simulation.isComplete && simulation.lastPainted !== null) { + setIsPlaying(false); + } + }, [simulation.isComplete, simulation.lastPainted]); + + // Run instant simulation (no animation) + const runInstantSimulation = useCallback(() => { + let position = 0; + const visited = new Set([0]); + let moveCount = 0; + let lastPainted = 0; + + while (visited.size < numPositions) { + const direction = Math.random() < 0.5 ? 1 : -1; + position = ((position + direction) % numPositions + numPositions) % numPositions; + + if (!visited.has(position)) { + lastPainted = position; + visited.add(position); + } + moveCount++; + } + + return { lastPainted, moveCount }; + }, [numPositions]); + + // Run to completion + const runToCompletion = useCallback(() => { + setIsPlaying(false); + + let currentSim = simulation; + let position = currentSim.position; + const visited = new Set(currentSim.visited); + let moveCount = currentSim.moveCount; + let lastPainted = currentSim.lastPainted; + const path = [...currentSim.path]; + + while (visited.size < numPositions) { + const direction = Math.random() < 0.5 ? 1 : -1; + position = ((position + direction) % numPositions + numPositions) % numPositions; + path.push(position); + + if (!visited.has(position)) { + lastPainted = position; + visited.add(position); + } + moveCount++; + } + + setSimulation({ + position, + visited, + path, + lastPainted, + isComplete: true, + moveCount + }); + }, [simulation, numPositions]); + + // Run batch simulations + const runBatchSimulations = useCallback((count: number) => { + const newCounts = { ...lastPaintedCounts }; + let batchMoves = 0; + + for (let i = 0; i < count; i++) { + const result = runInstantSimulation(); + newCounts[result.lastPainted] = (newCounts[result.lastPainted] || 0) + 1; + batchMoves += result.moveCount; + } + + setLastPaintedCounts(newCounts); + setTotalSimulations(prev => prev + count); + setTotalMoves(prev => prev + batchMoves); + setAverageMoves((totalMoves + batchMoves) / (totalSimulations + count)); + }, [lastPaintedCounts, runInstantSimulation, totalMoves, totalSimulations]); + + // Clear statistics + const clearStats = useCallback(() => { + setLastPaintedCounts({}); + setTotalSimulations(0); + setAverageMoves(0); + setTotalMoves(0); + setUserPrediction(null); + setShowPredictionResult(false); + }, []); + + // Reset everything when numPositions changes + useEffect(() => { + resetSimulation(); + clearStats(); + }, [numPositions, resetSimulation, clearStats]); + + // Convert position index to display number + const positionToNumber = (pos: number) => { + if (numPositions === 12) { + return pos === 0 ? 12 : pos; + } + return pos + 1; + }; + + // Calculate clock position for a given index + const getClockPosition = (index: number, radius: number) => { + const angle = (index / numPositions) * 2 * Math.PI - Math.PI / 2; + return { + x: Math.cos(angle) * radius, + y: Math.sin(angle) * radius + }; + }; + + // Prepare chart data + const chartData = Array.from({ length: numPositions - 1 }, (_, i) => { + const pos = i + 1; // 1 to n-1 (0/12 can't be last) + const count = lastPaintedCounts[pos] || 0; + const percentage = totalSimulations > 0 ? (count / totalSimulations) * 100 : 0; + return { + position: positionToNumber(pos), + count, + percentage, + label: `${positionToNumber(pos)}` + }; + }); + + const theoreticalProbability = 100 / (numPositions - 1); + + const shareUrl = typeof window !== 'undefined' + ? `${window.location.origin}/puzzles/ladybug-clock` + : ''; + + return ( +
+ {/* Header */} +
+

The Ladybug Clock Puzzle

+

+ A ladybug walks randomly on a clock face. What's the probability each number is painted last? +

+ {showSocialShare && ( +
+ +
+ )} +
+ +
+ {/* Clock Visualization */} + + +
+ + {/* Clock face background */} + + + + {/* Hour markers and numbers */} + {Array.from({ length: numPositions }, (_, i) => { + const pos = getClockPosition(i, 100); + const isPainted = simulation.visited.has(i); + const isLast = simulation.isComplete && simulation.lastPainted === i; + const isCurrent = simulation.position === i; + + return ( + + {/* Tick mark */} + + + {/* Number circle background */} + + + {/* Glow effect for newly painted or last */} + {(isPainted || isLast) && ( + + )} + + {/* Number text */} + + {positionToNumber(i)} + + + {/* Current position indicator */} + {isCurrent && ( + + )} + + ); + })} + + {/* Ladybug */} + {(() => { + const ladybugPos = getClockPosition(simulation.position, 100); + return ( + + {/* Ladybug body */} + + {/* Head */} + + {/* Center line */} + + {/* Spots */} + + + + + {/* Antennae */} + + + + ); + })()} + + {/* Center decoration */} + + +
+ + {/* Status display */} +
+
+ + Position: {positionToNumber(simulation.position)} + + + Moves: {simulation.moveCount} + + + Painted: {simulation.visited.size}/{numPositions} + +
+ + {simulation.isComplete && simulation.lastPainted !== null && ( +
+

+ 🎉 Complete! Last painted: {positionToNumber(simulation.lastPainted)} +

+
+ )} +
+
+
+ + {/* Controls and Stats */} +
+ {/* Simulation Controls */} + + + Simulation Controls + + +
+ + + + + + + +
+ +
+ + setSpeed(v[0])} + min={1} + max={100} + step={1} + className="w-full" + /> +
+ +
+ + setNumPositions(v[0])} + min={4} + max={24} + step={2} + className="w-full" + /> +
+
+
+ + {/* Batch Simulations */} + + + + + Batch Simulations + + + +
+ {[100, 1000, 10000].map(count => ( + + ))} + +
+ +
+

+ Total simulations: {totalSimulations.toLocaleString()} +

+ {totalSimulations > 0 && ( +

+ Average moves to complete: {averageMoves.toFixed(1)} +

+ )} +
+
+
+ + {/* User Prediction */} + {totalSimulations === 0 && ( + + + Make a Prediction! 🤔 + + +

+ Which number do you think is most likely to be painted last? +

+
+ {Array.from({ length: numPositions - 1 }, (_, i) => i + 1).map(pos => ( + + ))} +
+ {userPrediction !== null && ( +

+ You predicted: {positionToNumber(userPrediction)}. Run some simulations to see! +

+ )} +
+
+ )} +
+
+ + {/* Statistics Chart */} + {totalSimulations > 0 && ( + + + + Statistics: Which number was painted last? + {totalSimulations.toLocaleString()} simulations + + + +
+ + + + + `${v.toFixed(1)}%`} + domain={[0, Math.max(theoreticalProbability * 1.5, 15)]} + /> + [`${value.toFixed(2)}%`, 'Probability']} + /> + + + {chartData.map((entry, index) => ( + + ))} + + + +
+ + {userPrediction !== null && ( +
+

+ Your prediction ({positionToNumber(userPrediction)}):{' '} + {((lastPaintedCounts[userPrediction] || 0) / totalSimulations * 100).toFixed(2)}% + {' '}— Theory predicts all numbers have equal probability of{' '} + {theoreticalProbability.toFixed(2)}%! +

+
+ )} + +
+

+ The Surprising Result: Despite {numPositions === 12 ? '6' : Math.floor(numPositions / 2)} being the farthest from the start, + every number (1-{numPositions - 1}) has an equal probability of being painted last: exactly 1/{numPositions - 1} ≈ {theoreticalProbability.toFixed(2)}%! +

+
+
+
+ )} + + {/* Path History */} + {simulation.path.length > 1 && ( + + + Path History + + +
+ {simulation.path.map((pos, i) => ( + + {positionToNumber(pos)} + + ))} +
+
+
+ )} + + {/* Educational Info */} + + + + + About This Puzzle + + + +
+

The Rules

+
    +
  • A ladybug starts at the 12 o'clock position
  • +
  • Each second, it randomly moves one step clockwise or counterclockwise (50/50 chance)
  • +
  • When the ladybug visits a number, that number gets "painted"
  • +
  • The simulation ends when all numbers have been visited at least once
  • +
+ +

The Surprising Result

+

+ Intuitively, you might expect the number 6 (opposite to 12) to be the most likely to be painted last, + since it's the farthest from the starting point. However, the probability is exactly 1/{numPositions - 1} for + every number from 1 to {numPositions - 1}! This is because once all but one number is painted, the random walk + will eventually reach that last number, regardless of where it is. +

+ +

Mathematical Connections

+
    +
  • Random Walks on Cycles: This is an example of a random walk on a cycle graph
  • +
  • Cover Time: The number of moves to visit all vertices is called the "cover time"
  • +
  • Gambler's Ruin: Related to the classic probability problem of a gambler with a finite amount
  • +
+
+ + +
+
+
+ ); +}; + +export default LadybugClockPuzzle; diff --git a/src/pages/LadybugClockPuzzlePage.tsx b/src/pages/LadybugClockPuzzlePage.tsx new file mode 100644 index 0000000..df6554a --- /dev/null +++ b/src/pages/LadybugClockPuzzlePage.tsx @@ -0,0 +1,30 @@ +import Layout from '@/components/Layout'; +import LadybugClockPuzzle from '@/components/LadybugClockPuzzle'; +import { useLocation, Link } from 'react-router-dom'; +import { ArrowLeft } from 'lucide-react'; + +const LadybugClockPuzzlePage = () => { + const location = useLocation(); + const fromPuzzles = location.search.includes('from=puzzles'); + + return ( + + {fromPuzzles && ( +
+ + + Back to Puzzles + +
+ )} +
+ +
+
+ ); +}; + +export default LadybugClockPuzzlePage; diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index 14a38a9..323270e 100644 --- a/src/pages/theme-pages/Puzzles.tsx +++ b/src/pages/theme-pages/Puzzles.tsx @@ -105,6 +105,16 @@ const Puzzles = () => { difficulty: "Intermediate" as const, duration: "5-15 min", participants: "1 player" + }, + { + id: "ladybug-clock", + title: "The Ladybug Clock Puzzle", + description: "A ladybug walks randomly on a clock face, painting numbers as it visits. What's the probability that any number is painted last?", + path: "/puzzles/ladybug-clock", + tags: ["Probability", "Random Walk", "Simulation", "Statistics"], + difficulty: "Intermediate" as const, + duration: "5-15 min", + participants: "1 player" } ];