From 3797f3ae1e894aac5ad5b33101edb49e49aaf5df Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 07:08:04 +0000 Subject: [PATCH] Changes --- src/components/EternalDominationGame.tsx | 347 +++++++++++++++-------- 1 file changed, 235 insertions(+), 112 deletions(-) diff --git a/src/components/EternalDominationGame.tsx b/src/components/EternalDominationGame.tsx index a8d00d6..a3f493d 100644 --- a/src/components/EternalDominationGame.tsx +++ b/src/components/EternalDominationGame.tsx @@ -55,23 +55,54 @@ const getHexNeighbors = (x: number, y: number): Position[] => { return neighbors; }; -// Check if a position is on the border of the "interested rectangle" -// Following Figure 10: border forms cycle C around the rectangle +// 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) const isBorder = (x: number, y: number): boolean => { - return x === 0 || x === COLS - 1 || y === 0 || y === ROWS - 1; + // 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; + return false; }; -// Interior positions (not on border) +// Interior positions (inside the double border) const isInterior = (x: number, y: number): boolean => { - return x > 0 && x < COLS - 1 && y > 0 && y < ROWS - 1; + return x > 1 && x < COLS - 2 && 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) +const getBorderCycle = (): Position[] => { + const border: 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 }); + + // 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 }); + + return border; }; // Generate initial guard placement following the paper's U₃ configuration -// Interior guards follow a specific pattern, all border vertices are guarded +// ALL border vertices are guarded (double layer), plus interior guards in dominating pattern const generateInitialGuards = (): Set => { const guards = new Set(); - // All border vertices are guarded (forming cycle C) + // All border vertices are guarded (both layers - forming double protection) for (let x = 0; x < COLS; x++) { for (let y = 0; y < ROWS; y++) { if (isBorder(x, y)) { @@ -80,16 +111,14 @@ const generateInitialGuards = (): Set => { } } - // 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 = 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) { + // 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++) { + 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 + if ((x + 2 * y) % 4 === 0) { guards.add(posKey({ x, y })); } } @@ -115,15 +144,41 @@ 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; +}; + // 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 => { +// 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 +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 if (guards.has(attackKey)) { - return guards; + return { newGuards: guards, movements: [] }; } // Find adjacent guards that can move to defend @@ -131,12 +186,15 @@ const defendAttack = (guards: Set, attack: Position): Set | null const adjacentGuards = neighbors.filter(p => guards.has(posKey(p))); if (adjacentGuards.length === 0) { - return null; // Should not happen with proper dominating set + return null; } 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 (as per Figure 10) + // Strategy: prefer interior guards to move to attack position (as per Figure 10) + // This simulates the "internal shift toward attack" let defender: Position | null = null; for (const g of adjacentGuards) { if (isInterior(g.x, g.y)) { @@ -144,6 +202,16 @@ const defendAttack = (guards: Set, attack: Position): Set | null break; } } + + // 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 (!defender) { defender = adjacentGuards[0]; } @@ -151,79 +219,77 @@ const defendAttack = (guards: Set, attack: Position): Set | null // Move defender to attack position newGuards.delete(posKey(defender)); newGuards.add(attackKey); + movements.push({ from: defender, to: attack, type: 'interior' }); - // 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); - } + // 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 - // Ensure domination is maintained - ensureDomination(newGuards); - - return newGuards; -}; - -// 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 (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++) { + for (let y = 2; y < ROWS - 2; y++) { + if (!isDominated(x, y, newGuards)) { + // Find adjacent border guard to move in + const nbs = getHexNeighbors(x, y); + const borderNb = 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)); + newGuards.add(posKey({ x, y })); + movements.push({ from: borderNb, 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)) && + !movements.some(m => m.from.x === n.x && m.from.y === n.y) ); - 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; - } + + if (borderShifter) { + // Shift along complementary path + newGuards.delete(posKey(borderShifter)); + newGuards.add(posKey(borderNb)); + movements.push({ from: borderShifter, to: borderNb, type: 'path' }); } } } } } - } -}; - -// 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)) { - // 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(nn)); - guards.add(posKey(n)); - return; - } + + // Ensure border remains fully covered by shifting along paths + for (const pos of borderCycle) { + if (!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') + ); + + if (shifter) { + // Check if shifter has another border neighbor that can cover its old spot + const shifterNbs = getHexNeighbors(shifter.x, shifter.y); + const backfill = shifterNbs.find(n => + isBorder(n.x, n.y) && newGuards.has(posKey(n)) && + (n.x !== pos.x || n.y !== pos.y) + ); + + if (backfill) { + newGuards.delete(posKey(shifter)); + newGuards.add(posKey(pos)); + movements.push({ from: shifter, to: pos, type: 'path' }); } } } } } + + return { newGuards, movements }; }; const EternalDominationGame: React.FC = () => { @@ -232,6 +298,7 @@ const EternalDominationGame: React.FC = () => { 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 [lastMovements, setLastMovements] = useState>([]); const handleCellClick = useCallback((x: number, y: number) => { const attack: Position = { x, y }; @@ -241,15 +308,19 @@ const EternalDominationGame: React.FC = () => { return; } - const newGuards = defendAttack(guards, attack); + const result = defendAttack(guards, attack); - if (newGuards) { - setGuards(newGuards); + if (result) { + setGuards(result.newGuards); + setLastMovements(result.movements); setAttackHistory(prev => [...prev, attack]); setMoveCount(prev => prev + 1); - if (isFullyDominated(newGuards)) { - setMessage(`Attack defended! All vertices remain dominated. (Move ${moveCount + 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})`); } else { setMessage("Defense failed! Some vertices are not dominated."); } @@ -261,6 +332,7 @@ const EternalDominationGame: React.FC = () => { const handleReset = useCallback(() => { setGuards(generateInitialGuards()); setAttackHistory([]); + setLastMovements([]); setMessage("Click any cell to attack. Guards will shift to defend."); setMoveCount(0); }, []); @@ -273,8 +345,8 @@ const EternalDominationGame: React.FC = () => { const borderGuards = guardCount - interiorGuards; // Hexagonal cell dimensions - const cellWidth = 36; - const cellHeight = 32; + const cellWidth = 40; + const cellHeight = 36; const rowOffset = cellWidth / 2; return ( @@ -286,15 +358,15 @@ const EternalDominationGame: React.FC = () => { Eternal Domination on a Hexagonal Grid - 12×10 hexagonal grid F₃(12, 10) with m-eternal dominating set defense + 12×10 hexagonal grid with double-layer border defense (Figure 10)
Guards: {guardCount} - Border: {borderGuards} - Interior: {interiorGuards} + Border: {borderGuards} + Interior: {interiorGuards} Attacks: {moveCount}
@@ -311,8 +383,8 @@ const EternalDominationGame: React.FC = () => { {showInfo && (
-

m-Eternal Domination on Hexagonal Grid: Guards form a dominating set on F₃(n,m). Each attack must be defended by moving guards while maintaining domination.

-

Strategy (Figure 10): Border vertices form cycle C (always guarded). Interior guards shift toward attacks. Complementary paths along the border shift guards to fill gaps.

+

m-Eternal Domination (Figure 10): The border forms a double-layer cycle C with guards on every vertex. Interior guards follow U₃ configuration.

+

Defense Strategy: When attacked, interior guards shift toward attack. Guards may move from interior→border or border→interior. Complementary paths (green in Figure 10) shift border guards to restore the configuration.

Goal: Try to find an attack sequence that leaves some vertex undominated!

)} @@ -331,7 +403,7 @@ const EternalDominationGame: React.FC = () => { height: ROWS * cellHeight + 20 }} > - {/* Draw edges first */} + {/* Draw edges */} { 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; + + // Color border edges green (complementary paths) + const isBorderEdge = isBorder(x, y) && isBorder(neighbor.x, neighbor.y); + return ( { y1={cy} x2={ncx} y2={ncy} - stroke="currentColor" - strokeOpacity={0.2} - strokeWidth={1} + stroke={isBorderEdge ? "#22c55e" : "currentColor"} + strokeOpacity={isBorderEdge ? 0.6 : 0.2} + strokeWidth={isBorderEdge ? 2 : 1} /> ); }); }) )} + + {/* Draw movement arrows for last defense */} + {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 color = move.type === 'path' ? '#22c55e' : move.type === 'border' ? '#06b6d4' : '#ef4444'; + + return ( + + + + ); + })} + + + + + + {/* Cells */} @@ -372,27 +488,34 @@ const EternalDominationGame: React.FC = () => { const offset = y % 2 === 1 ? rowOffset : 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"; if (hasGuard) { - bgClass = onBorder - ? "bg-primary/40 border-primary" - : "bg-accent border-accent-foreground"; + 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"; + } } else if (!dominated) { - bgClass = "bg-destructive/30"; + bgClass = "bg-destructive/50 border-destructive"; } return (
handleCellClick(x, y)} - title={`(${x}, ${y}) - ${hasGuard ? "Guard" : dominated ? "Dominated" : "Undominated!"}`} + title={`(${x}, ${y}) - ${hasGuard ? (onBorder ? "Border Guard" : "Interior Guard") : dominated ? "Dominated" : "Undominated!"}`} > {hasGuard ? "🛡️" : ""}
@@ -405,16 +528,16 @@ const EternalDominationGame: React.FC = () => { {/* Legend */}
-
🛡️
- Border Guard +
🛡️
+ Border Guard (double layer)
-
🛡️
+
🛡️
Interior Guard
-
- Dominated +
+ Complementary paths (border cycle)