Reverted to commit 81f6873c4d

This commit is contained in:
gpt-engineer-app[bot] 2025-08-10 07:13:18 +00:00
parent 48c3f7a97a
commit aaa3b7f06f
6 changed files with 0 additions and 741 deletions

50
package-lock.json generated
View file

@ -43,7 +43,6 @@
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"html2canvas": "^1.4.1",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
@ -3361,15 +3360,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -4058,15 +4048,6 @@
"node": ">= 8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -4909,19 +4890,6 @@
"node": ">= 0.4"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -6933,15 +6901,6 @@
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -7165,15 +7124,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vaul": {
"version": "0.9.9",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",

View file

@ -46,7 +46,6 @@
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"html2canvas": "^1.4.1",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",

View file

@ -48,7 +48,6 @@ import GridTilingPuzzlePage from './pages/GridTilingPuzzlePage';
import SunnyLinesPuzzlePage from './pages/SunnyLinesPuzzlePage';
import SikiniaParliamentPuzzlePage from './pages/SikiniaParliamentPuzzlePage';
import NQueensPuzzlePage from './pages/NQueensPuzzlePage';
import DogsBunnyPuzzlePage from './pages/DogsBunnyPuzzlePage';
const queryClient = new QueryClient();
@ -105,7 +104,6 @@ const App = () => (
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
<Route path="/puzzles/n-queens" element={<NQueensPuzzlePage />} />
<Route path="/puzzles/dogs-bunny" element={<DogsBunnyPuzzlePage />} />
<Route path="/discrete-math/foundations/zeckendorf" element={<ZeckendorfGamePage />} />
<Route path="/discrete-math/foundations/zeckendorf-search" element={<ZeckendorfSearchTrickPage />} />

View file

@ -1,540 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Share2, Download, RotateCcw, Shuffle, Trophy } from 'lucide-react';
import { toast } from 'sonner';
import html2canvas from 'html2canvas';
type Location = {
id: string;
type: 'carrot' | 'bone' | 'flower' | 'well' | 'empty';
x: number;
y: number;
};
type Character = {
id: string;
type: 'bunny' | 'dog';
locationId: string;
};
type Edge = {
from: string;
to: string;
conditions: string[];
};
type PuzzleState = {
locations: Location[];
characters: Character[];
edges: Edge[];
moveCount: number;
isComplete: boolean;
};
interface DogsBunnyPuzzleProps {
difficulty?: 'easy' | 'medium' | 'hard';
seed?: string;
onComplete?: () => void;
}
const DogsBunnyPuzzle: React.FC<DogsBunnyPuzzleProps> = ({
difficulty = 'medium',
seed,
onComplete
}) => {
const [puzzleState, setPuzzleState] = useState<PuzzleState | null>(null);
const [selectedCharacter, setSelectedCharacter] = useState<string | null>(null);
const [validMoves, setValidMoves] = useState<string[]>([]);
// Generate puzzle based on difficulty and seed
const generatePuzzle = useCallback((diff: string, puzzleSeed?: string): PuzzleState => {
const rng = puzzleSeed ? seededRandom(puzzleSeed) : Math.random;
// Create locations in a circular/connected pattern using more space
const locations: Location[] = [
{ id: 'loc1', type: 'carrot', x: 100, y: 80 },
{ id: 'loc2', type: 'bone', x: 450, y: 120 },
{ id: 'loc3', type: 'flower', x: 520, y: 300 },
{ id: 'loc4', type: 'well', x: 400, y: 450 },
{ id: 'loc5', type: 'empty', x: 150, y: 450 },
{ id: 'loc6', type: 'empty', x: 50, y: 300 },
];
// Create characters - bunnies start away from carrot, dog away from bone
const characters: Character[] = [
{ id: 'bunny1', type: 'bunny', locationId: 'loc4' },
{ id: 'bunny2', type: 'bunny', locationId: 'loc5' },
{ id: 'dog1', type: 'dog', locationId: 'loc6' },
];
// Generate edges with conditions based on difficulty
const edges: Edge[] = [];
const locationTypes = ['carrot', 'bone', 'flower', 'well', 'bunny', 'dog'];
const conditionPrefixes = ['SOMEBODY AT', 'NOBODY AT'];
// Connect all locations with conditional edges
let edgeCount = 0;
const totalPossibleEdges = locations.length * (locations.length - 1);
const freeEdgePercentage = 0.33; // About 1/3 of edges will be free
for (let i = 0; i < locations.length; i++) {
for (let j = i + 1; j < locations.length; j++) {
// Decide if this edge should be free (no conditions)
const shouldBeFree = edgeCount < totalPossibleEdges * freeEdgePercentage && rng() < 0.5;
let conditions: string[] = [];
if (!shouldBeFree) {
const numConditions = diff === 'easy' ? 1 : diff === 'medium' ? 2 : 3;
for (let k = 0; k < numConditions; k++) {
const conditionType = locationTypes[Math.floor(rng() * locationTypes.length)];
const prefix = conditionPrefixes[Math.floor(rng() * conditionPrefixes.length)];
conditions.push(`${prefix} ${conditionType.toUpperCase()}`);
}
}
edges.push({
from: locations[i].id,
to: locations[j].id,
conditions: conditions
});
edges.push({
from: locations[j].id,
to: locations[i].id,
conditions: conditions
});
edgeCount += 2;
}
}
return {
locations,
characters,
edges,
moveCount: 0,
isComplete: false
};
}, []);
// Seeded random number generator
const seededRandom = (seed: string) => {
let hash = 0;
for (let i = 0; i < seed.length; i++) {
const char = seed.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return () => {
hash = Math.sin(hash) * 10000;
return hash - Math.floor(hash);
};
};
// Initialize puzzle
useEffect(() => {
const newPuzzle = generatePuzzle(difficulty, seed);
setPuzzleState(newPuzzle);
}, [difficulty, seed, generatePuzzle]);
// Check if conditions are met for an edge
const areConditionsMet = (edge: Edge): boolean => {
if (!puzzleState) return false;
return edge.conditions.every(condition => {
const parts = condition.split(' AT ');
if (parts.length !== 2) return false;
const isNobody = parts[0] === 'NOBODY';
const targetType = parts[1].toLowerCase();
const hasCharacterAtType = puzzleState.characters.some(char => {
const location = puzzleState.locations.find(loc => loc.id === char.locationId);
return location && (
location.type === targetType ||
char.type === targetType
);
});
return isNobody ? !hasCharacterAtType : hasCharacterAtType;
});
};
// Get valid moves for selected character
const getValidMoves = (characterId: string): string[] => {
if (!puzzleState) return [];
const character = puzzleState.characters.find(c => c.id === characterId);
if (!character) return [];
const validLocationIds: string[] = [];
const currentLocation = character.locationId;
puzzleState.edges.forEach(edge => {
if (edge.from === currentLocation && areConditionsMet(edge)) {
validLocationIds.push(edge.to);
}
});
return validLocationIds;
};
// Handle character selection
const handleCharacterSelect = (characterId: string) => {
setSelectedCharacter(characterId);
setValidMoves(getValidMoves(characterId));
};
// Handle character move
const handleMove = (targetLocationId: string) => {
if (!puzzleState || !selectedCharacter) return;
const updatedCharacters = puzzleState.characters.map(char =>
char.id === selectedCharacter
? { ...char, locationId: targetLocationId }
: char
);
const newMoveCount = puzzleState.moveCount + 1;
// Check if puzzle is complete
const isComplete = checkCompletion(updatedCharacters);
setPuzzleState({
...puzzleState,
characters: updatedCharacters,
moveCount: newMoveCount,
isComplete
});
setSelectedCharacter(null);
setValidMoves([]);
if (isComplete) {
toast.success(`Puzzle completed in ${newMoveCount} moves!`);
onComplete?.();
}
};
// Check if puzzle is complete
const checkCompletion = (characters: Character[]): boolean => {
const bunniesAtCarrot = characters.filter(char =>
char.type === 'bunny' &&
puzzleState?.locations.find(loc => loc.id === char.locationId)?.type === 'carrot'
).length;
const dogsAtBone = characters.filter(char =>
char.type === 'dog' &&
puzzleState?.locations.find(loc => loc.id === char.locationId)?.type === 'bone'
).length;
return bunniesAtCarrot === 2 && dogsAtBone === 1;
};
// Reset puzzle
const resetPuzzle = () => {
const newPuzzle = generatePuzzle(difficulty, seed);
setPuzzleState(newPuzzle);
setSelectedCharacter(null);
setValidMoves([]);
};
// Generate new puzzle
const generateNewPuzzle = () => {
const newSeed = Math.random().toString(36).substring(7);
const newPuzzle = generatePuzzle(difficulty, newSeed);
setPuzzleState(newPuzzle);
setSelectedCharacter(null);
setValidMoves([]);
// Update URL
const url = new URL(window.location.href);
url.searchParams.set('seed', newSeed);
window.history.pushState({}, '', url);
};
// Share puzzle
const sharePuzzle = async () => {
const url = new URL(window.location.href);
if (seed) url.searchParams.set('seed', seed);
url.searchParams.set('difficulty', difficulty);
try {
await navigator.clipboard.writeText(url.toString());
toast.success('Puzzle URL copied to clipboard!');
} catch (err) {
toast.error('Failed to copy URL');
}
};
// Screenshot puzzle
const screenshotPuzzle = async () => {
const element = document.getElementById('puzzle-area');
if (!element) return;
try {
const canvas = await html2canvas(element);
const link = document.createElement('a');
link.download = `dogs-bunny-puzzle-${difficulty}-${Date.now()}.png`;
link.href = canvas.toDataURL();
link.click();
toast.success('Screenshot saved!');
} catch (err) {
toast.error('Failed to take screenshot');
}
};
// Get character icon
const getCharacterIcon = (type: 'bunny' | 'dog') => {
return type === 'bunny' ? '🐰' : '🐕';
};
// Get location icon
const getLocationIcon = (type: string) => {
switch (type) {
case 'carrot': return '🥕';
case 'bone': return '🦴';
case 'flower': return '🌸';
case 'well': return '🪣';
default: return '';
}
};
// Get condition icon
const getConditionIcon = (condition: string) => {
if (condition.includes('CARROT')) return '🥕';
if (condition.includes('BONE')) return '🦴';
if (condition.includes('FLOWER')) return '🌸';
if (condition.includes('WELL')) return '🪣';
if (condition.includes('BUNNY')) return '🐰';
if (condition.includes('DOG')) return '🐕';
return '❓';
};
// Format condition text for tooltip
const formatCondition = (condition: string) => {
const parts = condition.split(' AT ');
if (parts.length !== 2) return condition;
const prefix = parts[0] === 'NOBODY' ? 'Nobody at' : 'Someone at';
const target = parts[1].toLowerCase();
return `${prefix} ${target}`;
};
if (!puzzleState) return <div>Loading...</div>;
return (
<div className="w-full max-w-6xl mx-auto p-4">
<Card>
<CardHeader>
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<CardTitle className="flex items-center gap-2">
Dogs & Bunny Puzzle
{puzzleState.isComplete && <Trophy className="h-5 w-5 text-yellow-500" />}
</CardTitle>
<p className="text-sm text-muted-foreground mt-1">
Get all bunnies to the carrot 🥕 and the dog to the bone 🦴
</p>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline">{difficulty}</Badge>
<Badge variant="secondary">Moves: {puzzleState.moveCount}</Badge>
</div>
</div>
</CardHeader>
<CardContent>
{/* Puzzle Area - Full Width */}
<div
id="puzzle-area"
className="relative bg-muted/10 rounded-lg p-6 min-h-[700px] w-full mb-6"
style={{ height: '700px' }}
>
{/* Edges */}
<svg className="absolute inset-0 w-full h-full pointer-events-none">
{puzzleState.edges.map((edge, index) => {
const fromLoc = puzzleState.locations.find(l => l.id === edge.from);
const toLoc = puzzleState.locations.find(l => l.id === edge.to);
if (!fromLoc || !toLoc) return null;
const isValid = selectedCharacter &&
puzzleState.characters.find(c => c.id === selectedCharacter)?.locationId === edge.from &&
areConditionsMet(edge);
return (
<g key={index}>
<line
x1={fromLoc.x + 20}
y1={fromLoc.y + 20}
x2={toLoc.x + 20}
y2={toLoc.y + 20}
stroke={isValid ? "hsl(var(--primary))" : "hsl(var(--border))"}
strokeWidth={isValid ? "3" : "1"}
strokeDasharray={isValid ? "none" : "5,5"}
/>
{/* Condition labels - only show if edge has conditions */}
{edge.conditions.length > 0 && (
<g>
{edge.conditions.length === 1 ? (
// Single condition
<circle
cx={(fromLoc.x + toLoc.x) / 2 + 30}
cy={(fromLoc.y + toLoc.y) / 2 + 30}
r="14"
fill="hsl(var(--background))"
stroke={edge.conditions[0].includes('NOBODY') ? "#ef4444" : "hsl(var(--border))"}
strokeWidth="2"
/>
) : (
// Multiple conditions - show as rounded rectangle
<rect
x={(fromLoc.x + toLoc.x) / 2 + 30 - 16}
y={(fromLoc.y + toLoc.y) / 2 + 16}
width="32"
height="28"
rx="14"
fill="hsl(var(--background))"
stroke={edge.conditions.some(c => c.includes('NOBODY')) ? "#ef4444" : "hsl(var(--border))"}
strokeWidth="2"
/>
)}
<text
x={(fromLoc.x + toLoc.x) / 2 + 30}
y={(fromLoc.y + toLoc.y) / 2 + 36}
fontSize="16"
textAnchor="middle"
className="pointer-events-none"
>
{getConditionIcon(edge.conditions[0])}
</text>
{edge.conditions.length > 1 && (
<text
x={(fromLoc.x + toLoc.x) / 2 + 42}
y={(fromLoc.y + toLoc.y) / 2 + 22}
fontSize="10"
textAnchor="middle"
className="pointer-events-none fill-muted-foreground"
>
+{edge.conditions.length - 1}
</text>
)}
<title>{edge.conditions.map(formatCondition).join(' AND ')}</title>
</g>
)}
</g>
);
})}
</svg>
{/* Locations */}
{puzzleState.locations.map((location) => {
const charactersHere = puzzleState.characters.filter(c => c.locationId === location.id);
const isValidMove = validMoves.includes(location.id);
return (
<div
key={location.id}
className={`absolute w-12 h-12 rounded-full border-2 flex items-center justify-center cursor-pointer transition-all ${
isValidMove
? 'border-primary bg-primary/20 scale-110'
: 'border-border bg-background hover:bg-muted'
}`}
style={{
left: location.x,
top: location.y,
}}
onClick={() => isValidMove && handleMove(location.id)}
>
<span className="text-xl">
{getLocationIcon(location.type)}
</span>
{/* Characters at this location */}
{charactersHere.map((char, index) => (
<div
key={char.id}
className={`absolute w-7 h-7 rounded-full border flex items-center justify-center cursor-pointer transition-all ${
selectedCharacter === char.id
? 'border-primary bg-primary text-primary-foreground scale-110'
: 'border-border bg-background hover:bg-muted'
}`}
style={{
right: -9 - (index * 14),
top: -9 - (index * 14),
}}
onClick={(e) => {
e.stopPropagation();
handleCharacterSelect(char.id);
}}
>
<span className="text-xs">
{getCharacterIcon(char.type)}
</span>
</div>
))}
</div>
);
})}
</div>
{/* Instructions & Controls - Below the graph */}
<div className="flex flex-col lg:flex-row gap-6">
<div className="flex-1 space-y-4">
<div className="space-y-2">
<h3 className="font-semibold">How to Play:</h3>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Click a character to select it</li>
<li> Click a highlighted location to move</li>
<li> Edges show movement conditions</li>
<li> Multiple animals can share locations</li>
</ul>
</div>
<Separator />
<div className="space-y-2">
<h3 className="font-semibold">Legend:</h3>
<div className="grid grid-cols-2 gap-2 text-sm">
<div>🐰 Bunny</div>
<div>🐕 Dog</div>
<div>🥕 Carrot</div>
<div>🦴 Bone</div>
<div>🌸 Flower</div>
<div>🪣 Well</div>
</div>
</div>
</div>
<div className="lg:w-80 space-y-2">
<Button onClick={resetPuzzle} variant="outline" className="w-full">
<RotateCcw className="h-4 w-4 mr-2" />
Reset
</Button>
<Button onClick={generateNewPuzzle} variant="outline" className="w-full">
<Shuffle className="h-4 w-4 mr-2" />
New Puzzle
</Button>
<Button onClick={sharePuzzle} variant="outline" className="w-full">
<Share2 className="h-4 w-4 mr-2" />
Share
</Button>
<Button onClick={screenshotPuzzle} variant="outline" className="w-full">
<Download className="h-4 w-4 mr-2" />
Screenshot
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
);
};
export default DogsBunnyPuzzle;

View file

@ -1,138 +0,0 @@
import { useEffect, useState } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import Layout from '@/components/Layout';
import DogsBunnyPuzzle from '@/components/DogsBunnyPuzzle';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ArrowLeft } from 'lucide-react';
const DogsBunnyPuzzlePage = () => {
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const [difficulty, setDifficulty] = useState<'easy' | 'medium' | 'hard'>('medium');
const [seed, setSeed] = useState<string | undefined>(undefined);
useEffect(() => {
const diffParam = searchParams.get('difficulty') as 'easy' | 'medium' | 'hard';
const seedParam = searchParams.get('seed');
if (diffParam && ['easy', 'medium', 'hard'].includes(diffParam)) {
setDifficulty(diffParam);
}
if (seedParam) {
setSeed(seedParam);
}
}, [searchParams]);
const handleDifficultyChange = (newDifficulty: 'easy' | 'medium' | 'hard') => {
setDifficulty(newDifficulty);
const newParams = new URLSearchParams(searchParams);
newParams.set('difficulty', newDifficulty);
setSearchParams(newParams);
};
const handleBackNavigation = () => {
const from = searchParams.get('from');
if (from === 'puzzles') {
navigate('/themes/puzzles');
} else {
navigate('/');
}
};
return (
<Layout>
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
onClick={handleBackNavigation}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
<div>
<h1 className="text-3xl font-bold text-foreground">Dogs & Bunny Puzzle</h1>
<p className="text-muted-foreground mt-1">
A logic puzzle about getting animals to their favorite spots
</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Difficulty:</span>
<Select value={difficulty} onValueChange={handleDifficultyChange}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="easy">Easy</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="hard">Hard</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Instructions */}
<div className="bg-muted/10 rounded-lg p-6 mb-8">
<h2 className="text-xl font-semibold mb-4">Puzzle Rules</h2>
<div className="grid md:grid-cols-2 gap-6">
<div>
<h3 className="font-medium mb-2">Objective:</h3>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Get all bunnies 🐰 to the carrot 🥕</li>
<li> Get the dog 🐕 to the bone 🦴</li>
</ul>
</div>
<div>
<h3 className="font-medium mb-2">Movement Rules:</h3>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Click a character to select it</li>
<li> You can only move along edges where all conditions are met</li>
<li> Multiple animals can occupy the same location</li>
<li> Edge labels show the required conditions</li>
</ul>
</div>
</div>
</div>
{/* Puzzle Component */}
<div className="flex justify-center">
<DogsBunnyPuzzle
difficulty={difficulty}
seed={seed}
onComplete={() => {
// Could add achievements or stats here
}}
/>
</div>
{/* Additional Info */}
<div className="mt-8 text-center text-sm text-muted-foreground">
<p>
Original puzzle concept by <a
href="https://twitter.com/lisperati"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Conrad Barski (@lisperati)
</a>
</p>
</div>
</div>
</div>
</Layout>
);
};
export default DogsBunnyPuzzlePage;

View file

@ -55,16 +55,6 @@ const Puzzles = () => {
difficulty: "Intermediate" as const,
duration: "5-15 min",
participants: "1 player"
},
{
id: "dogs-bunny",
title: "Dogs & Bunny Puzzle",
description: "Guide bunnies to the carrot and the dog to the bone using conditional movement rules. A graph traversal puzzle!",
path: "/puzzles/dogs-bunny",
tags: ["Graph Theory", "Logic", "Pathfinding", "Animals"],
difficulty: "Intermediate" as const,
duration: "10-25 min",
participants: "1 player"
}
];