Reverted to commit 81f6873c4d
This commit is contained in:
parent
48c3f7a97a
commit
aaa3b7f06f
6 changed files with 0 additions and 741 deletions
50
package-lock.json
generated
50
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 />} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue