diff --git a/src/components/EternalDominationGame.tsx b/src/components/EternalDominationGame.tsx index 8b5b347..cb01d45 100644 --- a/src/components/EternalDominationGame.tsx +++ b/src/components/EternalDominationGame.tsx @@ -5,14 +5,12 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com import { Shield, Target, RotateCcw, Info } from "lucide-react"; // 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 +// Border: columns 0 and 9, rows 0/1 (top) and 10/11 (bottom) const COLS = 10; const ROWS = 12; type Position = { x: number; y: number }; +type Movement = { from: Position; to: Position; type: 'interior' | 'toInterior' | 'toBorder' | 'pathShift' }; const posKey = (p: Position): string => `${p.x},${p.y}`; const parseKey = (key: string): Position => { @@ -20,34 +18,25 @@ const parseKey = (key: string): Position => { return { x, y }; }; -const isValidPos = (x: number, y: number): boolean => { - return x >= 0 && x < COLS && y >= 0 && y < ROWS; -}; +const isValidPos = (x: number, y: number): boolean => + x >= 0 && x < COLS && y >= 0 && y < ROWS; -// 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 +// Hexagonal adjacency (brick pattern) const getHexNeighbors = (x: number, y: number): Position[] => { const neighbors: Position[] = []; - // 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 }); const isEvenRow = y % 2 === 0; - if (isEvenRow) { - // 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 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 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 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,51 +44,22 @@ const getHexNeighbors = (x: number, y: number): Position[] => { return neighbors; }; -// 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) +// Border: left/right columns and top/bottom double rows const isBorder = (x: number, y: number): boolean => { if (x === 0 || x === COLS - 1) return true; if (y === 0 || y === 1 || y === ROWS - 2 || y === ROWS - 1) return true; return false; }; -// Interior: vertices not on the border -const isInterior = (x: number, y: number): boolean => { - return x > 0 && x < COLS - 1 && y > 1 && y < ROWS - 2; -}; +const isInterior = (x: number, y: number): boolean => + x > 0 && x < COLS - 1 && y > 1 && y < ROWS - 2; -// 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 cycle: Position[] = []; - - // 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 }); - - // 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 cycle; -}; - -// 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 +// Get the initial guard configuration U₃ +// All border vertices guarded + interior guards in dominating pattern const generateInitialGuards = (): Set => { const guards = new Set(); - // All border vertices are guarded (forming the protected cycle C) + // All border vertices for (let x = 0; x < COLS; x++) { for (let y = 0; y < ROWS; y++) { if (isBorder(x, y)) { @@ -108,14 +68,10 @@ const generateInitialGuards = (): Set => { } } - // 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 + // Interior guards: dominating set pattern from S₃ + // Place at positions where (x + 2*y) % 4 === 0 for (let x = 1; x < COLS - 1; x++) { for (let y = 2; y < ROWS - 2; y++) { - // 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 })); } @@ -125,13 +81,24 @@ const generateInitialGuards = (): Set => { return guards; }; -// Check if vertex is dominated +// Get the target pattern for interior (what we want to restore to) +const getTargetInteriorPattern = (): Set => { + const pattern = new Set(); + for (let x = 1; x < COLS - 1; x++) { + for (let y = 2; y < ROWS - 2; y++) { + if ((x + 2 * y) % 4 === 0) { + pattern.add(posKey({ x, y })); + } + } + } + return pattern; +}; + const isDominated = (x: number, y: number, guards: Set): boolean => { if (guards.has(posKey({ x, y }))) return true; return getHexNeighbors(x, y).some(p => guards.has(posKey(p))); }; -// Check if all vertices are dominated const isFullyDominated = (guards: Set): boolean => { for (let x = 0; x < COLS; x++) { for (let y = 0; y < ROWS; y++) { @@ -141,133 +108,203 @@ const isFullyDominated = (guards: Set): boolean => { return true; }; -// Find index in border cycle -const findInCycle = (cycle: Position[], pos: Position): number => { - return cycle.findIndex(p => p.x === pos.x && p.y === pos.y); +// Get ordered border cycle C (for path finding) +const getBorderCycleOrdered = (): Position[] => { + const cycle: Position[] = []; + + // Outer ring clockwise + for (let x = 0; x < COLS; x++) cycle.push({ x, y: 0 }); + for (let y = 1; y < ROWS; y++) cycle.push({ x: COLS - 1, y }); + for (let x = COLS - 2; x >= 0; x--) cycle.push({ x, y: ROWS - 1 }); + for (let y = ROWS - 2; y > 0; y--) cycle.push({ x: 0, y }); + + return cycle; }; -// 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); +// Find path on border between two positions (along the cycle) +const findBorderPath = (from: Position, to: Position): Position[] => { + const cycle = getBorderCycleOrdered(); + const fromIdx = cycle.findIndex(p => p.x === from.x && p.y === from.y); + const toIdx = cycle.findIndex(p => p.x === to.x && p.y === to.y); - // Already guarded - no defense needed - if (guards.has(attackKey)) { - return { newGuards: guards, movements: [] }; + if (fromIdx === -1 || toIdx === -1) return []; + + const n = cycle.length; + const path1: Position[] = []; + const path2: Position[] = []; + + // Clockwise path + for (let i = fromIdx; ; i = (i + 1) % n) { + path1.push(cycle[i]); + if (i === toIdx) break; } - // 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; // Cannot defend - no adjacent guard + // Counter-clockwise path + for (let i = fromIdx; ; i = (i - 1 + n) % n) { + path2.push(cycle[i]); + if (i === toIdx) break; } - const newGuards = new Set(guards); - const movements: Array<{ from: Position; to: Position; type: 'interior' | 'border' | 'path' }> = []; + return path1.length <= path2.length ? path1 : path2; +}; + +// BFS to find shortest path between two positions using hex neighbors +const findShortestPath = (from: Position, to: Position, guards: Set): Position[] => { + const queue: Position[][] = [[from]]; + const visited = new Set([posKey(from)]); - // 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; - break; - } - } - - // If no interior guard, use a border guard - if (!defender) { - defender = adjacentGuards[0]; - } - - // Move defender to attack position - newGuards.delete(posKey(defender)); - newGuards.add(attackKey); - movements.push({ from: defender, to: attack, type: 'interior' }); - - // 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)) { - // The defender left a gap in the interior domination pattern - // Find undominated interior vertices and fill from adjacent border guards + while (queue.length > 0) { + const path = queue.shift()!; + const current = path[path.length - 1]; - for (let x = 1; x < COLS - 1; x++) { - for (let y = 2; y < ROWS - 2; y++) { - if (!isDominated(x, y, newGuards)) { - // Find an adjacent border guard to move in (w₃→u₃ style) - const nbs = getHexNeighbors(x, y); - const borderGuard = nbs.find(n => isBorder(n.x, n.y) && newGuards.has(posKey(n))); - - if (borderGuard) { - newGuards.delete(posKey(borderGuard)); - newGuards.add(posKey({ x, y })); - movements.push({ from: borderGuard, to: { x, y }, type: 'border' }); - - // 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 (shifter) { - newGuards.delete(posKey(shifter)); - newGuards.add(posKey(borderGuard)); - movements.push({ from: shifter, to: borderGuard, type: 'path' }); - } - } - } + if (current.x === to.x && current.y === to.y) { + return path; + } + + for (const neighbor of getHexNeighbors(current.x, current.y)) { + const key = posKey(neighbor); + if (!visited.has(key)) { + visited.add(key); + queue.push([...path, neighbor]); } } } - // 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; + return []; +}; + +// Defense strategy following Figure 10 exactly: +// 1. Interior guards shift in chains toward the attack (v_t) +// 2. Two interior guards exit to border (u₁→w₁, u₂→w₂) +// 3. Two border guards enter interior (w₃→u₃, w₄→u₄) +// 4. Border guards shift along complementary paths P₁,₃ and P₂,₄ +const defendAttack = (guards: Set, attack: Position): { + newGuards: Set; + movements: Movement[]; +} | null => { + const attackKey = posKey(attack); - while (changed && iterations < 20) { - changed = false; - iterations++; + if (guards.has(attackKey)) { + return { newGuards: guards, movements: [] }; + } + + const neighbors = getHexNeighbors(attack.x, attack.y); + const adjacentGuards = neighbors.filter(p => guards.has(posKey(p))); + + if (adjacentGuards.length === 0) { + return null; + } + + const newGuards = new Set(guards); + const movements: Movement[] = []; + + // STEP 1: Find interior guard chains that will shift toward attack + // In Figure 10, the arrows show guards shifting in diagonal lines toward v_t + + // Find an interior guard adjacent to the attack to move there + 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 + if (!primaryDefender) { + primaryDefender = adjacentGuards[0]; + newGuards.delete(posKey(primaryDefender)); + newGuards.add(attackKey); + movements.push({ from: primaryDefender, to: attack, type: isBorder(primaryDefender.x, primaryDefender.y) ? 'toBorder' : 'interior' }); - for (const pos of borderCycle) { - 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) - ); + // If we moved a border guard, shift along border to fill gap + if (isBorder(primaryDefender.x, primaryDefender.y)) { + shiftBorderToFillGap(newGuards, movements, primaryDefender); + } + + return { newGuards, movements }; + } + + // STEP 2: Shift interior guards in a chain toward the attack + // 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' }); - if (shifter) { - // Check if shifter can be safely moved (has backup) - const shifterNbs = getHexNeighbors(shifter.x, shifter.y); - const hasBackup = shifterNbs.some(n => - isBorder(n.x, n.y) && - newGuards.has(posKey(n)) && - (n.x !== pos.x || n.y !== pos.y) - ); - - if (hasBackup) { - newGuards.delete(posKey(shifter)); - newGuards.add(posKey(pos)); - movements.push({ from: shifter, to: pos, type: 'path' }); - changed = true; + // 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); + } + + // STEP 3: Check if we need a second chain (Figure 10 shows two chains) + // This happens when the attack disrupts multiple domination lines + + // Find interior positions that are now undominated and need filling + for (let x = 1; x < COLS - 1; x++) { + for (let y = 2; y < ROWS - 2; y++) { + if (!isDominated(x, y, newGuards)) { + // Need to get a guard here + const nbs = getHexNeighbors(x, y); + const nearbyGuard = nbs.find(n => newGuards.has(posKey(n)) && isInterior(n.x, n.y)); + + if (nearbyGuard) { + newGuards.delete(posKey(nearbyGuard)); + newGuards.add(posKey({ x, y })); + movements.push({ from: nearbyGuard, to: { x, y }, type: 'interior' }); + } else { + // Use border guard + const borderGuard = nbs.find(n => newGuards.has(posKey(n)) && isBorder(n.x, n.y)); + if (borderGuard) { + newGuards.delete(posKey(borderGuard)); + newGuards.add(posKey({ x, y })); + movements.push({ from: borderGuard, to: { x, y }, type: 'toInterior' }); + shiftBorderToFillGap(newGuards, movements, borderGuard); } } } @@ -277,13 +314,60 @@ const defendAttack = (guards: Set, attack: Position): { return { newGuards, movements }; }; +// Shift border guards along the cycle to fill a gap (complementary path shifting) +const shiftBorderToFillGap = ( + guards: Set, + 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 [guards, setGuards] = useState>(generateInitialGuards); const [attackHistory, setAttackHistory] = useState([]); const [message, setMessage] = useState("Click any unguarded cell to attack."); const [moveCount, setMoveCount] = useState(0); const [showInfo, setShowInfo] = useState(false); - const [lastMovements, setLastMovements] = useState>([]); + const [lastMovements, setLastMovements] = useState([]); const handleCellClick = useCallback((x: number, y: number) => { const attack: Position = { x, y }; @@ -301,8 +385,16 @@ const EternalDominationGame: React.FC = () => { 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)) { - setMessage(`Attack defended! ${result.movements.length} guard${result.movements.length !== 1 ? 's' : ''} moved.`); + 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!"); } @@ -326,11 +418,21 @@ const EternalDominationGame: React.FC = () => { }).length; const borderGuards = guardCount - interiorGuards; - // Cell dimensions for rendering const cellW = 44; const cellH = 40; const offset = cellW / 2; + // Color mapping for movement types + const getMovementColor = (type: string) => { + switch (type) { + case 'interior': return '#ef4444'; // Red - interior chain shift + case 'toInterior': return '#06b6d4'; // Cyan - w₃→u₃ (border to interior) + case 'toBorder': return '#f97316'; // Orange - u₁→w₁ (interior to border) + case 'pathShift': return '#22c55e'; // Green - complementary path shift + default: return '#888'; + } + }; + return (
@@ -340,7 +442,7 @@ const EternalDominationGame: React.FC = () => { Eternal Domination (Figure 10) - 10×12 extended good rectangle with double-layer border + 10×12 grid with double-layer border — defense via interior chains and complementary path shifts @@ -365,8 +467,13 @@ const EternalDominationGame: React.FC = () => { {showInfo && (
-

Figure 10 Setup: The border forms cycle C with guards on all vertices (columns 0/9 and rows 0/1/10/11). Interior guards follow U₃ pattern.

-

Defense (Claim 2): Interior guards shift toward attack. Two guards move interior→border (u₁→w₁, u₂→w₂), two move border→interior (w₃→u₃, w₄→u₄). Complementary paths P₁,₃ and P₂,₄ (green) shift border guards.

+

Figure 10 Defense Strategy:

+
    +
  • Red arrows: Interior guards shift in chains toward attack (v_t)
  • +
  • Orange: Interior guard pushed to border (u₁→w₁, u₂→w₂)
  • +
  • Cyan: Border guard enters interior (w₃→u₃, w₄→u₄)
  • +
  • Green arrows: Complementary path shifts (P₁,₃ and P₂,₄) restore border
  • +
)} @@ -375,7 +482,7 @@ const EternalDominationGame: React.FC = () => { {message}
- {/* Grid rendering */} + {/* Grid */}
{ height: ROWS * cellH + 20 }} > - {/* Edges */} + {/* Edges */} {Array.from({ length: ROWS }, (_, y) => Array.from({ length: COLS }, (_, x) => { const rowOffset = y % 2 === 1 ? offset : 0; @@ -403,7 +510,6 @@ const EternalDominationGame: React.FC = () => { const ncx = nb.x * cellW + cellW / 2 + nbOffset + 10; const ncy = nb.y * cellH + cellH / 2 + 10; - // Green edges for border cycle (complementary paths) const isBorderEdge = isBorder(x, y) && isBorder(nb.x, nb.y); return ( @@ -411,8 +517,8 @@ const EternalDominationGame: React.FC = () => { key={`${x}-${y}-${i}`} x1={cx} y1={cy} x2={ncx} y2={ncy} stroke={isBorderEdge ? "#22c55e" : "currentColor"} - strokeOpacity={isBorderEdge ? 0.7 : 0.15} - strokeWidth={isBorderEdge ? 2.5 : 1} + strokeOpacity={isBorderEdge ? 0.6 : 0.15} + strokeWidth={isBorderEdge ? 2 : 1} /> ); }); @@ -428,7 +534,7 @@ const EternalDominationGame: React.FC = () => { 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'; + const color = getMovementColor(move.type); return ( { x1={x1} y1={y1} x2={x2} y2={y2} stroke={color} strokeWidth={3} - strokeOpacity={0.85} + strokeOpacity={0.9} markerEnd={`url(#arrow-${move.type})`} /> ); @@ -446,10 +552,13 @@ const EternalDominationGame: React.FC = () => { - + - + + + + @@ -481,7 +590,7 @@ const EternalDominationGame: React.FC = () => { top: y * cellH + 10 + (cellH - 36) / 2, }} onClick={() => handleCellClick(x, y)} - title={`(${x},${y}) ${hasGuard ? (onBorder ? "Border" : "Interior") : dominated ? "" : "UNDOMINATED"}`} + title={`(${x},${y})`} > {hasGuard && }
@@ -497,17 +606,17 @@ const EternalDominationGame: React.FC = () => {
- Border guard + Border
- Interior guard + Interior
- Complementary paths (P₁,₃ / P₂,₄) + Path shifts (P₁,₃/P₂,₄)