import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { useToast } from "@/components/ui/use-toast"; import { Share2, Download, Shuffle, Copy, Link as LinkIcon } from "lucide-react"; import * as htmlToImage from "html-to-image"; import { saveAs } from "file-saver"; import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string"; import SocialShare from "@/components/SocialShare"; import { cn } from "@/lib/utils"; // Types export type NodeType = "bone" | "flower" | "carrot" | "well" | "blank"; interface Node { id: string; type: NodeType; x: number; y: number; } type EdgeCond = { kind: "somebodyAt" | "nobodyAt"; type: NodeType }; interface Edge { id: string; from: string; to: string; conditions: EdgeCond[]; } interface PuzzleSpec { nodes: Node[]; edges: Edge[]; start: { dog: string; bunnies: string[] }; goalType: NodeType; // typically "bone" difficulty: Difficulty; } type Difficulty = "easy" | "medium" | "hard"; // Helpers const NODE_TYPES: NodeType[] = ["bone", "flower", "carrot", "well", "blank"]; function randInt(n: number) { return Math.floor(Math.random() * n); } function pickOne(arr: T[]): T { return arr[randInt(arr.length)]; } function uid(prefix = "id"): string { return `${prefix}-${Math.random().toString(36).slice(2, 8)}`; } function serializeState(dog: string, bunnies: string[]) { const sorted = [...bunnies].sort(); return `${dog}|${sorted.join(',')}`; } function conditionsSatisfied(conds: EdgeCond[], nodesById: Record, entitiesAtType: (t: NodeType) => number) { return conds.every((c) => { const count = entitiesAtType(c.type); if (c.kind === "somebodyAt") return count > 0; if (c.kind === "nobodyAt") return count === 0; return true; }); } function layoutCircle(n: number, radius = 220) { const angleStep = (2 * Math.PI) / n; const centerX = 360; const centerY = 300; return Array.from({ length: n }).map((_, i) => ({ x: centerX + radius * Math.cos(i * angleStep - Math.PI / 2), y: centerY + radius * Math.sin(i * angleStep - Math.PI / 2), })); } function genPuzzle(difficulty: Difficulty): PuzzleSpec { const params = { easy: { nNodes: 7, nBunnies: 2, outDeg: [1, 2], condsPerEdge: [1, 1], maxSteps: 12 }, medium: { nNodes: 9, nBunnies: 3, outDeg: [1, 3], condsPerEdge: [1, 2], maxSteps: 20 }, hard: { nNodes: 11, nBunnies: 4, outDeg: [2, 3], condsPerEdge: [2, 3], maxSteps: 30 }, }[difficulty]; // Nodes const positions = layoutCircle(params.nNodes); const nodes: Node[] = positions.map((p, i) => ({ id: `n${i}`, type: pickOne(NODE_TYPES), x: p.x, y: p.y, })); // ensure at least one bone node if (!nodes.some((n) => n.type === "bone")) nodes[randInt(nodes.length)].type = "bone"; // Edges const edges: Edge[] = []; for (let i = 0; i < nodes.length; i++) { const outMin = params.outDeg[0]; const outMax = params.outDeg[1]; const outCount = outMin + randInt(outMax - outMin + 1); const targets = new Set(); while (targets.size < outCount) { const t = randInt(nodes.length); if (t !== i) targets.add(t); } [...targets].forEach((t) => { const condCount = params.condsPerEdge[0] + randInt(params.condsPerEdge[1] - params.condsPerEdge[0] + 1); const conditions: EdgeCond[] = []; for (let k = 0; k < condCount; k++) { const kind = Math.random() < 0.6 ? "somebodyAt" : "nobodyAt"; const type = pickOne(NODE_TYPES); conditions.push({ kind, type }); } edges.push({ id: uid("e"), from: nodes[i].id, to: nodes[t].id, conditions }); }); } // Start positions const boneNodes = nodes.filter((n) => n.type === "bone"); const startDog = pickOne(nodes).id; const bunnies: string[] = []; for (let i = 0; i < params.nBunnies; i++) bunnies.push(pickOne(nodes).id); return { nodes, edges, start: { dog: startDog, bunnies }, goalType: "bone", difficulty, }; } function solve(spec: PuzzleSpec, maxSteps: number): string[] | null { // BFS state space search to get dog to any node of goalType const nodesById = Object.fromEntries(spec.nodes.map((n) => [n.id, n] as const)); const outEdges = new Map(); spec.edges.forEach((e) => { if (!outEdges.has(e.from)) outEdges.set(e.from, []); outEdges.get(e.from)!.push(e); }); const start = { dog: spec.start.dog, bunnies: [...spec.start.bunnies] }; const startKey = serializeState(start.dog, start.bunnies); const q: { dog: string; bunnies: string[]; path: string[] }[] = [{ ...start, path: [] }]; const seen = new Set([startKey]); const entitiesAtType = (dog: string, bunnies: string[]) => (t: NodeType) => { let count = 0; if (nodesById[dog].type === t) count++; bunnies.forEach((b) => { if (nodesById[b].type === t) count++; }); return count; }; for (let depth = 0; depth <= maxSteps && q.length; depth++) { const cur = q.shift(); if (!cur) break; const dogAtGoal = nodesById[cur.dog].type === spec.goalType; if (dogAtGoal) return cur.path; const entAt = entitiesAtType(cur.dog, cur.bunnies); // Moves: try moving dog and each bunny along available edges const candidates: Array<{ who: "dog" | number; from: string }> = [ { who: "dog", from: cur.dog }, ...cur.bunnies.map((id, idx) => ({ who: idx, from: id })), ]; for (const c of candidates) { const outs = outEdges.get(c.from) || []; for (const e of outs) { if (!conditionsSatisfied(e.conditions, nodesById, entAt)) continue; const nextDog = c.who === "dog" ? e.to : cur.dog; const nextBunnies = cur.bunnies.map((b, i) => (c.who === i ? e.to : b)); const key = serializeState(nextDog, nextBunnies); if (seen.has(key)) continue; seen.add(key); q.push({ dog: nextDog, bunnies: nextBunnies, path: [...cur.path, `${c.who === "dog" ? "Dog" : `Bunny ${c.who + 1}`}: ${c.from}→${e.to}`] }); } } } return null; } function ensureSolvable(difficulty: Difficulty): PuzzleSpec { const limits = { easy: 12, medium: 20, hard: 30 } as const; for (let attempts = 0; attempts < 40; attempts++) { const candidate = genPuzzle(difficulty); const sol = solve(candidate, limits[difficulty]); if (sol) return candidate; } // Fallback — even if unsolved after attempts, return a simple hand-crafted tiny solvable graph const nodes: Node[] = layoutCircle(6).map((p, i) => ({ id: `n${i}`, type: i === 3 ? "bone" : pickOne(NODE_TYPES), x: p.x, y: p.y })); nodes[3].type = "bone"; const edges: Edge[] = [ { id: uid("e"), from: "n0", to: "n1", conditions: [{ kind: "somebodyAt", type: "flower" }] }, { id: uid("e"), from: "n1", to: "n2", conditions: [{ kind: "somebodyAt", type: "carrot" }] }, { id: uid("e"), from: "n2", to: "n3", conditions: [{ kind: "nobodyAt", type: "well" }] }, { id: uid("e"), from: "n4", to: "n3", conditions: [{ kind: "somebodyAt", type: "carrot" }] }, ]; return { nodes, edges, start: { dog: "n0", bunnies: ["n4"] }, goalType: "bone", difficulty }; } function encodeSpec(spec: PuzzleSpec) { return compressToEncodedURIComponent(JSON.stringify(spec)); } function decodeSpec(s: string): PuzzleSpec | null { try { const txt = decompressFromEncodedURIComponent(s); if (!txt) return null; return JSON.parse(txt) as PuzzleSpec; } catch { return null; } } // Icons for node types (simple emoji for now to keep it lightweight) const ICON: Record = { bone: "🦴", carrot: "🥕", flower: "🌸", well: "🪣", blank: "⬤", }; // Component const DogsBunnyPuzzle: React.FC = () => { const { toast } = useToast(); const containerRef = useRef(null); const urlParam = new URLSearchParams(typeof window !== "undefined" ? window.location.search : ""); const paramPuzzle = urlParam.get("p"); const urlDifficulty = (urlParam.get("d") as Difficulty) || undefined; const [difficulty, setDifficulty] = useState(urlDifficulty || "easy"); const [spec, setSpec] = useState(() => { const decoded = paramPuzzle ? decodeSpec(paramPuzzle) : null; return decoded || ensureSolvable(urlDifficulty || "easy"); }); const nodesById = useMemo(() => Object.fromEntries(spec.nodes.map((n) => [n.id, n] as const)), [spec.nodes]); const outEdges = useMemo(() => { const m = new Map(); spec.edges.forEach((e) => { if (!m.has(e.from)) m.set(e.from, []); m.get(e.from)!.push(e); }); return m; }, [spec.edges]); const [dog, setDog] = useState(spec.start.dog); const [bunnies, setBunnies] = useState(spec.start.bunnies); const [selected, setSelected] = useState<{ who: "dog" | number; nodeId: string } | null>(null); const [solution, setSolution] = useState(null); useEffect(() => { document.title = "Dogs & Bunny Puzzle – Interactive"; const metaDesc = document.querySelector('meta[name="description"]'); if (metaDesc) metaDesc.setAttribute("content", "Play the Dogs & Bunny puzzle. Generate random solvable puzzles, share via URL, and export screenshots."); }, []); const entAtType = useCallback( (t: NodeType) => { let count = nodesById[dog].type === t ? 1 : 0; bunnies.forEach((b) => { if (nodesById[b].type === t) count++; }); return count; }, [dog, bunnies, nodesById] ); const availableMoves = useMemo(() => { if (!selected) return [] as { to: string; e: Edge }[]; const outs = outEdges.get(selected.nodeId) || []; return outs .filter((e) => conditionsSatisfied(e.conditions, nodesById, entAtType)) .map((e) => ({ to: e.to, e })); }, [selected, outEdges, entAtType, nodesById]); const doMove = (to: string) => { if (!selected) return; if (selected.who === "dog") setDog(to); else setBunnies((bb) => bb.map((b, i) => (i === selected.who ? to : b))); setSelected(null); }; const reset = () => { setDog(spec.start.dog); setBunnies(spec.start.bunnies); setSelected(null); setSolution(null); }; const regenerate = (d: Difficulty) => { const next = ensureSolvable(d); setSpec(next); setDog(next.start.dog); setBunnies(next.start.bunnies); setSelected(null); setSolution(null); setDifficulty(d); // update URL with encoded puzzle for shareability const params = new URLSearchParams(window.location.search); params.set("p", encodeSpec(next)); params.set("d", d); const newUrl = `${window.location.pathname}?${params.toString()}`; window.history.replaceState({}, "", newUrl); }; const copyLink = async () => { try { const encoded = encodeSpec({ ...spec, start: { dog, bunnies } }); const url = new URL(window.location.href); url.searchParams.set("p", encoded); url.searchParams.set("d", difficulty); await navigator.clipboard.writeText(url.toString()); toast({ title: "Link copied", description: "Share this exact puzzle and current state." }); } catch (e) { toast({ title: "Copy failed", description: "Could not copy link.", variant: "destructive" }); } }; const exportPng = async () => { if (!containerRef.current) return; try { const dataUrl = await htmlToImage.toPng(containerRef.current, { pixelRatio: 2, cacheBust: true, backgroundColor: "transparent" }); saveAs(dataUrl, "dogs-bunny-puzzle.png"); } catch (e) { toast({ title: "Export failed", description: "Please try again.", variant: "destructive" }); } }; const computeSolution = () => { const sol = solve({ ...spec, start: { dog, bunnies } }, { easy: 12, medium: 20, hard: 30 }[spec.difficulty]); setSolution(sol); if (!sol) toast({ title: "No solution found", description: "This state might be a dead end." }); }; // Render helpers const size = { w: 720, h: 520 }; const nodeEl = (n: Node) => { const isSelected = selected?.nodeId === n.id; const isDog = dog === n.id; const bunnyCount = bunnies.filter((b) => b === n.id).length; const onSelect = () => { if (isDog) setSelected({ who: "dog", nodeId: n.id }); else if (bunnyCount > 0) { // pick the first bunny at this node (simple UX) const idx = bunnies.findIndex((b) => b === n.id); setSelected({ who: idx as number, nodeId: n.id }); } else setSelected(null); }; const atGoal = nodesById[dog].type === spec.goalType && n.id === dog; return ( {n.type} {ICON[n.type]} {isDog && ( 🐶 )} {bunnyCount > 0 && ( {"🐰".repeat(Math.min(3, bunnyCount))} )} {atGoal && ( Goal! )} ); }; const edgeEl = (e: Edge) => { const a = nodesById[e.from]; const b = nodesById[e.to]; const midx = (a.x + b.x) / 2; const midy = (a.y + b.y) / 2; // arrow line const dx = b.x - a.x; const dy = b.y - a.y; const len = Math.hypot(dx, dy) || 1; const ux = dx / len; const uy = dy / len; const endx = b.x - ux * 34; const endy = b.y - uy * 34; const active = conditionsSatisfied(e.conditions, nodesById, entAtType); return (
`${c.kind === "somebodyAt" ? "somebody" : "nobody"} at ${c.type}`).join(" AND ")}> {e.conditions.map((c, i) => ( {c.kind === "somebodyAt" ? "∃" : "∄"} {ICON[c.type]} {i < e.conditions.length - 1 ? " ∧" : ""} ))}
); }; const puzzleEncoded = useMemo(() => encodeSpec(spec), [spec]); const shareUrl = useMemo(() => { const u = new URL(window.location.href); u.searchParams.set("p", puzzleEncoded); u.searchParams.set("d", spec.difficulty); return u.toString(); }, [puzzleEncoded, spec.difficulty]); return (

Dogs & Bunny Puzzle

{/* edges first */} {spec.edges.map(edgeEl)} {/* nodes on top */} {spec.nodes.map(nodeEl)}

How to Play

  • Click the dog or a bunny to see available moves; click a destination node to move along active edges.
  • Edges are active only if all shown conditions are currently true.
  • Goal: move the dog to any bone node.

Share & Export

Copy a link to share the exact puzzle. Export a PNG snapshot of the puzzle or your current solution.

{solution && (

Solver Output

{solution ? (
    {solution.map((s, i) => (
  1. {s}
  2. ))}
) : (

No solution found.

)}
)}
); }; export default DogsBunnyPuzzle;