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.
### Forgejo and Dokploy
Pushes and pull requests targeting `main` are verified by the Forgejo Actions workflow in `.forgejo/workflows/ci.yml`. The production container is built from `Dockerfile` and serves the Vite output with NGINX, including React Router fallback handling.
Dokploy is connected to the `Websites/interactives` Forgejo repository with autodeploy enabled. A successful push to `main` triggers a fresh Dockerfile build and replaces the running service only after the image has built successfully.
## Can I connect a custom domain to my Lovable project?
Yes, you can!

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

View file

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

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 { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Shield, Target, RotateCcw, Info } from "lucide-react";
import SocialShare from '@/components/SocialShare';
// Extended good rectangle: 10 columns × 12 rows (following Fig. 10)
// Border: columns 0 and 9, rows 0/1 (top) and 10/11 (bottom)
@ -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 [attackHistory, setAttackHistory] = useState<Position[]>([]);
const [message, setMessage] = useState<string>("Click any unguarded cell to attack.");
@ -679,6 +684,13 @@ const EternalDominationGame: React.FC = () => {
)}
</CardContent>
</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>
);
};

View file

@ -4,8 +4,13 @@ import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import SocialShare from '@/components/SocialShare';
const FerrersRogersRamanujan = () => {
interface FerrersRogersRamanujanProps {
showSocialShare?: boolean;
}
const FerrersRogersRamanujan = ({ showSocialShare = true }: FerrersRogersRamanujanProps) => {
const [n, setN] = useState(14);
const [partitionSliders, setPartitionSliders] = 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.
</p>
</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>
</Card>
);

View file

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

View file

@ -67,7 +67,7 @@ const InteractiveGallery = () => {
<div className="space-y-6">
<div className="space-y-4">
<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>
{/* Search bar */}
@ -103,4 +103,4 @@ const InteractiveGallery = () => {
</Layout>
);
};
export default InteractiveGallery;
export default InteractiveGallery;

View file

@ -18,7 +18,7 @@ interface Interactive {
}
// 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',
title: 'Binary Number Representation',
@ -288,6 +288,15 @@ const allInteractives: Interactive[] = [
path: '/puzzles/ladybug-clock',
theme: 'Puzzles',
dateAdded: '2025-01-21'
},
{
id: 'erdos-discrepancy',
title: 'The Erdős Discrepancy Problem',
description: 'Can you write instructions to survive the tunnel forever? Explore this famous mathematical puzzle through the Prisoner\'s Walk game.',
tags: ['erdos', 'discrepancy', 'sequences', 'game-theory', 'impossibility', 'puzzle', 'number-theory'],
path: '/puzzles/erdos-discrepancy',
theme: 'Puzzles',
dateAdded: '2025-01-22'
}
];
@ -314,7 +323,7 @@ const InteractiveIndex = () => {
// Filter and sort interactives
const filteredAndSortedInteractives = useMemo(() => {
let filtered = allInteractives.filter(interactive => {
const filtered = allInteractives.filter(interactive => {
const matchesSearch = interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
@ -540,4 +549,4 @@ const InteractiveIndex = () => {
);
};
export default InteractiveIndex;
export default InteractiveIndex;

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

View file

@ -27,7 +27,11 @@ const heapColors = [
'#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 [showSetup, setShowSetup] = useState(false);
const [userHeapCount, setUserHeapCount] = useState(3);
@ -275,16 +279,14 @@ const NimGame: React.FC = () => {
</CardContent>
</Card>
{/* Social Share */}
<div className="flex justify-center">
{showSocialShare && (
<SocialShare
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!"
url={`${window.location.origin}/games/nim`}
/>
</div>
)}
</div>
);
};
export default NimGame;
export default NimGame;

View file

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

View file

@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast";
import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import SocialShare from '@/components/SocialShare';
// Rules of Inference Playground (Example 1: Resolution)
// Layout: 3 columns (Premises | Notepad lines with rule margin | Rules)
@ -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 [exampleId, setExampleId] = useState(EXAMPLES[0].id);
const example = useMemo(() => EXAMPLES.find((e) => e.id === exampleId)!, [exampleId]);
@ -579,6 +580,13 @@ export default function RulesOfInferencePlayground({ onComplete }: { onComplete?
</AlertDialogFooter>
</AlertDialogContent>
</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>
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,29 +1,13 @@
import BulgarianSolitaire from "@/components/BulgarianSolitaire";
import { useSearchParams } from "react-router-dom";
const BulgarianSolitairePage = () => {
const [searchParams] = useSearchParams();
const from = searchParams.get('from');
if (from === 'puzzles') {
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8 space-y-6">
<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} />
</div>
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<BulgarianSolitaire showSocialShare={true} />
</div>
);
}
return <BulgarianSolitaire showSocialShare={true} />;
</div>
);
};
export default BulgarianSolitairePage;
export default BulgarianSolitairePage;

View file

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

View file

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

View file

@ -1,28 +1,13 @@
import Layout from '@/components/Layout';
import DominoRetilingPuzzle from '@/components/DominoRetilingPuzzle';
import { useLocation, Link } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
const DominoRetilingPuzzlePage = () => {
const location = useLocation();
const fromPuzzles = location.search.includes('from=puzzles');
return (
<Layout>
{fromPuzzles && (
<div className="max-w-4xl mx-auto px-6 pt-4">
<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>
)}
<DominoRetilingPuzzle />
</Layout>
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<DominoRetilingPuzzle showSocialShare={true} />
</div>
</div>
);
};
export default DominoRetilingPuzzlePage;
export default DominoRetilingPuzzlePage;

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 SocialShare from "@/components/SocialShare";
import { Badge } from "@/components/ui/badge";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
const EternalDominationGamePage = () => {
return (
<Layout>
<div className="py-8 px-4">
<div className="max-w-4xl mx-auto space-y-6">
<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 className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<EternalDominationGame showSocialShare={true} />
</div>
</Layout>
</div>
);
};

View file

@ -1,27 +1,12 @@
import Layout from "@/components/Layout";
import FerrersRogersRamanujan from "@/components/FerrersRogersRamanujan";
import SocialShare from "@/components/SocialShare";
const FerrersRogersRamanujanPage = () => {
return (
<Layout>
<div className="container mx-auto py-8 px-4 space-y-8">
<div className="space-y-2">
<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>
<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 className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<FerrersRogersRamanujan showSocialShare={true} />
</div>
</Layout>
</div>
);
};

View file

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

View file

@ -1,29 +1,12 @@
import Layout from '@/components/Layout';
import LadybugClockPuzzle from '@/components/LadybugClockPuzzle';
import { useLocation, Link } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
const LadybugClockPuzzlePage = () => {
const location = useLocation();
const fromPuzzles = location.search.includes('from=puzzles');
return (
<Layout>
{fromPuzzles && (
<div className="max-w-6xl mx-auto px-4 pt-4">
<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 className="py-8">
<LadybugClockPuzzle />
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<LadybugClockPuzzle showSocialShare={true} />
</div>
</Layout>
</div>
);
};

View file

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

View file

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

View file

@ -1,29 +1,13 @@
import PebblePlacementGame from "@/components/PebblePlacementGame";
import { useSearchParams } from "react-router-dom";
const PebblePlacementGamePage = () => {
const [searchParams] = useSearchParams();
const from = searchParams.get('from');
if (from === 'puzzles') {
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8 space-y-6">
<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} />
</div>
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<PebblePlacementGame showSocialShare={true} />
</div>
);
}
return <PebblePlacementGame showSocialShare={true} />;
</div>
);
};
export default PebblePlacementGamePage;
export default PebblePlacementGamePage;

View file

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

View file

@ -1,34 +1,10 @@
import { useState } from "react";
import RentDivisionPuzzle from "@/components/RentDivisionPuzzle";
import SocialShare from "@/components/SocialShare";
const RentDivisionPuzzlePage = () => {
const [isCompleted, setIsCompleted] = useState(false);
return (
<div
className={`min-h-screen transition-colors duration-500 p-6 ${
isCompleted ? "bg-green-50/50 dark:bg-green-950/20" : "bg-background"
}`}
>
<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 className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<RentDivisionPuzzle showSocialShare={true} />
</div>
</div>
);

View file

@ -1,34 +1,13 @@
import { useState } from "react";
import RulesOfInferencePlayground from "@/components/RulesOfInferencePlayground";
import SocialShare from "@/components/SocialShare";
const RulesOfInferencePlaygroundPage = () => {
const [isCompleted, setIsCompleted] = useState(false);
return (
<div
className={`min-h-screen transition-colors duration-500 p-6 ${
isCompleted ? 'bg-green-50/50 dark:bg-green-950/20' : 'bg-background'
}`}
>
<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 className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<RulesOfInferencePlayground showSocialShare={true} />
</div>
</div>
);
};
export default RulesOfInferencePlaygroundPage;
export default RulesOfInferencePlaygroundPage;

View file

@ -1,29 +1,13 @@
import StackingBlocks from "@/components/StackingBlocks";
import { useSearchParams } from "react-router-dom";
const StackingBlocksPage = () => {
const [searchParams] = useSearchParams();
const from = searchParams.get('from');
if (from === 'puzzles') {
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8 space-y-6">
<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} />
</div>
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<StackingBlocks showSocialShare={true} />
</div>
);
}
return <StackingBlocks showSocialShare={true} />;
</div>
);
};
export default StackingBlocksPage;
export default StackingBlocksPage;

View file

@ -1,7 +1,13 @@
import SubtractionGame from '@/components/SubtractionGame';
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,12 +2,12 @@ import TernaryNumberGame from '@/components/TernaryNumberGame';
const TernaryNumberGamePage = () => {
return (
<div className="min-h-screen bg-background p-6">
<div className="max-w-4xl mx-auto">
<TernaryNumberGame />
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<TernaryNumberGame showSocialShare={true} />
</div>
</div>
);
};
export default TernaryNumberGamePage;
export default TernaryNumberGamePage;

View file

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

View file

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

View file

@ -115,6 +115,16 @@ const Puzzles = () => {
difficulty: "Intermediate" as const,
duration: "5-15 min",
participants: "1 player"
},
{
id: "erdos-discrepancy",
title: "The Erdős Discrepancy Problem",
description: "Can you write instructions to survive the prisoner's tunnel? Explore this famous mathematical puzzle and discover why no sequence can keep you safe forever!",
path: "/puzzles/erdos-discrepancy",
tags: ["Number Theory", "Sequences", "Impossibility", "Game Theory"],
difficulty: "Advanced" as const,
duration: "10-30 min",
participants: "1 player"
}
];

View file

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

View file

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