Add AI bot tests and update documentation
This commit is contained in:
921
test/ai-bot.test.js
Normal file
921
test/ai-bot.test.js
Normal file
@@ -0,0 +1,921 @@
|
||||
const { describe, it, beforeEach } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { HexMap, HexCell, CELL_TYPES, MAP_SIZE } = require('../public/map.js');
|
||||
const { AIBot } = require('../public/ai-bot.js');
|
||||
|
||||
/**
|
||||
* Mock GameUI for testing AIBot
|
||||
* Provides minimal interface needed for AI bot to function
|
||||
*/
|
||||
class MockGameUI {
|
||||
constructor() {
|
||||
this.selectedCell = null;
|
||||
this.currentTarget = null;
|
||||
this.executedMoves = [];
|
||||
this.turnEnded = false;
|
||||
}
|
||||
|
||||
endTurn() {
|
||||
this.turnEnded = true;
|
||||
}
|
||||
|
||||
executeAttack() {
|
||||
this.executedMoves.push({
|
||||
from: this.selectedCell,
|
||||
to: this.currentTarget
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.selectedCell = null;
|
||||
this.currentTarget = null;
|
||||
this.executedMoves = [];
|
||||
this.turnEnded = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a clean map for testing
|
||||
*/
|
||||
function createTestMap(size = 5) {
|
||||
const map = new HexMap(size);
|
||||
// Clear all cells to empty for predictable testing
|
||||
map.cells.forEach(cell => {
|
||||
cell.type = CELL_TYPES.EMPTY;
|
||||
cell.dice = [];
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to set up player cells with specific strength
|
||||
*/
|
||||
function setupPlayerCell(map, q, r, playerId, strength) {
|
||||
const cellType = playerId === 1 ? CELL_TYPES.PLAYER1 : CELL_TYPES.PLAYER2;
|
||||
const cell = map.getCell(q, r);
|
||||
cell.type = cellType;
|
||||
cell.setStrength(strength);
|
||||
return cell;
|
||||
}
|
||||
|
||||
describe('AIBot', () => {
|
||||
describe('Instantiation', () => {
|
||||
it('should create AIBot with playerId, map, and gameUI', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
assert.strictEqual(bot.playerId, 1);
|
||||
assert.strictEqual(bot.map, map);
|
||||
assert.strictEqual(bot.gameUI, gameUI);
|
||||
assert.strictEqual(bot.thinkingTime, 1000);
|
||||
});
|
||||
|
||||
it('should work with different player IDs', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
|
||||
const bot1 = new AIBot(1, map, gameUI);
|
||||
const bot2 = new AIBot(2, map, gameUI);
|
||||
|
||||
assert.strictEqual(bot1.playerId, 1);
|
||||
assert.strictEqual(bot2.playerId, 2);
|
||||
});
|
||||
|
||||
it('should have default thinking time of 1000ms', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
assert.strictEqual(bot.thinkingTime, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPossibleMoves', () => {
|
||||
it('should return empty array when player has no cells', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
const moves = bot.findPossibleMoves([]);
|
||||
|
||||
assert.deepStrictEqual(moves, []);
|
||||
});
|
||||
|
||||
it('should return empty array when all player cells have strength <= 1', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 1);
|
||||
setupPlayerCell(map, 2, 3, 1, 0);
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
assert.deepStrictEqual(moves, []);
|
||||
});
|
||||
|
||||
it('should identify valid attack targets on adjacent enemy cells', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Player 1 at (2,2) with strength 8
|
||||
setupPlayerCell(map, 2, 2, 1, 8);
|
||||
// Player 2 at adjacent (2,1) with strength 4
|
||||
setupPlayerCell(map, 2, 1, 2, 4);
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
// Should find at least one attack move to the enemy cell
|
||||
assert.ok(moves.length >= 1, 'Should find at least one move');
|
||||
|
||||
// Find the attack move targeting the enemy
|
||||
const attackMove = moves.find(m => m.to.q === 2 && m.to.r === 1);
|
||||
assert.ok(attackMove, 'Should find attack move to enemy cell');
|
||||
assert.strictEqual(attackMove.from.q, 2);
|
||||
assert.strictEqual(attackMove.from.r, 2);
|
||||
assert.strictEqual(attackMove.attackStrength, 7); // 8 - 1
|
||||
assert.strictEqual(attackMove.defenseStrength, 4);
|
||||
assert.strictEqual(attackMove.type, 'attack');
|
||||
});
|
||||
|
||||
it('should identify valid expansion targets on adjacent empty cells', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Player 1 at (2,2) with strength 6
|
||||
setupPlayerCell(map, 2, 2, 1, 6);
|
||||
// Empty cell at adjacent (2,3)
|
||||
const emptyCell = map.getCell(2, 3);
|
||||
emptyCell.type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
// Find expansion move to the specific empty cell
|
||||
const expansionMove = moves.find(m => m.to.q === 2 && m.to.r === 3);
|
||||
assert.ok(expansionMove, 'Should find expansion move to empty cell at (2,3)');
|
||||
assert.strictEqual(expansionMove.attackStrength, 5); // 6 - 1
|
||||
assert.strictEqual(expansionMove.type, 'expand');
|
||||
});
|
||||
|
||||
it('should NOT include moves to own cells', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Player 1 owns two adjacent cells
|
||||
setupPlayerCell(map, 2, 2, 1, 8);
|
||||
setupPlayerCell(map, 2, 3, 1, 4);
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
// Should only have moves from each cell to neighbors that are NOT owned by player 1
|
||||
moves.forEach(move => {
|
||||
assert.notStrictEqual(move.to.getOwner(), 1, 'Move target should not be own cell');
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip cells with strength <= 1 as source', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Player 1 cell with strength 1 (cannot attack)
|
||||
setupPlayerCell(map, 2, 2, 1, 1);
|
||||
// Empty adjacent cell
|
||||
const emptyCell = map.getCell(2, 1);
|
||||
emptyCell.type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
assert.strictEqual(moves.length, 0, 'Should not generate moves from cell with strength 1');
|
||||
});
|
||||
|
||||
it('should find multiple moves from multiple player cells', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Two player 1 cells with strength > 1
|
||||
setupPlayerCell(map, 1, 1, 1, 5);
|
||||
setupPlayerCell(map, 3, 3, 1, 6);
|
||||
// Enemy cell adjacent to first
|
||||
setupPlayerCell(map, 1, 0, 2, 3);
|
||||
// Empty cell adjacent to second
|
||||
const emptyCell = map.getCell(3, 2);
|
||||
emptyCell.type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
assert.ok(moves.length >= 2, 'Should find moves from both cells');
|
||||
});
|
||||
|
||||
it('should calculate correct attack and defense strengths', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 10);
|
||||
setupPlayerCell(map, 2, 1, 2, 6);
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
// Find the move targeting the enemy cell at (2,1)
|
||||
const attackMove = moves.find(m => m.to.q === 2 && m.to.r === 1);
|
||||
assert.ok(attackMove, 'Should find attack move');
|
||||
assert.strictEqual(attackMove.attackStrength, 9); // 10 - 1
|
||||
assert.strictEqual(attackMove.defenseStrength, 6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('movePriority', () => {
|
||||
it('should give higher priority to winning attacks', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Winning attack: attack 8 vs defense 3
|
||||
const winningAttack = {
|
||||
type: 'attack',
|
||||
attackStrength: 8,
|
||||
defenseStrength: 3,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
// Losing attack: attack 3 vs defense 8
|
||||
const losingAttack = {
|
||||
type: 'attack',
|
||||
attackStrength: 3,
|
||||
defenseStrength: 8,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
const winningPriority = bot.movePriority(winningAttack);
|
||||
const losingPriority = bot.movePriority(losingAttack);
|
||||
|
||||
assert.ok(winningPriority > losingPriority, 'Winning attack should have higher priority');
|
||||
assert.ok(winningPriority >= 100, 'Winning attack should have base priority of 100+');
|
||||
});
|
||||
|
||||
it('should give positive priority to expansion moves', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
const expansionMove = {
|
||||
type: 'expand',
|
||||
attackStrength: 5,
|
||||
defenseStrength: 0,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
const priority = bot.movePriority(expansionMove);
|
||||
|
||||
assert.ok(priority >= 50, 'Expansion should have base priority of 50+');
|
||||
});
|
||||
|
||||
it('should prefer attacks over expansion when attack is favorable', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Favorable attack: 10 vs 2
|
||||
const attack = {
|
||||
type: 'attack',
|
||||
attackStrength: 10,
|
||||
defenseStrength: 2,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
// Expansion with same strength
|
||||
const expansion = {
|
||||
type: 'expand',
|
||||
attackStrength: 10,
|
||||
defenseStrength: 0,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
const attackPriority = bot.movePriority(attack);
|
||||
const expansionPriority = bot.movePriority(expansion);
|
||||
|
||||
assert.ok(attackPriority > expansionPriority, 'Favorable attack should beat expansion');
|
||||
});
|
||||
|
||||
it('should add bonus based on attack strength', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
const weakExpansion = {
|
||||
type: 'expand',
|
||||
attackStrength: 2,
|
||||
defenseStrength: 0,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
const strongExpansion = {
|
||||
type: 'expand',
|
||||
attackStrength: 8,
|
||||
defenseStrength: 0,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
const weakPriority = bot.movePriority(weakExpansion);
|
||||
const strongPriority = bot.movePriority(strongExpansion);
|
||||
|
||||
assert.ok(strongPriority > weakPriority, 'Stronger expansion should have higher priority');
|
||||
});
|
||||
|
||||
it('should penalize risky attacks', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
const riskyAttack = {
|
||||
type: 'attack',
|
||||
attackStrength: 3,
|
||||
defenseStrength: 7,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
const priority = bot.movePriority(riskyAttack);
|
||||
|
||||
assert.ok(priority < 50, 'Risky attack should have reduced priority');
|
||||
});
|
||||
|
||||
it('should rank moves: strong attack > weak attack > expansion', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
const strongAttack = {
|
||||
type: 'attack',
|
||||
attackStrength: 10,
|
||||
defenseStrength: 2,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
const weakAttack = {
|
||||
type: 'attack',
|
||||
attackStrength: 5,
|
||||
defenseStrength: 4,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
const expansion = {
|
||||
type: 'expand',
|
||||
attackStrength: 5,
|
||||
defenseStrength: 0,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
const strongPriority = bot.movePriority(strongAttack);
|
||||
const weakPriority = bot.movePriority(weakAttack);
|
||||
const expansionPriority = bot.movePriority(expansion);
|
||||
|
||||
assert.ok(strongPriority > weakPriority, 'Strong attack should beat weak attack');
|
||||
assert.ok(weakPriority > expansionPriority || strongPriority > expansionPriority,
|
||||
'Attacks should generally be preferred over expansion');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI prefers attacking weak enemies', () => {
|
||||
it('should prefer attacking weak enemy over empty cell', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 8);
|
||||
// Weak enemy at (2,1)
|
||||
setupPlayerCell(map, 2, 1, 2, 2);
|
||||
// Empty cell at (2,3)
|
||||
const emptyCell = map.getCell(2, 3);
|
||||
emptyCell.type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
// Sort by priority
|
||||
moves.sort((a, b) => bot.movePriority(b) - bot.movePriority(a));
|
||||
|
||||
assert.strictEqual(moves[0].type, 'attack', 'Should prefer attack on weak enemy');
|
||||
assert.strictEqual(moves[0].to.getOwner(), 2, 'Should target enemy cell');
|
||||
});
|
||||
|
||||
it('should prefer attacking very weak enemy (strength 1)', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 6);
|
||||
// Very weak enemy
|
||||
setupPlayerCell(map, 2, 1, 2, 1);
|
||||
// Empty cell
|
||||
const emptyCell = map.getCell(3, 2);
|
||||
emptyCell.type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
moves.sort((a, b) => bot.movePriority(b) - bot.movePriority(a));
|
||||
|
||||
const attackMove = moves.find(m => m.type === 'attack');
|
||||
const expandMove = moves.find(m => m.type === 'expand');
|
||||
|
||||
assert.ok(attackMove, 'Should have attack move');
|
||||
assert.ok(expandMove, 'Should have expansion move');
|
||||
assert.ok(bot.movePriority(attackMove) > bot.movePriority(expandMove),
|
||||
'Attack on weak enemy should be preferred');
|
||||
});
|
||||
|
||||
it('should calculate advantage correctly for attack prioritization', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Attack with +5 advantage
|
||||
const bigAdvantage = {
|
||||
type: 'attack',
|
||||
attackStrength: 10,
|
||||
defenseStrength: 5,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
// Attack with +1 advantage
|
||||
const smallAdvantage = {
|
||||
type: 'attack',
|
||||
attackStrength: 6,
|
||||
defenseStrength: 5,
|
||||
from: { q: 0, r: 0 },
|
||||
to: { q: 0, r: 1 }
|
||||
};
|
||||
|
||||
const bigPriority = bot.movePriority(bigAdvantage);
|
||||
const smallPriority = bot.movePriority(smallAdvantage);
|
||||
|
||||
assert.ok(bigPriority > smallPriority, 'Bigger advantage should have higher priority');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI does not select moves with strength <= 1', () => {
|
||||
it('should not generate moves from cells with strength 1', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 1);
|
||||
const emptyCell = map.getCell(2, 1);
|
||||
emptyCell.type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
assert.strictEqual(moves.length, 0, 'Should not generate any moves');
|
||||
});
|
||||
|
||||
it('should not generate moves from cells with strength 0', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 0);
|
||||
const emptyCell = map.getCell(2, 1);
|
||||
emptyCell.type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
assert.strictEqual(moves.length, 0, 'Should not generate any moves');
|
||||
});
|
||||
|
||||
it('should generate moves from cells with strength 2', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 2);
|
||||
const emptyCell = map.getCell(2, 1);
|
||||
emptyCell.type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
assert.ok(moves.length > 0, 'Should generate moves from cell with strength 2');
|
||||
assert.strictEqual(moves[0].attackStrength, 1, 'Attack strength should be 1');
|
||||
});
|
||||
|
||||
it('should filter out cells with strength <= 1 when multiple cells exist', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Strong cell
|
||||
setupPlayerCell(map, 2, 2, 1, 8);
|
||||
// Weak cell
|
||||
setupPlayerCell(map, 3, 3, 1, 1);
|
||||
// Empty cells adjacent to both
|
||||
map.getCell(2, 1).type = CELL_TYPES.EMPTY;
|
||||
map.getCell(3, 2).type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
// All moves should come from the strong cell only
|
||||
moves.forEach(move => {
|
||||
assert.ok(move.from.getStrength() > 1, 'Move source should have strength > 1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI respects map boundaries', () => {
|
||||
it('should not generate moves outside map boundaries', () => {
|
||||
const map = createTestMap(5);
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Cell at corner
|
||||
setupPlayerCell(map, 0, 0, 1, 8);
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
// All moves should be within bounds
|
||||
moves.forEach(move => {
|
||||
assert.ok(move.to.q >= 0 && move.to.q < 5, 'Target q should be in bounds');
|
||||
assert.ok(move.to.r >= 0 && move.to.r < 5, 'Target r should be in bounds');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle edge cells correctly', () => {
|
||||
const map = createTestMap(5);
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Cell at edge
|
||||
setupPlayerCell(map, 0, 2, 1, 8);
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
// All moves should be within bounds
|
||||
moves.forEach(move => {
|
||||
assert.ok(move.to.q >= 0 && move.to.q < 5, 'Target q should be in bounds');
|
||||
assert.ok(move.to.r >= 0 && move.to.r < 5, 'Target r should be in bounds');
|
||||
});
|
||||
});
|
||||
|
||||
it('should use map.getNeighbors which respects boundaries', () => {
|
||||
const map = createTestMap(5);
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Corner cell should have only 2 neighbors
|
||||
setupPlayerCell(map, 0, 0, 1, 8);
|
||||
// Make adjacent cells passable
|
||||
map.getCell(0, 1).type = CELL_TYPES.EMPTY;
|
||||
map.getCell(1, 0).type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
// Should only have moves to valid neighbors
|
||||
assert.ok(moves.length <= 2, 'Corner cell should have at most 2 moves');
|
||||
});
|
||||
|
||||
it('should not include blocked cells as targets', () => {
|
||||
const map = createTestMap(5);
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 8);
|
||||
// Block an adjacent cell
|
||||
map.getCell(2, 1).type = CELL_TYPES.BLOCKED;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
// Should not have move to blocked cell
|
||||
const hasBlockedTarget = moves.some(m => m.to.type === CELL_TYPES.BLOCKED);
|
||||
assert.strictEqual(hasBlockedTarget, false, 'Should not target blocked cells');
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeMove', () => {
|
||||
it('should set selectedCell and currentTarget on gameUI', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 8);
|
||||
setupPlayerCell(map, 2, 1, 2, 4);
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
bot.executeMove(moves[0]);
|
||||
|
||||
assert.strictEqual(gameUI.selectedCell, moves[0].from);
|
||||
assert.strictEqual(gameUI.currentTarget, moves[0].to);
|
||||
});
|
||||
|
||||
it('should call executeAttack on gameUI', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 8);
|
||||
setupPlayerCell(map, 2, 1, 2, 4);
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
|
||||
bot.executeMove(moves[0]);
|
||||
|
||||
assert.strictEqual(gameUI.executedMoves.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('playTurn', () => {
|
||||
it('should end turn when no moves available (cells with strength <= 1)', async () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
// Player has cells but all with strength <= 1 (no valid moves)
|
||||
setupPlayerCell(map, 2, 2, 1, 1);
|
||||
|
||||
bot.thinkingTime = 0; // Skip waiting for faster tests
|
||||
|
||||
await bot.playTurn();
|
||||
|
||||
assert.strictEqual(gameUI.turnEnded, true);
|
||||
});
|
||||
|
||||
it('should execute best move when moves are available', async () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 8);
|
||||
setupPlayerCell(map, 2, 1, 2, 2); // Weak enemy
|
||||
|
||||
bot.thinkingTime = 0; // Skip waiting
|
||||
|
||||
await bot.playTurn();
|
||||
|
||||
assert.strictEqual(gameUI.executedMoves.length, 1);
|
||||
assert.strictEqual(gameUI.selectedCell.q, 2);
|
||||
assert.strictEqual(gameUI.selectedCell.r, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration: Full AI decision making', () => {
|
||||
it('should choose attack over expansion when attack is favorable', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 10);
|
||||
// Weak enemy
|
||||
setupPlayerCell(map, 2, 1, 2, 3);
|
||||
// Empty cell
|
||||
map.getCell(3, 2).type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
moves.sort((a, b) => bot.movePriority(b) - bot.movePriority(a));
|
||||
|
||||
assert.strictEqual(moves[0].type, 'attack', 'Should prefer favorable attack');
|
||||
});
|
||||
|
||||
it('should handle multiple potential targets correctly', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 8);
|
||||
// Multiple enemies with different strengths
|
||||
setupPlayerCell(map, 2, 1, 2, 2); // Weak
|
||||
setupPlayerCell(map, 1, 2, 2, 6); // Strong
|
||||
// Empty cell
|
||||
map.getCell(3, 2).type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
moves.sort((a, b) => bot.movePriority(b) - bot.movePriority(a));
|
||||
|
||||
// Best move should be attack on weak enemy
|
||||
assert.strictEqual(moves[0].type, 'attack');
|
||||
assert.strictEqual(moves[0].defenseStrength, 2);
|
||||
});
|
||||
|
||||
it('should prefer expansion when no favorable attacks exist', () => {
|
||||
const map = createTestMap();
|
||||
const gameUI = new MockGameUI();
|
||||
const bot = new AIBot(1, map, gameUI);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 5);
|
||||
// Strong enemy (risky attack)
|
||||
setupPlayerCell(map, 2, 1, 2, 8);
|
||||
// Empty cell
|
||||
map.getCell(3, 2).type = CELL_TYPES.EMPTY;
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
const moves = bot.findPossibleMoves(playerCells);
|
||||
moves.sort((a, b) => bot.movePriority(b) - bot.movePriority(a));
|
||||
|
||||
// Expansion might be preferred over risky attack
|
||||
const bestMove = moves[0];
|
||||
assert.ok(
|
||||
bestMove.type === 'expand' || bestMove.attackStrength > bestMove.defenseStrength,
|
||||
'Should prefer safe moves'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GameUI.distributeSupply', () => {
|
||||
// We need to test distributeSupply which is a method on GameUI
|
||||
// Since GameUI has DOM dependencies, we'll create a minimal mock
|
||||
|
||||
class MinimalGameUI {
|
||||
constructor(map, currentPlayer) {
|
||||
this.map = map;
|
||||
this.currentPlayer = currentPlayer;
|
||||
}
|
||||
|
||||
// Copy the distributeSupply method logic
|
||||
distributeSupply(supply) {
|
||||
const playerCells = this.map.getPlayerCells(this.currentPlayer);
|
||||
const eligibleCells = playerCells.filter(cell => !cell.isMaxStrength());
|
||||
|
||||
if (eligibleCells.length === 0 || supply === 0) return;
|
||||
|
||||
let remainingSupply = supply;
|
||||
while (remainingSupply > 0 && eligibleCells.length > 0) {
|
||||
const randomCell = eligibleCells[Math.floor(Math.random() * eligibleCells.length)];
|
||||
const currentStrength = randomCell.getStrength();
|
||||
|
||||
if (currentStrength < 48) {
|
||||
randomCell.setStrength(currentStrength + 1);
|
||||
remainingSupply--;
|
||||
}
|
||||
|
||||
if (randomCell.isMaxStrength()) {
|
||||
eligibleCells.splice(eligibleCells.indexOf(randomCell), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('should add strength one-by-one to eligible cells', () => {
|
||||
const map = createTestMap();
|
||||
const currentPlayer = 1;
|
||||
const gameUI = new MinimalGameUI(map, currentPlayer);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 5);
|
||||
setupPlayerCell(map, 2, 3, 1, 3);
|
||||
|
||||
const initialStrength = map.getPlayerCells(1)
|
||||
.reduce((sum, c) => sum + c.getStrength(), 0);
|
||||
|
||||
gameUI.distributeSupply(3);
|
||||
|
||||
const finalStrength = map.getPlayerCells(1)
|
||||
.reduce((sum, c) => sum + c.getStrength(), 0);
|
||||
|
||||
assert.strictEqual(finalStrength, initialStrength + 3, 'Should add exactly 3 strength');
|
||||
});
|
||||
|
||||
it('should only add to cells that are not at max strength', () => {
|
||||
const map = createTestMap();
|
||||
const currentPlayer = 1;
|
||||
const gameUI = new MinimalGameUI(map, currentPlayer);
|
||||
|
||||
// Cell at max strength (48)
|
||||
const maxCell = setupPlayerCell(map, 2, 2, 1, 48);
|
||||
// Cell below max
|
||||
setupPlayerCell(map, 2, 3, 1, 10);
|
||||
|
||||
gameUI.distributeSupply(5);
|
||||
|
||||
assert.strictEqual(maxCell.getStrength(), 48, 'Max cell should not increase');
|
||||
});
|
||||
|
||||
it('should stop when supply is exhausted', () => {
|
||||
const map = createTestMap();
|
||||
const currentPlayer = 1;
|
||||
const gameUI = new MinimalGameUI(map, currentPlayer);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 5);
|
||||
|
||||
gameUI.distributeSupply(3);
|
||||
|
||||
const cell = map.getCell(2, 2);
|
||||
assert.strictEqual(cell.getStrength(), 8, 'Should add exactly 3 to strength');
|
||||
});
|
||||
|
||||
it('should stop when all cells reach max strength', () => {
|
||||
const map = createTestMap();
|
||||
const currentPlayer = 1;
|
||||
const gameUI = new MinimalGameUI(map, currentPlayer);
|
||||
|
||||
// Cell near max
|
||||
setupPlayerCell(map, 2, 2, 1, 47);
|
||||
|
||||
// Try to add more than can fit
|
||||
gameUI.distributeSupply(10);
|
||||
|
||||
const cell = map.getCell(2, 2);
|
||||
assert.strictEqual(cell.getStrength(), 48, 'Should cap at max strength');
|
||||
});
|
||||
|
||||
it('should do nothing when supply is 0', () => {
|
||||
const map = createTestMap();
|
||||
const currentPlayer = 1;
|
||||
const gameUI = new MinimalGameUI(map, currentPlayer);
|
||||
|
||||
setupPlayerCell(map, 2, 2, 1, 5);
|
||||
|
||||
const initialStrength = map.getCell(2, 2).getStrength();
|
||||
gameUI.distributeSupply(0);
|
||||
const finalStrength = map.getCell(2, 2).getStrength();
|
||||
|
||||
assert.strictEqual(finalStrength, initialStrength, 'Should not change with 0 supply');
|
||||
});
|
||||
|
||||
it('should do nothing when no eligible cells exist', () => {
|
||||
const map = createTestMap();
|
||||
const currentPlayer = 1;
|
||||
const gameUI = new MinimalGameUI(map, currentPlayer);
|
||||
|
||||
// No player cells
|
||||
gameUI.distributeSupply(5);
|
||||
|
||||
const playerCells = map.getPlayerCells(1);
|
||||
assert.strictEqual(playerCells.length, 0);
|
||||
});
|
||||
|
||||
it('should distribute to random eligible cells', () => {
|
||||
const map = createTestMap();
|
||||
const currentPlayer = 1;
|
||||
const gameUI = new MinimalGameUI(map, currentPlayer);
|
||||
|
||||
// Multiple eligible cells
|
||||
setupPlayerCell(map, 1, 1, 1, 5);
|
||||
setupPlayerCell(map, 2, 2, 1, 5);
|
||||
setupPlayerCell(map, 3, 3, 1, 5);
|
||||
|
||||
gameUI.distributeSupply(6);
|
||||
|
||||
const totalStrength = map.getPlayerCells(1)
|
||||
.reduce((sum, c) => sum + c.getStrength(), 0);
|
||||
|
||||
assert.strictEqual(totalStrength, 15 + 6, 'Should distribute all supply');
|
||||
|
||||
// At least some cells should have received supply (random distribution)
|
||||
const cells = map.getPlayerCells(1);
|
||||
const receivedSupply = cells.filter(c => c.getStrength() > 5);
|
||||
assert.ok(receivedSupply.length > 0, 'Some cells should receive supply');
|
||||
});
|
||||
|
||||
it('should handle multiple cells with different initial strengths', () => {
|
||||
const map = createTestMap();
|
||||
const currentPlayer = 1;
|
||||
const gameUI = new MinimalGameUI(map, currentPlayer);
|
||||
|
||||
setupPlayerCell(map, 1, 1, 1, 3);
|
||||
setupPlayerCell(map, 2, 2, 1, 6);
|
||||
setupPlayerCell(map, 3, 3, 1, 4);
|
||||
|
||||
const initialTotal = map.getPlayerCells(1)
|
||||
.reduce((sum, c) => sum + c.getStrength(), 0);
|
||||
|
||||
gameUI.distributeSupply(5);
|
||||
|
||||
const finalTotal = map.getPlayerCells(1)
|
||||
.reduce((sum, c) => sum + c.getStrength(), 0);
|
||||
|
||||
assert.strictEqual(finalTotal, initialTotal + 5, 'Should add exactly 5 total');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user