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",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"html2canvas": "^1.4.1",
|
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
|
@ -3361,15 +3360,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
|
@ -4058,15 +4048,6 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
|
|
@ -4909,19 +4890,6 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
|
@ -6933,15 +6901,6 @@
|
||||||
"tailwindcss": ">=3.0.0 || insiders"
|
"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": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
|
|
@ -7165,15 +7124,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/vaul": {
|
||||||
"version": "0.9.9",
|
"version": "0.9.9",
|
||||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
|
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"html2canvas": "^1.4.1",
|
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ import GridTilingPuzzlePage from './pages/GridTilingPuzzlePage';
|
||||||
import SunnyLinesPuzzlePage from './pages/SunnyLinesPuzzlePage';
|
import SunnyLinesPuzzlePage from './pages/SunnyLinesPuzzlePage';
|
||||||
import SikiniaParliamentPuzzlePage from './pages/SikiniaParliamentPuzzlePage';
|
import SikiniaParliamentPuzzlePage from './pages/SikiniaParliamentPuzzlePage';
|
||||||
import NQueensPuzzlePage from './pages/NQueensPuzzlePage';
|
import NQueensPuzzlePage from './pages/NQueensPuzzlePage';
|
||||||
import DogsBunnyPuzzlePage from './pages/DogsBunnyPuzzlePage';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|
@ -105,7 +104,6 @@ const App = () => (
|
||||||
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
||||||
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
|
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
|
||||||
<Route path="/puzzles/n-queens" element={<NQueensPuzzlePage />} />
|
<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" element={<ZeckendorfGamePage />} />
|
||||||
<Route path="/discrete-math/foundations/zeckendorf-search" element={<ZeckendorfSearchTrickPage />} />
|
<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,
|
difficulty: "Intermediate" as const,
|
||||||
duration: "5-15 min",
|
duration: "5-15 min",
|
||||||
participants: "1 player"
|
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