Implement double-layer border hex grid defense
Adds Eternal Domination on a Hexagonal Grid with a true double-border (outer and inner) and updated defense logic to follow Figure 10 style movements, including interior-right shifts and border complement-path adjustments. Includes guard generation, domination checks, and hex grid rendering. X-Lovable-Edit-ID: edt-1484a53e-a02f-4d8a-9599-2858d45929de
This commit is contained in:
commit
75d51143e1
1 changed files with 235 additions and 112 deletions
|
|
@ -55,23 +55,54 @@ const getHexNeighbors = (x: number, y: number): Position[] => {
|
||||||
return neighbors;
|
return neighbors;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if a position is on the border of the "interested rectangle"
|
// Following Figure 10: the border is a DOUBLE LAYER forming cycle C
|
||||||
// Following Figure 10: border forms cycle C around the rectangle
|
// 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 => {
|
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 => {
|
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
|
// 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<string> => {
|
const generateInitialGuards = (): Set<string> => {
|
||||||
const guards = new Set<string>();
|
const guards = new Set<string>();
|
||||||
|
|
||||||
// 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 x = 0; x < COLS; x++) {
|
||||||
for (let y = 0; y < ROWS; y++) {
|
for (let y = 0; y < ROWS; y++) {
|
||||||
if (isBorder(x, y)) {
|
if (isBorder(x, y)) {
|
||||||
|
|
@ -80,16 +111,14 @@ const generateInitialGuards = (): Set<string> => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interior guards following the eternal dominating set pattern
|
// Interior guards following the U₃ eternal dominating set pattern
|
||||||
// Based on the paper: we need interior guards at density 7/15 nm
|
// Interior is 8x6 grid (from x=2 to x=9, y=2 to y=7)
|
||||||
// Pattern: guards at positions creating a dominating set
|
// Place guards to ensure domination at density ~7/15
|
||||||
// Using pattern similar to Figure 10's interior placement
|
for (let x = 2; x < COLS - 2; x++) {
|
||||||
for (let x = 1; x < COLS - 1; x++) {
|
for (let y = 2; y < ROWS - 2; y++) {
|
||||||
for (let y = 1; y < ROWS - 1; y++) {
|
// Pattern based on Figure 10 interior placement
|
||||||
// Place guards following a hexagonal domination pattern
|
// Creates a dominating set where every interior vertex is within distance 1 of a guard
|
||||||
// Every vertex in interior needs to be within distance 1 of a guard
|
if ((x + 2 * y) % 4 === 0) {
|
||||||
// Pattern: (2x + y) mod 5 === 0 creates suitable coverage
|
|
||||||
if ((2 * x + y) % 5 === 0) {
|
|
||||||
guards.add(posKey({ x, y }));
|
guards.add(posKey({ x, y }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,15 +144,41 @@ const isFullyDominated = (guards: Set<string>): boolean => {
|
||||||
return true;
|
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:
|
// Defense strategy from Figure 10 and page 23:
|
||||||
// 1. Interior guards shift toward attack
|
// 1. Interior guards shift toward attack (one guard moves to v_t)
|
||||||
// 2. If a guard moves from interior to border, border guards shift along complementary paths
|
// 2. This may push guards from interior to border (u₁→w₁, u₂→w₂)
|
||||||
const defendAttack = (guards: Set<string>, attack: Position): Set<string> | null => {
|
// 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<string>, attack: Position): {
|
||||||
|
newGuards: Set<string>;
|
||||||
|
movements: Array<{ from: Position; to: Position; type: 'interior' | 'border' | 'path' }>
|
||||||
|
} | null => {
|
||||||
const attackKey = posKey(attack);
|
const attackKey = posKey(attack);
|
||||||
|
|
||||||
// If attack is on a guard, already defended
|
// If attack is on a guard, already defended
|
||||||
if (guards.has(attackKey)) {
|
if (guards.has(attackKey)) {
|
||||||
return guards;
|
return { newGuards: guards, movements: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find adjacent guards that can move to defend
|
// Find adjacent guards that can move to defend
|
||||||
|
|
@ -131,12 +186,15 @@ const defendAttack = (guards: Set<string>, attack: Position): Set<string> | null
|
||||||
const adjacentGuards = neighbors.filter(p => guards.has(posKey(p)));
|
const adjacentGuards = neighbors.filter(p => guards.has(posKey(p)));
|
||||||
|
|
||||||
if (adjacentGuards.length === 0) {
|
if (adjacentGuards.length === 0) {
|
||||||
return null; // Should not happen with proper dominating set
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newGuards = new Set(guards);
|
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;
|
let defender: Position | null = null;
|
||||||
for (const g of adjacentGuards) {
|
for (const g of adjacentGuards) {
|
||||||
if (isInterior(g.x, g.y)) {
|
if (isInterior(g.x, g.y)) {
|
||||||
|
|
@ -144,6 +202,16 @@ const defendAttack = (guards: Set<string>, attack: Position): Set<string> | null
|
||||||
break;
|
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) {
|
if (!defender) {
|
||||||
defender = adjacentGuards[0];
|
defender = adjacentGuards[0];
|
||||||
}
|
}
|
||||||
|
|
@ -151,79 +219,77 @@ const defendAttack = (guards: Set<string>, attack: Position): Set<string> | null
|
||||||
// Move defender to attack position
|
// Move defender to attack position
|
||||||
newGuards.delete(posKey(defender));
|
newGuards.delete(posKey(defender));
|
||||||
newGuards.add(attackKey);
|
newGuards.add(attackKey);
|
||||||
|
movements.push({ from: defender, to: attack, type: 'interior' });
|
||||||
|
|
||||||
// If defender was interior and attack is on border, apply border shift strategy
|
// Now we need to restore the configuration:
|
||||||
// Following the complementary paths P₁,₃ and P₂,₄ from Figure 10
|
// If an interior guard moved, we need to shift guards along the border to fill the gap
|
||||||
if (isInterior(defender.x, defender.y)) {
|
// Following Figure 10: find complementary paths to shift guards
|
||||||
// Interior to border movement: shift guards along border cycle
|
|
||||||
// to maintain the dominating set property
|
|
||||||
applyBorderShift(newGuards, defender, attack);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure domination is maintained
|
if (isInterior(defender.x, defender.y) || getBorderLayer(defender.x, defender.y) === 1) {
|
||||||
ensureDomination(newGuards);
|
// 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))
|
||||||
|
);
|
||||||
|
|
||||||
return newGuards;
|
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' });
|
||||||
|
|
||||||
// Border shift strategy: when interior guard moves out, shift border guards
|
// Now shift guards along border path to fill the gap at borderNb
|
||||||
// along complementary paths to fill gaps
|
// Find another border guard to shift toward borderNb
|
||||||
const applyBorderShift = (guards: Set<string>, from: Position, to: Position): void => {
|
const borderNbNeighbors = getHexNeighbors(borderNb.x, borderNb.y);
|
||||||
// Check if any interior vertices became undominated
|
const borderShifter = borderNbNeighbors.find(n =>
|
||||||
for (let x = 1; x < COLS - 1; x++) {
|
isBorder(n.x, n.y) && newGuards.has(posKey(n)) &&
|
||||||
for (let y = 1; y < ROWS - 1; y++) {
|
!movements.some(m => m.from.x === n.x && m.from.y === n.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 all vertices remain dominated after movement
|
if (borderShifter) {
|
||||||
const ensureDomination = (guards: Set<string>): void => {
|
// Shift along complementary path
|
||||||
for (let x = 0; x < COLS; x++) {
|
newGuards.delete(posKey(borderShifter));
|
||||||
for (let y = 0; y < ROWS; y++) {
|
newGuards.add(posKey(borderNb));
|
||||||
if (!isDominated(x, y, guards)) {
|
movements.push({ from: borderShifter, to: borderNb, type: 'path' });
|
||||||
// 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 = () => {
|
const EternalDominationGame: React.FC = () => {
|
||||||
|
|
@ -232,6 +298,7 @@ const EternalDominationGame: React.FC = () => {
|
||||||
const [message, setMessage] = useState<string>("Click any cell to attack. Guards will shift to defend.");
|
const [message, setMessage] = useState<string>("Click any cell to attack. Guards will shift to defend.");
|
||||||
const [moveCount, setMoveCount] = useState(0);
|
const [moveCount, setMoveCount] = useState(0);
|
||||||
const [showInfo, setShowInfo] = useState(false);
|
const [showInfo, setShowInfo] = useState(false);
|
||||||
|
const [lastMovements, setLastMovements] = useState<Array<{ from: Position; to: Position; type: string }>>([]);
|
||||||
|
|
||||||
const handleCellClick = useCallback((x: number, y: number) => {
|
const handleCellClick = useCallback((x: number, y: number) => {
|
||||||
const attack: Position = { x, y };
|
const attack: Position = { x, y };
|
||||||
|
|
@ -241,15 +308,19 @@ const EternalDominationGame: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newGuards = defendAttack(guards, attack);
|
const result = defendAttack(guards, attack);
|
||||||
|
|
||||||
if (newGuards) {
|
if (result) {
|
||||||
setGuards(newGuards);
|
setGuards(result.newGuards);
|
||||||
|
setLastMovements(result.movements);
|
||||||
setAttackHistory(prev => [...prev, attack]);
|
setAttackHistory(prev => [...prev, attack]);
|
||||||
setMoveCount(prev => prev + 1);
|
setMoveCount(prev => prev + 1);
|
||||||
|
|
||||||
if (isFullyDominated(newGuards)) {
|
if (isFullyDominated(result.newGuards)) {
|
||||||
setMessage(`Attack defended! All vertices remain dominated. (Move ${moveCount + 1})`);
|
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 {
|
} else {
|
||||||
setMessage("Defense failed! Some vertices are not dominated.");
|
setMessage("Defense failed! Some vertices are not dominated.");
|
||||||
}
|
}
|
||||||
|
|
@ -261,6 +332,7 @@ const EternalDominationGame: React.FC = () => {
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
setGuards(generateInitialGuards());
|
setGuards(generateInitialGuards());
|
||||||
setAttackHistory([]);
|
setAttackHistory([]);
|
||||||
|
setLastMovements([]);
|
||||||
setMessage("Click any cell to attack. Guards will shift to defend.");
|
setMessage("Click any cell to attack. Guards will shift to defend.");
|
||||||
setMoveCount(0);
|
setMoveCount(0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -273,8 +345,8 @@ const EternalDominationGame: React.FC = () => {
|
||||||
const borderGuards = guardCount - interiorGuards;
|
const borderGuards = guardCount - interiorGuards;
|
||||||
|
|
||||||
// Hexagonal cell dimensions
|
// Hexagonal cell dimensions
|
||||||
const cellWidth = 36;
|
const cellWidth = 40;
|
||||||
const cellHeight = 32;
|
const cellHeight = 36;
|
||||||
const rowOffset = cellWidth / 2;
|
const rowOffset = cellWidth / 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -286,15 +358,15 @@ const EternalDominationGame: React.FC = () => {
|
||||||
Eternal Domination on a Hexagonal Grid
|
Eternal Domination on a Hexagonal Grid
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
12×10 hexagonal grid F₃(12, 10) with m-eternal dominating set defense
|
12×10 hexagonal grid with double-layer border defense (Figure 10)
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-2 items-center justify-between">
|
<div className="flex flex-wrap gap-2 items-center justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Badge variant="outline">Guards: {guardCount}</Badge>
|
<Badge variant="outline">Guards: {guardCount}</Badge>
|
||||||
<Badge variant="secondary">Border: {borderGuards}</Badge>
|
<Badge variant="secondary" className="bg-cyan-500/20 text-cyan-700 dark:text-cyan-300">Border: {borderGuards}</Badge>
|
||||||
<Badge variant="secondary">Interior: {interiorGuards}</Badge>
|
<Badge variant="secondary" className="bg-red-500/20 text-red-700 dark:text-red-300">Interior: {interiorGuards}</Badge>
|
||||||
<Badge variant="outline">Attacks: {moveCount}</Badge>
|
<Badge variant="outline">Attacks: {moveCount}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
@ -311,8 +383,8 @@ const EternalDominationGame: React.FC = () => {
|
||||||
|
|
||||||
{showInfo && (
|
{showInfo && (
|
||||||
<div className="p-4 bg-muted rounded-lg text-sm space-y-2">
|
<div className="p-4 bg-muted rounded-lg text-sm space-y-2">
|
||||||
<p><strong>m-Eternal Domination on Hexagonal Grid:</strong> Guards form a dominating set on F₃(n,m). Each attack must be defended by moving guards while maintaining domination.</p>
|
<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>Strategy (Figure 10):</strong> Border vertices form cycle C (always guarded). Interior guards shift toward attacks. Complementary paths along the border shift guards to fill gaps.</p>
|
<p><strong>Defense Strategy:</strong> 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.</p>
|
||||||
<p><strong>Goal:</strong> Try to find an attack sequence that leaves some vertex undominated!</p>
|
<p><strong>Goal:</strong> Try to find an attack sequence that leaves some vertex undominated!</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -331,7 +403,7 @@ const EternalDominationGame: React.FC = () => {
|
||||||
height: ROWS * cellHeight + 20
|
height: ROWS * cellHeight + 20
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Draw edges first */}
|
{/* Draw edges */}
|
||||||
<svg
|
<svg
|
||||||
className="absolute inset-0 pointer-events-none"
|
className="absolute inset-0 pointer-events-none"
|
||||||
width={COLS * cellWidth + rowOffset + 20}
|
width={COLS * cellWidth + rowOffset + 20}
|
||||||
|
|
@ -349,6 +421,10 @@ const EternalDominationGame: React.FC = () => {
|
||||||
const nOffset = neighbor.y % 2 === 1 ? rowOffset : 0;
|
const nOffset = neighbor.y % 2 === 1 ? rowOffset : 0;
|
||||||
const ncx = neighbor.x * cellWidth + cellWidth / 2 + nOffset + 10;
|
const ncx = neighbor.x * cellWidth + cellWidth / 2 + nOffset + 10;
|
||||||
const ncy = neighbor.y * cellHeight + cellHeight / 2 + 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 (
|
return (
|
||||||
<line
|
<line
|
||||||
key={`${x}-${y}-${i}`}
|
key={`${x}-${y}-${i}`}
|
||||||
|
|
@ -356,14 +432,54 @@ const EternalDominationGame: React.FC = () => {
|
||||||
y1={cy}
|
y1={cy}
|
||||||
x2={ncx}
|
x2={ncx}
|
||||||
y2={ncy}
|
y2={ncy}
|
||||||
stroke="currentColor"
|
stroke={isBorderEdge ? "#22c55e" : "currentColor"}
|
||||||
strokeOpacity={0.2}
|
strokeOpacity={isBorderEdge ? 0.6 : 0.2}
|
||||||
strokeWidth={1}
|
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 (
|
||||||
|
<g key={`move-${i}`}>
|
||||||
|
<line
|
||||||
|
x1={x1}
|
||||||
|
y1={y1}
|
||||||
|
x2={x2}
|
||||||
|
y2={y2}
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeOpacity={0.8}
|
||||||
|
markerEnd="url(#arrowhead)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{/* Cells */}
|
{/* Cells */}
|
||||||
|
|
@ -372,27 +488,34 @@ const EternalDominationGame: React.FC = () => {
|
||||||
const offset = y % 2 === 1 ? rowOffset : 0;
|
const offset = y % 2 === 1 ? rowOffset : 0;
|
||||||
const hasGuard = guards.has(posKey({ x, y }));
|
const hasGuard = guards.has(posKey({ x, y }));
|
||||||
const onBorder = isBorder(x, y);
|
const onBorder = isBorder(x, y);
|
||||||
|
const borderLayer = getBorderLayer(x, y);
|
||||||
const dominated = isDominated(x, y, guards);
|
const dominated = isDominated(x, y, guards);
|
||||||
|
|
||||||
let bgClass = "bg-muted hover:bg-muted/80";
|
let bgClass = "bg-muted hover:bg-muted/80";
|
||||||
if (hasGuard) {
|
if (hasGuard) {
|
||||||
bgClass = onBorder
|
if (onBorder) {
|
||||||
? "bg-primary/40 border-primary"
|
// Cyan for border guards (both layers)
|
||||||
: "bg-accent border-accent-foreground";
|
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) {
|
} else if (!dominated) {
|
||||||
bgClass = "bg-destructive/30";
|
bgClass = "bg-destructive/50 border-destructive";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${x}-${y}`}
|
key={`${x}-${y}`}
|
||||||
className={`absolute w-7 h-7 rounded-full border flex items-center justify-center text-sm cursor-pointer transition-all hover:scale-110 ${bgClass}`}
|
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}`}
|
||||||
style={{
|
style={{
|
||||||
left: x * cellWidth + offset + 10 + (cellWidth - 28) / 2,
|
left: x * cellWidth + offset + 10 + (cellWidth - 32) / 2,
|
||||||
top: y * cellHeight + 10 + (cellHeight - 28) / 2,
|
top: y * cellHeight + 10 + (cellHeight - 32) / 2,
|
||||||
}}
|
}}
|
||||||
onClick={() => handleCellClick(x, y)}
|
onClick={() => handleCellClick(x, y)}
|
||||||
title={`(${x}, ${y}) - ${hasGuard ? "Guard" : dominated ? "Dominated" : "Undominated!"}`}
|
title={`(${x}, ${y}) - ${hasGuard ? (onBorder ? "Border Guard" : "Interior Guard") : dominated ? "Dominated" : "Undominated!"}`}
|
||||||
>
|
>
|
||||||
{hasGuard ? "🛡️" : ""}
|
{hasGuard ? "🛡️" : ""}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -405,16 +528,16 @@ const EternalDominationGame: React.FC = () => {
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
<div className="flex flex-wrap gap-4 text-sm">
|
<div className="flex flex-wrap gap-4 text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-6 h-6 bg-primary/40 border border-primary rounded-full flex items-center justify-center text-xs">🛡️</div>
|
<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</span>
|
<span>Border Guard (double layer)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-6 h-6 bg-accent border border-accent-foreground rounded-full flex items-center justify-center text-xs">🛡️</div>
|
<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>
|
<span>Interior Guard</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-6 h-6 bg-muted border border-border rounded-full"></div>
|
<div className="w-3 h-0.5 bg-green-500"></div>
|
||||||
<span>Dominated</span>
|
<span>Complementary paths (border cycle)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue