This commit is contained in:
gpt-engineer-app[bot] 2026-01-20 08:47:28 +00:00
parent abbfb79cea
commit 2c3acba1d2

View file

@ -1,4 +1,4 @@
import React, { useState, useCallback } from "react"; import React, { useState, useCallback, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
@ -148,37 +148,120 @@ const findBorderPath = (from: Position, to: Position): Position[] => {
return path1.length <= path2.length ? path1 : path2; return path1.length <= path2.length ? path1 : path2;
}; };
// BFS to find shortest path between two positions using hex neighbors // === Pattern + simultaneous-move defense (no teleporting) =====================
const findShortestPath = (from: Position, to: Position, guards: Set<string>): Position[] => {
const queue: Position[][] = [[from]];
const visited = new Set<string>([posKey(from)]);
while (queue.length > 0) { // Interior configurations come in 4 residue classes.
const path = queue.shift()!; // We model the paper's “restore configuration via border patchwork” as:
const current = path[path.length - 1]; // after each attack we move (simultaneously) to the interior residue class that
// contains the attacked vertex, while keeping the entire border guarded.
const interiorResidue = (p: Position): number => ((p.x + 2 * p.y) % 4 + 4) % 4;
if (current.x === to.x && current.y === to.y) { const getInteriorPatternByResidue = (r: number): Set<string> => {
return path; const pattern = new Set<string>();
} for (let x = 1; x < COLS - 1; x++) {
for (let y = 2; y < ROWS - 2; y++) {
for (const neighbor of getHexNeighbors(current.x, current.y)) { if (interiorResidue({ x, y }) === r) pattern.add(posKey({ x, y }));
const key = posKey(neighbor);
if (!visited.has(key)) {
visited.add(key);
queue.push([...path, neighbor]);
} }
} }
} return pattern;
return [];
}; };
// Defense strategy following Figure 10 exactly: const getAllBorderKeys = (): Set<string> => {
// 1. Interior guards shift in chains toward the attack (v_t) const border = new Set<string>();
// 2. Two interior guards exit to border (u₁→w₁, u₂→w₂) for (let x = 0; x < COLS; x++) {
// 3. Two border guards enter interior (w₃→u₃, w₄→u₄) for (let y = 0; y < ROWS; y++) {
// 4. Border guards shift along complementary paths P₁,₃ and P₂,₄ if (isBorder(x, y)) border.add(posKey({ x, y }));
const defendAttack = (guards: Set<string>, attack: Position): { }
}
return border;
};
const BORDER_KEYS = getAllBorderKeys();
const getTargetGuardsForResidue = (r: number): Set<string> => {
const target = new Set<string>(BORDER_KEYS);
for (const k of getInteriorPatternByResidue(r)) target.add(k);
return target;
};
const inferCurrentInteriorResidue = (guards: Set<string>): number => {
for (const k of guards) {
const p = parseKey(k);
if (isInterior(p.x, p.y)) return interiorResidue(p);
}
// Should never happen (we always have interior guards), but default safely.
return 0;
};
// HopcroftKarp for perfect matching in the “guards → target cells” bipartite graph.
// Left: current guard indices. Right: target indices.
const hopcroftKarp = (adj: number[][], leftSize: number, rightSize: number) => {
const NIL = -1;
const pairU = new Array<number>(leftSize).fill(NIL);
const pairV = new Array<number>(rightSize).fill(NIL);
const dist = new Array<number>(leftSize).fill(0);
const bfs = (): boolean => {
const q: number[] = [];
for (let u = 0; u < leftSize; u++) {
if (pairU[u] === NIL) {
dist[u] = 0;
q.push(u);
} else {
dist[u] = Number.POSITIVE_INFINITY;
}
}
let foundFreeVertex = false;
while (q.length) {
const u = q.shift()!;
for (const v of adj[u]) {
const u2 = pairV[v];
if (u2 !== NIL) {
if (dist[u2] === Number.POSITIVE_INFINITY) {
dist[u2] = dist[u] + 1;
q.push(u2);
}
} else {
foundFreeVertex = true;
}
}
}
return foundFreeVertex;
};
const dfs = (u: number): boolean => {
for (const v of adj[u]) {
const u2 = pairV[v];
if (u2 === NIL || (dist[u2] === dist[u] + 1 && dfs(u2))) {
pairU[u] = v;
pairV[v] = u;
return true;
}
}
dist[u] = Number.POSITIVE_INFINITY;
return false;
};
let matching = 0;
while (bfs()) {
for (let u = 0; u < leftSize; u++) {
if (pairU[u] === NIL && dfs(u)) matching++;
}
}
return { matching, pairU };
};
// Defense = one simultaneous move of all guards (each guard moves at most 1 step).
// We compute the intended post-defense configuration (target) and then find a
// perfect matching that assigns each current guard to a unique reachable target.
const defendAttack = (
guards: Set<string>,
attack: Position
): {
newGuards: Set<string>; newGuards: Set<string>;
movements: Movement[]; movements: Movement[];
} | null => { } | null => {
@ -188,158 +271,83 @@ const defendAttack = (guards: Set<string>, attack: Position): {
return { newGuards: guards, movements: [] }; return { newGuards: guards, movements: [] };
} }
// Must be defendable: some guard adjacent to v_t.
const neighbors = getHexNeighbors(attack.x, attack.y); const neighbors = getHexNeighbors(attack.x, attack.y);
const adjacentGuards = neighbors.filter(p => guards.has(posKey(p))); if (!neighbors.some((p) => guards.has(posKey(p)))) return null;
if (adjacentGuards.length === 0) { const currentResidue = inferCurrentInteriorResidue(guards);
const targetResidue = interiorResidue(attack);
// Target config: same “family” of configuration, but switch residue class so that
// the attacked vertex becomes a guard position again.
const target = getTargetGuardsForResidue(targetResidue);
if (!target.has(attackKey)) {
// Should never happen by construction.
return null; return null;
} }
const newGuards = new Set(guards); // Build bipartite graph: each guard can go to {self hex-neighbors} ∩ target.
const fromKeys = Array.from(guards);
const toKeys = Array.from(target);
if (fromKeys.length !== toKeys.length) return null;
const toIndex = new Map<string, number>();
for (let i = 0; i < toKeys.length; i++) toIndex.set(toKeys[i], i);
const adj: number[][] = new Array(fromKeys.length);
for (let i = 0; i < fromKeys.length; i++) {
const from = parseKey(fromKeys[i]);
const reachable: string[] = [posKey(from), ...getHexNeighbors(from.x, from.y).map(posKey)];
const edges: number[] = [];
for (const k of reachable) {
const idx = toIndex.get(k);
if (idx !== undefined) edges.push(idx);
}
// Deterministic order helps keep the visual behavior stable.
edges.sort((a, b) => a - b);
adj[i] = edges;
}
const { matching, pairU } = hopcroftKarp(adj, fromKeys.length, toKeys.length);
if (matching !== fromKeys.length) return null;
const movements: Movement[] = []; const movements: Movement[] = [];
for (let u = 0; u < fromKeys.length; u++) {
const v = pairU[u];
if (v === -1) return null;
const from = parseKey(fromKeys[u]);
const to = parseKey(toKeys[v]);
if (from.x !== to.x || from.y !== to.y) {
const fromBorder = isBorder(from.x, from.y);
const toBorder = isBorder(to.x, to.y);
// STEP 1: Find interior guard chains that will shift toward attack let type: Movement['type'] = 'interior';
// In Figure 10, the arrows show guards shifting in diagonal lines toward v_t if (fromBorder && !toBorder) type = 'toInterior';
else if (!fromBorder && toBorder) type = 'toBorder';
else if (fromBorder && toBorder) type = 'pathShift';
// Find an interior guard adjacent to the attack to move there movements.push({ from, to, type });
let primaryDefender: Position | null = null;
for (const g of adjacentGuards) {
if (isInterior(g.x, g.y)) {
primaryDefender = g;
break;
} }
} }
// If attack is on border or no interior guard adjacent, use border guard // Safety invariants: no creation / no disappearance.
if (!primaryDefender) { if (target.size !== guards.size) return null;
primaryDefender = adjacentGuards[0];
newGuards.delete(posKey(primaryDefender));
newGuards.add(attackKey);
movements.push({ from: primaryDefender, to: attack, type: isBorder(primaryDefender.x, primaryDefender.y) ? 'toBorder' : 'interior' });
// If we moved a border guard, shift along border to fill gap // Theorem-backed invariant: target configuration is dominating.
if (isBorder(primaryDefender.x, primaryDefender.y)) { // If our modeling is wrong, fail loudly rather than “teleport”.
shiftBorderToFillGap(newGuards, movements, primaryDefender); if (!isFullyDominated(target)) return null;
}
return { newGuards, movements }; // Additional sanity: everybody moved at most one step.
} // (This should be guaranteed by the edge construction.)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _unused = currentResidue;
// STEP 2: Shift interior guards in a chain toward the attack return { newGuards: target, movements };
// This mimics the diagonal arrows in Figure 10
// Move primary defender to attack
newGuards.delete(posKey(primaryDefender));
newGuards.add(attackKey);
movements.push({ from: primaryDefender, to: attack, type: 'interior' });
// Now we need to fill the gap left by the primary defender
// Find another interior guard that can shift into that position
let currentGap = primaryDefender;
let chainLength = 0;
const maxChain = 8;
while (chainLength < maxChain && isInterior(currentGap.x, currentGap.y)) {
const gapNeighbors = getHexNeighbors(currentGap.x, currentGap.y);
let filler: Position | null = null;
// Look for an interior guard to shift into the gap
for (const n of gapNeighbors) {
if (isInterior(n.x, n.y) && newGuards.has(posKey(n))) {
// Prefer guards that are further from the target pattern
filler = n;
break;
}
}
if (filler) {
newGuards.delete(posKey(filler));
newGuards.add(posKey(currentGap));
movements.push({ from: filler, to: currentGap, type: 'interior' });
currentGap = filler;
chainLength++;
} else {
// No interior guard to fill - need border guard (w₃→u₃ movement)
const borderFiller = gapNeighbors.find(n =>
isBorder(n.x, n.y) && newGuards.has(posKey(n))
);
if (borderFiller) {
newGuards.delete(posKey(borderFiller));
newGuards.add(posKey(currentGap));
movements.push({ from: borderFiller, to: currentGap, type: 'toInterior' });
// Now fill the border gap (this creates the need for P₁,₃ or P₂,₄)
shiftBorderToFillGap(newGuards, movements, borderFiller);
}
break;
}
}
// If chain ended at border (interior guard pushed to border = u₁→w₁)
if (isBorder(currentGap.x, currentGap.y) && !newGuards.has(posKey(currentGap))) {
// The gap is on border, needs to be filled by border shift
shiftBorderToFillGap(newGuards, movements, currentGap);
}
// NOTE: The paper's strategy restores the original configuration via the border "patchwork"
// (complementary paths) rather than ad-hoc re-domination fixes.
//
// The previous implementation included an extra "scan for undominated vertices and move a nearby guard"
// step, which can look like guards "manifest" and is not part of the Figure 10 defense.
// Safety: guards are never created/destroyed—only moved.
if (newGuards.size !== guards.size) return null;
return { newGuards, movements };
}; };
// Shift border guards along the cycle to fill a gap (complementary path shifting)
const shiftBorderToFillGap = (
guards: Set<string>,
movements: Movement[],
gap: Position
): void => {
if (guards.has(posKey(gap))) return; // No gap
// Find adjacent border guard to shift into the gap
const gapNeighbors = getHexNeighbors(gap.x, gap.y);
// Find a border guard that can shift in and has a backup
for (const n of gapNeighbors) {
if (isBorder(n.x, n.y) && guards.has(posKey(n))) {
// Check if this guard has another border neighbor that could back it up
const nNeighbors = getHexNeighbors(n.x, n.y);
const hasBackup = nNeighbors.some(nn =>
isBorder(nn.x, nn.y) &&
guards.has(posKey(nn)) &&
(nn.x !== gap.x || nn.y !== gap.y)
);
if (hasBackup) {
guards.delete(posKey(n));
guards.add(posKey(gap));
movements.push({ from: n, to: gap, type: 'pathShift' });
// Recursively fill the new gap
if (!guards.has(posKey(n))) {
shiftBorderToFillGap(guards, movements, n);
}
return;
}
}
}
// If no backup found, just do the shift anyway (maintaining domination)
for (const n of gapNeighbors) {
if (isBorder(n.x, n.y) && guards.has(posKey(n))) {
guards.delete(posKey(n));
guards.add(posKey(gap));
movements.push({ from: n, to: gap, type: 'pathShift' });
return;
}
}
};
const EternalDominationGame: React.FC = () => { const EternalDominationGame: React.FC = () => {
const [guards, setGuards] = useState<Set<string>>(generateInitialGuards); const [guards, setGuards] = useState<Set<string>>(generateInitialGuards);
@ -349,7 +357,63 @@ const EternalDominationGame: React.FC = () => {
const [showInfo, setShowInfo] = useState(false); const [showInfo, setShowInfo] = useState(false);
const [lastMovements, setLastMovements] = useState<Movement[]>([]); const [lastMovements, setLastMovements] = useState<Movement[]>([]);
const handleCellClick = useCallback((x: number, y: number) => { const [pendingDefense, setPendingDefense] = useState<{
endGuards: Set<string>;
movements: Movement[];
summary: { interiorMoves: number; borderIn: number; borderOut: number; pathShifts: number };
} | null>(null);
const [activeMoveIdx, setActiveMoveIdx] = useState<number>(-1);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const activeMove =
pendingDefense && activeMoveIdx >= 0 && activeMoveIdx < pendingDefense.movements.length
? pendingDefense.movements[activeMoveIdx]
: null;
useEffect(() => {
if (!pendingDefense) return;
// No moves? Apply immediately.
if (pendingDefense.movements.length === 0) {
setGuards(pendingDefense.endGuards);
setPendingDefense(null);
setActiveMoveIdx(-1);
return;
}
if (activeMoveIdx >= pendingDefense.movements.length) {
// Finish: apply the simultaneous move.
setGuards(pendingDefense.endGuards);
setLastMovements(pendingDefense.movements);
const { interiorMoves, borderIn, pathShifts } = pendingDefense.summary;
let desc = `Defended! Interior shifts: ${interiorMoves}`;
if (borderIn > 0) desc += `, w→u: ${borderIn}`;
if (pathShifts > 0) desc += `, path shifts: ${pathShifts}`;
setMessage(desc);
setPendingDefense(null);
setActiveMoveIdx(-1);
return;
}
// Show one movement at a time (visualizing a simultaneous sweep).
setLastMovements([pendingDefense.movements[activeMoveIdx]]);
setMessage(`Defending… step ${activeMoveIdx + 1}/${pendingDefense.movements.length}`);
timerRef.current = setTimeout(() => {
setActiveMoveIdx((i) => i + 1);
}, 220);
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [pendingDefense, activeMoveIdx]);
const handleCellClick = useCallback(
(x: number, y: number) => {
if (pendingDefense) return; // ignore clicks during animation
const attack: Position = { x, y }; const attack: Position = { x, y };
if (guards.has(posKey(attack))) { if (guards.has(posKey(attack))) {
@ -359,31 +423,34 @@ const EternalDominationGame: React.FC = () => {
const result = defendAttack(guards, attack); const result = defendAttack(guards, attack);
if (result) { if (!result) {
setGuards(result.newGuards); setMessage("Cannot defend under the rules (no legal simultaneous move found). ");
setLastMovements(result.movements); return;
setAttackHistory(prev => [...prev, attack]);
setMoveCount(prev => prev + 1);
const interiorMoves = result.movements.filter(m => m.type === 'interior').length;
const borderIn = result.movements.filter(m => m.type === 'toInterior').length;
const borderOut = result.movements.filter(m => m.type === 'toBorder').length;
const pathShifts = result.movements.filter(m => m.type === 'pathShift').length;
if (isFullyDominated(result.newGuards)) {
let desc = `Defended! Interior shifts: ${interiorMoves}`;
if (borderIn > 0) desc += `, w→u: ${borderIn}`;
if (pathShifts > 0) desc += `, path shifts: ${pathShifts}`;
setMessage(desc);
} else {
setMessage("Defense incomplete - some vertices undominated!");
} }
} else {
setMessage("Cannot defend - no adjacent guards!"); const interiorMoves = result.movements.filter((m) => m.type === "interior").length;
} const borderIn = result.movements.filter((m) => m.type === "toInterior").length;
}, [guards]); const borderOut = result.movements.filter((m) => m.type === "toBorder").length;
const pathShifts = result.movements.filter((m) => m.type === "pathShift").length;
setAttackHistory((prev) => [...prev, attack]);
setMoveCount((prev) => prev + 1);
setPendingDefense({
endGuards: result.newGuards,
movements: result.movements,
summary: { interiorMoves, borderIn, borderOut, pathShifts },
});
setActiveMoveIdx(0);
},
[guards, pendingDefense]
);
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current);
setPendingDefense(null);
setActiveMoveIdx(-1);
setGuards(generateInitialGuards()); setGuards(generateInitialGuards());
setAttackHistory([]); setAttackHistory([]);
setLastMovements([]); setLastMovements([]);
@ -392,7 +459,7 @@ const EternalDominationGame: React.FC = () => {
}, []); }, []);
const guardCount = guards.size; const guardCount = guards.size;
const interiorGuards = Array.from(guards).filter(k => { const interiorGuards = Array.from(guards).filter((k) => {
const p = parseKey(k); const p = parseKey(k);
return isInterior(p.x, p.y); return isInterior(p.x, p.y);
}).length; }).length;