diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index e9a45b4..0000000 --- a/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -.git -.github -.forgejo -dist -node_modules -npm-debug.log* -Dockerfile* -README.md diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml deleted file mode 100644 index 40a10b9..0000000 --- a/.forgejo/workflows/ci.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ci-${{ forgejo.ref }} - cancel-in-progress: true - -jobs: - verify: - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Lint - run: npm run lint - - - name: Build - run: npm run build diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ff67f4c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM node:22-alpine AS build - -WORKDIR /app - -COPY package.json package-lock.json ./ -RUN npm ci - -COPY . . -RUN npm run build - -FROM nginx:1.27-alpine AS runtime - -COPY nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=build /app/dist /usr/share/nginx/html - -EXPOSE 80 - -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -q --spider http://127.0.0.1/ || exit 1 diff --git a/README.md b/README.md index 672b6b0..e0eb78a 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,6 @@ This project is built with: Simply open [Lovable](https://lovable.dev/projects/5ee9d1e7-af25-47e9-ab4c-bdbca4554e32) and click on Share -> Publish. -### Forgejo and Dokploy - -Pushes and pull requests targeting `main` are verified by the Forgejo Actions workflow in `.forgejo/workflows/ci.yml`. The production container is built from `Dockerfile` and serves the Vite output with NGINX, including React Router fallback handling. - -Dokploy is connected to the `Websites/interactives` Forgejo repository with autodeploy enabled. A successful push to `main` triggers a fresh Dockerfile build and replaces the running service only after the image has built successfully. - ## Can I connect a custom domain to my Lovable project? Yes, you can! diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index aca9d71..0000000 --- a/nginx.conf +++ /dev/null @@ -1,22 +0,0 @@ -server { - listen 80; - listen [::]:80; - server_name _; - - root /usr/share/nginx/html; - index index.html; - - location / { - try_files $uri $uri/ /index.html; - } - - location = /index.html { - add_header Cache-Control "no-cache"; - } - - location ~* \.(?:css|js|mjs|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - try_files $uri =404; - } -} diff --git a/src/App.tsx b/src/App.tsx index f94e757..55ab15d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,6 @@ import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import ErrorBoundary from "@/components/ErrorBoundary"; -import CommandSearch from "@/components/CommandSearch"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import Index from "./pages/Index"; import InteractiveIndex from "./components/InteractiveIndex"; @@ -68,7 +67,6 @@ import BagchalGamePage from './pages/BagchalGamePage'; import AscDescGridPuzzlePage from './pages/AscDescGridPuzzlePage'; import EternalDominationGamePage from './pages/EternalDominationGamePage'; import LadybugClockPuzzlePage from './pages/LadybugClockPuzzlePage'; -import ErdosDiscrepancyPuzzlePage from './pages/ErdosDiscrepancyPuzzlePage'; const queryClient = new QueryClient(); @@ -79,7 +77,6 @@ const App = () => ( - } /> } /> @@ -147,7 +144,6 @@ const App = () => ( } /> } /> } /> - } /> } /> } /> } /> diff --git a/src/components/AssistedNimGame.tsx b/src/components/AssistedNimGame.tsx index d04028c..ee3fa28 100644 --- a/src/components/AssistedNimGame.tsx +++ b/src/components/AssistedNimGame.tsx @@ -66,11 +66,7 @@ const getOddPowers = (heaps: number[]): Set => { return oddPowers; }; -interface AssistedNimGameProps { - showSocialShare?: boolean; -} - -const AssistedNimGame: React.FC = ({ showSocialShare = true }) => { +const AssistedNimGame: React.FC = () => { const [mode, setMode] = useState(null); const [showSetup, setShowSetup] = useState(false); const [userHeapCount, setUserHeapCount] = useState(3); @@ -388,14 +384,14 @@ const AssistedNimGame: React.FC = ({ showSocialShare = tru - {showSocialShare && ( - - )} + {/* Social Share */} + ); }; -export default AssistedNimGame; \ No newline at end of file +export default AssistedNimGame; \ No newline at end of file diff --git a/src/components/CommandSearch.tsx b/src/components/CommandSearch.tsx deleted file mode 100644 index 1b5a6fb..0000000 --- a/src/components/CommandSearch.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { allInteractives } from "@/components/InteractiveIndex"; -import { - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { Badge } from "@/components/ui/badge"; - -const CommandSearch = () => { - const [open, setOpen] = useState(false); - const navigate = useNavigate(); - - useEffect(() => { - const down = (e: KeyboardEvent) => { - if (e.key === "k" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - setOpen((prev) => !prev); - } - }; - document.addEventListener("keydown", down); - return () => document.removeEventListener("keydown", down); - }, []); - - const handleSelect = (path: string) => { - setOpen(false); - navigate(path); - }; - - return ( - - - - No interactives found. - - {allInteractives.map((interactive) => ( - handleSelect(interactive.path)} - > -
-
- {interactive.title} - - {interactive.theme} - -
- - {interactive.description} - -
-
- ))} -
-
-
- ); -}; - -export default CommandSearch; diff --git a/src/components/ErdosDiscrepancyPuzzle.tsx b/src/components/ErdosDiscrepancyPuzzle.tsx deleted file mode 100644 index 8046d40..0000000 --- a/src/components/ErdosDiscrepancyPuzzle.tsx +++ /dev/null @@ -1,902 +0,0 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Slider } from '@/components/ui/slider'; -import { - Play, RotateCcw, Info, ChevronDown, ChevronUp, - ArrowLeft, ArrowRight, Skull, AlertTriangle, - BookOpen, Zap, Eye, Grid3X3, TrendingUp, Pause, - Volume2, VolumeX, Target, Trophy -} from 'lucide-react'; -import SocialShare from '@/components/SocialShare'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; - -interface ErdosDiscrepancyPuzzleProps { - showSocialShare?: boolean; -} - -type GamePhase = 'intro' | 'writing' | 'captor-reveal' | 'walking' | 'fallen' | 'survived'; -type Direction = 1 | -1 | null; - -interface DiscrepancyResult { - d: number; - k: number; - sum: number; - positions: number[]; -} - -const ErdosDiscrepancyPuzzle: React.FC = ({ showSocialShare = false }) => { - // Core game state - const [sequence, setSequence] = useState([]); - const [discrepancyBound, setDiscrepancyBound] = useState(1); - const [gamePhase, setGamePhase] = useState('intro'); - const [showRules, setShowRules] = useState(false); - - // Walk visualization state - const [selectedD, setSelectedD] = useState(1); - const [walkPosition, setWalkPosition] = useState(0); - const [walkStep, setWalkStep] = useState(0); - const [isWalking, setIsWalking] = useState(false); - const [walkSpeed, setWalkSpeed] = useState(500); - - // Prisoner's Walk story mode - const [captorChosenD, setCaptorChosenD] = useState(null); - const [prisonerPosition, setPrisonerPosition] = useState(0); - const [prisonerStep, setPrisonerStep] = useState(0); - - // Heatmap state - const [hoveredCell, setHoveredCell] = useState<{d: number, k: number} | null>(null); - const [highlightedSubsequence, setHighlightedSubsequence] = useState([]); - - // Best score tracking - const [bestLength, setBestLength] = useState(null); - - // Load best score from localStorage - useEffect(() => { - const saved = localStorage.getItem(`erdos-best-length-c${discrepancyBound}`); - if (saved) { - setBestLength(parseInt(saved)); - } - }, [discrepancyBound]); - - // Calculate all discrepancies for current sequence - const allDiscrepancies = useMemo((): DiscrepancyResult[] => { - if (sequence.length === 0) return []; - - const results: DiscrepancyResult[] = []; - const n = sequence.length; - - for (let d = 1; d <= n; d++) { - let sum = 0; - const positions: number[] = []; - - for (let k = 1; k * d <= n; k++) { - const pos = k * d - 1; // 0-indexed - if (sequence[pos] !== null) { - sum += sequence[pos]!; - positions.push(k * d); - results.push({ - d, - k, - sum, - positions: [...positions] - }); - } - } - } - - return results; - }, [sequence]); - - // Find maximum discrepancy - const maxDiscrepancy = useMemo(() => { - if (allDiscrepancies.length === 0) return { value: 0, result: null }; - - let maxAbs = 0; - let maxResult: DiscrepancyResult | null = null; - - for (const result of allDiscrepancies) { - if (Math.abs(result.sum) > maxAbs) { - maxAbs = Math.abs(result.sum); - maxResult = result; - } - } - - return { value: maxAbs, result: maxResult }; - }, [allDiscrepancies]); - - // Check if current sequence violates bound - const hasViolation = maxDiscrepancy.value > discrepancyBound; - - // Find worst step size for captor - const findWorstD = useCallback((): number => { - let worstD = 1; - let worstDiscrepancy = 0; - - for (const result of allDiscrepancies) { - if (Math.abs(result.sum) > worstDiscrepancy) { - worstDiscrepancy = Math.abs(result.sum); - worstD = result.d; - } - } - - return worstD; - }, [allDiscrepancies]); - - // Get discrepancy for specific d,k - const getDiscrepancy = (d: number, k: number): number | null => { - const result = allDiscrepancies.find(r => r.d === d && r.k === k); - return result ? result.sum : null; - }; - - // Toggle sequence value - const togglePosition = (index: number) => { - if (gamePhase !== 'writing') return; - - setSequence(prev => { - const newSeq = [...prev]; - if (newSeq[index] === null) { - newSeq[index] = 1; - } else if (newSeq[index] === 1) { - newSeq[index] = -1; - } else { - newSeq[index] = 1; - } - return newSeq; - }); - }; - - // Add new position to sequence - const addPosition = () => { - if (gamePhase !== 'writing') return; - setSequence(prev => [...prev, null]); - }; - - // Start the game - const startGame = () => { - setSequence([null, null, null, null, null]); - setGamePhase('writing'); - setWalkPosition(0); - setWalkStep(0); - setPrisonerPosition(0); - setPrisonerStep(0); - setCaptorChosenD(null); - setHighlightedSubsequence([]); - }; - - // Reset the game - const resetGame = () => { - setSequence([]); - setGamePhase('intro'); - setWalkPosition(0); - setWalkStep(0); - setIsWalking(false); - setPrisonerPosition(0); - setPrisonerStep(0); - setCaptorChosenD(null); - setHighlightedSubsequence([]); - }; - - // Handle captor reveal (Prisoner's Walk mode) - const revealCaptor = () => { - const worstD = findWorstD(); - setCaptorChosenD(worstD); - setGamePhase('captor-reveal'); - setPrisonerPosition(0); - setPrisonerStep(0); - }; - - // Start prisoner walking - const startPrisonerWalk = () => { - if (captorChosenD === null) return; - setGamePhase('walking'); - setPrisonerPosition(0); - setPrisonerStep(0); - }; - - // Prisoner walk animation - useEffect(() => { - if (gamePhase !== 'walking' || captorChosenD === null) return; - - const interval = setInterval(() => { - setPrisonerStep(prev => { - const nextStep = prev + 1; - const posIndex = nextStep * captorChosenD - 1; - - if (posIndex >= sequence.length || sequence[posIndex] === null) { - // Sequence ended without falling - setGamePhase('survived'); - clearInterval(interval); - - // Save best score - const validLength = sequence.filter(s => s !== null).length; - if (!bestLength || validLength > bestLength) { - setBestLength(validLength); - localStorage.setItem(`erdos-best-length-c${discrepancyBound}`, validLength.toString()); - } - - return prev; - } - - const newPosition = prisonerPosition + sequence[posIndex]!; - setPrisonerPosition(newPosition); - - if (Math.abs(newPosition) > discrepancyBound) { - setGamePhase('fallen'); - clearInterval(interval); - return nextStep; - } - - return nextStep; - }); - }, walkSpeed); - - return () => clearInterval(interval); - }, [gamePhase, captorChosenD, sequence, prisonerPosition, discrepancyBound, walkSpeed, bestLength]); - - // Manual walk control - const walkSubsequence = () => { - if (isWalking) { - setIsWalking(false); - return; - } - - setWalkPosition(0); - setWalkStep(0); - setIsWalking(true); - }; - - // Walk animation effect - useEffect(() => { - if (!isWalking) return; - - const interval = setInterval(() => { - setWalkStep(prev => { - const nextStep = prev + 1; - const posIndex = nextStep * selectedD - 1; - - if (posIndex >= sequence.length || sequence[posIndex] === null) { - setIsWalking(false); - return prev; - } - - setWalkPosition(current => current + sequence[posIndex]!); - return nextStep; - }); - }, walkSpeed); - - return () => clearInterval(interval); - }, [isWalking, selectedD, sequence, walkSpeed]); - - // Get color for heatmap cell - const getHeatmapColor = (discrepancy: number | null): string => { - if (discrepancy === null) return 'bg-muted/30'; - - const absVal = Math.abs(discrepancy); - if (absVal > discrepancyBound) { - return 'bg-destructive/80 text-destructive-foreground'; - } - if (absVal === discrepancyBound) { - return 'bg-amber-500/70 text-white'; - } - if (absVal === 0) { - return 'bg-emerald-500/50 text-emerald-900 dark:text-emerald-100'; - } - - const intensity = absVal / discrepancyBound; - if (intensity < 0.5) { - return 'bg-emerald-400/40 text-emerald-900 dark:text-emerald-100'; - } - return 'bg-amber-400/50 text-amber-900 dark:text-amber-100'; - }; - - // Max valid k for given d - const maxK = (d: number) => Math.floor(sequence.length / d); - - // Highlighted positions for selected d - const selectedPositions = useMemo(() => { - const positions: number[] = []; - for (let k = 1; k * selectedD <= sequence.length; k++) { - positions.push(k * selectedD); - } - return positions; - }, [selectedD, sequence.length]); - - // Get fill status of sequence - const filledCount = sequence.filter(s => s !== null).length; - const allFilled = filledCount === sequence.length && sequence.length > 0; - - // Known optimal sequences - const optimalSequences: Record = { - 1: [1, 1, -1, 1, -1, -1, -1, 1, 1, 1, -1], // One of the optimal 11-length sequences for C=1 - }; - - const loadOptimalSequence = () => { - const optimal = optimalSequences[discrepancyBound]; - if (optimal) { - setSequence([...optimal]); - setGamePhase('writing'); - } - }; - - return ( -
- {/* Header */} -
-
-

- The Erdős Discrepancy Problem -

-

- Can you write instructions to survive the tunnel forever? -

-
-
- -
-
- - {/* Rules Panel */} - {showRules && ( - - -
-
-

- The Prisoner's Dilemma -

-

- You're trapped in a tunnel with a deadly cliff {discrepancyBound} step{discrepancyBound > 1 ? 's' : ''} to your LEFT and - a pit of vipers {discrepancyBound} step{discrepancyBound > 1 ? 's' : ''} to your RIGHT. -

-

- Write a list of instructions: each is either LEFT or RIGHT. -

-

- The evil captor then picks a step size d and you follow every d-th instruction. -

-
-
-

- The Impossible Task -

-

- C = 1: Maximum possible sequence length is 11 -

-

- C = 2: Maximum possible sequence length is 1,160 -

-

- Terence Tao proved in 2015: No infinite sequence can survive! -

-
-
-
-
- )} - - {/* Game Phase: Intro */} - {gamePhase === 'intro' && ( - - -
- -

The Prisoner's Walk

-

- You wake up in a dark tunnel. There's a cliff to your left and vipers to your right. - Your captor demands you write a sequence of LEFT/RIGHT instructions... - but they get to choose which instructions you follow. -

- -
- -
- 1 - setDiscrepancyBound(v[0])} - min={1} - max={3} - step={1} - className="flex-1" - /> - 3 -
-

- C=1: Max 11 steps | C=2: Max 1,160 steps | C=3: Unknown (very large) -

-
- -
- - {discrepancyBound === 1 && ( - - )} -
- - {bestLength && ( -

- Your best: {bestLength} steps with C={discrepancyBound} -

- )} -
-
-
- )} - - {/* Main Game UI */} - {gamePhase !== 'intro' && ( -
- {/* Status Bar */} -
- - Length: {sequence.length} - - - Filled: {filledCount}/{sequence.length} - - - Max Discrepancy: {maxDiscrepancy.value} - {maxDiscrepancy.result && ` (d=${maxDiscrepancy.result.d}, k=${maxDiscrepancy.result.k})`} - - - Bound: C = {discrepancyBound} - - {bestLength && ( - - - Best: {bestLength} - - )} -
- - {/* Violation Alert */} - {hasViolation && gamePhase === 'writing' && ( - - - Bound Exceeded! - - The subsequence with d={maxDiscrepancy.result?.d}, k={maxDiscrepancy.result?.k} has - discrepancy {maxDiscrepancy.value}, which exceeds your bound of {discrepancyBound}. - - - )} - - - - - - Sequence - - - - Walk - - - - Heatmap - - - - {/* Sequence Builder Tab */} - - - - - Write Your Instructions - {gamePhase === 'writing' && ( - Click to toggle - )} - - - - {/* Sequence Grid */} -
-
- {sequence.map((val, idx) => { - const pos = idx + 1; - const isHighlighted = highlightedSubsequence.includes(pos) || selectedPositions.includes(pos); - const isViolationPart = maxDiscrepancy.result?.positions.includes(pos) && hasViolation; - - return ( - - ); - })} - - {gamePhase === 'writing' && ( - - )} -
-
- - {/* Controls */} -
- {gamePhase === 'writing' && ( - <> - - - - )} -
-
-
-
- - {/* Walk Visualization Tab */} - - - - Walk Visualization - - - {/* Step size selector */} -
-
- -
- {Array.from({ length: Math.min(sequence.length, 8) }, (_, i) => i + 1).map(d => ( - - ))} -
-
- -
- - setWalkSpeed(1000 - v[0])} - min={0} - max={900} - step={100} - className="w-24" - /> -
-
- - {/* Number line visualization */} -
- {/* Danger zones */} -
- CLIFF -
-
- VIPERS -
- - {/* Safe zone markers */} -
- {Array.from({ length: discrepancyBound * 2 + 1 }, (_, i) => i - discrepancyBound).map(pos => ( -
- {pos} -
- ))} -
- - {/* Walker position */} -
-
- 👤 -
-
-
- - {/* Walk info */} -
- Position: {walkPosition} - Steps taken: {walkStep} - Following: positions {selectedPositions.slice(0, 5).join(', ')}{selectedPositions.length > 5 ? '...' : ''} -
- - {/* Walk controls */} -
- - -
-
-
-
- - {/* Heatmap Tab */} - - {/* This is actually handled in the walk tab above */} - - - - - - Discrepancy Heatmap - - -

- Each cell shows the sum of values at positions d, 2d, 3d, ..., kd. - Red cells exceed the bound. -

- -
- - - - - {Array.from({ length: Math.min(12, sequence.length) }, (_, i) => i + 1).map(k => ( - - ))} - - - - {Array.from({ length: Math.min(8, sequence.length) }, (_, i) => i + 1).map(d => ( - - - {Array.from({ length: Math.min(12, sequence.length) }, (_, i) => i + 1).map(k => { - const discrepancy = getDiscrepancy(d, k); - const isValid = d * k <= sequence.length; - - return ( - - ); - })} - - ))} - -
d \ k{k}
{d} { - if (isValid) { - setHoveredCell({ d, k }); - const result = allDiscrepancies.find(r => r.d === d && r.k === k); - if (result) { - setHighlightedSubsequence(result.positions); - } - } - }} - onMouseLeave={() => { - setHoveredCell(null); - setHighlightedSubsequence([]); - }} - > - {isValid && discrepancy !== null ? discrepancy : ''} -
-
- - {hoveredCell && ( -
- d={hoveredCell.d}, k={hoveredCell.k}: Looking at positions{' '} - {Array.from({ length: hoveredCell.k }, (_, i) => (i + 1) * hoveredCell.d).join(', ')} -
- )} -
-
-
-
- - {/* Captor Reveal Phase */} - {gamePhase === 'captor-reveal' && captorChosenD !== null && ( - - - -

The Captor Speaks...

-

- "Interesting sequence... Let me see... I choose d = {captorChosenD}!" -

-

- You will follow every {captorChosenD === 1 ? '' : captorChosenD === 2 ? '2nd' : captorChosenD === 3 ? '3rd' : `${captorChosenD}th`} instruction: - positions {Array.from({ length: Math.floor(sequence.length / captorChosenD) }, (_, i) => (i + 1) * captorChosenD).slice(0, 8).join(', ')}... -

- -
-
- )} - - {/* Walking Phase */} - {gamePhase === 'walking' && captorChosenD !== null && ( - - -
-

Walking with d = {captorChosenD}...

-

- Step {prisonerStep} | Position: {prisonerPosition} -

-
- - {/* Tunnel visualization */} -
-
- 🏔️ -
-
- 🐍 -
- -
- 🚶 -
-
-
-
- )} - - {/* Fallen Phase */} - {gamePhase === 'fallen' && ( - - - You Fell! - - After {prisonerStep} steps with d={captorChosenD}, you ended up at position {prisonerPosition}. - The captor found your weakness! Your sequence lasted {sequence.length} instructions. -
- - -
-
-
- )} - - {/* Survived Phase (temporary - sequence ended) */} - {gamePhase === 'survived' && ( - - - Sequence Complete! - - Your {sequence.length}-instruction sequence survived d={captorChosenD}! - But remember: no sequence can survive forever. Try making it longer! -
- - -
-
-
- )} -
- )} - - {/* Acknowledgement */} - - -

Credits & History

-

- This puzzle is based on the Erdős Discrepancy Problem, posed by Paul Erdős in the 1930s - (with a $500 prize!). The "Prisoner's Walk" framing comes from various mathematical expositions, - popularized by James Grime's singingbanana video. -

-

- 2014: Boris Konev & Alexei Lisitsa proved C=2 has maximum length 1,160 using SAT solvers - (generating a 13GB proof!). 2015: Terence Tao proved that NO infinite sequence - can have bounded discrepancy, settling the problem completely. -

-
-
- - {/* Social Share */} - {showSocialShare !== false && ( - - )} -
- ); -}; - -export default ErdosDiscrepancyPuzzle; diff --git a/src/components/EternalDominationGame.tsx b/src/components/EternalDominationGame.tsx index fd2e461..4720bf1 100644 --- a/src/components/EternalDominationGame.tsx +++ b/src/components/EternalDominationGame.tsx @@ -3,7 +3,6 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Shield, Target, RotateCcw, Info } from "lucide-react"; -import SocialShare from '@/components/SocialShare'; // Extended good rectangle: 10 columns × 12 rows (following Fig. 10) // Border: columns 0 and 9, rows 0/1 (top) and 10/11 (bottom) @@ -350,11 +349,7 @@ const defendAttack = ( }; -interface EternalDominationGameProps { - showSocialShare?: boolean; -} - -const EternalDominationGame: React.FC = ({ showSocialShare = true }) => { +const EternalDominationGame: React.FC = () => { const [guards, setGuards] = useState>(generateInitialGuards); const [attackHistory, setAttackHistory] = useState([]); const [message, setMessage] = useState("Click any unguarded cell to attack."); @@ -684,13 +679,6 @@ const EternalDominationGame: React.FC = ({ showSocia )} - - {showSocialShare && ( - - )} ); }; diff --git a/src/components/FerrersRogersRamanujan.tsx b/src/components/FerrersRogersRamanujan.tsx index a5c95ac..1da4500 100644 --- a/src/components/FerrersRogersRamanujan.tsx +++ b/src/components/FerrersRogersRamanujan.tsx @@ -4,13 +4,8 @@ import { Button } from "@/components/ui/button"; import { Slider } from "@/components/ui/slider"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; -import SocialShare from '@/components/SocialShare'; -interface FerrersRogersRamanujanProps { - showSocialShare?: boolean; -} - -const FerrersRogersRamanujan = ({ showSocialShare = true }: FerrersRogersRamanujanProps) => { +const FerrersRogersRamanujan = () => { const [n, setN] = useState(14); const [partitionSliders, setPartitionSliders] = useState([]); const [committedPartition, setCommittedPartition] = useState([]); @@ -335,13 +330,6 @@ const FerrersRogersRamanujan = ({ showSocialShare = true }: FerrersRogersRamanuj The hook sizes form a new partition where parts differ by at least 2.

- - {showSocialShare && ( - - )} ); diff --git a/src/components/GoldCoinGame.tsx b/src/components/GoldCoinGame.tsx index 456dc17..f2dd211 100644 --- a/src/components/GoldCoinGame.tsx +++ b/src/components/GoldCoinGame.tsx @@ -18,11 +18,7 @@ interface GameState { moveHistory: string[]; } -interface GoldCoinGameProps { - showSocialShare?: boolean; -} - -const GoldCoinGame: React.FC = ({ showSocialShare = true }) => { +const GoldCoinGame: React.FC = () => { const [gameState, setGameState] = useState({ track: Array(15).fill('empty'), currentPlayer: 0, @@ -425,14 +421,13 @@ const GoldCoinGame: React.FC = ({ showSocialShare = true }) = - {showSocialShare && ( - - )} + ); }; -export default GoldCoinGame; \ No newline at end of file +export default GoldCoinGame; \ No newline at end of file diff --git a/src/components/InteractiveGallery.tsx b/src/components/InteractiveGallery.tsx index 24a3a03..1086712 100644 --- a/src/components/InteractiveGallery.tsx +++ b/src/components/InteractiveGallery.tsx @@ -67,7 +67,7 @@ const InteractiveGallery = () => {

Data Structures and Algorithms

-

Interactive explorations of elementary data structures and algorithms.

+

Interactive explorations of elementary data structures and algorithms.

{/* Search bar */} @@ -103,4 +103,4 @@ const InteractiveGallery = () => { ); }; -export default InteractiveGallery; +export default InteractiveGallery; \ No newline at end of file diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index 4223abe..a9ce43c 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -18,7 +18,7 @@ interface Interactive { } // This would ideally be generated from the actual components, but for now we'll hardcode -export const allInteractives: Interactive[] = [ +const allInteractives: Interactive[] = [ { id: 'binary-number-game', title: 'Binary Number Representation', @@ -288,15 +288,6 @@ export const allInteractives: Interactive[] = [ path: '/puzzles/ladybug-clock', theme: 'Puzzles', dateAdded: '2025-01-21' - }, - { - id: 'erdos-discrepancy', - title: 'The Erdős Discrepancy Problem', - description: 'Can you write instructions to survive the tunnel forever? Explore this famous mathematical puzzle through the Prisoner\'s Walk game.', - tags: ['erdos', 'discrepancy', 'sequences', 'game-theory', 'impossibility', 'puzzle', 'number-theory'], - path: '/puzzles/erdos-discrepancy', - theme: 'Puzzles', - dateAdded: '2025-01-22' } ]; @@ -323,7 +314,7 @@ const InteractiveIndex = () => { // Filter and sort interactives const filteredAndSortedInteractives = useMemo(() => { - const filtered = allInteractives.filter(interactive => { + let filtered = allInteractives.filter(interactive => { const matchesSearch = interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) || interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) || interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())); @@ -549,4 +540,4 @@ const InteractiveIndex = () => { ); }; -export default InteractiveIndex; +export default InteractiveIndex; \ No newline at end of file diff --git a/src/components/LadybugClockPuzzle.tsx b/src/components/LadybugClockPuzzle.tsx index 7de133f..4c6de70 100644 --- a/src/components/LadybugClockPuzzle.tsx +++ b/src/components/LadybugClockPuzzle.tsx @@ -8,9 +8,11 @@ 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; @@ -19,10 +21,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, @@ -32,32 +34,33 @@ const LadybugClockPuzzle: React.FC = ({ 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 @@ -80,15 +83,18 @@ const LadybugClockPuzzle: React.FC = ({ 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, @@ -105,11 +111,13 @@ const LadybugClockPuzzle: React.FC = ({ if (!isPlaying || simulation.isComplete) { return; } + const timeoutId = setTimeout(() => { if (isPlayingRef.current && !simulation.isComplete) { step(); } }, getDelay()); + return () => clearTimeout(timeoutId); }, [isPlaying, simulation, step, getDelay]); @@ -126,40 +134,44 @@ const LadybugClockPuzzle: React.FC = ({ 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); - const currentSim = simulation; + + 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, @@ -172,15 +184,15 @@ const LadybugClockPuzzle: React.FC = ({ // 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); @@ -213,7 +225,7 @@ const LadybugClockPuzzle: React.FC = ({ // 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 @@ -221,12 +233,10 @@ const LadybugClockPuzzle: React.FC = ({ }; // 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, @@ -234,9 +244,15 @@ const LadybugClockPuzzle: React.FC = ({ 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

@@ -256,34 +272,71 @@ const LadybugClockPuzzle: React.FC = ({ {/* 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 */} @@ -298,8 +351,9 @@ const LadybugClockPuzzle: React.FC = ({ {/* Antennae */} - ; - })()} + + ); + })()} {/* Center decoration */} @@ -320,11 +374,13 @@ const LadybugClockPuzzle: React.FC = ({
- {simulation.isComplete && simulation.lastPainted !== null &&
+ {simulation.isComplete && simulation.lastPainted !== null && ( +

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

-
} +
+ )}
@@ -338,17 +394,32 @@ const LadybugClockPuzzle: React.FC = ({
- - - @@ -361,12 +432,26 @@ const LadybugClockPuzzle: React.FC = ({
- 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" + />
@@ -381,9 +466,16 @@ const LadybugClockPuzzle: React.FC = ({
- {[100, 1000, 10000].map(count => )} + + ))} @@ -393,15 +485,18 @@ const LadybugClockPuzzle: React.FC = ({

Total simulations: {totalSimulations.toLocaleString()}

- {totalSimulations > 0 &&

+ {totalSimulations > 0 && ( +

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

} +

+ )}
{/* User Prediction */} - {totalSimulations === 0 && + {totalSimulations === 0 && ( + Make a Prediction! 🤔 @@ -409,23 +504,33 @@ const LadybugClockPuzzle: React.FC = ({

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? @@ -435,42 +540,52 @@ const LadybugClockPuzzle: React.FC = ({
- + - `${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)}%!

-
} +
+ )}

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

-
} +
+ )} {/* 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 */} @@ -513,14 +641,19 @@ const LadybugClockPuzzle: React.FC = ({
  • 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
    @@ -537,12 +670,21 @@ const LadybugClockPuzzle: React.FC = ({ {' '}and demonstrated in{' '} this YouTube short - . + + . It illustrates a beautiful result about random walks on cycle graphs: every position (except the start) has equal probability of being visited last.

    - {showSocialShare && } - ; + {showSocialShare && ( + + )} + + ); }; + export default LadybugClockPuzzle; diff --git a/src/components/NimGame.tsx b/src/components/NimGame.tsx index 5fe070c..924a523 100644 --- a/src/components/NimGame.tsx +++ b/src/components/NimGame.tsx @@ -27,11 +27,7 @@ const heapColors = [ '#f87171', '#a78bfa', '#fbbf24', '#60a5fa', '#34d399', '#f472b6', '#f87171', '#a78bfa', '#facc15', '#38bdf8', ]; -interface NimGameProps { - showSocialShare?: boolean; -} - -const NimGame: React.FC = ({ showSocialShare = true }) => { +const NimGame: React.FC = () => { const [mode, setMode] = useState(null); const [showSetup, setShowSetup] = useState(false); const [userHeapCount, setUserHeapCount] = useState(3); @@ -279,14 +275,16 @@ const NimGame: React.FC = ({ showSocialShare = true }) => { - {showSocialShare && ( + {/* Social Share */} +
    - )} +
    ); }; -export default NimGame; \ No newline at end of file +export default NimGame; \ No newline at end of file diff --git a/src/components/RentDivisionPuzzle.tsx b/src/components/RentDivisionPuzzle.tsx index 51f9951..b06a5b1 100644 --- a/src/components/RentDivisionPuzzle.tsx +++ b/src/components/RentDivisionPuzzle.tsx @@ -11,7 +11,6 @@ import { SelectValue, } from "@/components/ui/select"; import { RefreshCw, User, UserRound, Users, UserCheck } from "lucide-react"; -import SocialShare from '@/components/SocialShare'; const AGENTS = ["Alice", "Bob", "Charlie", "Dana"]; const AGENT_ICONS = [User, UserRound, Users, UserCheck]; @@ -24,10 +23,9 @@ const ROOM_COLORS = [ interface RentDivisionPuzzleProps { onComplete?: (completed: boolean) => void; - showSocialShare?: boolean; } -const RentDivisionPuzzle = ({ onComplete, showSocialShare = true }: RentDivisionPuzzleProps) => { +const RentDivisionPuzzle = ({ onComplete }: RentDivisionPuzzleProps) => { // V-matrix: valuations[agent][room] const [valuations, setValuations] = useState(() => Array.from({ length: 4 }, () => @@ -471,12 +469,6 @@ const RentDivisionPuzzle = ({ onComplete, showSocialShare = true }: RentDivision

    - {showSocialShare && ( - - )} ); }; diff --git a/src/components/RulesOfInferencePlayground.tsx b/src/components/RulesOfInferencePlayground.tsx index 54d85b9..1f226f8 100644 --- a/src/components/RulesOfInferencePlayground.tsx +++ b/src/components/RulesOfInferencePlayground.tsx @@ -3,7 +3,6 @@ import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; -import SocialShare from '@/components/SocialShare'; // Rules of Inference Playground (Example 1: Resolution) // Layout: 3 columns (Premises | Notepad lines with rule margin | Rules) @@ -197,7 +196,7 @@ function DropZone({ children, onDrop, className }: { children?: React.ReactNode; ); } -export default function RulesOfInferencePlayground({ onComplete, showSocialShare = true }: { onComplete?: (completed: boolean) => void; showSocialShare?: boolean }) { +export default function RulesOfInferencePlayground({ onComplete }: { onComplete?: (completed: boolean) => void }) { const { toast } = useToast(); const [exampleId, setExampleId] = useState(EXAMPLES[0].id); const example = useMemo(() => EXAMPLES.find((e) => e.id === exampleId)!, [exampleId]); @@ -580,13 +579,6 @@ export default function RulesOfInferencePlayground({ onComplete, showSocialShare - - {showSocialShare && ( - - )} ); -} +} \ No newline at end of file diff --git a/src/components/ZeckendorfGame.tsx b/src/components/ZeckendorfGame.tsx index 1655e46..439b0d0 100644 --- a/src/components/ZeckendorfGame.tsx +++ b/src/components/ZeckendorfGame.tsx @@ -34,7 +34,7 @@ const ZeckendorfGame: React.FC = ({ showSocialShare = true // Generate a random combination of Fibonacci numbers const numTerms = Math.floor(Math.random() * 4) + 2; // 2-5 terms let target = 0; - const usedIndices = new Set(); + let usedIndices = new Set(); for (let i = 0; i < numTerms; i++) { let index; @@ -235,4 +235,4 @@ const ZeckendorfGame: React.FC = ({ showSocialShare = true ); }; -export default ZeckendorfGame; +export default ZeckendorfGame; \ No newline at end of file diff --git a/src/components/guessing-game/QuestionGuessingMode.tsx b/src/components/guessing-game/QuestionGuessingMode.tsx index d76a11a..d30855a 100644 --- a/src/components/guessing-game/QuestionGuessingMode.tsx +++ b/src/components/guessing-game/QuestionGuessingMode.tsx @@ -217,7 +217,7 @@ const QuestionGuessingMode: React.FC = ({ case 'odd': baseCondition = (num % 2 !== 0); break; - case 'prime': { + case 'prime': const isPrimeNum = (n: number): boolean => { if (n < 2) return false; for (let i = 2; i <= Math.sqrt(n); i++) { @@ -227,7 +227,6 @@ const QuestionGuessingMode: React.FC = ({ }; baseCondition = isPrimeNum(num); break; - } case 'perfectSquare': baseCondition = Math.sqrt(num) % 1 === 0; break; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 69d63a4..56a0979 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -21,7 +21,7 @@ const Command = React.forwardRef< )) Command.displayName = CommandPrimitive.displayName -type CommandDialogProps = DialogProps +interface CommandDialogProps extends DialogProps {} const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 12c9136..9f9a6dc 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -2,7 +2,8 @@ import * as React from "react" import { cn } from "@/lib/utils" -export type TextareaProps = React.TextareaHTMLAttributes +export interface TextareaProps + extends React.TextareaHTMLAttributes {} const Textarea = React.forwardRef( ({ className, ...props }, ref) => { diff --git a/src/pages/AscDescGridPuzzlePage.tsx b/src/pages/AscDescGridPuzzlePage.tsx index da7dde4..7963768 100644 --- a/src/pages/AscDescGridPuzzlePage.tsx +++ b/src/pages/AscDescGridPuzzlePage.tsx @@ -1,12 +1,13 @@ +import Layout from "@/components/Layout"; import AscDescGridPuzzle from "@/components/AscDescGridPuzzle"; const AscDescGridPuzzlePage = () => { return ( -
    -
    - + +
    +
    -
    + ); }; diff --git a/src/pages/AssistedNimGamePage.tsx b/src/pages/AssistedNimGamePage.tsx index 61b9527..233302a 100644 --- a/src/pages/AssistedNimGamePage.tsx +++ b/src/pages/AssistedNimGamePage.tsx @@ -1,13 +1,12 @@ +import React from 'react'; import AssistedNimGame from '@/components/AssistedNimGame'; const AssistedNimGamePage = () => { return ( -
    -
    - -
    +
    +
    ); }; -export default AssistedNimGamePage; \ No newline at end of file +export default AssistedNimGamePage; \ No newline at end of file diff --git a/src/pages/BagchalGamePage.tsx b/src/pages/BagchalGamePage.tsx index 3d65664..2e1ff48 100644 --- a/src/pages/BagchalGamePage.tsx +++ b/src/pages/BagchalGamePage.tsx @@ -1,12 +1,13 @@ +import Layout from "@/components/Layout"; import BagchalGame from "@/components/BagchalGame"; const BagchalGamePage = () => { return ( -
    +
    -
    + ); }; diff --git a/src/pages/BalancedTernaryGamePage.tsx b/src/pages/BalancedTernaryGamePage.tsx index bd70437..61f4057 100644 --- a/src/pages/BalancedTernaryGamePage.tsx +++ b/src/pages/BalancedTernaryGamePage.tsx @@ -2,12 +2,12 @@ import BalancedTernaryGame from '@/components/BalancedTernaryGame'; const BalancedTernaryGamePage = () => { return ( -
    -
    - +
    +
    +
    ); }; -export default BalancedTernaryGamePage; +export default BalancedTernaryGamePage; \ No newline at end of file diff --git a/src/pages/BinaryNumberGamePage.tsx b/src/pages/BinaryNumberGamePage.tsx index 15b8a5c..d12b6c1 100644 --- a/src/pages/BinaryNumberGamePage.tsx +++ b/src/pages/BinaryNumberGamePage.tsx @@ -2,12 +2,12 @@ import BinaryNumberGame from '@/components/BinaryNumberGame'; const BinaryNumberGamePage = () => { return ( -
    -
    - +
    +
    +
    ); }; -export default BinaryNumberGamePage; +export default BinaryNumberGamePage; \ No newline at end of file diff --git a/src/pages/BulgarianSolitairePage.tsx b/src/pages/BulgarianSolitairePage.tsx index 0f6651d..2a9dfc6 100644 --- a/src/pages/BulgarianSolitairePage.tsx +++ b/src/pages/BulgarianSolitairePage.tsx @@ -1,13 +1,29 @@ import BulgarianSolitaire from "@/components/BulgarianSolitaire"; +import { useSearchParams } from "react-router-dom"; const BulgarianSolitairePage = () => { - return ( -
    -
    - + const [searchParams] = useSearchParams(); + const from = searchParams.get('from'); + + if (from === 'puzzles') { + return ( +
    +
    +
    + +
    + +
    -
    - ); + ); + } + + return ; }; -export default BulgarianSolitairePage; +export default BulgarianSolitairePage; \ No newline at end of file diff --git a/src/pages/BurnsidesLemmaPage.tsx b/src/pages/BurnsidesLemmaPage.tsx index 713c547..c3b7303 100644 --- a/src/pages/BurnsidesLemmaPage.tsx +++ b/src/pages/BurnsidesLemmaPage.tsx @@ -1,12 +1,13 @@ +import Layout from '@/components/Layout'; import BurnsidesLemma from '@/components/BurnsidesLemma'; const BurnsidesLemmaPage = () => { return ( -
    +
    -
    + ); }; diff --git a/src/pages/CubeColoringPage.tsx b/src/pages/CubeColoringPage.tsx index 1d8d941..30ddb00 100644 --- a/src/pages/CubeColoringPage.tsx +++ b/src/pages/CubeColoringPage.tsx @@ -1,12 +1,16 @@ +import Layout from '@/components/Layout'; import CubeColoring from '@/components/CubeColoring'; const CubeColoringPage = () => { return ( -
    +
    - +
    -
    + ); }; diff --git a/src/pages/DominoRetilingPuzzlePage.tsx b/src/pages/DominoRetilingPuzzlePage.tsx index 8170ba1..e3f7af7 100644 --- a/src/pages/DominoRetilingPuzzlePage.tsx +++ b/src/pages/DominoRetilingPuzzlePage.tsx @@ -1,13 +1,28 @@ +import Layout from '@/components/Layout'; import DominoRetilingPuzzle from '@/components/DominoRetilingPuzzle'; +import { useLocation, Link } from 'react-router-dom'; +import { ArrowLeft } from 'lucide-react'; const DominoRetilingPuzzlePage = () => { + const location = useLocation(); + const fromPuzzles = location.search.includes('from=puzzles'); + return ( -
    -
    - -
    -
    + + {fromPuzzles && ( +
    + + + Back to Puzzles + +
    + )} + +
    ); }; -export default DominoRetilingPuzzlePage; +export default DominoRetilingPuzzlePage; \ No newline at end of file diff --git a/src/pages/ErdosDiscrepancyPuzzlePage.tsx b/src/pages/ErdosDiscrepancyPuzzlePage.tsx deleted file mode 100644 index 2578ef0..0000000 --- a/src/pages/ErdosDiscrepancyPuzzlePage.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import ErdosDiscrepancyPuzzle from "@/components/ErdosDiscrepancyPuzzle"; - -const ErdosDiscrepancyPuzzlePage = () => { - return ( -
    -
    - -
    -
    - ); -}; - -export default ErdosDiscrepancyPuzzlePage; diff --git a/src/pages/EternalDominationGamePage.tsx b/src/pages/EternalDominationGamePage.tsx index 54506fe..bd81bb9 100644 --- a/src/pages/EternalDominationGamePage.tsx +++ b/src/pages/EternalDominationGamePage.tsx @@ -1,12 +1,87 @@ +import Layout from "@/components/Layout"; import EternalDominationGame from "@/components/EternalDominationGame"; +import SocialShare from "@/components/SocialShare"; +import { Badge } from "@/components/ui/badge"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; const EternalDominationGamePage = () => { return ( -
    -
    - + +
    +
    +
    + Miscellany +

    Eternal Domination on a Grid

    +

    + Explore the m-eternal domination problem on a 12×10 grid. Guards must maintain + a dominating set while defending against arbitrary attack sequences. +

    +
    + + + + + + Mathematical Background + +

    + Dominating Set: A subset S of vertices in a graph G such that + every vertex not in S has a neighbor in S. Think of guards placed at certain + vertices that can "watch" their neighbors. +

    +

    + m-Eternal Domination: A two-player game where the defender + places guards on vertices, and the attacker repeatedly attacks unguarded vertices. + The defender responds by moving guards (all can move simultaneously, but only to + adjacent vertices) such that one guard ends on the attacked vertex and the guards + still form a dominating set. +

    +

    + The Challenge: The defender wins if they can maintain a dominating + set forever against any attack sequence. The m-eternal domination number γ∞(G) is + the minimum number of guards needed to win. +

    +
    +
    + + + Defense Strategy + +

    + This implementation uses the strategy from research on finite grids: +

    +
      +
    • Border guards: All vertices on the border (rows 0, 1, 8, 9 and + columns 0, 11) are always guarded, forming a protective cycle C.
    • +
    • Interior guards: Placed in a pattern that ensures every interior + vertex is dominated by exactly one guard.
    • +
    • Defense mechanism: When attacked, interior guards shift toward + the attack, potentially pushing a guard to the border. Border guards shift along + "complementary paths" to fill the resulting gaps.
    • +
    +
    +
    + + + Reference + +

    + Based on: "m-Eternal Domination and Variants on Some Classes of Finite and + Infinite Graphs" by Calamoneri et al. (CIAC 2025), which establishes bounds + and strategies for eternal domination on various grid types including square, + hexagonal, and triangular grids. +

    +
    +
    +
    + + +
    -
    + ); }; diff --git a/src/pages/FerrersRogersRamanujanPage.tsx b/src/pages/FerrersRogersRamanujanPage.tsx index 35253d1..c343f8e 100644 --- a/src/pages/FerrersRogersRamanujanPage.tsx +++ b/src/pages/FerrersRogersRamanujanPage.tsx @@ -1,12 +1,27 @@ +import Layout from "@/components/Layout"; import FerrersRogersRamanujan from "@/components/FerrersRogersRamanujan"; +import SocialShare from "@/components/SocialShare"; const FerrersRogersRamanujanPage = () => { return ( -
    -
    - + +
    +
    +

    Ferrers Diagram & Rogers-Ramanujan Partition

    +

    + Explore integer partitions through visual Ferrers diagrams and discover the elegant Rogers-Ramanujan transformation. +

    +
    + + + +
    -
    + ); }; diff --git a/src/pages/GoldCoinGamePage.tsx b/src/pages/GoldCoinGamePage.tsx index 0ef4b78..d838ae9 100644 --- a/src/pages/GoldCoinGamePage.tsx +++ b/src/pages/GoldCoinGamePage.tsx @@ -1,13 +1,12 @@ +import React from 'react'; import GoldCoinGame from '@/components/GoldCoinGame'; const GoldCoinGamePage = () => { return ( -
    -
    - -
    +
    +
    ); }; -export default GoldCoinGamePage; \ No newline at end of file +export default GoldCoinGamePage; \ No newline at end of file diff --git a/src/pages/LadybugClockPuzzlePage.tsx b/src/pages/LadybugClockPuzzlePage.tsx index 71bdf85..df6554a 100644 --- a/src/pages/LadybugClockPuzzlePage.tsx +++ b/src/pages/LadybugClockPuzzlePage.tsx @@ -1,12 +1,29 @@ +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 + +
    + )} +
    +
    -
    + ); }; diff --git a/src/pages/NimGamePage.tsx b/src/pages/NimGamePage.tsx index f34463a..aa202f6 100644 --- a/src/pages/NimGamePage.tsx +++ b/src/pages/NimGamePage.tsx @@ -1,13 +1,12 @@ +import React from 'react'; import NimGame from '@/components/NimGame'; const NimGamePage = () => { return ( -
    -
    - -
    +
    +
    ); }; -export default NimGamePage; \ No newline at end of file +export default NimGamePage; \ No newline at end of file diff --git a/src/pages/ParityBitsGamePage.tsx b/src/pages/ParityBitsGamePage.tsx index 58949d2..6ba5a0b 100644 --- a/src/pages/ParityBitsGamePage.tsx +++ b/src/pages/ParityBitsGamePage.tsx @@ -1,13 +1,12 @@ +import Layout from "@/components/Layout"; import ParityBitsGame from "@/components/ParityBitsGame"; const ParityBitsGamePage = () => { return ( -
    -
    - -
    -
    + + + ); }; -export default ParityBitsGamePage; +export default ParityBitsGamePage; \ No newline at end of file diff --git a/src/pages/PebblePlacementGamePage.tsx b/src/pages/PebblePlacementGamePage.tsx index c359304..e6013d1 100644 --- a/src/pages/PebblePlacementGamePage.tsx +++ b/src/pages/PebblePlacementGamePage.tsx @@ -1,13 +1,29 @@ import PebblePlacementGame from "@/components/PebblePlacementGame"; +import { useSearchParams } from "react-router-dom"; const PebblePlacementGamePage = () => { - return ( -
    -
    - + const [searchParams] = useSearchParams(); + const from = searchParams.get('from'); + + if (from === 'puzzles') { + return ( +
    +
    +
    + +
    + +
    -
    - ); + ); + } + + return ; }; -export default PebblePlacementGamePage; +export default PebblePlacementGamePage; \ No newline at end of file diff --git a/src/pages/PresentsPuzzlePage.tsx b/src/pages/PresentsPuzzlePage.tsx index edb710a..cc51817 100644 --- a/src/pages/PresentsPuzzlePage.tsx +++ b/src/pages/PresentsPuzzlePage.tsx @@ -1,12 +1,13 @@ +import Layout from '@/components/Layout'; import PresentsPuzzle from "@/components/PresentsPuzzle"; const PresentsPuzzlePage = () => { return ( -
    -
    + +
    -
    + ); }; diff --git a/src/pages/RentDivisionPuzzlePage.tsx b/src/pages/RentDivisionPuzzlePage.tsx index 154033f..63dad7f 100644 --- a/src/pages/RentDivisionPuzzlePage.tsx +++ b/src/pages/RentDivisionPuzzlePage.tsx @@ -1,10 +1,34 @@ +import { useState } from "react"; import RentDivisionPuzzle from "@/components/RentDivisionPuzzle"; +import SocialShare from "@/components/SocialShare"; const RentDivisionPuzzlePage = () => { + const [isCompleted, setIsCompleted] = useState(false); + return ( -
    -
    - +
    +
    +
    +

    + Envy-Free Rent Division +

    +

    + Explore fair division by assigning rooms and setting rents. Can you find an + allocation where no one envies another? +

    +
    + + + +
    ); diff --git a/src/pages/RulesOfInferencePlaygroundPage.tsx b/src/pages/RulesOfInferencePlaygroundPage.tsx index 34a2fd3..8c9a2f8 100644 --- a/src/pages/RulesOfInferencePlaygroundPage.tsx +++ b/src/pages/RulesOfInferencePlaygroundPage.tsx @@ -1,13 +1,34 @@ +import { useState } from "react"; import RulesOfInferencePlayground from "@/components/RulesOfInferencePlayground"; +import SocialShare from "@/components/SocialShare"; const RulesOfInferencePlaygroundPage = () => { + const [isCompleted, setIsCompleted] = useState(false); + return ( -
    -
    - +
    +
    +
    +

    Rules of Inference Playground

    +

    + Practice applying resolution and equivalence rules to derive conclusions step by step. +

    +
    + + + +
    ); }; -export default RulesOfInferencePlaygroundPage; +export default RulesOfInferencePlaygroundPage; \ No newline at end of file diff --git a/src/pages/StackingBlocksPage.tsx b/src/pages/StackingBlocksPage.tsx index ccb4f78..7239dfc 100644 --- a/src/pages/StackingBlocksPage.tsx +++ b/src/pages/StackingBlocksPage.tsx @@ -1,13 +1,29 @@ import StackingBlocks from "@/components/StackingBlocks"; +import { useSearchParams } from "react-router-dom"; const StackingBlocksPage = () => { - return ( -
    -
    - + const [searchParams] = useSearchParams(); + const from = searchParams.get('from'); + + if (from === 'puzzles') { + return ( +
    +
    +
    + +
    + +
    -
    - ); + ); + } + + return ; }; -export default StackingBlocksPage; +export default StackingBlocksPage; \ No newline at end of file diff --git a/src/pages/SubtractionGamePage.tsx b/src/pages/SubtractionGamePage.tsx index 75c3d78..302ca9c 100644 --- a/src/pages/SubtractionGamePage.tsx +++ b/src/pages/SubtractionGamePage.tsx @@ -1,13 +1,7 @@ import SubtractionGame from '@/components/SubtractionGame'; const SubtractionGamePage = () => { - return ( -
    -
    - -
    -
    - ); + return ; }; -export default SubtractionGamePage; +export default SubtractionGamePage; \ No newline at end of file diff --git a/src/pages/TernaryNumberGamePage.tsx b/src/pages/TernaryNumberGamePage.tsx index a85620d..79ba882 100644 --- a/src/pages/TernaryNumberGamePage.tsx +++ b/src/pages/TernaryNumberGamePage.tsx @@ -2,12 +2,12 @@ import TernaryNumberGame from '@/components/TernaryNumberGame'; const TernaryNumberGamePage = () => { return ( -
    -
    - +
    +
    +
    ); }; -export default TernaryNumberGamePage; +export default TernaryNumberGamePage; \ No newline at end of file diff --git a/src/pages/ZeckendorfGamePage.tsx b/src/pages/ZeckendorfGamePage.tsx index c585236..130ecfa 100644 --- a/src/pages/ZeckendorfGamePage.tsx +++ b/src/pages/ZeckendorfGamePage.tsx @@ -2,12 +2,12 @@ import ZeckendorfGame from '@/components/ZeckendorfGame'; const ZeckendorfGamePage = () => { return ( -
    -
    - +
    +
    +
    ); }; -export default ZeckendorfGamePage; \ No newline at end of file +export default ZeckendorfGamePage; \ No newline at end of file diff --git a/src/pages/ZeckendorfSearchTrickPage.tsx b/src/pages/ZeckendorfSearchTrickPage.tsx index f5cc437..59e977f 100644 --- a/src/pages/ZeckendorfSearchTrickPage.tsx +++ b/src/pages/ZeckendorfSearchTrickPage.tsx @@ -2,12 +2,12 @@ import ZeckendorfSearchTrick from '@/components/ZeckendorfSearchTrick'; const ZeckendorfSearchTrickPage = () => { return ( -
    -
    - +
    +
    +
    ); }; -export default ZeckendorfSearchTrickPage; \ No newline at end of file +export default ZeckendorfSearchTrickPage; \ No newline at end of file diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index 5e6b9b1..323270e 100644 --- a/src/pages/theme-pages/Puzzles.tsx +++ b/src/pages/theme-pages/Puzzles.tsx @@ -115,16 +115,6 @@ const Puzzles = () => { difficulty: "Intermediate" as const, duration: "5-15 min", participants: "1 player" - }, - { - id: "erdos-discrepancy", - title: "The Erdős Discrepancy Problem", - description: "Can you write instructions to survive the prisoner's tunnel? Explore this famous mathematical puzzle and discover why no sequence can keep you safe forever!", - path: "/puzzles/erdos-discrepancy", - tags: ["Number Theory", "Sequences", "Impossibility", "Game Theory"], - difficulty: "Advanced" as const, - duration: "10-30 min", - participants: "1 player" } ]; diff --git a/src/pages/theme-pages/discrete-math/Foundations.tsx b/src/pages/theme-pages/discrete-math/Foundations.tsx index c8eec1b..c4eba6c 100644 --- a/src/pages/theme-pages/discrete-math/Foundations.tsx +++ b/src/pages/theme-pages/discrete-math/Foundations.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router-dom"; import { Search } from "lucide-react"; import Layout from "@/components/Layout"; import InteractiveCard from "@/components/InteractiveCard"; +import SocialShare from "@/components/SocialShare"; import BinaryNumberGame from "@/components/BinaryNumberGame"; import TernaryNumberGame from "@/components/TernaryNumberGame"; import BalancedTernaryGame from "@/components/BalancedTernaryGame"; @@ -79,6 +80,12 @@ const Foundations = () => {
    + +
    ; } @@ -110,4 +117,4 @@ const Foundations = () => {
    ; }; -export default Foundations; +export default Foundations; \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts index a934352..a4b00a3 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,5 +1,4 @@ import type { Config } from "tailwindcss"; -import tailwindcssAnimate from "tailwindcss-animate"; export default { darkMode: ["class"], @@ -123,5 +122,5 @@ export default { } } }, - plugins: [tailwindcssAnimate], + plugins: [require("tailwindcss-animate")], } satisfies Config;