This commit is contained in:
gpt-engineer-app[bot] 2026-01-20 08:20:45 +00:00
parent 75d51143e1
commit 8729409e8e

View file

@ -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<string> => {
const guards = new Set<string>();
// 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<string> => {
}
}
// 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<string> => {
return guards;
};
// Check if a vertex is dominated
// Check if vertex is dominated
const isDominated = (x: number, y: number, guards: Set<string>): 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<string>): 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<string>, attack: Position): {
newGuards: Set<string>;
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<string>, 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<string>, 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<string>, attack: Position): {
const EternalDominationGame: React.FC = () => {
const [guards, setGuards] = useState<Set<string>>(generateInitialGuards);
const [attackHistory, setAttackHistory] = useState<Position[]>([]);
const [message, setMessage] = useState<string>("Click any cell to attack. Guards will shift to defend.");
const [message, setMessage] = useState<string>("Click any unguarded cell to attack.");
const [moveCount, setMoveCount] = useState(0);
const [showInfo, setShowInfo] = useState(false);
const [lastMovements, setLastMovements] = useState<Array<{ from: Position; to: Position; type: string }>>([]);
@ -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 (
<div className="space-y-6">
@ -355,24 +337,24 @@ const EternalDominationGame: React.FC = () => {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="w-5 h-5" />
Eternal Domination on a Hexagonal Grid
Eternal Domination (Figure 10)
</CardTitle>
<CardDescription>
12×10 hexagonal grid with double-layer border defense (Figure 10)
10×12 extended good rectangle with double-layer border
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-2 items-center justify-between">
<div className="flex gap-2">
<div className="flex gap-2 flex-wrap">
<Badge variant="outline">Guards: {guardCount}</Badge>
<Badge variant="secondary" className="bg-cyan-500/20 text-cyan-700 dark:text-cyan-300">Border: {borderGuards}</Badge>
<Badge variant="secondary" className="bg-red-500/20 text-red-700 dark:text-red-300">Interior: {interiorGuards}</Badge>
<Badge className="bg-cyan-600/80">Border: {borderGuards}</Badge>
<Badge className="bg-red-600/80">Interior: {interiorGuards}</Badge>
<Badge variant="outline">Attacks: {moveCount}</Badge>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => setShowInfo(!showInfo)}>
<Info className="w-4 h-4 mr-1" />
{showInfo ? "Hide" : "Show"} Info
Info
</Button>
<Button variant="outline" size="sm" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-1" />
@ -383,9 +365,8 @@ const EternalDominationGame: React.FC = () => {
{showInfo && (
<div className="p-4 bg-muted rounded-lg text-sm space-y-2">
<p><strong>m-Eternal Domination (Figure 10):</strong> The border forms a double-layer cycle C with guards on every vertex. Interior guards follow U configuration.</p>
<p><strong>Defense Strategy:</strong> When attacked, interior guards shift toward attack. Guards may move from interiorborder or borderinterior. Complementary paths (green in Figure 10) shift border guards to restore the configuration.</p>
<p><strong>Goal:</strong> Try to find an attack sequence that leaves some vertex undominated!</p>
<p><strong>Figure 10 Setup:</strong> The border forms cycle C with guards on all vertices (columns 0/9 and rows 0/1/10/11). Interior guards follow U pattern.</p>
<p><strong>Defense (Claim 2):</strong> Interior guards shift toward attack. Two guards move interiorborder (uw, uw), two move borderinterior (wu, wu). Complementary paths P, and P, (green) shift border guards.</p>
</div>
)}
@ -394,90 +375,82 @@ const EternalDominationGame: React.FC = () => {
{message}
</div>
{/* Hexagonal Grid */}
{/* Grid rendering */}
<div className="flex justify-center overflow-x-auto pb-4">
<div
className="relative"
style={{
width: COLS * cellWidth + rowOffset + 20,
height: ROWS * cellHeight + 20
width: COLS * cellW + offset + 20,
height: ROWS * cellH + 20
}}
>
{/* Draw edges */}
{/* Edges */}
<svg
className="absolute inset-0 pointer-events-none"
width={COLS * cellWidth + rowOffset + 20}
height={ROWS * cellHeight + 20}
width={COLS * cellW + offset + 20}
height={ROWS * cellH + 20}
>
{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 (
<line
key={`${x}-${y}-${i}`}
x1={cx}
y1={cy}
x2={ncx}
y2={ncy}
x1={cx} y1={cy} x2={ncx} y2={ncy}
stroke={isBorderEdge ? "#22c55e" : "currentColor"}
strokeOpacity={isBorderEdge ? 0.6 : 0.2}
strokeWidth={isBorderEdge ? 2 : 1}
strokeOpacity={isBorderEdge ? 0.7 : 0.15}
strokeWidth={isBorderEdge ? 2.5 : 1}
/>
);
});
})
)}
{/* 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 (
<g key={`move-${i}`}>
<line
x1={x1}
y1={y1}
x2={x2}
y2={y2}
stroke={color}
strokeWidth={3}
strokeOpacity={0.8}
markerEnd="url(#arrowhead)"
/>
</g>
<line
key={`mv-${i}`}
x1={x1} y1={y1} x2={x2} y2={y2}
stroke={color}
strokeWidth={3}
strokeOpacity={0.85}
markerEnd={`url(#arrow-${move.type})`}
/>
);
})}
<defs>
<marker
id="arrowhead"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
>
<polygon points="0 0, 10 3.5, 0 7" fill="#ef4444" />
<marker id="arrow-interior" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#ef4444" />
</marker>
<marker id="arrow-border" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#06b6d4" />
</marker>
<marker id="arrow-path" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#22c55e" />
</marker>
</defs>
</svg>
@ -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 (
<div
key={`${x}-${y}`}
className={`absolute w-8 h-8 rounded-full border-2 flex items-center justify-center text-sm cursor-pointer transition-all hover:scale-110 ${bgClass}`}
className={`absolute w-9 h-9 rounded-full border-2 flex items-center justify-center text-sm cursor-pointer transition-colors ${bgClass}`}
style={{
left: x * cellWidth + offset + 10 + (cellWidth - 32) / 2,
top: y * cellHeight + 10 + (cellHeight - 32) / 2,
left: x * cellW + rowOffset + 10 + (cellW - 36) / 2,
top: y * cellH + 10 + (cellH - 36) / 2,
}}
onClick={() => 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 && <Shield className="w-4 h-4" />}
</div>
);
})
@ -528,29 +494,31 @@ const EternalDominationGame: React.FC = () => {
{/* Legend */}
<div className="flex flex-wrap gap-4 text-sm">
<div className="flex items-center gap-2">
<div className="w-6 h-6 bg-cyan-500/50 border-2 border-cyan-600 rounded-full flex items-center justify-center text-xs">🛡</div>
<span>Border Guard (double layer)</span>
<div className="w-6 h-6 bg-cyan-500/60 border-2 border-cyan-600 rounded-full flex items-center justify-center">
<Shield className="w-3 h-3" />
</div>
<span>Border guard</span>
</div>
<div className="flex items-center gap-2">
<div className="w-6 h-6 bg-red-500/50 border-2 border-red-600 rounded-full flex items-center justify-center text-xs">🛡</div>
<span>Interior Guard</span>
<div className="w-6 h-6 bg-red-500/60 border-2 border-red-600 rounded-full flex items-center justify-center">
<Shield className="w-3 h-3" />
</div>
<span>Interior guard</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-0.5 bg-green-500"></div>
<span>Complementary paths (border cycle)</span>
<div className="w-4 h-0.5 bg-green-500"></div>
<span>Complementary paths (P, / P,)</span>
</div>
</div>
{attackHistory.length > 0 && (
<div className="text-sm">
<p className="text-muted-foreground mb-1">Recent attacks:</p>
<div className="flex flex-wrap gap-1">
{attackHistory.slice(-10).map((pos, i) => (
<Badge key={i} variant="outline" className="text-xs">
({pos.x}, {pos.y})
</Badge>
))}
</div>
<span className="text-muted-foreground">Attacks: </span>
{attackHistory.slice(-8).map((pos, i) => (
<Badge key={i} variant="outline" className="text-xs mx-0.5">
({pos.x},{pos.y})
</Badge>
))}
</div>
)}
</CardContent>