diff --git a/package-lock.json b/package-lock.json index 316d834..7e686ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5e0f581..50578b1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 4361b25..fadaae4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 = () => ( } /> } /> } /> - } /> } /> } /> diff --git a/src/components/DogsBunnyPuzzle.tsx b/src/components/DogsBunnyPuzzle.tsx deleted file mode 100644 index a63ec1c..0000000 --- a/src/components/DogsBunnyPuzzle.tsx +++ /dev/null @@ -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 = ({ - difficulty = 'medium', - seed, - onComplete -}) => { - const [puzzleState, setPuzzleState] = useState(null); - const [selectedCharacter, setSelectedCharacter] = useState(null); - const [validMoves, setValidMoves] = useState([]); - - // 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
Loading...
; - - return ( -
- - -
-
- - Dogs & Bunny Puzzle - {puzzleState.isComplete && } - -

- Get all bunnies to the carrot 🥕 and the dog to the bone 🦴 -

-
-
- {difficulty} - Moves: {puzzleState.moveCount} -
-
-
- - - {/* Puzzle Area - Full Width */} -
- {/* Edges */} - - {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 ( - - - {/* Condition labels - only show if edge has conditions */} - {edge.conditions.length > 0 && ( - - {edge.conditions.length === 1 ? ( - // Single condition - - ) : ( - // Multiple conditions - show as rounded rectangle - c.includes('NOBODY')) ? "#ef4444" : "hsl(var(--border))"} - strokeWidth="2" - /> - )} - - {getConditionIcon(edge.conditions[0])} - - {edge.conditions.length > 1 && ( - - +{edge.conditions.length - 1} - - )} - {edge.conditions.map(formatCondition).join(' AND ')} - - )} - - ); - })} - - - {/* Locations */} - {puzzleState.locations.map((location) => { - const charactersHere = puzzleState.characters.filter(c => c.locationId === location.id); - const isValidMove = validMoves.includes(location.id); - - return ( -
isValidMove && handleMove(location.id)} - > - - {getLocationIcon(location.type)} - - {/* Characters at this location */} - {charactersHere.map((char, index) => ( -
{ - e.stopPropagation(); - handleCharacterSelect(char.id); - }} - > - - {getCharacterIcon(char.type)} - -
- ))} -
- ); - })} -
- - {/* Instructions & Controls - Below the graph */} -
-
-
-

How to Play:

-
    -
  • • Click a character to select it
  • -
  • • Click a highlighted location to move
  • -
  • • Edges show movement conditions
  • -
  • • Multiple animals can share locations
  • -
-
- - - -
-

Legend:

-
-
🐰 Bunny
-
🐕 Dog
-
🥕 Carrot
-
🦴 Bone
-
🌸 Flower
-
🪣 Well
-
-
- -
- -
- - - - -
-
-
-
-
- ); -}; - -export default DogsBunnyPuzzle; \ No newline at end of file diff --git a/src/pages/DogsBunnyPuzzlePage.tsx b/src/pages/DogsBunnyPuzzlePage.tsx deleted file mode 100644 index 5ddbd27..0000000 --- a/src/pages/DogsBunnyPuzzlePage.tsx +++ /dev/null @@ -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(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 ( - -
-
- {/* Header */} -
-
- -
-

Dogs & Bunny Puzzle

-

- A logic puzzle about getting animals to their favorite spots -

-
-
- -
-
- Difficulty: - -
-
-
- - {/* Instructions */} -
-

Puzzle Rules

-
-
-

Objective:

-
    -
  • • Get all bunnies 🐰 to the carrot 🥕
  • -
  • • Get the dog 🐕 to the bone 🦴
  • -
-
-
-

Movement Rules:

-
    -
  • • Click a character to select it
  • -
  • • You can only move along edges where all conditions are met
  • -
  • • Multiple animals can occupy the same location
  • -
  • • Edge labels show the required conditions
  • -
-
-
-
- - {/* Puzzle Component */} -
- { - // Could add achievements or stats here - }} - /> -
- - {/* Additional Info */} -
-

- Original puzzle concept by - Conrad Barski (@lisperati) - -

-
-
-
-
- ); -}; - -export default DogsBunnyPuzzlePage; \ No newline at end of file diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index ba587b3..3cf3896 100644 --- a/src/pages/theme-pages/Puzzles.tsx +++ b/src/pages/theme-pages/Puzzles.tsx @@ -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" } ];