Reverted to commit 81f6873c4d

This commit is contained in:
gpt-engineer-app[bot] 2025-08-10 05:29:14 +00:00
parent 61feaaf9e9
commit c257a5f531
6 changed files with 1 additions and 553 deletions

View file

@ -48,7 +48,7 @@ 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();
const App = () => (
@ -103,7 +103,6 @@ const App = () => (
<Route path="/puzzles/sikinia-parliament" element={<SikiniaParliamentPuzzlePage />} />
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
<Route path="/puzzles/dogs-bunny" element={<DogsBunnyPuzzlePage />} />
<Route path="/puzzles/n-queens" element={<NQueensPuzzlePage />} />
<Route path="/discrete-math/foundations/zeckendorf" element={<ZeckendorfGamePage />} />
<Route path="/discrete-math/foundations/zeckendorf-search" element={<ZeckendorfSearchTrickPage />} />

View file

@ -1,497 +0,0 @@
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<T>(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<string, Node>, 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<number>();
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<string, Edge[]>();
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<NodeType, string> = {
bone: "🦴",
carrot: "🥕",
flower: "🌸",
well: "🪣",
blank: "⬤",
};
// Component
const DogsBunnyPuzzle: React.FC = () => {
const { toast } = useToast();
const containerRef = useRef<HTMLDivElement | null>(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<Difficulty>(urlDifficulty || "easy");
const [spec, setSpec] = useState<PuzzleSpec>(() => {
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<string, Edge[]>();
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<string[]>(spec.start.bunnies);
const [selected, setSelected] = useState<{ who: "dog" | number; nodeId: string } | null>(null);
const [solution, setSolution] = useState<string[] | null>(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 (
<g key={n.id} onClick={onSelect} className="cursor-pointer">
<circle cx={n.x} cy={n.y} r={32} className={cn("fill-background stroke-border", isSelected && "ring-2 ring-primary fill-accent")} />
<text x={n.x} y={n.y - 20} textAnchor="middle" className="fill-muted-foreground text-[10px] select-none">
{n.type}
</text>
<text x={n.x} y={n.y + 4} textAnchor="middle" className="text-xl select-none">
{ICON[n.type]}
</text>
{isDog && (
<text x={n.x - 18} y={n.y + 26} textAnchor="middle" className="select-none">🐶</text>
)}
{bunnyCount > 0 && (
<text x={n.x + 10} y={n.y + 26} textAnchor="middle" className="select-none">{"🐰".repeat(Math.min(3, bunnyCount))}</text>
)}
{atGoal && (
<text x={n.x} y={n.y - 38} textAnchor="middle" className="fill-primary text-xs">Goal!</text>
)}
</g>
);
};
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 (
<g key={e.id}>
<line x1={a.x} y1={a.y} x2={endx} y2={endy} className={cn("stroke-border", active ? "stroke-primary" : "opacity-60")} markerEnd="url(#arrow)" />
<foreignObject x={midx - 70} y={midy - 14} width={140} height={28}>
<div className={cn("px-1 py-0.5 text-[10px] rounded-md border", active ? "border-primary text-foreground" : "border-muted text-muted-foreground opacity-70")}
title={e.conditions.map((c) => `${c.kind === "somebodyAt" ? "somebody" : "nobody"} at ${c.type}`).join(" AND ")}>
{e.conditions.map((c, i) => (
<span key={i} className="mr-1">
{c.kind === "somebodyAt" ? "∃" : "∄"} {ICON[c.type]}
{i < e.conditions.length - 1 ? " ∧" : ""}
</span>
))}
</div>
</foreignObject>
</g>
);
};
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 (
<section className="space-y-4">
<header className="flex items-center justify-between gap-2">
<h1 className="text-2xl font-bold">Dogs & Bunny Puzzle</h1>
<div className="flex items-center gap-2">
<select
aria-label="Difficulty"
className="h-10 rounded-md border bg-background px-3"
value={difficulty}
onChange={(e) => regenerate(e.target.value as Difficulty)}
>
<option value="easy">Easy</option>
<option value="medium">Medium</option>
<option value="hard">Hard</option>
</select>
<Button variant="secondary" onClick={() => regenerate(difficulty)} title="New puzzle"><Shuffle className="mr-1" />New</Button>
<Button variant="outline" onClick={reset} title="Reset">Reset</Button>
<Button variant="outline" onClick={computeSolution} title="Try solve">Solve</Button>
<Button variant="outline" onClick={copyLink} title="Copy shareable link"><Copy className="mr-1" />Link</Button>
<Button variant="default" onClick={exportPng} title="Export PNG"><Download className="mr-1" />PNG</Button>
</div>
</header>
<Card className="p-2" ref={containerRef}>
<svg width={size.w} height={size.h} viewBox={`0 0 ${size.w} ${size.h}`}>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" className="fill-muted-foreground" />
</marker>
</defs>
{/* edges first */}
{spec.edges.map(edgeEl)}
{/* nodes on top */}
{spec.nodes.map(nodeEl)}
</svg>
</Card>
<div className="grid md:grid-cols-2 gap-4">
<Card className="p-4">
<h2 className="font-semibold mb-2">How to Play</h2>
<ul className="list-disc pl-5 text-sm text-muted-foreground space-y-1">
<li>Click the dog or a bunny to see available moves; click a destination node to move along active edges.</li>
<li>Edges are active only if all shown conditions are currently true.</li>
<li>Goal: move the dog to any bone node.</li>
</ul>
</Card>
<Card className="p-4">
<h2 className="font-semibold mb-2">Share & Export</h2>
<p className="text-sm text-muted-foreground mb-2">Copy a link to share the exact puzzle. Export a PNG snapshot of the puzzle or your current solution.</p>
<SocialShare title="Dogs & Bunny Puzzle" description="A cute logic puzzle with dependencies" url={shareUrl} />
</Card>
</div>
{solution && (
<Card className="p-4">
<h2 className="font-semibold mb-2">Solver Output</h2>
{solution ? (
<ol className="list-decimal pl-5 text-sm text-muted-foreground space-y-1">
{solution.map((s, i) => (
<li key={i}>{s}</li>
))}
</ol>
) : (
<p className="text-sm text-muted-foreground">No solution found.</p>
)}
</Card>
)}
</section>
);
};
export default DogsBunnyPuzzle;

View file

@ -1,17 +0,0 @@
import React from "react";
import Layout from "@/components/Layout";
import DogsBunnyPuzzle from "@/components/DogsBunnyPuzzle";
const DogsBunnyPuzzlePage: React.FC = () => {
return (
<Layout>
<main className="py-10 px-4">
<div className="max-w-6xl mx-auto space-y-4">
<DogsBunnyPuzzle />
</div>
</main>
</Layout>
);
};
export default DogsBunnyPuzzlePage;

View file

@ -55,16 +55,6 @@ const Puzzles = () => {
difficulty: "Intermediate" as const,
duration: "5-15 min",
participants: "1 player"
},
{
id: "dogs-bunny",
title: "Dogs & Bunny Puzzle",
description: "Move the dog to a bone by navigating edges whose conditions depend on where animals are.",
path: "/puzzles/dogs-bunny",
tags: ["Logic", "Graph", "Dependencies"],
difficulty: "Intermediate" as const,
duration: "5-20 min",
participants: "1 player"
}
];