diff --git a/src/assets/fig10-reference.jpg b/src/assets/fig10-reference.jpg new file mode 100644 index 0000000..935096a Binary files /dev/null and b/src/assets/fig10-reference.jpg differ diff --git a/src/components/EternalDominationGame.tsx b/src/components/EternalDominationGame.tsx index fa99623..a8d00d6 100644 --- a/src/components/EternalDominationGame.tsx +++ b/src/components/EternalDominationGame.tsx @@ -4,15 +4,18 @@ import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Shield, Target, RotateCcw, Info } from "lucide-react"; -// 12 columns (x: 0-11) × 10 rows (y: 0-9) +// Hexagonal grid F₃(12, 10) - 12 columns × 10 rows +// Following the "brick wall" offset pattern for hexagonal grids const COLS = 12; const ROWS = 10; type Position = { x: number; y: number }; -// Check if a position is on the border of the rectangle -const isBorder = (x: number, y: number): boolean => { - return x === 0 || x === COLS - 1 || y === 0 || y === 1 || y === ROWS - 2 || y === ROWS - 1; +// 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 @@ -20,35 +23,55 @@ const isValidPos = (x: number, y: number): boolean => { return x >= 0 && x < COLS && y >= 0 && y < ROWS; }; -// Get adjacent positions (4-connectivity for square grid) -const getAdjacent = (x: number, y: number): Position[] => { +// 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) +const getHexNeighbors = (x: number, y: number): Position[] => { const neighbors: Position[] = []; - const deltas = [[-1, 0], [1, 0], [0, -1], [0, 1]]; - for (const [dx, dy] of deltas) { - const nx = x + dx; - const ny = y + dy; - if (isValidPos(nx, ny)) { - neighbors.push({ x: nx, y: ny }); - } + + // Horizontal neighbors (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 + 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 + 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 + 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 + 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 }); } + return neighbors; }; -// Position key for Set operations -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 a position is on the border of the "interested rectangle" +// Following Figure 10: border forms cycle C around the rectangle +const isBorder = (x: number, y: number): boolean => { + return x === 0 || x === COLS - 1 || y === 0 || y === ROWS - 1; }; -// Initial guard placement based on the paper's configuration -// Following the pattern from Theorem 4 (square grid eternal dominating set): -// S defined as: (0,0) ∈ S; if (x,y) ∈ S then (x+2,y+1), (x-1,y+2), (x-2,y-1), (x+1,y-2) ∈ S -// Plus all border vertices are guarded for the finite case +// Interior positions (not on border) +const isInterior = (x: number, y: number): boolean => { + return x > 0 && x < COLS - 1 && y > 0 && y < ROWS - 1; +}; + +// Generate initial guard placement following the paper's U₃ configuration +// Interior guards follow a specific pattern, all border vertices are guarded const generateInitialGuards = (): Set => { const guards = new Set(); - // Add all border vertices (the cycle C from Figure 10) + // All border vertices are guarded (forming cycle C) for (let x = 0; x < COLS; x++) { for (let y = 0; y < ROWS; y++) { if (isBorder(x, y)) { @@ -57,14 +80,15 @@ const generateInitialGuards = (): Set => { } } - // Add interior guards following the pattern from Theorem 4 - // For square grid: (0,0) starts, then (x+2,y+1), (x-1,y+2), etc. - // We place guards so each interior vertex is dominated by exactly one guard - // Pattern: guards at positions where (2x + y) mod 5 === 0 + // Interior guards following the eternal dominating set pattern + // Based on the paper: we need interior guards at density 7/15 nm + // Pattern: guards at positions creating a dominating set + // Using pattern similar to Figure 10's interior placement for (let x = 1; x < COLS - 1; x++) { - for (let y = 2; y < ROWS - 2; y++) { - // Interior region: x in [1, COLS-2], y in [2, ROWS-3] - // Use the diagonal pattern: place guards at specific intervals + for (let y = 1; y < ROWS - 1; y++) { + // Place guards following a hexagonal domination pattern + // Every vertex in interior needs to be within distance 1 of a guard + // Pattern: (2x + y) mod 5 === 0 creates suitable coverage if ((2 * x + y) % 5 === 0) { guards.add(posKey({ x, y })); } @@ -74,11 +98,11 @@ const generateInitialGuards = (): Set => { return guards; }; -// Check if a vertex is dominated (has a guard on it or adjacent to a guard) +// Check if a vertex is dominated const isDominated = (x: number, y: number, guards: Set): boolean => { if (guards.has(posKey({ x, y }))) return true; - const adj = getAdjacent(x, y); - return adj.some(p => guards.has(posKey(p))); + const neighbors = getHexNeighbors(x, y); + return neighbors.some(p => guards.has(posKey(p))); }; // Check if all vertices are dominated @@ -91,34 +115,31 @@ const isFullyDominated = (guards: Set): boolean => { return true; }; -// Find the optimal defense move for an attack -// Returns new guard positions after defending +// Defense strategy from Figure 10 and page 23: +// 1. Interior guards shift toward attack +// 2. If a guard moves from interior to border, border guards shift along complementary paths const defendAttack = (guards: Set, attack: Position): Set | null => { const attackKey = posKey(attack); - // If attack is on a guard, no movement needed + // If attack is on a guard, already defended if (guards.has(attackKey)) { return guards; } // Find adjacent guards that can move to defend - const adjacent = getAdjacent(attack.x, attack.y); - const adjacentGuards = adjacent.filter(p => guards.has(posKey(p))); + const neighbors = getHexNeighbors(attack.x, attack.y); + const adjacentGuards = neighbors.filter(p => guards.has(posKey(p))); if (adjacentGuards.length === 0) { - return null; // Cannot defend - should not happen with proper dominating set + return null; // Should not happen with proper dominating set } - // Strategy based on Figure 10: - // 1. Move interior guard toward attack - // 2. Shift guards along border paths to maintain coverage - const newGuards = new Set(guards); - // Pick the best guard to move (prefer interior guards) + // Strategy: prefer interior guards to move (as per Figure 10) let defender: Position | null = null; for (const g of adjacentGuards) { - if (!isBorder(g.x, g.y)) { + if (isInterior(g.x, g.y)) { defender = g; break; } @@ -131,57 +152,71 @@ const defendAttack = (guards: Set, attack: Position): Set | null newGuards.delete(posKey(defender)); newGuards.add(attackKey); - // If defender was interior and moved to border, we need to shift border guards - // to maintain the dominating set property - if (!isBorder(defender.x, defender.y) && isBorder(attack.x, attack.y)) { - // The complementary path strategy from Figure 10 - // Shift guards along the border cycle to fill the gap left by interior movement - performBorderShift(newGuards, attack, defender); + // If defender was interior and attack is on border, apply border shift strategy + // Following the complementary paths P₁,₃ and P₂,₄ from Figure 10 + if (isInterior(defender.x, defender.y)) { + // Interior to border movement: shift guards along border cycle + // to maintain the dominating set property + applyBorderShift(newGuards, defender, attack); } - // Verify we still have a dominating set, if not, apply additional shifts - ensureDominatingSet(newGuards, attack); + // Ensure domination is maintained + ensureDomination(newGuards); return newGuards; }; -// Shift guards along border to maintain coverage -const performBorderShift = (guards: Set, to: Position, from: Position): void => { - // Simple strategy: if we created a gap, try to fill it with neighboring guard shifts - const fromKey = posKey(from); - - // Find border neighbors of the vacated position that have guards - const neighbors = getAdjacent(from.x, from.y); - for (const n of neighbors) { - if (isBorder(n.x, n.y) && guards.has(posKey(n))) { - // Found a border guard that can help - // For now, we don't need to shift if the vacated position was interior - break; +// Border shift strategy: when interior guard moves out, shift border guards +// along complementary paths to fill gaps +const applyBorderShift = (guards: Set, from: Position, to: Position): void => { + // Check if any interior vertices became undominated + for (let x = 1; x < COLS - 1; x++) { + for (let y = 1; y < ROWS - 1; y++) { + if (!isDominated(x, y, guards)) { + // Find a neighboring guard to shift in + const neighbors = getHexNeighbors(x, y); + for (const n of neighbors) { + if (guards.has(posKey(n))) { + // Check if we can shift this guard + const nNeighbors = getHexNeighbors(n.x, n.y); + const canShift = nNeighbors.some(nn => + guards.has(posKey(nn)) && + posKey(nn) !== posKey(n) && + posKey(nn) !== posKey(to) + ); + if (canShift || isBorder(n.x, n.y)) { + // Shift a border guard toward the gap + const borderNeighbors = neighbors.filter(nb => + isBorder(nb.x, nb.y) && guards.has(posKey(nb)) + ); + if (borderNeighbors.length > 0) { + const shifter = borderNeighbors[0]; + guards.delete(posKey(shifter)); + guards.add(posKey({ x, y })); + return; + } + } + } + } + } } } }; -// Ensure dominating set property after movement -const ensureDominatingSet = (guards: Set, attackPos: Position): void => { - // Check all vertices are still dominated - // If not, this would indicate an issue with our defense strategy - // In practice, with the border fully guarded, we maintain domination - - // Add back any critical positions if needed +// Ensure all vertices remain dominated after movement +const ensureDomination = (guards: Set): void => { for (let x = 0; x < COLS; x++) { for (let y = 0; y < ROWS; y++) { if (!isDominated(x, y, guards)) { - // Find nearest guard and shift toward this position - const adj = getAdjacent(x, y); - for (const a of adj) { - const adjKey = posKey(a); - // Try to find a guard that can shift - const adjAdj = getAdjacent(a.x, a.y); - for (const aa of adjAdj) { - if (guards.has(posKey(aa)) && !posKey(aa).includes(posKey(attackPos))) { + // Emergency: place a guard at nearest available position + const neighbors = getHexNeighbors(x, y); + for (const n of neighbors) { + const nNeighbors = getHexNeighbors(n.x, n.y); + for (const nn of nNeighbors) { + if (guards.has(posKey(nn))) { // Shift this guard - guards.delete(posKey(aa)); - guards.add(adjKey); + guards.delete(posKey(nn)); + guards.add(posKey(n)); return; } } @@ -194,20 +229,18 @@ const ensureDominatingSet = (guards: Set, attackPos: Position): void => const EternalDominationGame: React.FC = () => { const [guards, setGuards] = useState>(generateInitialGuards); const [attackHistory, setAttackHistory] = useState([]); - const [message, setMessage] = useState("Click any cell to attack. Guards will move to defend."); + const [message, setMessage] = useState("Click any cell to attack. Guards will shift to defend."); const [moveCount, setMoveCount] = useState(0); const [showInfo, setShowInfo] = useState(false); const handleCellClick = useCallback((x: number, y: number) => { const attack: Position = { x, y }; - // Check if attack is valid (not on a guard) if (guards.has(posKey(attack))) { setMessage("Cannot attack a guarded position. Choose an unguarded cell."); return; } - // Defend the attack const newGuards = defendAttack(guards, attack); if (newGuards) { @@ -228,65 +261,41 @@ const EternalDominationGame: React.FC = () => { const handleReset = useCallback(() => { setGuards(generateInitialGuards()); setAttackHistory([]); - setMessage("Click any cell to attack. Guards will move to defend."); + setMessage("Click any cell to attack. Guards will shift to defend."); setMoveCount(0); }, []); - const getCellStyle = (x: number, y: number): string => { - const key = posKey({ x, y }); - const hasGuard = guards.has(key); - const isOnBorder = isBorder(x, y); - const dominated = isDominated(x, y, guards); - - let base = "w-8 h-8 border border-border flex items-center justify-center text-lg cursor-pointer transition-all hover:scale-105 "; - - if (hasGuard) { - base += isOnBorder - ? "bg-primary/30 border-primary" - : "bg-accent/50 border-accent-foreground"; - } else if (dominated) { - base += "bg-muted hover:bg-muted/80"; - } else { - base += "bg-destructive/20 hover:bg-destructive/30"; // Should not happen - } - - return base; - }; - const guardCount = guards.size; const interiorGuards = Array.from(guards).filter(k => { const p = parseKey(k); - return !isBorder(p.x, p.y); + return isInterior(p.x, p.y); }).length; const borderGuards = guardCount - interiorGuards; + // Hexagonal cell dimensions + const cellWidth = 36; + const cellHeight = 32; + const rowOffset = cellWidth / 2; + return (
- Eternal Domination on a Grid + Eternal Domination on a Hexagonal Grid - 12×10 grid with m-eternal dominating set defense strategy + 12×10 hexagonal grid F₃(12, 10) with m-eternal dominating set defense
- - Total Guards: {guardCount} - - - Border: {borderGuards} - - - Interior: {interiorGuards} - - - Attacks: {moveCount} - + Guards: {guardCount} + Border: {borderGuards} + Interior: {interiorGuards} + Attacks: {moveCount}
- {/* Grid */} + {/* Hexagonal Grid */}
-
-
-
- {Array.from({ length: COLS }, (_, i) => ( -
{i}
- ))} -
-
- {Array.from({ length: ROWS }, (_, y) => ( -
-
- {y} -
- {Array.from({ length: COLS }, (_, x) => ( +
+ {/* Draw edges first */} + + {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; + + 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; + return ( + + ); + }); + }) + )} + + + {/* Cells */} + {Array.from({ length: ROWS }, (_, y) => + Array.from({ length: COLS }, (_, x) => { + const offset = y % 2 === 1 ? rowOffset : 0; + const hasGuard = guards.has(posKey({ x, y })); + const onBorder = isBorder(x, y); + const dominated = isDominated(x, y, guards); + + let bgClass = "bg-muted hover:bg-muted/80"; + if (hasGuard) { + bgClass = onBorder + ? "bg-primary/40 border-primary" + : "bg-accent border-accent-foreground"; + } else if (!dominated) { + bgClass = "bg-destructive/30"; + } + + return (
handleCellClick(x, y)} - title={`(${x}, ${y}) - ${guards.has(posKey({ x, y })) ? "Guard" : "Empty"}`} + title={`(${x}, ${y}) - ${hasGuard ? "Guard" : dominated ? "Dominated" : "Undominated!"}`} > - {guards.has(posKey({ x, y })) ? "🛡️" : ""} + {hasGuard ? "🛡️" : ""}
- ))} -
- ))} + ); + }) + )}
{/* Legend */}
-
🛡️
+
🛡️
Border Guard
-
🛡️
+
🛡️
Interior Guard
-
- Dominated (unguarded) +
+ Dominated
- {/* Attack History */} {attackHistory.length > 0 && (

Recent attacks: