Compare commits

..

11 commits

Author SHA1 Message Date
Neeldhara Misra
3d31dc1b55 Add Forgejo CI and Dokploy deployment
Some checks are pending
CI / verify (push) Waiting to run
2026-07-20 14:22:11 +05:30
Neeldhara Misra
263c87d6c7
Merge pull request #3 from neeldhara/devin/1771444783-consistency-and-search
Standardize page consistency and add Cmd+K search
2026-02-19 02:02:24 +05:30
Devin AI
aff14bdf2f Remove duplicate SocialShare from Foundations theme page
Components now render SocialShare internally via showSocialShare prop,
so the page-level SocialShare block was causing duplicates.

Co-Authored-By: Neeldhara Misra <mail@neeldhara.com>
2026-02-18 20:27:43 +00:00
Devin AI
8f2a086df6 Standardize page wrappers, add showSocialShare prop, and add Cmd+K search
- Standardize all page wrappers to use consistent min-h-screen bg-background pattern
- Remove Layout wrapper usage from pages that had it
- Remove page-level SocialShare and headers, move into components
- Add showSocialShare prop to all interactive components for consistent control
- Add Cmd+K / Ctrl+K command palette search across all interactives
- Export allInteractives from InteractiveIndex for reuse

Co-Authored-By: Neeldhara Misra <mail@neeldhara.com>
2026-02-18 20:07:30 +00:00
gpt-engineer-app[bot]
dc8300f358 Add Erdős discrepancy interactive
Introduce a full Erdős Discrepancy interactive (Prisoner's Walk) with sequence builder, walk visualization, heatmap, and educational panels. Adds new ErdosDiscrepancyPuzzle component, page wrapper, routing, and index/puzzles page entries. Also wires up SocialShare support and an MVP route /puzzles/erdos-discrepancy.

X-Lovable-Edit-ID: edt-656659b7-ea52-4d0d-880b-ae0c70666491
2026-01-22 03:04:29 +00:00
gpt-engineer-app[bot]
828f2dba66 Changes 2026-01-22 03:04:28 +00:00
gpt-engineer-app[bot]
f4f633f420 Shrink prediction buttons
Adjust the prediction option buttons to be smaller and fit on one line:
- reduce gap from 2 to 1.5
- make each button 8x8 with extra small text and padding removed

X-Lovable-Edit-ID: edt-decfb124-7935-4ae9-bfd0-f42c3b1cfe81
2026-01-21 16:03:12 +00:00
gpt-engineer-app[bot]
cb5a243cf3 Changes 2026-01-21 16:03:12 +00:00
gpt-engineer-app[bot]
8894b03926 Visual edit in Lovable
Edited UI in Lovable
2026-01-21 16:02:29 +00:00
gpt-engineer-app[bot]
f7db939d49 Remove spoiler from ack text
Trim second line in acknowledgement to avoid spoilers; keep credits and link to Mindbenders and YouTube short intact.

X-Lovable-Edit-ID: edt-ae34477a-baa8-4293-abd6-df754361e275
2026-01-21 16:00:13 +00:00
gpt-engineer-app[bot]
ca5d1e89b8 Changes 2026-01-21 16:00:13 +00:00
50 changed files with 1398 additions and 609 deletions

8
.dockerignore Normal file
View file

@ -0,0 +1,8 @@
.git
.github
.forgejo
dist
node_modules
npm-debug.log*
Dockerfile*
README.md

41
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,41 @@
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

19
Dockerfile Normal file
View file

@ -0,0 +1,19 @@
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

View file

@ -64,6 +64,12 @@ This project is built with:
Simply open [Lovable](https://lovable.dev/projects/5ee9d1e7-af25-47e9-ab4c-bdbca4554e32) and click on Share -> Publish. 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? ## Can I connect a custom domain to my Lovable project?
Yes, you can! Yes, you can!

22
nginx.conf Normal file
View file

@ -0,0 +1,22 @@
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;
}
}

View file

@ -3,6 +3,7 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import ErrorBoundary from "@/components/ErrorBoundary"; import ErrorBoundary from "@/components/ErrorBoundary";
import CommandSearch from "@/components/CommandSearch";
import { BrowserRouter, Routes, Route } from "react-router-dom"; import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index"; import Index from "./pages/Index";
import InteractiveIndex from "./components/InteractiveIndex"; import InteractiveIndex from "./components/InteractiveIndex";
@ -67,6 +68,7 @@ import BagchalGamePage from './pages/BagchalGamePage';
import AscDescGridPuzzlePage from './pages/AscDescGridPuzzlePage'; import AscDescGridPuzzlePage from './pages/AscDescGridPuzzlePage';
import EternalDominationGamePage from './pages/EternalDominationGamePage'; import EternalDominationGamePage from './pages/EternalDominationGamePage';
import LadybugClockPuzzlePage from './pages/LadybugClockPuzzlePage'; import LadybugClockPuzzlePage from './pages/LadybugClockPuzzlePage';
import ErdosDiscrepancyPuzzlePage from './pages/ErdosDiscrepancyPuzzlePage';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -77,6 +79,7 @@ const App = () => (
<Toaster /> <Toaster />
<Sonner /> <Sonner />
<BrowserRouter> <BrowserRouter>
<CommandSearch />
<Routes> <Routes>
<Route path="/" element={<Index />} /> <Route path="/" element={<Index />} />
<Route path="/index" element={<InteractiveIndex />} /> <Route path="/index" element={<InteractiveIndex />} />
@ -144,6 +147,7 @@ const App = () => (
<Route path="/puzzles/domino-retiling" element={<DominoRetilingPuzzlePage />} /> <Route path="/puzzles/domino-retiling" element={<DominoRetilingPuzzlePage />} />
<Route path="/puzzles/asc-desc-grid" element={<AscDescGridPuzzlePage />} /> <Route path="/puzzles/asc-desc-grid" element={<AscDescGridPuzzlePage />} />
<Route path="/puzzles/ladybug-clock" element={<LadybugClockPuzzlePage />} /> <Route path="/puzzles/ladybug-clock" element={<LadybugClockPuzzlePage />} />
<Route path="/puzzles/erdos-discrepancy" element={<ErdosDiscrepancyPuzzlePage />} />
<Route path="/discrete-math/foundations/zeckendorf" element={<ZeckendorfGamePage />} /> <Route path="/discrete-math/foundations/zeckendorf" element={<ZeckendorfGamePage />} />
<Route path="/discrete-math/foundations/zeckendorf-search" element={<ZeckendorfSearchTrickPage />} /> <Route path="/discrete-math/foundations/zeckendorf-search" element={<ZeckendorfSearchTrickPage />} />
<Route path="/discrete-math/foundations/rules-of-inference" element={<RulesOfInferencePlaygroundPage />} /> <Route path="/discrete-math/foundations/rules-of-inference" element={<RulesOfInferencePlaygroundPage />} />

View file

@ -66,7 +66,11 @@ const getOddPowers = (heaps: number[]): Set<number> => {
return oddPowers; return oddPowers;
}; };
const AssistedNimGame: React.FC = () => { interface AssistedNimGameProps {
showSocialShare?: boolean;
}
const AssistedNimGame: React.FC<AssistedNimGameProps> = ({ showSocialShare = true }) => {
const [mode, setMode] = useState<Mode | null>(null); const [mode, setMode] = useState<Mode | null>(null);
const [showSetup, setShowSetup] = useState(false); const [showSetup, setShowSetup] = useState(false);
const [userHeapCount, setUserHeapCount] = useState(3); const [userHeapCount, setUserHeapCount] = useState(3);
@ -384,12 +388,12 @@ const AssistedNimGame: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
{/* Social Share */} {showSocialShare && (
<SocialShare <SocialShare
title="Assisted Game of Nim - XOR Strategy Visualization" title="Assisted Game of Nim - XOR Strategy Visualization"
description="Learn the mathematical strategy behind Nim with visual power-of-2 breakdown!" description="Learn the mathematical strategy behind Nim with visual power-of-2 breakdown!"
url={window.location.href}
/> />
)}
</div> </div>
); );
}; };

View file

@ -0,0 +1,65 @@
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 (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Search interactives by title or description..." />
<CommandList>
<CommandEmpty>No interactives found.</CommandEmpty>
<CommandGroup heading="Interactives">
{allInteractives.map((interactive) => (
<CommandItem
key={interactive.id}
value={`${interactive.title} ${interactive.description}`}
onSelect={() => handleSelect(interactive.path)}
>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium">{interactive.title}</span>
<Badge variant="outline" className="text-xs">
{interactive.theme}
</Badge>
</div>
<span className="text-xs text-muted-foreground line-clamp-1">
{interactive.description}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
);
};
export default CommandSearch;

View file

@ -0,0 +1,902 @@
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<ErdosDiscrepancyPuzzleProps> = ({ showSocialShare = false }) => {
// Core game state
const [sequence, setSequence] = useState<Direction[]>([]);
const [discrepancyBound, setDiscrepancyBound] = useState(1);
const [gamePhase, setGamePhase] = useState<GamePhase>('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<number | null>(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<number[]>([]);
// Best score tracking
const [bestLength, setBestLength] = useState<number | null>(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<number, Direction[]> = {
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 (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">
The Erdős Discrepancy Problem
</h1>
<p className="text-muted-foreground mt-1">
Can you write instructions to survive the tunnel forever?
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setShowRules(!showRules)}
>
{showRules ? <ChevronUp className="w-4 h-4 mr-1" /> : <Info className="w-4 h-4 mr-1" />}
{showRules ? 'Hide' : 'Rules'}
</Button>
</div>
</div>
{/* Rules Panel */}
{showRules && (
<Card className="bg-muted/30">
<CardContent className="pt-4">
<div className="grid md:grid-cols-2 gap-6">
<div>
<h3 className="font-semibold text-foreground mb-2 flex items-center gap-2">
<Skull className="w-4 h-4" /> The Prisoner's Dilemma
</h3>
<p className="text-sm text-muted-foreground mb-2">
You're trapped in a tunnel with a <span className="text-destructive font-semibold">deadly cliff {discrepancyBound} step{discrepancyBound > 1 ? 's' : ''} to your LEFT</span> and
a <span className="text-destructive font-semibold">pit of vipers {discrepancyBound} step{discrepancyBound > 1 ? 's' : ''} to your RIGHT</span>.
</p>
<p className="text-sm text-muted-foreground mb-2">
Write a list of instructions: each is either <span className="text-primary font-semibold">LEFT</span> or <span className="text-blue-500 font-semibold">RIGHT</span>.
</p>
<p className="text-sm text-muted-foreground">
The evil captor then picks a step size <strong>d</strong> and you follow every d-th instruction.
</p>
</div>
<div>
<h3 className="font-semibold text-foreground mb-2 flex items-center gap-2">
<AlertTriangle className="w-4 h-4" /> The Impossible Task
</h3>
<p className="text-sm text-muted-foreground mb-2">
<strong>C = 1:</strong> Maximum possible sequence length is <strong>11</strong>
</p>
<p className="text-sm text-muted-foreground mb-2">
<strong>C = 2:</strong> Maximum possible sequence length is <strong>1,160</strong>
</p>
<p className="text-sm text-muted-foreground">
Terence Tao proved in 2015: <em>No infinite sequence can survive!</em>
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Game Phase: Intro */}
{gamePhase === 'intro' && (
<Card className="border-2 border-dashed">
<CardContent className="py-12 text-center">
<div className="max-w-lg mx-auto">
<Skull className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
<h2 className="text-2xl font-bold mb-4">The Prisoner's Walk</h2>
<p className="text-muted-foreground mb-6">
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.
</p>
<div className="mb-6">
<label className="text-sm font-medium mb-2 block">
Discrepancy Bound (C): {discrepancyBound}
</label>
<div className="flex items-center gap-4 max-w-xs mx-auto">
<span className="text-xs">1</span>
<Slider
value={[discrepancyBound]}
onValueChange={(v) => setDiscrepancyBound(v[0])}
min={1}
max={3}
step={1}
className="flex-1"
/>
<span className="text-xs">3</span>
</div>
<p className="text-xs text-muted-foreground mt-2">
C=1: Max 11 steps | C=2: Max 1,160 steps | C=3: Unknown (very large)
</p>
</div>
<div className="flex gap-3 justify-center">
<Button onClick={startGame} size="lg">
<Play className="w-4 h-4 mr-2" />
Start Writing
</Button>
{discrepancyBound === 1 && (
<Button variant="outline" onClick={loadOptimalSequence} size="lg">
<Trophy className="w-4 h-4 mr-2" />
Load Optimal (11)
</Button>
)}
</div>
{bestLength && (
<p className="text-sm text-muted-foreground mt-4">
Your best: <strong>{bestLength}</strong> steps with C={discrepancyBound}
</p>
)}
</div>
</CardContent>
</Card>
)}
{/* Main Game UI */}
{gamePhase !== 'intro' && (
<div className="space-y-6">
{/* Status Bar */}
<div className="flex flex-wrap gap-3 items-center">
<Badge variant="outline" className="text-sm">
Length: {sequence.length}
</Badge>
<Badge variant="outline" className="text-sm">
Filled: {filledCount}/{sequence.length}
</Badge>
<Badge
variant={hasViolation ? "destructive" : maxDiscrepancy.value === discrepancyBound ? "secondary" : "outline"}
className="text-sm"
>
Max Discrepancy: {maxDiscrepancy.value}
{maxDiscrepancy.result && ` (d=${maxDiscrepancy.result.d}, k=${maxDiscrepancy.result.k})`}
</Badge>
<Badge variant="outline" className="text-sm">
Bound: C = {discrepancyBound}
</Badge>
{bestLength && (
<Badge variant="secondary" className="text-sm">
<Trophy className="w-3 h-3 mr-1" />
Best: {bestLength}
</Badge>
)}
</div>
{/* Violation Alert */}
{hasViolation && gamePhase === 'writing' && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Bound Exceeded!</AlertTitle>
<AlertDescription>
The subsequence with d={maxDiscrepancy.result?.d}, k={maxDiscrepancy.result?.k} has
discrepancy {maxDiscrepancy.value}, which exceeds your bound of {discrepancyBound}.
</AlertDescription>
</Alert>
)}
<Tabs defaultValue="sequence" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="sequence" className="flex items-center gap-1">
<Target className="w-4 h-4" />
<span className="hidden sm:inline">Sequence</span>
</TabsTrigger>
<TabsTrigger value="walk" className="flex items-center gap-1">
<TrendingUp className="w-4 h-4" />
<span className="hidden sm:inline">Walk</span>
</TabsTrigger>
<TabsTrigger value="heatmap" className="flex items-center gap-1">
<Grid3X3 className="w-4 h-4" />
<span className="hidden sm:inline">Heatmap</span>
</TabsTrigger>
</TabsList>
{/* Sequence Builder Tab */}
<TabsContent value="sequence" className="mt-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg flex items-center gap-2">
Write Your Instructions
{gamePhase === 'writing' && (
<Badge variant="secondary" className="ml-2">Click to toggle</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent>
{/* Sequence Grid */}
<div className="mb-4 overflow-x-auto">
<div className="flex gap-1 min-w-max pb-2">
{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 (
<button
key={idx}
onClick={() => togglePosition(idx)}
disabled={gamePhase !== 'writing'}
className={`
relative w-10 h-14 rounded-lg border-2 flex flex-col items-center justify-center
transition-all duration-200
${val === 1 ? 'bg-blue-500/20 border-blue-500 text-blue-600 dark:text-blue-400' : ''}
${val === -1 ? 'bg-rose-500/20 border-rose-500 text-rose-600 dark:text-rose-400' : ''}
${val === null ? 'bg-muted/50 border-muted-foreground/30 text-muted-foreground' : ''}
${isHighlighted ? 'ring-2 ring-amber-400 ring-offset-1' : ''}
${isViolationPart ? 'ring-2 ring-destructive ring-offset-1' : ''}
${gamePhase === 'writing' ? 'hover:scale-105 cursor-pointer' : 'cursor-default'}
`}
>
<span className="text-[10px] absolute top-0.5 left-1 opacity-50">{pos}</span>
{val === 1 && <ArrowRight className="w-5 h-5" />}
{val === -1 && <ArrowLeft className="w-5 h-5" />}
{val === null && <span className="text-lg">?</span>}
</button>
);
})}
{gamePhase === 'writing' && (
<button
onClick={addPosition}
className="w-10 h-14 rounded-lg border-2 border-dashed border-muted-foreground/30
flex items-center justify-center text-muted-foreground
hover:border-primary hover:text-primary transition-colors"
>
+
</button>
)}
</div>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-2">
{gamePhase === 'writing' && (
<>
<Button
onClick={revealCaptor}
disabled={!allFilled}
className="flex-1 sm:flex-none"
>
<Skull className="w-4 h-4 mr-2" />
Face the Captor
</Button>
<Button variant="outline" onClick={resetGame}>
<RotateCcw className="w-4 h-4 mr-2" />
Reset
</Button>
</>
)}
</div>
</CardContent>
</Card>
</TabsContent>
{/* Walk Visualization Tab */}
<TabsContent value="walk" className="mt-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Walk Visualization</CardTitle>
</CardHeader>
<CardContent>
{/* Step size selector */}
<div className="mb-4 flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2">
<label className="text-sm font-medium">Step size (d):</label>
<div className="flex gap-1">
{Array.from({ length: Math.min(sequence.length, 8) }, (_, i) => i + 1).map(d => (
<Button
key={d}
variant={selectedD === d ? "default" : "outline"}
size="sm"
onClick={() => {
setSelectedD(d);
setWalkPosition(0);
setWalkStep(0);
setIsWalking(false);
}}
className="w-8 h-8 p-0"
>
{d}
</Button>
))}
</div>
</div>
<div className="flex items-center gap-2">
<label className="text-sm">Speed:</label>
<Slider
value={[1000 - walkSpeed]}
onValueChange={(v) => setWalkSpeed(1000 - v[0])}
min={0}
max={900}
step={100}
className="w-24"
/>
</div>
</div>
{/* Number line visualization */}
<div className="relative h-32 bg-muted/20 rounded-lg overflow-hidden mb-4">
{/* Danger zones */}
<div
className="absolute top-0 bottom-0 bg-destructive/20 flex items-center justify-center"
style={{ left: 0, width: `${(discrepancyBound / (discrepancyBound * 2 + 3)) * 100}%` }}
>
<span className="text-destructive text-xs font-bold rotate-90">CLIFF</span>
</div>
<div
className="absolute top-0 bottom-0 bg-destructive/20 flex items-center justify-center"
style={{ right: 0, width: `${(discrepancyBound / (discrepancyBound * 2 + 3)) * 100}%` }}
>
<span className="text-destructive text-xs font-bold rotate-90">VIPERS</span>
</div>
{/* Safe zone markers */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center">
{Array.from({ length: discrepancyBound * 2 + 1 }, (_, i) => i - discrepancyBound).map(pos => (
<div
key={pos}
className={`
w-8 h-8 mx-1 rounded-full flex items-center justify-center text-xs font-bold
${pos === 0 ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}
${Math.abs(pos) === discrepancyBound ? 'border-2 border-amber-500' : ''}
`}
>
{pos}
</div>
))}
</div>
{/* Walker position */}
<div
className="absolute top-1/2 -translate-y-1/2 transition-all duration-300"
style={{
left: `calc(50% + ${walkPosition * 40}px - 12px)`,
}}
>
<div className="w-6 h-6 bg-amber-500 rounded-full flex items-center justify-center text-white text-xs animate-pulse">
👤
</div>
</div>
</div>
{/* Walk info */}
<div className="flex items-center justify-between mb-4 text-sm">
<span>Position: <strong>{walkPosition}</strong></span>
<span>Steps taken: <strong>{walkStep}</strong></span>
<span>Following: positions {selectedPositions.slice(0, 5).join(', ')}{selectedPositions.length > 5 ? '...' : ''}</span>
</div>
{/* Walk controls */}
<div className="flex gap-2">
<Button onClick={walkSubsequence} variant="outline" className="flex-1">
{isWalking ? <Pause className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
{isWalking ? 'Pause' : 'Walk d=' + selectedD}
</Button>
<Button
variant="outline"
onClick={() => {
setWalkPosition(0);
setWalkStep(0);
setIsWalking(false);
}}
>
<RotateCcw className="w-4 h-4" />
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Heatmap Tab */}
<TabsContent value="walk" className="mt-4">
{/* This is actually handled in the walk tab above */}
</TabsContent>
<TabsContent value="heatmap" className="mt-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Discrepancy Heatmap</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Each cell shows the sum of values at positions d, 2d, 3d, ..., kd.
Red cells exceed the bound.
</p>
<div className="overflow-x-auto">
<table className="text-xs">
<thead>
<tr>
<th className="p-1 text-muted-foreground">d \ k</th>
{Array.from({ length: Math.min(12, sequence.length) }, (_, i) => i + 1).map(k => (
<th key={k} className="p-1 w-8 text-center text-muted-foreground">{k}</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: Math.min(8, sequence.length) }, (_, i) => i + 1).map(d => (
<tr key={d}>
<td className="p-1 font-medium text-muted-foreground">{d}</td>
{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 (
<td
key={k}
className={`
p-1 w-8 h-8 text-center cursor-pointer transition-all
${isValid ? getHeatmapColor(discrepancy) : 'bg-transparent'}
${hoveredCell?.d === d && hoveredCell?.k === k ? 'ring-2 ring-primary' : ''}
`}
onMouseEnter={() => {
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 : ''}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
{hoveredCell && (
<div className="mt-4 p-3 bg-muted/50 rounded-lg text-sm">
<strong>d={hoveredCell.d}, k={hoveredCell.k}:</strong> Looking at positions{' '}
{Array.from({ length: hoveredCell.k }, (_, i) => (i + 1) * hoveredCell.d).join(', ')}
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Captor Reveal Phase */}
{gamePhase === 'captor-reveal' && captorChosenD !== null && (
<Card className="border-destructive/50 bg-destructive/5">
<CardContent className="py-6 text-center">
<Skull className="w-12 h-12 mx-auto text-destructive mb-4" />
<h3 className="text-xl font-bold mb-2">The Captor Speaks...</h3>
<p className="text-muted-foreground mb-4">
"Interesting sequence... Let me see... I choose <strong className="text-destructive text-2xl">d = {captorChosenD}</strong>!"
</p>
<p className="text-sm text-muted-foreground mb-4">
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(', ')}...
</p>
<Button onClick={startPrisonerWalk} size="lg" variant="destructive">
<Play className="w-4 h-4 mr-2" />
Begin Walking
</Button>
</CardContent>
</Card>
)}
{/* Walking Phase */}
{gamePhase === 'walking' && captorChosenD !== null && (
<Card>
<CardContent className="py-6">
<div className="text-center mb-4">
<h3 className="text-lg font-bold">Walking with d = {captorChosenD}...</h3>
<p className="text-sm text-muted-foreground">
Step {prisonerStep} | Position: {prisonerPosition}
</p>
</div>
{/* Tunnel visualization */}
<div className="relative h-24 bg-gradient-to-r from-destructive/30 via-muted/20 to-destructive/30 rounded-lg overflow-hidden">
<div className="absolute inset-y-0 left-0 w-8 flex items-center justify-center">
<span className="text-2xl">🏔</span>
</div>
<div className="absolute inset-y-0 right-0 w-8 flex items-center justify-center">
<span className="text-2xl">🐍</span>
</div>
<div
className="absolute top-1/2 -translate-y-1/2 text-3xl transition-all duration-300"
style={{
left: `calc(50% + ${prisonerPosition * (100 / (discrepancyBound * 2 + 2))}% - 16px)`
}}
>
🚶
</div>
</div>
</CardContent>
</Card>
)}
{/* Fallen Phase */}
{gamePhase === 'fallen' && (
<Alert variant="destructive">
<Skull className="h-4 w-4" />
<AlertTitle>You Fell!</AlertTitle>
<AlertDescription>
After {prisonerStep} steps with d={captorChosenD}, you ended up at position {prisonerPosition}.
The captor found your weakness! Your sequence lasted {sequence.length} instructions.
<div className="mt-4 flex gap-2">
<Button onClick={startGame} variant="outline" size="sm">
Try Again
</Button>
<Button onClick={resetGame} variant="outline" size="sm">
<RotateCcw className="w-4 h-4 mr-2" />
Reset
</Button>
</div>
</AlertDescription>
</Alert>
)}
{/* Survived Phase (temporary - sequence ended) */}
{gamePhase === 'survived' && (
<Alert className="border-emerald-500 bg-emerald-500/10">
<Trophy className="h-4 w-4 text-emerald-500" />
<AlertTitle className="text-emerald-600 dark:text-emerald-400">Sequence Complete!</AlertTitle>
<AlertDescription>
Your {sequence.length}-instruction sequence survived d={captorChosenD}!
But remember: no sequence can survive forever. Try making it longer!
<div className="mt-4 flex gap-2">
<Button onClick={() => setGamePhase('writing')} variant="outline" size="sm">
Extend Sequence
</Button>
<Button onClick={resetGame} variant="outline" size="sm">
<RotateCcw className="w-4 h-4 mr-2" />
New Game
</Button>
</div>
</AlertDescription>
</Alert>
)}
</div>
)}
{/* Acknowledgement */}
<Card className="bg-amber-50 dark:bg-amber-950/20 border-amber-200 dark:border-amber-800">
<CardContent className="py-4">
<h3 className="font-semibold text-foreground mb-2">Credits & History</h3>
<p className="text-sm text-muted-foreground mb-2">
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.
</p>
<p className="text-sm text-muted-foreground">
<strong>2014:</strong> Boris Konev & Alexei Lisitsa proved C=2 has maximum length 1,160 using SAT solvers
(generating a 13GB proof!). <strong>2015:</strong> Terence Tao proved that NO infinite sequence
can have bounded discrepancy, settling the problem completely.
</p>
</CardContent>
</Card>
{/* Social Share */}
{showSocialShare !== false && (
<SocialShare
title="Erdős Discrepancy Problem"
description="Can you survive the prisoner's tunnel? Explore why no sequence of instructions can keep you safe forever!"
url={typeof window !== 'undefined' ? `${window.location.origin}/puzzles/erdos-discrepancy` : ''}
/>
)}
</div>
);
};
export default ErdosDiscrepancyPuzzle;

View file

@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Shield, Target, RotateCcw, Info } from "lucide-react"; import { Shield, Target, RotateCcw, Info } from "lucide-react";
import SocialShare from '@/components/SocialShare';
// Extended good rectangle: 10 columns × 12 rows (following Fig. 10) // Extended good rectangle: 10 columns × 12 rows (following Fig. 10)
// Border: columns 0 and 9, rows 0/1 (top) and 10/11 (bottom) // Border: columns 0 and 9, rows 0/1 (top) and 10/11 (bottom)
@ -349,7 +350,11 @@ const defendAttack = (
}; };
const EternalDominationGame: React.FC = () => { interface EternalDominationGameProps {
showSocialShare?: boolean;
}
const EternalDominationGame: React.FC<EternalDominationGameProps> = ({ showSocialShare = true }) => {
const [guards, setGuards] = useState<Set<string>>(generateInitialGuards); const [guards, setGuards] = useState<Set<string>>(generateInitialGuards);
const [attackHistory, setAttackHistory] = useState<Position[]>([]); const [attackHistory, setAttackHistory] = useState<Position[]>([]);
const [message, setMessage] = useState<string>("Click any unguarded cell to attack."); const [message, setMessage] = useState<string>("Click any unguarded cell to attack.");
@ -679,6 +684,13 @@ const EternalDominationGame: React.FC = () => {
)} )}
</CardContent> </CardContent>
</Card> </Card>
{showSocialShare && (
<SocialShare
title="Eternal Domination on a Grid"
description="Explore the m-eternal domination problem - can you attack in a way that breaks the defense?"
/>
)}
</div> </div>
); );
}; };

View file

@ -4,8 +4,13 @@ import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { toast } from "sonner"; import { toast } from "sonner";
import SocialShare from '@/components/SocialShare';
const FerrersRogersRamanujan = () => { interface FerrersRogersRamanujanProps {
showSocialShare?: boolean;
}
const FerrersRogersRamanujan = ({ showSocialShare = true }: FerrersRogersRamanujanProps) => {
const [n, setN] = useState(14); const [n, setN] = useState(14);
const [partitionSliders, setPartitionSliders] = useState<number[]>([]); const [partitionSliders, setPartitionSliders] = useState<number[]>([]);
const [committedPartition, setCommittedPartition] = useState<number[]>([]); const [committedPartition, setCommittedPartition] = useState<number[]>([]);
@ -330,6 +335,13 @@ const FerrersRogersRamanujan = () => {
The hook sizes form a new partition where parts differ by at least 2. The hook sizes form a new partition where parts differ by at least 2.
</p> </p>
</div> </div>
{showSocialShare && (
<SocialShare
title="Ferrers Diagram & Rogers-Ramanujan Partition"
description="Explore integer partitions through visual Ferrers diagrams and discover the elegant Rogers-Ramanujan transformation."
/>
)}
</div> </div>
</Card> </Card>
); );

View file

@ -18,7 +18,11 @@ interface GameState {
moveHistory: string[]; moveHistory: string[];
} }
const GoldCoinGame: React.FC = () => { interface GoldCoinGameProps {
showSocialShare?: boolean;
}
const GoldCoinGame: React.FC<GoldCoinGameProps> = ({ showSocialShare = true }) => {
const [gameState, setGameState] = useState<GameState>({ const [gameState, setGameState] = useState<GameState>({
track: Array(15).fill('empty'), track: Array(15).fill('empty'),
currentPlayer: 0, currentPlayer: 0,
@ -421,11 +425,12 @@ const GoldCoinGame: React.FC = () => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{showSocialShare && (
<SocialShare <SocialShare
title="The Gold Coin Game" title="The Gold Coin Game"
description="A strategic two-player game where you move coins to acquire the gold coin!" description="A strategic two-player game where you move coins to acquire the gold coin!"
url={window.location.href}
/> />
)}
</div> </div>
); );
}; };

View file

@ -67,7 +67,7 @@ const InteractiveGallery = () => {
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<h1 className="text-3xl font-bold text-foreground">Data Structures and Algorithms</h1> <h1 className="text-3xl font-bold text-foreground">Data Structures and Algorithms</h1>
<p className="text-muted-foreground text-lg">Interactive explorations of elementary data structures and algorithms.</p> <p className="text-muted-foreground text-lg">Interactive explorations of elementary data structures and algorithms.</p>
</div> </div>
{/* Search bar */} {/* Search bar */}

View file

@ -18,7 +18,7 @@ interface Interactive {
} }
// This would ideally be generated from the actual components, but for now we'll hardcode // This would ideally be generated from the actual components, but for now we'll hardcode
const allInteractives: Interactive[] = [ export const allInteractives: Interactive[] = [
{ {
id: 'binary-number-game', id: 'binary-number-game',
title: 'Binary Number Representation', title: 'Binary Number Representation',
@ -288,6 +288,15 @@ const allInteractives: Interactive[] = [
path: '/puzzles/ladybug-clock', path: '/puzzles/ladybug-clock',
theme: 'Puzzles', theme: 'Puzzles',
dateAdded: '2025-01-21' 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'
} }
]; ];
@ -314,7 +323,7 @@ const InteractiveIndex = () => {
// Filter and sort interactives // Filter and sort interactives
const filteredAndSortedInteractives = useMemo(() => { const filteredAndSortedInteractives = useMemo(() => {
let filtered = allInteractives.filter(interactive => { const filtered = allInteractives.filter(interactive => {
const matchesSearch = interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) || const matchesSearch = interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) || interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())); interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));

View file

@ -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 { Play, Pause, RotateCcw, SkipForward, FastForward, Info, ExternalLink, BarChart3 } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Cell } from 'recharts'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Cell } from 'recharts';
import SocialShare from '@/components/SocialShare'; import SocialShare from '@/components/SocialShare';
interface LadybugClockPuzzleProps { interface LadybugClockPuzzleProps {
showSocialShare?: boolean; showSocialShare?: boolean;
} }
interface SimulationState { interface SimulationState {
position: number; // 0-11 (0 = 12 o'clock, 1 = 1 o'clock, etc.) position: number; // 0-11 (0 = 12 o'clock, 1 = 1 o'clock, etc.)
visited: Set<number>; visited: Set<number>;
@ -21,10 +19,10 @@ interface SimulationState {
isComplete: boolean; isComplete: boolean;
moveCount: number; moveCount: number;
} }
const CLOCK_NUMBERS = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; const CLOCK_NUMBERS = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({
const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare = true }) => { showSocialShare = true
}) => {
// Simulation state // Simulation state
const [simulation, setSimulation] = useState<SimulationState>({ const [simulation, setSimulation] = useState<SimulationState>({
position: 0, position: 0,
@ -53,14 +51,13 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
// Refs // Refs
const animationRef = useRef<number | null>(null); const animationRef = useRef<number | null>(null);
const isPlayingRef = useRef(isPlaying); const isPlayingRef = useRef(isPlaying);
useEffect(() => { useEffect(() => {
isPlayingRef.current = isPlaying; isPlayingRef.current = isPlaying;
}, [isPlaying]); }, [isPlaying]);
// Get delay from speed (inverse relationship) // Get delay from speed (inverse relationship)
const getDelay = useCallback(() => { const getDelay = useCallback(() => {
return Math.max(50, 1000 - (speed * 9)); return Math.max(50, 1000 - speed * 9);
}, [speed]); }, [speed]);
// Reset simulation // Reset simulation
@ -87,14 +84,11 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
// Random move: clockwise or counterclockwise // Random move: clockwise or counterclockwise
const direction = Math.random() < 0.5 ? 1 : -1; const direction = Math.random() < 0.5 ? 1 : -1;
const newPosition = ((prev.position + direction) % numPositions + numPositions) % numPositions; const newPosition = ((prev.position + direction) % numPositions + numPositions) % numPositions;
const newVisited = new Set(prev.visited); const newVisited = new Set(prev.visited);
const wasNewlyPainted = !newVisited.has(newPosition); const wasNewlyPainted = !newVisited.has(newPosition);
newVisited.add(newPosition); newVisited.add(newPosition);
const isNowComplete = newVisited.size === numPositions; const isNowComplete = newVisited.size === numPositions;
const lastPainted = isNowComplete && wasNewlyPainted ? newPosition : prev.lastPainted; const lastPainted = isNowComplete && wasNewlyPainted ? newPosition : prev.lastPainted;
return { return {
position: newPosition, position: newPosition,
visited: newVisited, visited: newVisited,
@ -111,13 +105,11 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
if (!isPlaying || simulation.isComplete) { if (!isPlaying || simulation.isComplete) {
return; return;
} }
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
if (isPlayingRef.current && !simulation.isComplete) { if (isPlayingRef.current && !simulation.isComplete) {
step(); step();
} }
}, getDelay()); }, getDelay());
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, [isPlaying, simulation, step, getDelay]); }, [isPlaying, simulation, step, getDelay]);
@ -134,44 +126,40 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
const visited = new Set([0]); const visited = new Set([0]);
let moveCount = 0; let moveCount = 0;
let lastPainted = 0; let lastPainted = 0;
while (visited.size < numPositions) { while (visited.size < numPositions) {
const direction = Math.random() < 0.5 ? 1 : -1; const direction = Math.random() < 0.5 ? 1 : -1;
position = ((position + direction) % numPositions + numPositions) % numPositions; position = ((position + direction) % numPositions + numPositions) % numPositions;
if (!visited.has(position)) { if (!visited.has(position)) {
lastPainted = position; lastPainted = position;
visited.add(position); visited.add(position);
} }
moveCount++; moveCount++;
} }
return {
return { lastPainted, moveCount }; lastPainted,
moveCount
};
}, [numPositions]); }, [numPositions]);
// Run to completion // Run to completion
const runToCompletion = useCallback(() => { const runToCompletion = useCallback(() => {
setIsPlaying(false); setIsPlaying(false);
const currentSim = simulation;
let currentSim = simulation;
let position = currentSim.position; let position = currentSim.position;
const visited = new Set(currentSim.visited); const visited = new Set(currentSim.visited);
let moveCount = currentSim.moveCount; let moveCount = currentSim.moveCount;
let lastPainted = currentSim.lastPainted; let lastPainted = currentSim.lastPainted;
const path = [...currentSim.path]; const path = [...currentSim.path];
while (visited.size < numPositions) { while (visited.size < numPositions) {
const direction = Math.random() < 0.5 ? 1 : -1; const direction = Math.random() < 0.5 ? 1 : -1;
position = ((position + direction) % numPositions + numPositions) % numPositions; position = ((position + direction) % numPositions + numPositions) % numPositions;
path.push(position); path.push(position);
if (!visited.has(position)) { if (!visited.has(position)) {
lastPainted = position; lastPainted = position;
visited.add(position); visited.add(position);
} }
moveCount++; moveCount++;
} }
setSimulation({ setSimulation({
position, position,
visited, visited,
@ -184,15 +172,15 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
// Run batch simulations // Run batch simulations
const runBatchSimulations = useCallback((count: number) => { const runBatchSimulations = useCallback((count: number) => {
const newCounts = { ...lastPaintedCounts }; const newCounts = {
...lastPaintedCounts
};
let batchMoves = 0; let batchMoves = 0;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const result = runInstantSimulation(); const result = runInstantSimulation();
newCounts[result.lastPainted] = (newCounts[result.lastPainted] || 0) + 1; newCounts[result.lastPainted] = (newCounts[result.lastPainted] || 0) + 1;
batchMoves += result.moveCount; batchMoves += result.moveCount;
} }
setLastPaintedCounts(newCounts); setLastPaintedCounts(newCounts);
setTotalSimulations(prev => prev + count); setTotalSimulations(prev => prev + count);
setTotalMoves(prev => prev + batchMoves); setTotalMoves(prev => prev + batchMoves);
@ -225,7 +213,7 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
// Calculate clock position for a given index // Calculate clock position for a given index
const getClockPosition = (index: number, radius: number) => { 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 { return {
x: Math.cos(angle) * radius, x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius y: Math.sin(angle) * radius
@ -233,10 +221,12 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
}; };
// Prepare chart data // 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 pos = i + 1; // 1 to n-1 (0/12 can't be last)
const count = lastPaintedCounts[pos] || 0; const count = lastPaintedCounts[pos] || 0;
const percentage = totalSimulations > 0 ? (count / totalSimulations) * 100 : 0; const percentage = totalSimulations > 0 ? count / totalSimulations * 100 : 0;
return { return {
position: positionToNumber(pos), position: positionToNumber(pos),
count, count,
@ -244,15 +234,9 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
label: `${positionToNumber(pos)}` label: `${positionToNumber(pos)}`
}; };
}); });
const theoreticalProbability = 100 / (numPositions - 1); const theoreticalProbability = 100 / (numPositions - 1);
const shareUrl = typeof window !== 'undefined' ? `${window.location.origin}/puzzles/ladybug-clock` : '';
const shareUrl = typeof window !== 'undefined' return <div className="w-full max-w-6xl mx-auto px-4 space-y-6">
? `${window.location.origin}/puzzles/ladybug-clock`
: '';
return (
<div className="w-full max-w-6xl mx-auto px-4 space-y-6">
{/* Header */} {/* Header */}
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<h1 className="text-3xl font-bold text-foreground">The Ladybug Clock Puzzle</h1> <h1 className="text-3xl font-bold text-foreground">The Ladybug Clock Puzzle</h1>
@ -272,71 +256,34 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
<circle cx="0" cy="0" r="120" fill="none" stroke="#3d3d5c" strokeWidth="1" strokeDasharray="4 4" /> <circle cx="0" cy="0" r="120" fill="none" stroke="#3d3d5c" strokeWidth="1" strokeDasharray="4 4" />
{/* Hour markers and numbers */} {/* Hour markers and numbers */}
{Array.from({ length: numPositions }, (_, i) => { {Array.from({
length: numPositions
}, (_, i) => {
const pos = getClockPosition(i, 100); const pos = getClockPosition(i, 100);
const isPainted = simulation.visited.has(i); const isPainted = simulation.visited.has(i);
const isLast = simulation.isComplete && simulation.lastPainted === i; const isLast = simulation.isComplete && simulation.lastPainted === i;
const isCurrent = simulation.position === i; const isCurrent = simulation.position === i;
return <g key={i}>
return (
<g key={i}>
{/* Tick mark */} {/* Tick mark */}
<line <line x1={getClockPosition(i, 115).x} y1={getClockPosition(i, 115).y} x2={getClockPosition(i, 125).x} y2={getClockPosition(i, 125).y} stroke={isPainted ? '#f59e0b' : '#6b7280'} strokeWidth="2" />
x1={getClockPosition(i, 115).x}
y1={getClockPosition(i, 115).y}
x2={getClockPosition(i, 125).x}
y2={getClockPosition(i, 125).y}
stroke={isPainted ? '#f59e0b' : '#6b7280'}
strokeWidth="2"
/>
{/* Number circle background */} {/* Number circle background */}
<circle <circle cx={pos.x} cy={pos.y} r="20" fill={isLast ? '#ef4444' : isPainted ? '#f59e0b' : '#374151'} className="transition-all duration-300" />
cx={pos.x}
cy={pos.y}
r="20"
fill={isLast ? '#ef4444' : isPainted ? '#f59e0b' : '#374151'}
className="transition-all duration-300"
/>
{/* Glow effect for newly painted or last */} {/* Glow effect for newly painted or last */}
{(isPainted || isLast) && ( {(isPainted || isLast) && <circle cx={pos.x} cy={pos.y} r="24" fill="none" stroke={isLast ? '#ef4444' : '#f59e0b'} strokeWidth="2" opacity="0.5" />}
<circle
cx={pos.x}
cy={pos.y}
r="24"
fill="none"
stroke={isLast ? '#ef4444' : '#f59e0b'}
strokeWidth="2"
opacity="0.5"
/>
)}
{/* Number text */} {/* Number text */}
<text <text x={pos.x} y={pos.y} textAnchor="middle" dominantBaseline="central" fill={isPainted ? '#1a1a2e' : '#d1d5db'} fontSize="14" fontWeight="bold" className="select-none">
x={pos.x}
y={pos.y}
textAnchor="middle"
dominantBaseline="central"
fill={isPainted ? '#1a1a2e' : '#d1d5db'}
fontSize="14"
fontWeight="bold"
className="select-none"
>
{positionToNumber(i)} {positionToNumber(i)}
</text> </text>
</g> </g>;
);
})} })}
{/* Ladybug */} {/* Ladybug */}
{(() => { {(() => {
const ladybugPos = getClockPosition(simulation.position, 100); const ladybugPos = getClockPosition(simulation.position, 100);
return ( return <g transform={`translate(${ladybugPos.x}, ${ladybugPos.y})`} className="transition-transform duration-200">
<g
transform={`translate(${ladybugPos.x}, ${ladybugPos.y})`}
className="transition-transform duration-200"
>
{/* Ladybug body */} {/* Ladybug body */}
<ellipse cx="0" cy="-30" rx="12" ry="15" fill="#dc2626" /> <ellipse cx="0" cy="-30" rx="12" ry="15" fill="#dc2626" />
{/* Head */} {/* Head */}
@ -351,8 +298,7 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
{/* Antennae */} {/* Antennae */}
<line x1="-3" y1="-52" x2="-6" y2="-58" stroke="#1f2937" strokeWidth="1.5" /> <line x1="-3" y1="-52" x2="-6" y2="-58" stroke="#1f2937" strokeWidth="1.5" />
<line x1="3" y1="-52" x2="6" y2="-58" stroke="#1f2937" strokeWidth="1.5" /> <line x1="3" y1="-52" x2="6" y2="-58" stroke="#1f2937" strokeWidth="1.5" />
</g> </g>;
);
})()} })()}
{/* Center decoration */} {/* Center decoration */}
@ -374,13 +320,11 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
</Badge> </Badge>
</div> </div>
{simulation.isComplete && simulation.lastPainted !== null && ( {simulation.isComplete && simulation.lastPainted !== null && <div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg">
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg">
<p className="text-red-400 font-semibold"> <p className="text-red-400 font-semibold">
🎉 Complete! Last painted: <span className="text-red-300 text-lg">{positionToNumber(simulation.lastPainted)}</span> 🎉 Complete! Last painted: <span className="text-red-300 text-lg">{positionToNumber(simulation.lastPainted)}</span>
</p> </p>
</div> </div>}
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -394,32 +338,17 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button <Button onClick={() => setIsPlaying(!isPlaying)} disabled={simulation.isComplete} variant={isPlaying ? "destructive" : "default"} size="sm">
onClick={() => setIsPlaying(!isPlaying)}
disabled={simulation.isComplete}
variant={isPlaying ? "destructive" : "default"}
size="sm"
>
{isPlaying ? <Pause className="w-4 h-4 mr-1" /> : <Play className="w-4 h-4 mr-1" />} {isPlaying ? <Pause className="w-4 h-4 mr-1" /> : <Play className="w-4 h-4 mr-1" />}
{isPlaying ? 'Pause' : 'Play'} {isPlaying ? 'Pause' : 'Play'}
</Button> </Button>
<Button <Button onClick={step} disabled={simulation.isComplete || isPlaying} variant="outline" size="sm">
onClick={step}
disabled={simulation.isComplete || isPlaying}
variant="outline"
size="sm"
>
<SkipForward className="w-4 h-4 mr-1" /> <SkipForward className="w-4 h-4 mr-1" />
Step Step
</Button> </Button>
<Button <Button onClick={runToCompletion} disabled={simulation.isComplete} variant="outline" size="sm">
onClick={runToCompletion}
disabled={simulation.isComplete}
variant="outline"
size="sm"
>
<FastForward className="w-4 h-4 mr-1" /> <FastForward className="w-4 h-4 mr-1" />
Complete Complete
</Button> </Button>
@ -432,26 +361,12 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm text-muted-foreground">Animation Speed</label> <label className="text-sm text-muted-foreground">Animation Speed</label>
<Slider <Slider value={[speed]} onValueChange={v => setSpeed(v[0])} min={1} max={100} step={1} className="w-full" />
value={[speed]}
onValueChange={(v) => setSpeed(v[0])}
min={1}
max={100}
step={1}
className="w-full"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm text-muted-foreground">Number of Positions: {numPositions}</label> <label className="text-sm text-muted-foreground">Number of Positions: {numPositions}</label>
<Slider <Slider value={[numPositions]} onValueChange={v => setNumPositions(v[0])} min={4} max={24} step={2} className="w-full" />
value={[numPositions]}
onValueChange={(v) => setNumPositions(v[0])}
min={4}
max={24}
step={2}
className="w-full"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -466,16 +381,9 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{[100, 1000, 10000].map(count => ( {[100, 1000, 10000].map(count => <Button key={count} onClick={() => runBatchSimulations(count)} variant="outline" size="sm">
<Button
key={count}
onClick={() => runBatchSimulations(count)}
variant="outline"
size="sm"
>
Run {count.toLocaleString()} Run {count.toLocaleString()}
</Button> </Button>)}
))}
<Button onClick={clearStats} variant="ghost" size="sm"> <Button onClick={clearStats} variant="ghost" size="sm">
Clear Stats Clear Stats
</Button> </Button>
@ -485,18 +393,15 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Total simulations: <span className="text-foreground font-medium">{totalSimulations.toLocaleString()}</span> Total simulations: <span className="text-foreground font-medium">{totalSimulations.toLocaleString()}</span>
</p> </p>
{totalSimulations > 0 && ( {totalSimulations > 0 && <p className="text-muted-foreground">
<p className="text-muted-foreground">
Average moves to complete: <span className="text-foreground font-medium">{averageMoves.toFixed(1)}</span> Average moves to complete: <span className="text-foreground font-medium">{averageMoves.toFixed(1)}</span>
</p> </p>}
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* User Prediction */} {/* User Prediction */}
{totalSimulations === 0 && ( {totalSimulations === 0 && <Card className="border-primary/50 bg-primary/5">
<Card className="border-primary/50 bg-primary/5">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-lg">Make a Prediction! 🤔</CardTitle> <CardTitle className="text-lg">Make a Prediction! 🤔</CardTitle>
</CardHeader> </CardHeader>
@ -504,33 +409,23 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
<p className="text-sm text-muted-foreground mb-3"> <p className="text-sm text-muted-foreground mb-3">
Which number do you think is most likely to be painted last? Which number do you think is most likely to be painted last?
</p> </p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-1.5">
{Array.from({ length: numPositions - 1 }, (_, i) => i + 1).map(pos => ( {Array.from({
<Button length: numPositions - 1
key={pos} }, (_, i) => i + 1).map(pos => <Button key={pos} onClick={() => setUserPrediction(pos)} variant={userPrediction === pos ? "default" : "outline"} size="sm" className="w-8 h-8 text-xs p-0">
onClick={() => setUserPrediction(pos)}
variant={userPrediction === pos ? "default" : "outline"}
size="sm"
className="w-10 h-10"
>
{positionToNumber(pos)} {positionToNumber(pos)}
</Button> </Button>)}
))}
</div> </div>
{userPrediction !== null && ( {userPrediction !== null && <p className="mt-3 text-sm text-primary">
<p className="mt-3 text-sm text-primary">
You predicted: <strong>{positionToNumber(userPrediction)}</strong>. Run some simulations to see! You predicted: <strong>{positionToNumber(userPrediction)}</strong>. Run some simulations to see!
</p> </p>}
)}
</CardContent> </CardContent>
</Card> </Card>}
)}
</div> </div>
</div> </div>
{/* Statistics Chart */} {/* Statistics Chart */}
{totalSimulations > 0 && ( {totalSimulations > 0 && <Card>
<Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
Statistics: Which number was painted last? Statistics: Which number was painted last?
@ -540,52 +435,42 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
<CardContent> <CardContent>
<div className="h-64"> <div className="h-64">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}> <BarChart data={chartData} margin={{
top: 20,
right: 30,
left: 20,
bottom: 5
}}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" /> <CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="label" stroke="#9ca3af" /> <XAxis dataKey="label" stroke="#9ca3af" />
<YAxis <YAxis stroke="#9ca3af" tickFormatter={v => `${v.toFixed(1)}%`} domain={[0, Math.max(theoreticalProbability * 1.5, 15)]} />
stroke="#9ca3af" <Tooltip contentStyle={{
tickFormatter={(v) => `${v.toFixed(1)}%`} backgroundColor: '#1f2937',
domain={[0, Math.max(theoreticalProbability * 1.5, 15)]} border: '1px solid #374151'
/> }} labelStyle={{
<Tooltip color: '#f3f4f6'
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }} }} formatter={(value: number) => [`${value.toFixed(2)}%`, 'Probability']} />
labelStyle={{ color: '#f3f4f6' }} <ReferenceLine y={theoreticalProbability} stroke="#22c55e" strokeDasharray="5 5" label={{
formatter={(value: number) => [`${value.toFixed(2)}%`, 'Probability']}
/>
<ReferenceLine
y={theoreticalProbability}
stroke="#22c55e"
strokeDasharray="5 5"
label={{
value: `Theory: ${theoreticalProbability.toFixed(2)}%`, value: `Theory: ${theoreticalProbability.toFixed(2)}%`,
fill: '#22c55e', fill: '#22c55e',
fontSize: 12, fontSize: 12,
position: 'right' position: 'right'
}} }} />
/>
<Bar dataKey="percentage" radius={[4, 4, 0, 0]}> <Bar dataKey="percentage" radius={[4, 4, 0, 0]}>
{chartData.map((entry, index) => ( {chartData.map((entry, index) => <Cell key={`cell-${index}`} fill={userPrediction === index + 1 ? '#8b5cf6' : '#f59e0b'} />)}
<Cell
key={`cell-${index}`}
fill={userPrediction === index + 1 ? '#8b5cf6' : '#f59e0b'}
/>
))}
</Bar> </Bar>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
{userPrediction !== null && ( {userPrediction !== null && <div className="mt-4 p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
<div className="mt-4 p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
<p className="text-sm"> <p className="text-sm">
<strong className="text-purple-400">Your prediction ({positionToNumber(userPrediction)}):</strong>{' '} <strong className="text-purple-400">Your prediction ({positionToNumber(userPrediction)}):</strong>{' '}
{((lastPaintedCounts[userPrediction] || 0) / totalSimulations * 100).toFixed(2)}% {((lastPaintedCounts[userPrediction] || 0) / totalSimulations * 100).toFixed(2)}%
{' '} Theory predicts all numbers have equal probability of{' '} {' '} Theory predicts all numbers have equal probability of{' '}
<span className="text-green-400">{theoreticalProbability.toFixed(2)}%</span>! <span className="text-green-400">{theoreticalProbability.toFixed(2)}%</span>!
</p> </p>
</div> </div>}
)}
<div className="mt-4 p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg"> <div className="mt-4 p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg">
<p className="text-sm text-amber-200"> <p className="text-sm text-amber-200">
@ -594,34 +479,21 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
</p> </p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>}
)}
{/* Path History */} {/* Path History */}
{simulation.path.length > 1 && ( {simulation.path.length > 1 && <Card>
<Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-lg">Path History</CardTitle> <CardTitle className="text-lg">Path History</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto"> <div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
{simulation.path.map((pos, i) => ( {simulation.path.map((pos, i) => <Badge key={i} variant={i === 0 ? "default" : "outline"} className={`${simulation.isComplete && i === simulation.path.length - 1 ? 'bg-red-500 text-white' : ''}`}>
<Badge
key={i}
variant={i === 0 ? "default" : "outline"}
className={`${
simulation.isComplete && i === simulation.path.length - 1
? 'bg-red-500 text-white'
: ''
}`}
>
{positionToNumber(pos)} {positionToNumber(pos)}
</Badge> </Badge>)}
))}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>}
)}
{/* Educational Info */} {/* Educational Info */}
<Card> <Card>
@ -641,19 +513,14 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
<li>The simulation ends when all numbers have been visited at least once</li> <li>The simulation ends when all numbers have been visited at least once</li>
</ul> </ul>
<h4 className="text-foreground">The Surprising Result</h4>
<p className="text-muted-foreground">
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 <strong>1/{numPositions - 1}</strong> 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.
</p>
<h4 className="text-foreground">Mathematical Connections</h4>
<ul className="text-muted-foreground"> <ul className="text-muted-foreground">
<li><strong>Random Walks on Cycles:</strong> This is an example of a random walk on a cycle graph</li>
<li><strong>Cover Time:</strong> The number of moves to visit all vertices is called the "cover time"</li>
<li><strong>Gambler's Ruin:</strong> Related to the classic probability problem of a gambler with a finite amount</li>
</ul> </ul>
</div> </div>
</CardContent> </CardContent>
@ -670,21 +537,12 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({ showSocialShare
{' '}and demonstrated in{' '} {' '}and demonstrated in{' '}
<a href="https://www.youtube.com/shorts/t3jZ2xGOvYg" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline font-medium"> <a href="https://www.youtube.com/shorts/t3jZ2xGOvYg" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline font-medium">
this YouTube short this YouTube short
</a> </a>.
. It illustrates a beautiful result about random walks on cycle graphs: every position (except the start) has equal probability of being visited last.
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
{showSocialShare && ( {showSocialShare && <SocialShare title="The Ladybug Clock Puzzle" description="Explore random walks on a clock face and discover why every number has the same probability of being painted last!" url={shareUrl} />}
<SocialShare </div>;
title="The Ladybug Clock Puzzle"
description="Explore random walks on a clock face and discover why every number has the same probability of being painted last!"
url={shareUrl}
/>
)}
</div>
);
}; };
export default LadybugClockPuzzle; export default LadybugClockPuzzle;

View file

@ -27,7 +27,11 @@ const heapColors = [
'#f87171', '#a78bfa', '#fbbf24', '#60a5fa', '#34d399', '#f472b6', '#f87171', '#a78bfa', '#facc15', '#38bdf8', '#f87171', '#a78bfa', '#fbbf24', '#60a5fa', '#34d399', '#f472b6', '#f87171', '#a78bfa', '#facc15', '#38bdf8',
]; ];
const NimGame: React.FC = () => { interface NimGameProps {
showSocialShare?: boolean;
}
const NimGame: React.FC<NimGameProps> = ({ showSocialShare = true }) => {
const [mode, setMode] = useState<Mode | null>(null); const [mode, setMode] = useState<Mode | null>(null);
const [showSetup, setShowSetup] = useState(false); const [showSetup, setShowSetup] = useState(false);
const [userHeapCount, setUserHeapCount] = useState(3); const [userHeapCount, setUserHeapCount] = useState(3);
@ -275,14 +279,12 @@ const NimGame: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
{/* Social Share */} {showSocialShare && (
<div className="flex justify-center">
<SocialShare <SocialShare
title="Game of Nim" title="Game of Nim"
description="Play the classic two-player game of Nim! Take turns removing stones from heaps. The player to take the last stone wins!" description="Play the classic two-player game of Nim! Take turns removing stones from heaps. The player to take the last stone wins!"
url={`${window.location.origin}/games/nim`}
/> />
</div> )}
</div> </div>
); );
}; };

View file

@ -11,6 +11,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { RefreshCw, User, UserRound, Users, UserCheck } from "lucide-react"; import { RefreshCw, User, UserRound, Users, UserCheck } from "lucide-react";
import SocialShare from '@/components/SocialShare';
const AGENTS = ["Alice", "Bob", "Charlie", "Dana"]; const AGENTS = ["Alice", "Bob", "Charlie", "Dana"];
const AGENT_ICONS = [User, UserRound, Users, UserCheck]; const AGENT_ICONS = [User, UserRound, Users, UserCheck];
@ -23,9 +24,10 @@ const ROOM_COLORS = [
interface RentDivisionPuzzleProps { interface RentDivisionPuzzleProps {
onComplete?: (completed: boolean) => void; onComplete?: (completed: boolean) => void;
showSocialShare?: boolean;
} }
const RentDivisionPuzzle = ({ onComplete }: RentDivisionPuzzleProps) => { const RentDivisionPuzzle = ({ onComplete, showSocialShare = true }: RentDivisionPuzzleProps) => {
// V-matrix: valuations[agent][room] // V-matrix: valuations[agent][room]
const [valuations, setValuations] = useState<number[][]>(() => const [valuations, setValuations] = useState<number[][]>(() =>
Array.from({ length: 4 }, () => Array.from({ length: 4 }, () =>
@ -469,6 +471,12 @@ const RentDivisionPuzzle = ({ onComplete }: RentDivisionPuzzleProps) => {
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
{showSocialShare && (
<SocialShare
title="Envy-Free Rent Division"
description="Explore fair division by assigning rooms and setting rents to achieve envy-free allocations."
/>
)}
</div> </div>
); );
}; };

View file

@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; 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) // Rules of Inference Playground (Example 1: Resolution)
// Layout: 3 columns (Premises | Notepad lines with rule margin | Rules) // Layout: 3 columns (Premises | Notepad lines with rule margin | Rules)
@ -196,7 +197,7 @@ function DropZone({ children, onDrop, className }: { children?: React.ReactNode;
); );
} }
export default function RulesOfInferencePlayground({ onComplete }: { onComplete?: (completed: boolean) => void }) { export default function RulesOfInferencePlayground({ onComplete, showSocialShare = true }: { onComplete?: (completed: boolean) => void; showSocialShare?: boolean }) {
const { toast } = useToast(); const { toast } = useToast();
const [exampleId, setExampleId] = useState(EXAMPLES[0].id); const [exampleId, setExampleId] = useState(EXAMPLES[0].id);
const example = useMemo(() => EXAMPLES.find((e) => e.id === exampleId)!, [exampleId]); const example = useMemo(() => EXAMPLES.find((e) => e.id === exampleId)!, [exampleId]);
@ -579,6 +580,13 @@ export default function RulesOfInferencePlayground({ onComplete }: { onComplete?
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{showSocialShare && (
<SocialShare
title="Rules of Inference Playground"
description="Practice applying resolution and equivalence rules to derive conclusions step by step in this interactive logic playground."
/>
)}
</div> </div>
); );
} }

View file

@ -34,7 +34,7 @@ const ZeckendorfGame: React.FC<ZeckendorfGameProps> = ({ showSocialShare = true
// Generate a random combination of Fibonacci numbers // Generate a random combination of Fibonacci numbers
const numTerms = Math.floor(Math.random() * 4) + 2; // 2-5 terms const numTerms = Math.floor(Math.random() * 4) + 2; // 2-5 terms
let target = 0; let target = 0;
let usedIndices = new Set<number>(); const usedIndices = new Set<number>();
for (let i = 0; i < numTerms; i++) { for (let i = 0; i < numTerms; i++) {
let index; let index;

View file

@ -217,7 +217,7 @@ const QuestionGuessingMode: React.FC<QuestionGuessingModeProps> = ({
case 'odd': case 'odd':
baseCondition = (num % 2 !== 0); baseCondition = (num % 2 !== 0);
break; break;
case 'prime': case 'prime': {
const isPrimeNum = (n: number): boolean => { const isPrimeNum = (n: number): boolean => {
if (n < 2) return false; if (n < 2) return false;
for (let i = 2; i <= Math.sqrt(n); i++) { for (let i = 2; i <= Math.sqrt(n); i++) {
@ -227,6 +227,7 @@ const QuestionGuessingMode: React.FC<QuestionGuessingModeProps> = ({
}; };
baseCondition = isPrimeNum(num); baseCondition = isPrimeNum(num);
break; break;
}
case 'perfectSquare': case 'perfectSquare':
baseCondition = Math.sqrt(num) % 1 === 0; baseCondition = Math.sqrt(num) % 1 === 0;
break; break;

View file

@ -21,7 +21,7 @@ const Command = React.forwardRef<
)) ))
Command.displayName = CommandPrimitive.displayName Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {} type CommandDialogProps = DialogProps
const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return ( return (

View file

@ -2,8 +2,7 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export interface TextareaProps export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {

View file

@ -1,13 +1,12 @@
import Layout from "@/components/Layout";
import AscDescGridPuzzle from "@/components/AscDescGridPuzzle"; import AscDescGridPuzzle from "@/components/AscDescGridPuzzle";
const AscDescGridPuzzlePage = () => { const AscDescGridPuzzlePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="py-8"> <div className="container mx-auto px-4 py-8">
<AscDescGridPuzzle /> <AscDescGridPuzzle showSocialShare={true} />
</div>
</div> </div>
</Layout>
); );
}; };

View file

@ -1,10 +1,11 @@
import React from 'react';
import AssistedNimGame from '@/components/AssistedNimGame'; import AssistedNimGame from '@/components/AssistedNimGame';
const AssistedNimGamePage = () => { const AssistedNimGamePage = () => {
return ( return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<AssistedNimGame /> <AssistedNimGame showSocialShare={true} />
</div>
</div> </div>
); );
}; };

View file

@ -1,13 +1,12 @@
import Layout from "@/components/Layout";
import BagchalGame from "@/components/BagchalGame"; import BagchalGame from "@/components/BagchalGame";
const BagchalGamePage = () => { const BagchalGamePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<BagchalGame showSocialShare={true} /> <BagchalGame showSocialShare={true} />
</div> </div>
</Layout> </div>
); );
}; };

View file

@ -2,9 +2,9 @@ import BalancedTernaryGame from '@/components/BalancedTernaryGame';
const BalancedTernaryGamePage = () => { const BalancedTernaryGamePage = () => {
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto"> <div className="container mx-auto px-4 py-8">
<BalancedTernaryGame /> <BalancedTernaryGame showSocialShare={true} />
</div> </div>
</div> </div>
); );

View file

@ -2,9 +2,9 @@ import BinaryNumberGame from '@/components/BinaryNumberGame';
const BinaryNumberGamePage = () => { const BinaryNumberGamePage = () => {
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto"> <div className="container mx-auto px-4 py-8">
<BinaryNumberGame /> <BinaryNumberGame showSocialShare={true} />
</div> </div>
</div> </div>
); );

View file

@ -1,29 +1,13 @@
import BulgarianSolitaire from "@/components/BulgarianSolitaire"; import BulgarianSolitaire from "@/components/BulgarianSolitaire";
import { useSearchParams } from "react-router-dom";
const BulgarianSolitairePage = () => { const BulgarianSolitairePage = () => {
const [searchParams] = useSearchParams();
const from = searchParams.get('from');
if (from === 'puzzles') {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8 space-y-6"> <div className="container mx-auto px-4 py-8">
<div className="flex items-center gap-4">
<button
onClick={() => window.location.href = '/themes/puzzles'}
className="text-primary hover:text-primary/80 font-medium"
>
Back to Puzzles
</button>
</div>
<BulgarianSolitaire showSocialShare={true} /> <BulgarianSolitaire showSocialShare={true} />
</div> </div>
</div> </div>
); );
}
return <BulgarianSolitaire showSocialShare={true} />;
}; };
export default BulgarianSolitairePage; export default BulgarianSolitairePage;

View file

@ -1,13 +1,12 @@
import Layout from '@/components/Layout';
import BurnsidesLemma from '@/components/BurnsidesLemma'; import BurnsidesLemma from '@/components/BurnsidesLemma';
const BurnsidesLemmaPage = () => { const BurnsidesLemmaPage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<BurnsidesLemma showSocialShare={true} /> <BurnsidesLemma showSocialShare={true} />
</div> </div>
</Layout> </div>
); );
}; };

View file

@ -1,16 +1,12 @@
import Layout from '@/components/Layout';
import CubeColoring from '@/components/CubeColoring'; import CubeColoring from '@/components/CubeColoring';
const CubeColoringPage = () => { const CubeColoringPage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<CubeColoring <CubeColoring showSocialShare={true} />
showSocialShare={true} </div>
shareUrl={window.location.href}
/>
</div> </div>
</Layout>
); );
}; };

View file

@ -1,27 +1,12 @@
import Layout from '@/components/Layout';
import DominoRetilingPuzzle from '@/components/DominoRetilingPuzzle'; import DominoRetilingPuzzle from '@/components/DominoRetilingPuzzle';
import { useLocation, Link } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
const DominoRetilingPuzzlePage = () => { const DominoRetilingPuzzlePage = () => {
const location = useLocation();
const fromPuzzles = location.search.includes('from=puzzles');
return ( return (
<Layout> <div className="min-h-screen bg-background">
{fromPuzzles && ( <div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto px-6 pt-4"> <DominoRetilingPuzzle showSocialShare={true} />
<Link </div>
to="/themes/puzzles"
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-1" />
Back to Puzzles
</Link>
</div> </div>
)}
<DominoRetilingPuzzle />
</Layout>
); );
}; };

View file

@ -0,0 +1,13 @@
import ErdosDiscrepancyPuzzle from "@/components/ErdosDiscrepancyPuzzle";
const ErdosDiscrepancyPuzzlePage = () => {
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<ErdosDiscrepancyPuzzle showSocialShare={true} />
</div>
</div>
);
};
export default ErdosDiscrepancyPuzzlePage;

View file

@ -1,87 +1,12 @@
import Layout from "@/components/Layout";
import EternalDominationGame from "@/components/EternalDominationGame"; 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 = () => { const EternalDominationGamePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="py-8 px-4"> <div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6"> <EternalDominationGame showSocialShare={true} />
<div className="text-center space-y-4">
<Badge variant="outline" className="mb-2">Miscellany</Badge>
<h1 className="text-3xl font-bold">Eternal Domination on a Grid</h1>
<p className="text-muted-foreground max-w-2xl mx-auto">
Explore the m-eternal domination problem on a 12×10 grid. Guards must maintain
a dominating set while defending against arbitrary attack sequences.
</p>
</div>
<EternalDominationGame />
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="background">
<AccordionTrigger>Mathematical Background</AccordionTrigger>
<AccordionContent className="space-y-3 text-sm">
<p>
<strong>Dominating Set:</strong> 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.
</p>
<p>
<strong>m-Eternal Domination:</strong> 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.
</p>
<p>
<strong>The Challenge:</strong> 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.
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="strategy">
<AccordionTrigger>Defense Strategy</AccordionTrigger>
<AccordionContent className="space-y-3 text-sm">
<p>
This implementation uses the strategy from research on finite grids:
</p>
<ul className="list-disc pl-5 space-y-1">
<li><strong>Border guards:</strong> All vertices on the border (rows 0, 1, 8, 9 and
columns 0, 11) are always guarded, forming a protective cycle C.</li>
<li><strong>Interior guards:</strong> Placed in a pattern that ensures every interior
vertex is dominated by exactly one guard.</li>
<li><strong>Defense mechanism:</strong> 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.</li>
</ul>
</AccordionContent>
</AccordionItem>
<AccordionItem value="reference">
<AccordionTrigger>Reference</AccordionTrigger>
<AccordionContent className="text-sm">
<p>
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.
</p>
</AccordionContent>
</AccordionItem>
</Accordion>
<SocialShare
title="Eternal Domination on a Grid"
description="Explore the m-eternal domination problem - can you attack in a way that breaks the defense?"
/>
</div> </div>
</div> </div>
</Layout>
); );
}; };

View file

@ -1,27 +1,12 @@
import Layout from "@/components/Layout";
import FerrersRogersRamanujan from "@/components/FerrersRogersRamanujan"; import FerrersRogersRamanujan from "@/components/FerrersRogersRamanujan";
import SocialShare from "@/components/SocialShare";
const FerrersRogersRamanujanPage = () => { const FerrersRogersRamanujanPage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="container mx-auto py-8 px-4 space-y-8"> <div className="container mx-auto px-4 py-8">
<div className="space-y-2"> <FerrersRogersRamanujan showSocialShare={true} />
<h1 className="text-4xl font-bold">Ferrers Diagram & Rogers-Ramanujan Partition</h1>
<p className="text-muted-foreground text-lg">
Explore integer partitions through visual Ferrers diagrams and discover the elegant Rogers-Ramanujan transformation.
</p>
</div> </div>
<FerrersRogersRamanujan />
<SocialShare
title="Ferrers Diagram & Rogers-Ramanujan Partition"
description="Explore integer partitions through visual Ferrers diagrams and discover the elegant Rogers-Ramanujan transformation."
url="https://lovable.dev/themes/discrete-math/counting/ferrers-rogers-ramanujan"
/>
</div> </div>
</Layout>
); );
}; };

View file

@ -1,10 +1,11 @@
import React from 'react';
import GoldCoinGame from '@/components/GoldCoinGame'; import GoldCoinGame from '@/components/GoldCoinGame';
const GoldCoinGamePage = () => { const GoldCoinGamePage = () => {
return ( return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<GoldCoinGame /> <GoldCoinGame showSocialShare={true} />
</div>
</div> </div>
); );
}; };

View file

@ -1,29 +1,12 @@
import Layout from '@/components/Layout';
import LadybugClockPuzzle from '@/components/LadybugClockPuzzle'; import LadybugClockPuzzle from '@/components/LadybugClockPuzzle';
import { useLocation, Link } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
const LadybugClockPuzzlePage = () => { const LadybugClockPuzzlePage = () => {
const location = useLocation();
const fromPuzzles = location.search.includes('from=puzzles');
return ( return (
<Layout> <div className="min-h-screen bg-background">
{fromPuzzles && ( <div className="container mx-auto px-4 py-8">
<div className="max-w-6xl mx-auto px-4 pt-4"> <LadybugClockPuzzle showSocialShare={true} />
<Link
to="/themes/puzzles"
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-1" />
Back to Puzzles
</Link>
</div> </div>
)}
<div className="py-8">
<LadybugClockPuzzle />
</div> </div>
</Layout>
); );
}; };

View file

@ -1,10 +1,11 @@
import React from 'react';
import NimGame from '@/components/NimGame'; import NimGame from '@/components/NimGame';
const NimGamePage = () => { const NimGamePage = () => {
return ( return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<NimGame /> <NimGame showSocialShare={true} />
</div>
</div> </div>
); );
}; };

View file

@ -1,11 +1,12 @@
import Layout from "@/components/Layout";
import ParityBitsGame from "@/components/ParityBitsGame"; import ParityBitsGame from "@/components/ParityBitsGame";
const ParityBitsGamePage = () => { const ParityBitsGamePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<ParityBitsGame /> <div className="container mx-auto px-4 py-8">
</Layout> <ParityBitsGame showSocialShare={true} />
</div>
</div>
); );
}; };

View file

@ -1,29 +1,13 @@
import PebblePlacementGame from "@/components/PebblePlacementGame"; import PebblePlacementGame from "@/components/PebblePlacementGame";
import { useSearchParams } from "react-router-dom";
const PebblePlacementGamePage = () => { const PebblePlacementGamePage = () => {
const [searchParams] = useSearchParams();
const from = searchParams.get('from');
if (from === 'puzzles') {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8 space-y-6"> <div className="container mx-auto px-4 py-8">
<div className="flex items-center gap-4">
<button
onClick={() => window.location.href = '/themes/puzzles'}
className="text-primary hover:text-primary/80 font-medium"
>
Back to Puzzles
</button>
</div>
<PebblePlacementGame showSocialShare={true} /> <PebblePlacementGame showSocialShare={true} />
</div> </div>
</div> </div>
); );
}
return <PebblePlacementGame showSocialShare={true} />;
}; };
export default PebblePlacementGamePage; export default PebblePlacementGamePage;

View file

@ -1,13 +1,12 @@
import Layout from '@/components/Layout';
import PresentsPuzzle from "@/components/PresentsPuzzle"; import PresentsPuzzle from "@/components/PresentsPuzzle";
const PresentsPuzzlePage = () => { const PresentsPuzzlePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="max-w-6xl mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<PresentsPuzzle showSocialShare={true} /> <PresentsPuzzle showSocialShare={true} />
</div> </div>
</Layout> </div>
); );
}; };

View file

@ -1,34 +1,10 @@
import { useState } from "react";
import RentDivisionPuzzle from "@/components/RentDivisionPuzzle"; import RentDivisionPuzzle from "@/components/RentDivisionPuzzle";
import SocialShare from "@/components/SocialShare";
const RentDivisionPuzzlePage = () => { const RentDivisionPuzzlePage = () => {
const [isCompleted, setIsCompleted] = useState(false);
return ( return (
<div <div className="min-h-screen bg-background">
className={`min-h-screen transition-colors duration-500 p-6 ${ <div className="container mx-auto px-4 py-8">
isCompleted ? "bg-green-50/50 dark:bg-green-950/20" : "bg-background" <RentDivisionPuzzle showSocialShare={true} />
}`}
>
<div className="max-w-6xl mx-auto space-y-6">
<div className="space-y-2">
<h1 className="text-3xl font-bold text-foreground">
Envy-Free Rent Division
</h1>
<p className="text-muted-foreground">
Explore fair division by assigning rooms and setting rents. Can you find an
allocation where no one envies another?
</p>
</div>
<RentDivisionPuzzle onComplete={setIsCompleted} />
<SocialShare
title="Envy-Free Rent Division - Interactive Fair Division"
description="Explore fair division by assigning rooms and setting rents to achieve envy-free allocations."
url="https://lovable.dev/themes/social-choice/rent-division"
/>
</div> </div>
</div> </div>
); );

View file

@ -1,31 +1,10 @@
import { useState } from "react";
import RulesOfInferencePlayground from "@/components/RulesOfInferencePlayground"; import RulesOfInferencePlayground from "@/components/RulesOfInferencePlayground";
import SocialShare from "@/components/SocialShare";
const RulesOfInferencePlaygroundPage = () => { const RulesOfInferencePlaygroundPage = () => {
const [isCompleted, setIsCompleted] = useState(false);
return ( return (
<div <div className="min-h-screen bg-background">
className={`min-h-screen transition-colors duration-500 p-6 ${ <div className="container mx-auto px-4 py-8">
isCompleted ? 'bg-green-50/50 dark:bg-green-950/20' : 'bg-background' <RulesOfInferencePlayground showSocialShare={true} />
}`}
>
<div className="max-w-4xl mx-auto space-y-6">
<div className="space-y-2">
<h1 className="text-3xl font-bold text-foreground">Rules of Inference Playground</h1>
<p className="text-muted-foreground">
Practice applying resolution and equivalence rules to derive conclusions step by step.
</p>
</div>
<RulesOfInferencePlayground onComplete={setIsCompleted} />
<SocialShare
title="Rules of Inference Playground - Interactive Logic Learning"
description="Practice applying resolution and equivalence rules to derive conclusions step by step in this interactive logic playground."
url="https://lovable.dev/discrete-math/foundations/rules-of-inference"
/>
</div> </div>
</div> </div>
); );

View file

@ -1,29 +1,13 @@
import StackingBlocks from "@/components/StackingBlocks"; import StackingBlocks from "@/components/StackingBlocks";
import { useSearchParams } from "react-router-dom";
const StackingBlocksPage = () => { const StackingBlocksPage = () => {
const [searchParams] = useSearchParams();
const from = searchParams.get('from');
if (from === 'puzzles') {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8 space-y-6"> <div className="container mx-auto px-4 py-8">
<div className="flex items-center gap-4">
<button
onClick={() => window.location.href = '/themes/puzzles'}
className="text-primary hover:text-primary/80 font-medium"
>
Back to Puzzles
</button>
</div>
<StackingBlocks showSocialShare={true} /> <StackingBlocks showSocialShare={true} />
</div> </div>
</div> </div>
); );
}
return <StackingBlocks showSocialShare={true} />;
}; };
export default StackingBlocksPage; export default StackingBlocksPage;

View file

@ -1,7 +1,13 @@
import SubtractionGame from '@/components/SubtractionGame'; import SubtractionGame from '@/components/SubtractionGame';
const SubtractionGamePage = () => { const SubtractionGamePage = () => {
return <SubtractionGame />; return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<SubtractionGame showSocialShare={true} />
</div>
</div>
);
}; };
export default SubtractionGamePage; export default SubtractionGamePage;

View file

@ -2,9 +2,9 @@ import TernaryNumberGame from '@/components/TernaryNumberGame';
const TernaryNumberGamePage = () => { const TernaryNumberGamePage = () => {
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto"> <div className="container mx-auto px-4 py-8">
<TernaryNumberGame /> <TernaryNumberGame showSocialShare={true} />
</div> </div>
</div> </div>
); );

View file

@ -2,9 +2,9 @@ import ZeckendorfGame from '@/components/ZeckendorfGame';
const ZeckendorfGamePage = () => { const ZeckendorfGamePage = () => {
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto"> <div className="container mx-auto px-4 py-8">
<ZeckendorfGame /> <ZeckendorfGame showSocialShare={true} />
</div> </div>
</div> </div>
); );

View file

@ -2,9 +2,9 @@ import ZeckendorfSearchTrick from '@/components/ZeckendorfSearchTrick';
const ZeckendorfSearchTrickPage = () => { const ZeckendorfSearchTrickPage = () => {
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background">
<div className="max-w-6xl mx-auto"> <div className="container mx-auto px-4 py-8">
<ZeckendorfSearchTrick /> <ZeckendorfSearchTrick showSocialShare={true} />
</div> </div>
</div> </div>
); );

View file

@ -115,6 +115,16 @@ const Puzzles = () => {
difficulty: "Intermediate" as const, difficulty: "Intermediate" as const,
duration: "5-15 min", duration: "5-15 min",
participants: "1 player" 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"
} }
]; ];

View file

@ -3,7 +3,6 @@ import { Link } from "react-router-dom";
import { Search } from "lucide-react"; import { Search } from "lucide-react";
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import InteractiveCard from "@/components/InteractiveCard"; import InteractiveCard from "@/components/InteractiveCard";
import SocialShare from "@/components/SocialShare";
import BinaryNumberGame from "@/components/BinaryNumberGame"; import BinaryNumberGame from "@/components/BinaryNumberGame";
import TernaryNumberGame from "@/components/TernaryNumberGame"; import TernaryNumberGame from "@/components/TernaryNumberGame";
import BalancedTernaryGame from "@/components/BalancedTernaryGame"; import BalancedTernaryGame from "@/components/BalancedTernaryGame";
@ -80,12 +79,6 @@ const Foundations = () => {
</Link> </Link>
</div> </div>
<InteractiveComponent /> <InteractiveComponent />
<SocialShare
title={`${selectedInteractive.title} - Interactive Learning`}
description={selectedInteractive.description}
url={`${window.location.origin}${selectedInteractive.greenScreenPath}`}
/>
</div> </div>
</div>; </div>;
} }

View file

@ -1,4 +1,5 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
export default { export default {
darkMode: ["class"], darkMode: ["class"],
@ -122,5 +123,5 @@ export default {
} }
} }
}, },
plugins: [require("tailwindcss-animate")], plugins: [tailwindcssAnimate],
} satisfies Config; } satisfies Config;