diff --git a/src/components/LadybugClockPuzzle.tsx b/src/components/LadybugClockPuzzle.tsx index e61e078..1fab2ca 100644 --- a/src/components/LadybugClockPuzzle.tsx +++ b/src/components/LadybugClockPuzzle.tsx @@ -8,11 +8,9 @@ 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; @@ -21,10 +19,10 @@ interface SimulationState { 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 }) => { +const LadybugClockPuzzle: React.FC = ({ + showSocialShare = true +}) => { // Simulation state const [simulation, setSimulation] = useState({ position: 0, @@ -34,33 +32,32 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare 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)); + return Math.max(50, 1000 - speed * 9); }, [speed]); // Reset simulation @@ -83,18 +80,15 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare 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, @@ -111,13 +105,11 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare if (!isPlaying || simulation.isComplete) { return; } - const timeoutId = setTimeout(() => { if (isPlayingRef.current && !simulation.isComplete) { step(); } }, getDelay()); - return () => clearTimeout(timeoutId); }, [isPlaying, simulation, step, getDelay]); @@ -134,44 +126,40 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare 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 }; + 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, @@ -184,15 +172,15 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare // Run batch simulations const runBatchSimulations = useCallback((count: number) => { - const newCounts = { ...lastPaintedCounts }; + 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); @@ -225,7 +213,7 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare // Calculate clock position for a given index const getClockPosition = (index: number, radius: number) => { - const angle = (index / numPositions) * 2 * Math.PI - Math.PI / 2; + const angle = index / numPositions * 2 * Math.PI - Math.PI / 2; return { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius @@ -233,10 +221,12 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare }; // Prepare chart data - const chartData = Array.from({ length: numPositions - 1 }, (_, i) => { + 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; + const percentage = totalSimulations > 0 ? count / totalSimulations * 100 : 0; return { position: positionToNumber(pos), count, @@ -244,15 +234,9 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare label: `${positionToNumber(pos)}` }; }); - const theoreticalProbability = 100 / (numPositions - 1); - - const shareUrl = typeof window !== 'undefined' - ? `${window.location.origin}/puzzles/ladybug-clock` - : ''; - - return ( -
+ const shareUrl = typeof window !== 'undefined' ? `${window.location.origin}/puzzles/ladybug-clock` : ''; + return
{/* Header */}

The Ladybug Clock Puzzle

@@ -272,71 +256,34 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare {/* 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 ( - + {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) && ( - - )} + {(isPainted || isLast) && } {/* Number text */} - + {positionToNumber(i)} - - ); - })} + ; + })} {/* Ladybug */} {(() => { - const ladybugPos = getClockPosition(simulation.position, 100); - return ( - + const ladybugPos = getClockPosition(simulation.position, 100); + return {/* Ladybug body */} {/* Head */} @@ -351,9 +298,8 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare {/* Antennae */} - - ); - })()} + ; + })()} {/* Center decoration */} @@ -374,13 +320,11 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
- {simulation.isComplete && simulation.lastPainted !== null && ( -
+ {simulation.isComplete && simulation.lastPainted !== null &&

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

-
- )} +
}
@@ -394,32 +338,17 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
- - - @@ -432,26 +361,12 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
- setSpeed(v[0])} - min={1} - max={100} - step={1} - className="w-full" - /> + setSpeed(v[0])} min={1} max={100} step={1} className="w-full" />
- setNumPositions(v[0])} - min={4} - max={24} - step={2} - className="w-full" - /> + setNumPositions(v[0])} min={4} max={24} step={2} className="w-full" />
@@ -466,16 +381,9 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
- {[100, 1000, 10000].map(count => ( - - ))} + )} @@ -485,18 +393,15 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare

Total simulations: {totalSimulations.toLocaleString()}

- {totalSimulations > 0 && ( -

+ {totalSimulations > 0 &&

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

- )} +

}
{/* User Prediction */} - {totalSimulations === 0 && ( - + {totalSimulations === 0 && Make a Prediction! 🤔 @@ -505,32 +410,22 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare Which number do you think is most likely to be painted last?

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

+ {userPrediction !== null &&

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

- )} +

} -
- )} +
}
{/* Statistics Chart */} - {totalSimulations > 0 && ( - + {totalSimulations > 0 && Statistics: Which number was painted last? @@ -540,52 +435,42 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
- + - `${v.toFixed(1)}%`} - domain={[0, Math.max(theoreticalProbability * 1.5, 15)]} - /> - [`${value.toFixed(2)}%`, 'Probability']} - /> - + `${v.toFixed(1)}%`} domain={[0, Math.max(theoreticalProbability * 1.5, 15)]} /> + [`${value.toFixed(2)}%`, 'Probability']} /> + - {chartData.map((entry, index) => ( - - ))} + {chartData.map((entry, index) => )}
- {userPrediction !== null && ( -
+ {userPrediction !== null &&

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

-
- )} +
}

@@ -594,34 +479,21 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare

-
- )} +
} {/* Path History */} - {simulation.path.length > 1 && ( - + {simulation.path.length > 1 && Path History
- {simulation.path.map((pos, i) => ( - + {simulation.path.map((pos, i) => {positionToNumber(pos)} - - ))} + )}
-
- )} +
} {/* Educational Info */} @@ -641,19 +513,14 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
  • 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
    • + + +
    @@ -675,15 +542,7 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
    - {showSocialShare && ( - - )} - - ); + {showSocialShare && } + ; }; - -export default LadybugClockPuzzle; +export default LadybugClockPuzzle; \ No newline at end of file