Fix AI bot turn chain for multiple bots - prevent race conditions
This commit is contained in:
@@ -17,50 +17,63 @@ export class AIBot {
|
|||||||
* Execute AI turn - makes ONE move then ends turn
|
* Execute AI turn - makes ONE move then ends turn
|
||||||
*/
|
*/
|
||||||
async playTurn() {
|
async playTurn() {
|
||||||
console.log(`AI Player ${this.playerId} thinking...`);
|
console.log(`[AI-BOT P${this.playerId}] === Turn started ===`);
|
||||||
|
console.log(`[AI-BOT P${this.playerId}] Thinking...`);
|
||||||
// Get all player cells
|
|
||||||
const playerCells = this.map.getPlayerCells(this.playerId);
|
|
||||||
|
|
||||||
console.log(`AI Player ${this.playerId} has ${playerCells.length} cells`);
|
|
||||||
|
|
||||||
if (playerCells.length === 0) {
|
try {
|
||||||
console.log(`AI Player ${this.playerId} has no cells, ending turn`);
|
// Get all player cells
|
||||||
|
const playerCells = this.map.getPlayerCells(this.playerId);
|
||||||
|
|
||||||
|
console.log(`[AI-BOT P${this.playerId}] Has ${playerCells.length} cells`);
|
||||||
|
|
||||||
|
if (playerCells.length === 0) {
|
||||||
|
console.log(`[AI-BOT P${this.playerId}] No cells, ending turn`);
|
||||||
|
await this.wait(this.thinkingTime);
|
||||||
|
this.gameUI.endTurn();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all possible moves
|
||||||
|
const moves = this.findPossibleMoves(playerCells);
|
||||||
|
|
||||||
|
console.log(`[AI-BOT P${this.playerId}] Found ${moves.length} possible moves`);
|
||||||
|
|
||||||
|
if (moves.length === 0) {
|
||||||
|
// No moves available, end turn
|
||||||
|
console.log(`[AI-BOT P${this.playerId}] No valid moves, ending turn`);
|
||||||
|
await this.wait(this.thinkingTime);
|
||||||
|
this.gameUI.endTurn();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort moves by priority (attack > expand > reinforce)
|
||||||
|
moves.sort((a, b) => this.movePriority(b) - this.movePriority(a));
|
||||||
|
|
||||||
|
// Execute best move
|
||||||
|
const bestMove = moves[0];
|
||||||
|
console.log(`[AI-BOT P${this.playerId}] Selected move: from (${bestMove.from.q},${bestMove.from.r}) to (${bestMove.to.q},${bestMove.to.r}), type=${bestMove.type}, attackStr=${bestMove.attackStrength}, defStr=${bestMove.defenseStrength}`);
|
||||||
|
|
||||||
|
// Wait for thinking time
|
||||||
|
await this.wait(this.thinkingTime);
|
||||||
|
|
||||||
|
// Execute the move
|
||||||
|
this.gameUI.selectedCell = bestMove.from;
|
||||||
|
this.gameUI.currentTarget = bestMove.to;
|
||||||
|
this.gameUI.executeAttack();
|
||||||
|
|
||||||
|
console.log(`[AI-BOT P${this.playerId}] Move executed`);
|
||||||
|
|
||||||
|
// End turn after executing move
|
||||||
|
console.log(`[AI-BOT P${this.playerId}] Calling endTurn()`);
|
||||||
this.gameUI.endTurn();
|
this.gameUI.endTurn();
|
||||||
return;
|
|
||||||
}
|
console.log(`[AI-BOT P${this.playerId}] === Turn completed ===`);
|
||||||
|
} catch (error) {
|
||||||
// Find all possible moves
|
console.error(`[AI-BOT P${this.playerId}] Error during turn:`, error);
|
||||||
const moves = this.findPossibleMoves(playerCells);
|
// Still end turn on error to prevent game from getting stuck
|
||||||
|
|
||||||
console.log(`AI Player ${this.playerId} found ${moves.length} possible moves`);
|
|
||||||
|
|
||||||
if (moves.length === 0) {
|
|
||||||
// No moves available, end turn
|
|
||||||
console.log(`AI Player ${this.playerId} has no moves, ending turn`);
|
|
||||||
this.gameUI.endTurn();
|
this.gameUI.endTurn();
|
||||||
return;
|
throw error; // Re-throw so caller knows there was an error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort moves by priority (attack > expand > reinforce)
|
|
||||||
moves.sort((a, b) => this.movePriority(b) - this.movePriority(a));
|
|
||||||
|
|
||||||
// Execute best move
|
|
||||||
const bestMove = moves[0];
|
|
||||||
console.log(`AI Player ${this.playerId} selected move: from (${bestMove.from.q},${bestMove.from.r}) to (${bestMove.to.q},${bestMove.to.r}), type=${bestMove.type}`);
|
|
||||||
|
|
||||||
// Wait for thinking time
|
|
||||||
await this.wait(this.thinkingTime);
|
|
||||||
|
|
||||||
// Execute the move directly without using executeMove helper
|
|
||||||
this.gameUI.selectedCell = bestMove.from;
|
|
||||||
this.gameUI.currentTarget = bestMove.to;
|
|
||||||
this.gameUI.executeAttack();
|
|
||||||
|
|
||||||
console.log(`AI Player ${this.playerId} executed move`);
|
|
||||||
|
|
||||||
// End turn after executing move
|
|
||||||
this.gameUI.endTurn();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
101
public/game.js
101
public/game.js
@@ -54,10 +54,11 @@ class GameUI {
|
|||||||
this.gamePhase = 'movement';
|
this.gamePhase = 'movement';
|
||||||
this.hasMoved = false;
|
this.hasMoved = false;
|
||||||
this.isAIThinking = false;
|
this.isAIThinking = false;
|
||||||
|
this.isProcessingTurn = false; // Prevent re-entrancy during turn processing
|
||||||
|
|
||||||
this.offsetX = 0;
|
this.offsetX = 0;
|
||||||
this.offsetY = 0;
|
this.offsetY = 0;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +116,8 @@ class GameUI {
|
|||||||
this.gamePhase = 'movement';
|
this.gamePhase = 'movement';
|
||||||
this.hasMoved = false;
|
this.hasMoved = false;
|
||||||
this.isAIThinking = false;
|
this.isAIThinking = false;
|
||||||
|
this.isProcessingTurn = false;
|
||||||
|
|
||||||
// Initialize AI bots AFTER map is created
|
// Initialize AI bots AFTER map is created
|
||||||
this.aiBots = {};
|
this.aiBots = {};
|
||||||
for (let i = 1; i <= this.playerCount; i++) {
|
for (let i = 1; i <= this.playerCount; i++) {
|
||||||
@@ -123,16 +125,17 @@ class GameUI {
|
|||||||
this.aiBots[i] = new AIBot(i, this.map, this);
|
this.aiBots[i] = new AIBot(i, this.map, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize starting positions for all players
|
// Initialize starting positions for all players
|
||||||
this.initializePlayers();
|
this.initializePlayers();
|
||||||
|
|
||||||
this.centerMap();
|
this.centerMap();
|
||||||
this.render();
|
this.render();
|
||||||
this.createPlayerCards();
|
this.createPlayerCards();
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
this.log(`New game started with ${this.playerCount} players!`);
|
this.log(`New game started with ${this.playerCount} players!`);
|
||||||
|
console.log(`[GAME] New game started with ${this.playerCount} players`);
|
||||||
|
|
||||||
// Start first player's turn (AI if needed)
|
// Start first player's turn (AI if needed)
|
||||||
this.checkAndRunAITurn();
|
this.checkAndRunAITurn();
|
||||||
}
|
}
|
||||||
@@ -515,38 +518,82 @@ class GameUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async endTurn() {
|
async endTurn() {
|
||||||
if (this.gamePhase !== 'movement') return;
|
if (this.gamePhase !== 'movement') {
|
||||||
// Remove isAIThinking check - AI needs to call endTurn() after its move
|
console.log(`[GAME] endTurn() called but gamePhase is '${this.gamePhase}', ignoring`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply supply
|
// Prevent re-entrancy - only one turn processing at a time
|
||||||
const supply = this.map.calculateSupply(this.currentPlayer);
|
if (this.isProcessingTurn) {
|
||||||
this.distributeSupply(supply);
|
console.log(`[GAME] endTurn() called but isProcessingTurn is true, ignoring`);
|
||||||
this.log(`Player ${this.currentPlayer} received ${supply} supply`);
|
return;
|
||||||
|
}
|
||||||
// Next player
|
|
||||||
this.currentPlayer = (this.currentPlayer % this.playerCount) + 1;
|
|
||||||
this.hasMoved = false;
|
|
||||||
|
|
||||||
this.cancelSelection();
|
this.isProcessingTurn = true;
|
||||||
this.updateUI();
|
console.log(`[GAME] endTurn() - Player ${this.currentPlayer} ending turn`);
|
||||||
this.render();
|
|
||||||
|
|
||||||
this.log(`Player ${this.currentPlayer}'s turn`);
|
try {
|
||||||
|
// Apply supply
|
||||||
// Check if next player is AI
|
const supply = this.map.calculateSupply(this.currentPlayer);
|
||||||
this.checkAndRunAITurn();
|
this.distributeSupply(supply);
|
||||||
|
this.log(`Player ${this.currentPlayer} received ${supply} supply`);
|
||||||
|
console.log(`[GAME] Player ${this.currentPlayer} received ${supply} supply`);
|
||||||
|
|
||||||
|
// Next player
|
||||||
|
const previousPlayer = this.currentPlayer;
|
||||||
|
this.currentPlayer = (this.currentPlayer % this.playerCount) + 1;
|
||||||
|
this.hasMoved = false;
|
||||||
|
|
||||||
|
console.log(`[GAME] Turn transition: P${previousPlayer} -> P${this.currentPlayer}`);
|
||||||
|
|
||||||
|
this.cancelSelection();
|
||||||
|
this.updateUI();
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
this.log(`Player ${this.currentPlayer}'s turn`);
|
||||||
|
console.log(`[GAME] Player ${this.currentPlayer}'s turn started (${this.playerTypes[this.currentPlayer]})`);
|
||||||
|
|
||||||
|
// Check if next player is AI and await completion
|
||||||
|
await this.checkAndRunAITurn();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[GAME] Error in endTurn():`, error);
|
||||||
|
this.log(`Error during turn transition: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
this.isProcessingTurn = false;
|
||||||
|
console.log(`[GAME] endTurn() completed for Player ${this.currentPlayer}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAndRunAITurn() {
|
/**
|
||||||
|
* Check if current player is AI and run their turn
|
||||||
|
* @returns {Promise<boolean>} - true if AI turn was run, false if human turn
|
||||||
|
*/
|
||||||
|
async checkAndRunAITurn() {
|
||||||
if (this.playerTypes[this.currentPlayer] === 'ai' && this.aiBots[this.currentPlayer]) {
|
if (this.playerTypes[this.currentPlayer] === 'ai' && this.aiBots[this.currentPlayer]) {
|
||||||
|
console.log(`[AI] Player ${this.currentPlayer} is AI, starting turn`);
|
||||||
this.isAIThinking = true;
|
this.isAIThinking = true;
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
|
|
||||||
// Run AI turn and reset flag when done
|
try {
|
||||||
this.aiBots[this.currentPlayer].playTurn().then(() => {
|
// Run AI turn and wait for it to complete
|
||||||
|
await this.aiBots[this.currentPlayer].playTurn();
|
||||||
|
console.log(`[AI] Player ${this.currentPlayer} AI turn completed`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[AI] Error during Player ${this.currentPlayer} AI turn:`, error);
|
||||||
|
this.log(`AI error: ${error.message}`, 'error');
|
||||||
this.isAIThinking = false;
|
this.isAIThinking = false;
|
||||||
});
|
this.updateUI();
|
||||||
|
// Still advance to next player even on error
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
// Always reset the flag when AI turn completes (success or error)
|
||||||
|
this.isAIThinking = false;
|
||||||
|
console.log(`[AI] Player ${this.currentPlayer} isAIThinking reset to false`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
console.log(`[AI] Player ${this.currentPlayer} is human, no AI turn needed`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
distributeSupply(supply) {
|
distributeSupply(supply) {
|
||||||
|
|||||||
@@ -919,3 +919,311 @@ describe('GameUI.distributeSupply', () => {
|
|||||||
assert.strictEqual(finalTotal, initialTotal + 5, 'Should add exactly 5 total');
|
assert.strictEqual(finalTotal, initialTotal + 5, 'Should add exactly 5 total');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for multiple AI bots turn chain
|
||||||
|
* These tests verify that AI turns complete in sequence without race conditions
|
||||||
|
*/
|
||||||
|
describe('Multiple AI Bots Turn Chain', () => {
|
||||||
|
/**
|
||||||
|
* Mock GameUI that tracks turn order and AI completion
|
||||||
|
*/
|
||||||
|
class TurnTrackingGameUI {
|
||||||
|
constructor(playerCount, playerTypes) {
|
||||||
|
this.playerCount = playerCount;
|
||||||
|
this.playerTypes = playerTypes;
|
||||||
|
this.currentPlayer = 1;
|
||||||
|
this.turnOrder = [];
|
||||||
|
this.aiTurnsCompleted = [];
|
||||||
|
this.isAIThinking = false;
|
||||||
|
this.isProcessingTurn = false;
|
||||||
|
this.selectedCell = null;
|
||||||
|
this.currentTarget = null;
|
||||||
|
this.executedMoves = [];
|
||||||
|
this.aiBots = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
endTurn() {
|
||||||
|
this.turnOrder.push(this.currentPlayer);
|
||||||
|
this.currentPlayer = (this.currentPlayer % this.playerCount) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
executeAttack() {
|
||||||
|
this.executedMoves.push({
|
||||||
|
from: this.selectedCell,
|
||||||
|
to: this.currentTarget,
|
||||||
|
player: this.currentPlayer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
// No-op for testing
|
||||||
|
}
|
||||||
|
|
||||||
|
log(message) {
|
||||||
|
// No-op for testing
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAndRunAITurn() {
|
||||||
|
const player = this.currentPlayer; // Capture player before turn advances
|
||||||
|
if (this.playerTypes[player] === 'ai' && this.aiBots[player]) {
|
||||||
|
this.isAIThinking = true;
|
||||||
|
await this.aiBots[player].playTurn();
|
||||||
|
this.isAIThinking = false;
|
||||||
|
this.aiTurnsCompleted.push(player); // Record the player who took the turn
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Two AI bots', () => {
|
||||||
|
it('should complete both AI turns in sequence', async () => {
|
||||||
|
const map = createTestMap(5);
|
||||||
|
const gameUI = new TurnTrackingGameUI(2, { 1: 'ai', 2: 'ai' });
|
||||||
|
|
||||||
|
// Set up AI bots
|
||||||
|
const bot1 = new AIBot(1, map, gameUI);
|
||||||
|
const bot2 = new AIBot(2, map, gameUI);
|
||||||
|
gameUI.aiBots = { 1: bot1, 2: bot2 };
|
||||||
|
|
||||||
|
// Set up cells for both players
|
||||||
|
setupPlayerCell(map, 1, 1, 1, 5);
|
||||||
|
setupPlayerCell(map, 3, 3, 2, 5);
|
||||||
|
map.getCell(1, 0).type = CELL_TYPES.EMPTY;
|
||||||
|
map.getCell(3, 2).type = CELL_TYPES.EMPTY;
|
||||||
|
|
||||||
|
bot1.thinkingTime = 0;
|
||||||
|
bot2.thinkingTime = 0;
|
||||||
|
|
||||||
|
// Run AI turn for player 1 (currentPlayer starts at 1)
|
||||||
|
const result1 = await gameUI.checkAndRunAITurn();
|
||||||
|
|
||||||
|
// Verify player 1's turn completed
|
||||||
|
assert.strictEqual(result1, true, 'Should return true for AI turn');
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted.length, 1, 'Should have 1 completed turn');
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted[0], 1, 'First completed turn should be P1');
|
||||||
|
assert.strictEqual(gameUI.currentPlayer, 2, 'Current player should be 2 after P1 turn');
|
||||||
|
|
||||||
|
// Run AI turn for player 2
|
||||||
|
const result2 = await gameUI.checkAndRunAITurn();
|
||||||
|
|
||||||
|
// Verify both turns completed in order
|
||||||
|
assert.strictEqual(result2, true, 'Should return true for AI turn');
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted.length, 2, 'Should have 2 completed turns');
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted[0], 1, 'First turn should be P1');
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted[1], 2, 'Second turn should be P2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have race conditions between AI turns', async () => {
|
||||||
|
const map = createTestMap(5);
|
||||||
|
const gameUI = new TurnTrackingGameUI(2, { 1: 'ai', 2: 'ai' });
|
||||||
|
|
||||||
|
const bot1 = new AIBot(1, map, gameUI);
|
||||||
|
const bot2 = new AIBot(2, map, gameUI);
|
||||||
|
gameUI.aiBots = { 1: bot1, 2: bot2 };
|
||||||
|
|
||||||
|
setupPlayerCell(map, 1, 1, 1, 5);
|
||||||
|
setupPlayerCell(map, 3, 3, 2, 5);
|
||||||
|
map.getCell(1, 0).type = CELL_TYPES.EMPTY;
|
||||||
|
map.getCell(3, 2).type = CELL_TYPES.EMPTY;
|
||||||
|
|
||||||
|
bot1.thinkingTime = 0;
|
||||||
|
bot2.thinkingTime = 0;
|
||||||
|
|
||||||
|
let thinkingFlags = [];
|
||||||
|
|
||||||
|
// Override to track isAIThinking
|
||||||
|
const originalCheckAndRun = gameUI.checkAndRunAITurn.bind(gameUI);
|
||||||
|
gameUI.checkAndRunAITurn = async function() {
|
||||||
|
thinkingFlags.push({ player: this.currentPlayer, start: this.isAIThinking });
|
||||||
|
const result = await originalCheckAndRun();
|
||||||
|
thinkingFlags.push({ player: this.currentPlayer, end: this.isAIThinking });
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run both turns sequentially
|
||||||
|
await gameUI.checkAndRunAITurn();
|
||||||
|
await gameUI.checkAndRunAITurn();
|
||||||
|
|
||||||
|
// Verify no overlap - isAIThinking should be false at start of each turn
|
||||||
|
// and false at end of each turn
|
||||||
|
assert.strictEqual(thinkingFlags[0].start, false, 'isAIThinking should be false at start of P1 turn');
|
||||||
|
assert.strictEqual(thinkingFlags[1].end, false, 'isAIThinking should be false at end of P1 turn');
|
||||||
|
assert.strictEqual(thinkingFlags[2].start, false, 'isAIThinking should be false at start of P2 turn');
|
||||||
|
assert.strictEqual(thinkingFlags[3].end, false, 'isAIThinking should be false at end of P2 turn');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Three AI bots', () => {
|
||||||
|
it('should complete all three AI turns in correct sequence', async () => {
|
||||||
|
const map = createTestMap(5);
|
||||||
|
const gameUI = new TurnTrackingGameUI(3, { 1: 'ai', 2: 'ai', 3: 'ai' });
|
||||||
|
|
||||||
|
const bot1 = new AIBot(1, map, gameUI);
|
||||||
|
const bot2 = new AIBot(2, map, gameUI);
|
||||||
|
const bot3 = new AIBot(3, map, gameUI);
|
||||||
|
gameUI.aiBots = { 1: bot1, 2: bot2, 3: bot3 };
|
||||||
|
|
||||||
|
setupPlayerCell(map, 0, 0, 1, 5);
|
||||||
|
setupPlayerCell(map, 2, 2, 2, 5);
|
||||||
|
setupPlayerCell(map, 4, 4, 3, 5);
|
||||||
|
map.getCell(0, 1).type = CELL_TYPES.EMPTY;
|
||||||
|
map.getCell(2, 1).type = CELL_TYPES.EMPTY;
|
||||||
|
map.getCell(4, 3).type = CELL_TYPES.EMPTY;
|
||||||
|
|
||||||
|
[bot1, bot2, bot3].forEach(bot => bot.thinkingTime = 0);
|
||||||
|
|
||||||
|
// Run all three turns
|
||||||
|
await gameUI.checkAndRunAITurn();
|
||||||
|
await gameUI.checkAndRunAITurn();
|
||||||
|
await gameUI.checkAndRunAITurn();
|
||||||
|
|
||||||
|
// Verify all turns completed in order
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted.length, 3);
|
||||||
|
assert.deepStrictEqual(gameUI.aiTurnsCompleted, [1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mixed human and AI players', () => {
|
||||||
|
it('should only run AI turns, skip human turns', async () => {
|
||||||
|
const map = createTestMap(5);
|
||||||
|
// P1 human, P2 AI, P3 human, P4 AI
|
||||||
|
const gameUI = new TurnTrackingGameUI(4, { 1: 'human', 2: 'ai', 3: 'human', 4: 'ai' });
|
||||||
|
|
||||||
|
const bot2 = new AIBot(2, map, gameUI);
|
||||||
|
const bot4 = new AIBot(4, map, gameUI);
|
||||||
|
gameUI.aiBots = { 2: bot2, 4: bot4 };
|
||||||
|
|
||||||
|
setupPlayerCell(map, 0, 0, 2, 5);
|
||||||
|
setupPlayerCell(map, 4, 4, 4, 5);
|
||||||
|
map.getCell(0, 1).type = CELL_TYPES.EMPTY;
|
||||||
|
map.getCell(4, 3).type = CELL_TYPES.EMPTY;
|
||||||
|
|
||||||
|
[bot2, bot4].forEach(bot => bot.thinkingTime = 0);
|
||||||
|
|
||||||
|
// P1 is human - should return false (currentPlayer starts at 1)
|
||||||
|
const p1Result = await gameUI.checkAndRunAITurn();
|
||||||
|
assert.strictEqual(p1Result, false, 'P1 human should return false');
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted.length, 0);
|
||||||
|
assert.strictEqual(gameUI.currentPlayer, 1, 'Player should not advance in checkAndRunAITurn');
|
||||||
|
|
||||||
|
// Manually advance to P2 (simulating endTurn behavior)
|
||||||
|
gameUI.currentPlayer = 2;
|
||||||
|
|
||||||
|
// P2 is AI - should complete
|
||||||
|
const p2Result = await gameUI.checkAndRunAITurn();
|
||||||
|
assert.strictEqual(p2Result, true, 'P2 AI should return true');
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted.length, 1);
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted[0], 2);
|
||||||
|
|
||||||
|
// Manually advance to P3
|
||||||
|
gameUI.currentPlayer = 3;
|
||||||
|
|
||||||
|
// P3 is human - should return false
|
||||||
|
const p3Result = await gameUI.checkAndRunAITurn();
|
||||||
|
assert.strictEqual(p3Result, false, 'P3 human should return false');
|
||||||
|
|
||||||
|
// Manually advance to P4
|
||||||
|
gameUI.currentPlayer = 4;
|
||||||
|
|
||||||
|
// P4 is AI - should complete
|
||||||
|
const p4Result = await gameUI.checkAndRunAITurn();
|
||||||
|
assert.strictEqual(p4Result, true, 'P4 AI should return true');
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted.length, 2);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(gameUI.aiTurnsCompleted, [2, 4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AI with no moves', () => {
|
||||||
|
it('should end turn when AI has no valid moves', async () => {
|
||||||
|
const map = createTestMap(5);
|
||||||
|
const gameUI = new TurnTrackingGameUI(2, { 1: 'ai', 2: 'ai' });
|
||||||
|
|
||||||
|
const bot1 = new AIBot(1, map, gameUI);
|
||||||
|
gameUI.aiBots = { 1: bot1 };
|
||||||
|
|
||||||
|
// Player 1 has cell but with strength 1 (can't move)
|
||||||
|
setupPlayerCell(map, 1, 1, 1, 1);
|
||||||
|
|
||||||
|
bot1.thinkingTime = 0;
|
||||||
|
|
||||||
|
let endTurnCalled = false;
|
||||||
|
const originalEndTurn = gameUI.endTurn.bind(gameUI);
|
||||||
|
gameUI.endTurn = function() {
|
||||||
|
endTurnCalled = true;
|
||||||
|
return originalEndTurn();
|
||||||
|
};
|
||||||
|
|
||||||
|
await gameUI.checkAndRunAITurn();
|
||||||
|
|
||||||
|
assert.strictEqual(endTurnCalled, true, 'endTurn should be called when no moves available');
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should end turn when AI has no cells', async () => {
|
||||||
|
const map = createTestMap(5);
|
||||||
|
const gameUI = new TurnTrackingGameUI(2, { 1: 'ai', 2: 'human' });
|
||||||
|
|
||||||
|
const bot1 = new AIBot(1, map, gameUI);
|
||||||
|
gameUI.aiBots = { 1: bot1 };
|
||||||
|
|
||||||
|
// Player 1 has no cells
|
||||||
|
|
||||||
|
bot1.thinkingTime = 0;
|
||||||
|
|
||||||
|
let endTurnCalled = false;
|
||||||
|
const originalEndTurn = gameUI.endTurn.bind(gameUI);
|
||||||
|
gameUI.endTurn = function() {
|
||||||
|
endTurnCalled = true;
|
||||||
|
return originalEndTurn();
|
||||||
|
};
|
||||||
|
|
||||||
|
await gameUI.checkAndRunAITurn();
|
||||||
|
|
||||||
|
assert.strictEqual(endTurnCalled, true, 'endTurn should be called when AI has no cells');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Four AI bots stress test', () => {
|
||||||
|
it('should handle 4 AI bots completing turns without race conditions', async () => {
|
||||||
|
const map = createTestMap(5);
|
||||||
|
const gameUI = new TurnTrackingGameUI(4, { 1: 'ai', 2: 'ai', 3: 'ai', 4: 'ai' });
|
||||||
|
|
||||||
|
const bots = [];
|
||||||
|
for (let i = 1; i <= 4; i++) {
|
||||||
|
const bot = new AIBot(i, map, gameUI);
|
||||||
|
bot.thinkingTime = 0;
|
||||||
|
bots.push(bot);
|
||||||
|
gameUI.aiBots[i] = bot;
|
||||||
|
|
||||||
|
// Set up cells for each player
|
||||||
|
setupPlayerCell(map, i - 1, i - 1, i, 5);
|
||||||
|
// Set up empty target cells
|
||||||
|
const targetQ = i - 1;
|
||||||
|
const targetR = i < 4 ? i : 0;
|
||||||
|
if (map.getCell(targetQ, targetR)) {
|
||||||
|
map.getCell(targetQ, targetR).type = CELL_TYPES.EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run multiple full rounds
|
||||||
|
for (let round = 0; round < 3; round++) {
|
||||||
|
for (let i = 1; i <= 4; i++) {
|
||||||
|
await gameUI.checkAndRunAITurn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all 12 turns completed in order (3 rounds * 4 players)
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted.length, 12);
|
||||||
|
|
||||||
|
// Check the pattern repeats correctly
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const expectedPlayer = (i % 4) + 1;
|
||||||
|
assert.strictEqual(gameUI.aiTurnsCompleted[i], expectedPlayer,
|
||||||
|
`Turn ${i + 1} should be player ${expectedPlayer}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user