Fix AI bot turn chain for multiple bots - prevent race conditions

This commit is contained in:
sokol
2026-02-21 21:01:57 +03:00
parent 64c81da166
commit e427f1c68d
3 changed files with 435 additions and 67 deletions

View File

@@ -919,3 +919,311 @@ describe('GameUI.distributeSupply', () => {
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}`);
}
});
});
});