const { describe, it } = require('node:test'); const assert = require('node:assert'); const { HexMap, HexCell, CELL_TYPES, MAP_SIZE } = require('../src/map.js'); describe('HexCell', () => { it('should create a cell with axial coordinates', () => { const cell = new HexCell(3, 5); assert.strictEqual(cell.q, 3); assert.strictEqual(cell.r, 5); assert.strictEqual(cell.type, CELL_TYPES.EMPTY); assert.strictEqual(cell.dice.length, 0); }); it('should calculate strength correctly', () => { const cell = new HexCell(0, 0); // Empty cell has 0 strength assert.strictEqual(cell.getStrength(), 0); // Single die with value 4 = strength 4 cell.dice = [4]; assert.strictEqual(cell.getStrength(), 4); // Two dice: [6, 3] = (2-1)*6 + 3 = 9 cell.dice = [6, 3]; assert.strictEqual(cell.getStrength(), 9); // Three dice: [6, 6, 2] = (3-1)*6 + 2 = 14 cell.dice = [6, 6, 2]; assert.strictEqual(cell.getStrength(), 14); }); it('should detect max strength', () => { const cell = new HexCell(0, 0); cell.dice = [6, 6, 6, 6, 6, 6, 6, 6]; assert.strictEqual(cell.isMaxStrength(), true); cell.dice = [6, 6, 6, 6, 6, 6, 6, 5]; assert.strictEqual(cell.isMaxStrength(), false); }); it('should add dice up to limit', () => { const cell = new HexCell(0, 0); for (let i = 0; i < 8; i++) { assert.strictEqual(cell.addDie(6), true); } // 9th die should fail assert.strictEqual(cell.addDie(6), false); assert.strictEqual(cell.dice.length, 8); }); it('should set strength correctly', () => { const cell = new HexCell(0, 0); cell.setStrength(1); assert.deepStrictEqual(cell.dice, [1]); cell.setStrength(6); assert.deepStrictEqual(cell.dice, [6]); cell.setStrength(7); assert.deepStrictEqual(cell.dice, [6, 1]); cell.setStrength(14); assert.deepStrictEqual(cell.dice, [6, 6, 2]); cell.setStrength(0); assert.deepStrictEqual(cell.dice, []); }); it('should check passability and ownership', () => { const emptyCell = new HexCell(0, 0, CELL_TYPES.EMPTY); const blockedCell = new HexCell(0, 0, CELL_TYPES.BLOCKED); const player1Cell = new HexCell(0, 0, CELL_TYPES.PLAYER1); assert.strictEqual(emptyCell.isPassable(), true); assert.strictEqual(blockedCell.isPassable(), false); assert.strictEqual(emptyCell.isOwned(), false); assert.strictEqual(player1Cell.isOwned(), true); assert.strictEqual(emptyCell.getOwner(), 0); assert.strictEqual(player1Cell.getOwner(), 1); }); }); describe('HexMap', () => { it('should create a 20x20 map by default', () => { const map = new HexMap(); assert.strictEqual(map.size, MAP_SIZE); assert.strictEqual(map.cells.size, MAP_SIZE * MAP_SIZE); }); it('should generate cells with correct coordinates', () => { const map = new HexMap(5); for (let q = 0; q < 5; q++) { for (let r = 0; r < 5; r++) { const cell = map.getCell(q, r); assert.ok(cell, `Cell at ${q},${r} should exist`); assert.strictEqual(cell.q, q); assert.strictEqual(cell.r, r); } } }); it('should have some blocked cells', () => { const map = new HexMap(); const blockedCells = Array.from(map.cells.values()).filter( cell => cell.type === CELL_TYPES.BLOCKED ); // Should have some blocked cells (roughly 15%) assert.ok(blockedCells.length > 0, 'Should have at least one blocked cell'); assert.ok(blockedCells.length < map.cells.size / 2, 'Should not have more than 50% blocked'); }); it('should return passable cells', () => { const map = new HexMap(5); const passableCells = map.getPassableCells(); assert.ok(passableCells.length > 0); passableCells.forEach(cell => { assert.strictEqual(cell.isPassable(), true); }); }); it('should return neighbors correctly', () => { const map = new HexMap(5); // Clear all blocks for predictable testing map.cells.forEach(cell => { if (cell.type === CELL_TYPES.BLOCKED) { cell.type = CELL_TYPES.EMPTY; } }); // Center cell should have 6 neighbors const centerNeighbors = map.getNeighbors(2, 2); assert.strictEqual(centerNeighbors.length, 6); // Corner cell should have 2 neighbors const cornerNeighbors = map.getNeighbors(0, 0); assert.strictEqual(cornerNeighbors.length, 2); // Edge cell should have 3-4 neighbors const edgeNeighbors = map.getNeighbors(0, 2); assert.ok(edgeNeighbors.length >= 3 && edgeNeighbors.length <= 4); }); it('should set and clear ownership', () => { const map = new HexMap(5); // Clear the map first map.cells.forEach(cell => { cell.type = CELL_TYPES.EMPTY; }); // Set ownership on cells that exist assert.strictEqual(map.setOwner(2, 2, 1), true); const cell = map.getCell(2, 2); assert.strictEqual(cell.type, CELL_TYPES.PLAYER1); assert.strictEqual(cell.getOwner(), 1); // Set player 2 ownership on adjacent cell (2,3 is a neighbor of 2,2) assert.strictEqual(map.setOwner(2, 3, 2), true); const cell2 = map.getCell(2, 3); assert.strictEqual(cell2.type, CELL_TYPES.PLAYER2); assert.strictEqual(cell2.getOwner(), 2); // Clear ownership assert.strictEqual(map.clearOwner(2, 2), true); const clearedCell = map.getCell(2, 2); assert.strictEqual(clearedCell.type, CELL_TYPES.EMPTY); assert.strictEqual(clearedCell.getOwner(), 0); }); it('should return player cells', () => { const map = new HexMap(5); // First clear any existing ownership map.cells.forEach(cell => { cell.type = CELL_TYPES.EMPTY; }); map.setOwner(0, 0, 1); map.setOwner(0, 1, 1); map.setOwner(1, 0, 2); const player1Cells = map.getPlayerCells(1); const player2Cells = map.getPlayerCells(2); assert.strictEqual(player1Cells.length, 2); assert.strictEqual(player2Cells.length, 1); }); it('should calculate supply correctly', () => { const map = new HexMap(5); // First clear any existing ownership map.cells.forEach(cell => { cell.type = CELL_TYPES.EMPTY; }); map.setOwner(0, 0, 1); map.setOwner(0, 1, 1); map.setOwner(1, 0, 1); const supply = map.calculateSupply(1); assert.strictEqual(supply, 3); // 3 cells = 3 supply }); it('should not allow setting owner on blocked cell', () => { const map = new HexMap(5); // Find a blocked cell let blockedCell = null; map.cells.forEach(cell => { if (cell.type === CELL_TYPES.BLOCKED) { blockedCell = cell; } }); if (blockedCell) { assert.strictEqual(map.setOwner(blockedCell.q, blockedCell.r, 1), false); } }); }); describe('Target Cell Selection Logic', () => { it('should allow attacking adjacent enemy cells', () => { const map = new HexMap(5); // Clear map map.cells.forEach(cell => { cell.type = CELL_TYPES.EMPTY; }); // Set up: player 1 at (2,2), player 2 at adjacent (2,1) // Neighbors of (2,2) in axial coords: (3,2), (3,1), (2,1), (1,2), (1,3), (2,3) map.setOwner(2, 2, 1); map.setOwner(2, 1, 2); // (2,1) is a neighbor of (2,2) const p1Cell = map.getCell(2, 2); p1Cell.setStrength(8); const p2Cell = map.getCell(2, 1); p2Cell.setStrength(6); // Get neighbors of player 1 cell const neighbors = map.getNeighbors(2, 2); // Player 2 cell should be in neighbors const hasP2Cell = neighbors.some(n => n.q === 2 && n.r === 1); assert.strictEqual(hasP2Cell, true, 'Enemy cell should be adjacent'); // Valid target: not blocked and not own // All 6 neighbors are valid because none of them are owned by player 1 // (only the center cell 2,2 is owned by player 1, but it's not a neighbor of itself) const validTargets = neighbors.filter(n => n.type !== CELL_TYPES.BLOCKED && n.getOwner() !== 1 ); // Should have 6 valid targets (one is enemy at 2,1, rest are empty) assert.strictEqual(validTargets.length, 6, 'Should have 6 valid targets (enemy + 5 empty)'); // Verify enemy cell is in valid targets const enemyInTargets = validTargets.some(n => n.q === 2 && n.r === 1); assert.strictEqual(enemyInTargets, true); }); it('should allow capturing adjacent empty cells', () => { const map = new HexMap(5); // Clear map map.cells.forEach(cell => { cell.type = CELL_TYPES.EMPTY; }); // Set up: player 1 at (2,2), empty at adjacent (2,3) map.setOwner(2, 2, 1); const p1Cell = map.getCell(2, 2); p1Cell.setStrength(8); const emptyCell = map.getCell(2, 3); assert.strictEqual(emptyCell.type, CELL_TYPES.EMPTY); // Get neighbors of player 1 cell const neighbors = map.getNeighbors(2, 2); // Empty cell should be in neighbors and be a valid target const validTargets = neighbors.filter(n => n.type !== CELL_TYPES.BLOCKED && n.getOwner() !== 1 ); const hasEmptyCell = validTargets.some(n => n.q === 2 && n.r === 3); assert.strictEqual(hasEmptyCell, true, 'Empty cell should be a valid target'); }); it('should NOT allow attacking own cells', () => { const map = new HexMap(5); // Clear map map.cells.forEach(cell => { cell.type = CELL_TYPES.EMPTY; }); // Set up: player 1 owns two adjacent cells map.setOwner(2, 2, 1); map.setOwner(2, 3, 1); const p1Cell1 = map.getCell(2, 2); p1Cell1.setStrength(8); const p1Cell2 = map.getCell(2, 3); p1Cell2.setStrength(6); // Get neighbors of first player 1 cell const neighbors = map.getNeighbors(2, 2); // Valid targets should NOT include own cells const validTargets = neighbors.filter(n => n.type !== CELL_TYPES.BLOCKED && n.getOwner() !== 1 ); const hasOwnCell = validTargets.some(n => n.getOwner() === 1); assert.strictEqual(hasOwnCell, false, 'Own cells should not be valid targets'); }); it('should NOT allow attacking through blocked cells', () => { const map = new HexMap(5); // Clear map map.cells.forEach(cell => { cell.type = CELL_TYPES.EMPTY; }); // Set up: player 1 at (2,2), blocked at adjacent (2,3) map.setOwner(2, 2, 1); map.getCell(2, 3).type = CELL_TYPES.BLOCKED; const p1Cell = map.getCell(2, 2); p1Cell.setStrength(8); // Get neighbors of player 1 cell const neighbors = map.getNeighbors(2, 2); // Blocked cell should NOT be in passable neighbors const hasBlocked = neighbors.some(n => n.type === CELL_TYPES.BLOCKED); assert.strictEqual(hasBlocked, false, 'Blocked cells should not be neighbors'); }); it('should correctly identify all valid targets from center position', () => { const map = new HexMap(5); // Clear map map.cells.forEach(cell => { cell.type = CELL_TYPES.EMPTY; }); // Set up: player 1 at center (2,2) map.setOwner(2, 2, 1); const centerCell = map.getCell(2, 2); centerCell.setStrength(8); // Surround with different cell types // (2,1) - empty // (3,1) - enemy (player 2) // (3,2) - blocked // (2,3) - empty // (1,3) - enemy (player 2) // (1,2) - own (player 1) map.setOwner(3, 1, 2); map.getCell(3, 2).type = CELL_TYPES.BLOCKED; map.setOwner(1, 3, 2); map.setOwner(1, 2, 1); const neighbors = map.getNeighbors(2, 2); // Valid targets: empty cells and enemy cells, not blocked, not own const validTargets = neighbors.filter(n => n.type !== CELL_TYPES.BLOCKED && n.getOwner() !== 1 ); // Should have 3 valid targets: (2,1) empty, (3,1) enemy, (2,3) empty // (1,3) enemy, but need to check if it's actually a neighbor assert.ok(validTargets.length >= 2, 'Should have at least 2 valid targets'); // Verify no own cells in valid targets validTargets.forEach(t => { assert.notStrictEqual(t.getOwner(), 1, 'Valid target should not be own cell'); }); // Verify no blocked cells in valid targets validTargets.forEach(t => { assert.notStrictEqual(t.type, CELL_TYPES.BLOCKED, 'Valid target should not be blocked'); }); }); });