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)} ); })} {/* 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;