diff --git a/src/components/EternalDominationGame.tsx b/src/components/EternalDominationGame.tsx index a3f493d..8b5b347 100644 --- a/src/components/EternalDominationGame.tsx +++ b/src/components/EternalDominationGame.tsx @@ -4,50 +4,50 @@ import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Shield, Target, RotateCcw, Info } from "lucide-react"; -// Hexagonal grid F₃(12, 10) - 12 columns × 10 rows -// Following the "brick wall" offset pattern for hexagonal grids -const COLS = 12; -const ROWS = 10; +// Extended good rectangle: 10 columns × 12 rows (following Fig. 10) +// The border consists of: +// - Columns 0 and 9 (left and right edges) +// - Rows 0, 1 (top two rows) and rows 10, 11 (bottom two rows) +// This forms the double-layer border cycle C +const COLS = 10; +const ROWS = 12; type Position = { x: number; y: number }; -// Position utilities const posKey = (p: Position): string => `${p.x},${p.y}`; const parseKey = (key: string): Position => { const [x, y] = key.split(",").map(Number); return { x, y }; }; -// Check if position is valid const isValidPos = (x: number, y: number): boolean => { return x >= 0 && x < COLS && y >= 0 && y < ROWS; }; -// Hexagonal grid adjacency (brick pattern) -// Even rows: neighbors at (-1,0), (1,0), (-1,-1), (0,-1), (-1,1), (0,1) -// Odd rows: neighbors at (-1,0), (1,0), (0,-1), (1,-1), (0,1), (1,1) +// Hexagonal grid adjacency (brick/offset pattern as in Fig. 10) +// Even rows: neighbors at relative positions +// Odd rows: offset by 0.5 to the right visually const getHexNeighbors = (x: number, y: number): Position[] => { const neighbors: Position[] = []; - // Horizontal neighbors (same for all rows) + // Left and right (same for all rows) if (isValidPos(x - 1, y)) neighbors.push({ x: x - 1, y }); if (isValidPos(x + 1, y)) neighbors.push({ x: x + 1, y }); - // Diagonal neighbors depend on row parity (brick pattern) const isEvenRow = y % 2 === 0; if (isEvenRow) { - // Even row: upper-left, upper-right at x-1 and x + // Even row: upper neighbors at x-1 and x if (isValidPos(x - 1, y - 1)) neighbors.push({ x: x - 1, y: y - 1 }); if (isValidPos(x, y - 1)) neighbors.push({ x: x, y: y - 1 }); - // Lower neighbors + // Lower neighbors at x-1 and x if (isValidPos(x - 1, y + 1)) neighbors.push({ x: x - 1, y: y + 1 }); if (isValidPos(x, y + 1)) neighbors.push({ x: x, y: y + 1 }); } else { - // Odd row: upper-left, upper-right at x and x+1 + // Odd row: upper neighbors at x and x+1 if (isValidPos(x, y - 1)) neighbors.push({ x: x, y: y - 1 }); if (isValidPos(x + 1, y - 1)) neighbors.push({ x: x + 1, y: y - 1 }); - // Lower neighbors + // Lower neighbors at x and x+1 if (isValidPos(x, y + 1)) neighbors.push({ x: x, y: y + 1 }); if (isValidPos(x + 1, y + 1)) neighbors.push({ x: x + 1, y: y + 1 }); } @@ -55,54 +55,51 @@ const getHexNeighbors = (x: number, y: number): Position[] => { return neighbors; }; -// Following Figure 10: the border is a DOUBLE LAYER forming cycle C -// Outer border: x=0, x=COLS-1, y=0, y=ROWS-1 -// Inner border: x=1, x=COLS-2, y=1, y=ROWS-2 (but only the perimeter) +// Border definition from Fig. 10: +// The border of the extended good rectangle consists of: +// - x = 0 or x = COLS-1 (left/right columns) +// - y = 0 or y = 1 (top two rows) +// - y = ROWS-2 or y = ROWS-1 (bottom two rows) const isBorder = (x: number, y: number): boolean => { - // Outer layer - if (x === 0 || x === COLS - 1 || y === 0 || y === ROWS - 1) return true; - // Inner layer (second ring) - if (x === 1 || x === COLS - 2 || y === 1 || y === ROWS - 2) return true; + if (x === 0 || x === COLS - 1) return true; + if (y === 0 || y === 1 || y === ROWS - 2 || y === ROWS - 1) return true; return false; }; -// Interior positions (inside the double border) +// Interior: vertices not on the border const isInterior = (x: number, y: number): boolean => { - return x > 1 && x < COLS - 2 && y > 1 && y < ROWS - 2; + return x > 0 && x < COLS - 1 && y > 1 && y < ROWS - 2; }; -// Get the border layer (0 = outer, 1 = inner) -const getBorderLayer = (x: number, y: number): number => { - if (x === 0 || x === COLS - 1 || y === 0 || y === ROWS - 1) return 0; - if (x === 1 || x === COLS - 2 || y === 1 || y === ROWS - 2) return 1; - return -1; -}; - -// Get all border vertices in order (forming cycle C) +// Get ordered border cycle C (clockwise around the rectangle) +// This matches the structure in Fig. 10 where the green paths run along the border const getBorderCycle = (): Position[] => { - const border: Position[] = []; + const cycle: Position[] = []; - // Outer ring clockwise: top -> right -> bottom -> left - for (let x = 0; x < COLS; x++) border.push({ x, y: 0 }); - for (let y = 1; y < ROWS; y++) border.push({ x: COLS - 1, y }); - for (let x = COLS - 2; x >= 0; x--) border.push({ x, y: ROWS - 1 }); - for (let y = ROWS - 2; y >= 1; y--) border.push({ x: 0, y }); + // Top edge (y=0, left to right) + for (let x = 0; x < COLS; x++) cycle.push({ x, y: 0 }); + // Right edge (x=COLS-1, top to bottom, skip first) + for (let y = 1; y < ROWS; y++) cycle.push({ x: COLS - 1, y }); + // Bottom edge (y=ROWS-1, right to left, skip first) + for (let x = COLS - 2; x >= 0; x--) cycle.push({ x, y: ROWS - 1 }); + // Left edge (x=0, bottom to top, skip first and last) + for (let y = ROWS - 2; y > 0; y--) cycle.push({ x: 0, y }); - // Inner ring clockwise - for (let x = 1; x < COLS - 1; x++) border.push({ x, y: 1 }); - for (let y = 2; y < ROWS - 1; y++) border.push({ x: COLS - 2, y }); - for (let x = COLS - 3; x >= 1; x--) border.push({ x, y: ROWS - 2 }); - for (let y = ROWS - 3; y >= 2; y--) border.push({ x: 1, y }); + // Second layer (y=1 and y=ROWS-2) + for (let x = 1; x < COLS - 1; x++) cycle.push({ x, y: 1 }); + for (let x = 1; x < COLS - 1; x++) cycle.push({ x, y: ROWS - 2 }); - return border; + return cycle; }; -// Generate initial guard placement following the paper's U₃ configuration -// ALL border vertices are guarded (double layer), plus interior guards in dominating pattern +// Initial guard placement following U₃ from the paper: +// 1. ALL border vertices are guarded (both layers) +// 2. Interior guards placed according to S₃ pattern (eternal dominating set of T₃) +// From the proof: interior pattern uses density ~7/15 const generateInitialGuards = (): Set => { const guards = new Set(); - // All border vertices are guarded (both layers - forming double protection) + // All border vertices are guarded (forming the protected cycle C) for (let x = 0; x < COLS; x++) { for (let y = 0; y < ROWS; y++) { if (isBorder(x, y)) { @@ -111,13 +108,14 @@ const generateInitialGuards = (): Set => { } } - // Interior guards following the U₃ eternal dominating set pattern - // Interior is 8x6 grid (from x=2 to x=9, y=2 to y=7) - // Place guards to ensure domination at density ~7/15 - for (let x = 2; x < COLS - 2; x++) { + // Interior guards following the S₃ pattern from Theorem 6 + // The pattern: (0,0) ∈ S, and if (x,y) ∈ S then (x+2,y+2), (x+3,y-1), etc. + // Simplified: place guards to create a dominating set in the interior + // Using the pattern from Fig. 10 where interior guards are at specific positions + for (let x = 1; x < COLS - 1; x++) { for (let y = 2; y < ROWS - 2; y++) { - // Pattern based on Figure 10 interior placement - // Creates a dominating set where every interior vertex is within distance 1 of a guard + // Pattern based on the hexagonal tiling domination + // Every vertex needs to be within distance 1 of a guard if ((x + 2 * y) % 4 === 0) { guards.add(posKey({ x, y })); } @@ -127,11 +125,10 @@ const generateInitialGuards = (): Set => { return guards; }; -// Check if a vertex is dominated +// Check if vertex is dominated const isDominated = (x: number, y: number, guards: Set): boolean => { if (guards.has(posKey({ x, y }))) return true; - const neighbors = getHexNeighbors(x, y); - return neighbors.some(p => guards.has(posKey(p))); + return getHexNeighbors(x, y).some(p => guards.has(posKey(p))); }; // Check if all vertices are dominated @@ -144,58 +141,43 @@ const isFullyDominated = (guards: Set): boolean => { return true; }; -// Find a path along the border cycle between two positions -const findBorderPath = (from: Position, to: Position, borderCycle: Position[]): Position[] => { - const fromIdx = borderCycle.findIndex(p => p.x === from.x && p.y === from.y); - const toIdx = borderCycle.findIndex(p => p.x === to.x && p.y === to.y); - - if (fromIdx === -1 || toIdx === -1) return []; - - const path: Position[] = []; - const n = borderCycle.length; - - // Go clockwise - let i = fromIdx; - while (i !== toIdx) { - path.push(borderCycle[i]); - i = (i + 1) % n; - } - path.push(borderCycle[toIdx]); - - return path; +// Find index in border cycle +const findInCycle = (cycle: Position[], pos: Position): number => { + return cycle.findIndex(p => p.x === pos.x && p.y === pos.y); }; -// Defense strategy from Figure 10 and page 23: -// 1. Interior guards shift toward attack (one guard moves to v_t) -// 2. This may push guards from interior to border (u₁→w₁, u₂→w₂) -// 3. Guards shift from border to interior to fill gaps (w₃→u₃, w₄→u₄) -// 4. Border guards shift along complementary paths P₁,₃ and P₂,₄ to restore configuration +// Defense strategy from Fig. 10 and Claim 2: +// 1. Consider the attack on v_t in the interior +// 2. Interior guards shift toward attack (one moves to v_t) +// 3. Exactly two guards move from interior to border: u₁→w₁, u₂→w₂ +// 4. Exactly two guards move from border to interior: w₃→u₃, w₄→u₄ +// 5. Border guards shift along complementary paths P₁,₃ and P₂,₄ to restore coverage const defendAttack = (guards: Set, attack: Position): { newGuards: Set; movements: Array<{ from: Position; to: Position; type: 'interior' | 'border' | 'path' }> } | null => { const attackKey = posKey(attack); - // If attack is on a guard, already defended + // Already guarded - no defense needed if (guards.has(attackKey)) { return { newGuards: guards, movements: [] }; } - // Find adjacent guards that can move to defend + // Find adjacent guards const neighbors = getHexNeighbors(attack.x, attack.y); const adjacentGuards = neighbors.filter(p => guards.has(posKey(p))); if (adjacentGuards.length === 0) { - return null; + return null; // Cannot defend - no adjacent guard } const newGuards = new Set(guards); const movements: Array<{ from: Position; to: Position; type: 'interior' | 'border' | 'path' }> = []; - const borderCycle = getBorderCycle(); - // Strategy: prefer interior guards to move to attack position (as per Figure 10) - // This simulates the "internal shift toward attack" + // Step 1: Find an interior guard to move to the attack (prefer interior, then border) let defender: Position | null = null; + + // Prefer interior guards (as in Fig. 10, interior shifts toward attack) for (const g of adjacentGuards) { if (isInterior(g.x, g.y)) { defender = g; @@ -203,15 +185,7 @@ const defendAttack = (guards: Set, attack: Position): { } } - // If no interior guard adjacent, use inner border guard, then outer border - if (!defender) { - for (const g of adjacentGuards) { - if (getBorderLayer(g.x, g.y) === 1) { - defender = g; - break; - } - } - } + // If no interior guard, use a border guard if (!defender) { defender = adjacentGuards[0]; } @@ -221,68 +195,79 @@ const defendAttack = (guards: Set, attack: Position): { newGuards.add(attackKey); movements.push({ from: defender, to: attack, type: 'interior' }); - // Now we need to restore the configuration: - // If an interior guard moved, we need to shift guards along the border to fill the gap - // Following Figure 10: find complementary paths to shift guards + // Step 2: If defender was from interior, the interior pattern needs restoration + // Following Fig. 10: shifts may push a guard to border, and border guards fill gaps - if (isInterior(defender.x, defender.y) || getBorderLayer(defender.x, defender.y) === 1) { - // Find undominated interior vertices and fill from border - for (let x = 2; x < COLS - 2; x++) { + if (isInterior(defender.x, defender.y)) { + // The defender left a gap in the interior domination pattern + // Find undominated interior vertices and fill from adjacent border guards + + for (let x = 1; x < COLS - 1; x++) { for (let y = 2; y < ROWS - 2; y++) { if (!isDominated(x, y, newGuards)) { - // Find adjacent border guard to move in + // Find an adjacent border guard to move in (w₃→u₃ style) const nbs = getHexNeighbors(x, y); - const borderNb = nbs.find(n => - isBorder(n.x, n.y) && newGuards.has(posKey(n)) - ); + const borderGuard = nbs.find(n => isBorder(n.x, n.y) && newGuards.has(posKey(n))); - if (borderNb) { - // Move border guard to interior (w₃→u₃ or w₄→u₄) - newGuards.delete(posKey(borderNb)); + if (borderGuard) { + newGuards.delete(posKey(borderGuard)); newGuards.add(posKey({ x, y })); - movements.push({ from: borderNb, to: { x, y }, type: 'border' }); + movements.push({ from: borderGuard, to: { x, y }, type: 'border' }); - // Now shift guards along border path to fill the gap at borderNb - // Find another border guard to shift toward borderNb - const borderNbNeighbors = getHexNeighbors(borderNb.x, borderNb.y); - const borderShifter = borderNbNeighbors.find(n => - isBorder(n.x, n.y) && newGuards.has(posKey(n)) && + // Now the border has a gap at borderGuard position + // Shift along the border cycle to fill it (complementary path) + const borderNbs = getHexNeighbors(borderGuard.x, borderGuard.y); + const shifter = borderNbs.find(n => + isBorder(n.x, n.y) && + newGuards.has(posKey(n)) && !movements.some(m => m.from.x === n.x && m.from.y === n.y) ); - if (borderShifter) { - // Shift along complementary path - newGuards.delete(posKey(borderShifter)); - newGuards.add(posKey(borderNb)); - movements.push({ from: borderShifter, to: borderNb, type: 'path' }); + if (shifter) { + newGuards.delete(posKey(shifter)); + newGuards.add(posKey(borderGuard)); + movements.push({ from: shifter, to: borderGuard, type: 'path' }); } } } } } + } + + // Step 3: Ensure all border positions remain guarded (shift along paths) + // This implements the complementary path shifting from Fig. 10 + const borderCycle = getBorderCycle(); + let changed = true; + let iterations = 0; + + while (changed && iterations < 20) { + changed = false; + iterations++; - // Ensure border remains fully covered by shifting along paths for (const pos of borderCycle) { - if (!newGuards.has(posKey(pos))) { + if (isBorder(pos.x, pos.y) && !newGuards.has(posKey(pos))) { // Find adjacent border guard to shift const nbs = getHexNeighbors(pos.x, pos.y); const shifter = nbs.find(n => - isBorder(n.x, n.y) && newGuards.has(posKey(n)) && - !movements.some(m => m.to.x === n.x && m.to.y === n.y && m.type === 'path') + isBorder(n.x, n.y) && + newGuards.has(posKey(n)) && + !movements.some(m => m.to.x === n.x && m.to.y === n.y) ); if (shifter) { - // Check if shifter has another border neighbor that can cover its old spot + // Check if shifter can be safely moved (has backup) const shifterNbs = getHexNeighbors(shifter.x, shifter.y); - const backfill = shifterNbs.find(n => - isBorder(n.x, n.y) && newGuards.has(posKey(n)) && + const hasBackup = shifterNbs.some(n => + isBorder(n.x, n.y) && + newGuards.has(posKey(n)) && (n.x !== pos.x || n.y !== pos.y) ); - if (backfill) { + if (hasBackup) { newGuards.delete(posKey(shifter)); newGuards.add(posKey(pos)); movements.push({ from: shifter, to: pos, type: 'path' }); + changed = true; } } } @@ -295,7 +280,7 @@ const defendAttack = (guards: Set, attack: Position): { const EternalDominationGame: React.FC = () => { const [guards, setGuards] = useState>(generateInitialGuards); const [attackHistory, setAttackHistory] = useState([]); - const [message, setMessage] = useState("Click any cell to attack. Guards will shift to defend."); + const [message, setMessage] = useState("Click any unguarded cell to attack."); const [moveCount, setMoveCount] = useState(0); const [showInfo, setShowInfo] = useState(false); const [lastMovements, setLastMovements] = useState>([]); @@ -304,7 +289,7 @@ const EternalDominationGame: React.FC = () => { const attack: Position = { x, y }; if (guards.has(posKey(attack))) { - setMessage("Cannot attack a guarded position. Choose an unguarded cell."); + setMessage("Cannot attack a guarded position."); return; } @@ -317,23 +302,20 @@ const EternalDominationGame: React.FC = () => { setMoveCount(prev => prev + 1); if (isFullyDominated(result.newGuards)) { - const moveDesc = result.movements.length > 1 - ? `${result.movements.length} guards shifted` - : "1 guard moved"; - setMessage(`Attack defended! ${moveDesc}. All vertices remain dominated. (Move ${moveCount + 1})`); + setMessage(`Attack defended! ${result.movements.length} guard${result.movements.length !== 1 ? 's' : ''} moved.`); } else { - setMessage("Defense failed! Some vertices are not dominated."); + setMessage("Defense incomplete - some vertices undominated!"); } } else { - setMessage("Cannot defend this attack - no adjacent guards!"); + setMessage("Cannot defend - no adjacent guards!"); } - }, [guards, moveCount]); + }, [guards]); const handleReset = useCallback(() => { setGuards(generateInitialGuards()); setAttackHistory([]); setLastMovements([]); - setMessage("Click any cell to attack. Guards will shift to defend."); + setMessage("Click any unguarded cell to attack."); setMoveCount(0); }, []); @@ -344,10 +326,10 @@ const EternalDominationGame: React.FC = () => { }).length; const borderGuards = guardCount - interiorGuards; - // Hexagonal cell dimensions - const cellWidth = 40; - const cellHeight = 36; - const rowOffset = cellWidth / 2; + // Cell dimensions for rendering + const cellW = 44; + const cellH = 40; + const offset = cellW / 2; return (
@@ -355,24 +337,24 @@ const EternalDominationGame: React.FC = () => { - Eternal Domination on a Hexagonal Grid + Eternal Domination (Figure 10) - 12×10 hexagonal grid with double-layer border defense (Figure 10) + 10×12 extended good rectangle with double-layer border
-
+
Guards: {guardCount} - Border: {borderGuards} - Interior: {interiorGuards} + Border: {borderGuards} + Interior: {interiorGuards} Attacks: {moveCount}
- {/* Hexagonal Grid */} + {/* Grid rendering */}
- {/* Draw edges */} + {/* Edges */} {Array.from({ length: ROWS }, (_, y) => Array.from({ length: COLS }, (_, x) => { - const offset = y % 2 === 1 ? rowOffset : 0; - const cx = x * cellWidth + cellWidth / 2 + offset + 10; - const cy = y * cellHeight + cellHeight / 2 + 10; + const rowOffset = y % 2 === 1 ? offset : 0; + const cx = x * cellW + cellW / 2 + rowOffset + 10; + const cy = y * cellH + cellH / 2 + 10; return getHexNeighbors(x, y) .filter(n => n.x > x || (n.x === x && n.y > y)) - .map((neighbor, i) => { - const nOffset = neighbor.y % 2 === 1 ? rowOffset : 0; - const ncx = neighbor.x * cellWidth + cellWidth / 2 + nOffset + 10; - const ncy = neighbor.y * cellHeight + cellHeight / 2 + 10; + .map((nb, i) => { + const nbOffset = nb.y % 2 === 1 ? offset : 0; + const ncx = nb.x * cellW + cellW / 2 + nbOffset + 10; + const ncy = nb.y * cellH + cellH / 2 + 10; - // Color border edges green (complementary paths) - const isBorderEdge = isBorder(x, y) && isBorder(neighbor.x, neighbor.y); + // Green edges for border cycle (complementary paths) + const isBorderEdge = isBorder(x, y) && isBorder(nb.x, nb.y); return ( ); }); }) )} - {/* Draw movement arrows for last defense */} + {/* Movement arrows */} {lastMovements.map((move, i) => { - const fromOffset = move.from.y % 2 === 1 ? rowOffset : 0; - const toOffset = move.to.y % 2 === 1 ? rowOffset : 0; - const x1 = move.from.x * cellWidth + cellWidth / 2 + fromOffset + 10; - const y1 = move.from.y * cellHeight + cellHeight / 2 + 10; - const x2 = move.to.x * cellWidth + cellWidth / 2 + toOffset + 10; - const y2 = move.to.y * cellHeight + cellHeight / 2 + 10; + const fromOff = move.from.y % 2 === 1 ? offset : 0; + const toOff = move.to.y % 2 === 1 ? offset : 0; + const x1 = move.from.x * cellW + cellW / 2 + fromOff + 10; + const y1 = move.from.y * cellH + cellH / 2 + 10; + const x2 = move.to.x * cellW + cellW / 2 + toOff + 10; + const y2 = move.to.y * cellH + cellH / 2 + 10; const color = move.type === 'path' ? '#22c55e' : move.type === 'border' ? '#06b6d4' : '#ef4444'; return ( - - - + ); })} - - + + + + + + + + @@ -485,39 +458,32 @@ const EternalDominationGame: React.FC = () => { {/* Cells */} {Array.from({ length: ROWS }, (_, y) => Array.from({ length: COLS }, (_, x) => { - const offset = y % 2 === 1 ? rowOffset : 0; + const rowOffset = y % 2 === 1 ? offset : 0; const hasGuard = guards.has(posKey({ x, y })); const onBorder = isBorder(x, y); - const borderLayer = getBorderLayer(x, y); const dominated = isDominated(x, y, guards); - let bgClass = "bg-muted hover:bg-muted/80"; + let bgClass = "bg-muted/60 hover:bg-muted"; if (hasGuard) { - if (onBorder) { - // Cyan for border guards (both layers) - bgClass = borderLayer === 0 - ? "bg-cyan-500/50 border-cyan-600" - : "bg-cyan-400/40 border-cyan-500"; - } else { - // Red/accent for interior guards - bgClass = "bg-red-500/50 border-red-600"; - } + bgClass = onBorder + ? "bg-cyan-500/60 border-cyan-600" + : "bg-red-500/60 border-red-600"; } else if (!dominated) { - bgClass = "bg-destructive/50 border-destructive"; + bgClass = "bg-destructive/60 border-destructive"; } return (
handleCellClick(x, y)} - title={`(${x}, ${y}) - ${hasGuard ? (onBorder ? "Border Guard" : "Interior Guard") : dominated ? "Dominated" : "Undominated!"}`} + title={`(${x},${y}) ${hasGuard ? (onBorder ? "Border" : "Interior") : dominated ? "" : "UNDOMINATED"}`} > - {hasGuard ? "🛡️" : ""} + {hasGuard && }
); }) @@ -528,29 +494,31 @@ const EternalDominationGame: React.FC = () => { {/* Legend */}
-
🛡️
- Border Guard (double layer) +
+ +
+ Border guard
-
🛡️
- Interior Guard +
+ +
+ Interior guard
-
- Complementary paths (border cycle) +
+ Complementary paths (P₁,₃ / P₂,₄)
{attackHistory.length > 0 && (
-

Recent attacks:

-
- {attackHistory.slice(-10).map((pos, i) => ( - - ({pos.x}, {pos.y}) - - ))} -
+ Attacks: + {attackHistory.slice(-8).map((pos, i) => ( + + ({pos.x},{pos.y}) + + ))}
)}