Fix AI multiple moves and player colors

This commit is contained in:
sokol
2026-02-21 21:25:22 +03:00
parent 1d04a99bd7
commit f3be577a32
5 changed files with 105 additions and 40 deletions

View File

@@ -14,7 +14,8 @@ export class AIBot {
} }
/** /**
* Execute AI turn - makes ONE move then ends turn * Execute AI turn - makes MULTIPLE moves until no valid moves remain, then ends turn
* According to game rules, ANY cell with strength > 1 can move if it has valid targets
*/ */
async playTurn() { async playTurn() {
console.log(`[AI-BOT P${this.playerId}] === Turn started ===`); console.log(`[AI-BOT P${this.playerId}] === Turn started ===`);
@@ -22,7 +23,7 @@ export class AIBot {
try { try {
// Get all player cells // Get all player cells
const playerCells = this.map.getPlayerCells(this.playerId); let playerCells = this.map.getPlayerCells(this.playerId);
console.log(`[AI-BOT P${this.playerId}] Has ${playerCells.length} cells`); console.log(`[AI-BOT P${this.playerId}] Has ${playerCells.length} cells`);
@@ -33,37 +34,76 @@ export class AIBot {
return; return;
} }
// Find all possible moves let moveCount = 0;
const moves = this.findPossibleMoves(playerCells); let consecutiveNoMoves = 0;
const maxConsecutiveNoMoves = 3; // Prevent infinite loops
const maxMovesPerTurn = 50; // Maximum moves per turn to prevent infinite loops in tests
console.log(`[AI-BOT P${this.playerId}] Found ${moves.length} possible moves`); // Loop: keep finding and executing moves until no more valid moves exist
while (consecutiveNoMoves < maxConsecutiveNoMoves && moveCount < maxMovesPerTurn) {
// Re-fetch player cells each iteration (board state changes)
playerCells = this.map.getPlayerCells(this.playerId);
if (moves.length === 0) { if (playerCells.length === 0) {
// No moves available, end turn console.log(`[AI-BOT P${this.playerId}] No cells remaining, ending turn`);
console.log(`[AI-BOT P${this.playerId}] No valid moves, ending turn`); break;
}
// Find all possible moves from current board state
const moves = this.findPossibleMoves(playerCells);
console.log(`[AI-BOT P${this.playerId}] Found ${moves.length} possible moves (move #${moveCount + 1})`);
if (moves.length === 0) {
// No moves available this iteration
consecutiveNoMoves++;
console.log(`[AI-BOT P${this.playerId}] No valid moves this iteration (${consecutiveNoMoves}/${maxConsecutiveNoMoves})`);
if (consecutiveNoMoves >= maxConsecutiveNoMoves) {
console.log(`[AI-BOT P${this.playerId}] No more valid moves after ${moveCount} moves, ending turn`);
break;
}
// Small delay before re-checking
await this.wait(200);
continue;
}
// Reset counter when we find valid moves
consecutiveNoMoves = 0;
// Check if we've reached max moves limit
if (moveCount >= maxMovesPerTurn) {
console.log(`[AI-BOT P${this.playerId}] Reached max moves limit (${maxMovesPerTurn}), ending turn`);
break;
}
// 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 between moves
await this.wait(this.thinkingTime); await this.wait(this.thinkingTime);
this.gameUI.endTurn();
return; // Execute the move
this.gameUI.selectedCell = bestMove.from;
this.gameUI.currentTarget = bestMove.to;
this.gameUI.executeAttack();
moveCount++;
console.log(`[AI-BOT P${this.playerId}] Move #${moveCount} executed`);
// Clear selection after move (if method exists)
if (typeof this.gameUI.cancelSelection === 'function') {
this.gameUI.cancelSelection();
}
} }
// Sort moves by priority (attack > expand > reinforce) // End turn after all moves are executed
moves.sort((a, b) => this.movePriority(b) - this.movePriority(a)); console.log(`[AI-BOT P${this.playerId}] Total moves this turn: ${moveCount}`);
// 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()`); console.log(`[AI-BOT P${this.playerId}] Calling endTurn()`);
this.gameUI.endTurn(); this.gameUI.endTurn();

View File

@@ -13,10 +13,10 @@ const ANIMATION_DURATION = 300;
// Player colors // Player colors
const PLAYER_COLORS = { const PLAYER_COLORS = {
1: '#4ecca3', 1: '#4ecca3', // teal/green
2: '#e94560', 2: '#e94560', // red/pink
3: '#f9ed69', 3: '#f9ed69', // yellow
4: '#a8e6cf' 4: '#00adb5' // cyan/blue
}; };
// Colors // Colors
@@ -26,7 +26,7 @@ const COLORS = {
player1: '#4ecca3', player1: '#4ecca3',
player2: '#e94560', player2: '#e94560',
player3: '#f9ed69', player3: '#f9ed69',
player4: '#a8e6cf', player4: '#00adb5',
highlight: 'rgba(255, 255, 255, 0.3)', highlight: 'rgba(255, 255, 255, 0.3)',
selected: 'rgba(233, 69, 96, 0.6)', selected: 'rgba(233, 69, 96, 0.6)',
target: 'rgba(78, 204, 163, 0.5)', target: 'rgba(78, 204, 163, 0.5)',

View File

@@ -14,6 +14,8 @@ const CELL_TYPES = {
BLOCKED: 1, // Impassable terrain BLOCKED: 1, // Impassable terrain
PLAYER1: 2, // Player 1 owned PLAYER1: 2, // Player 1 owned
PLAYER2: 3, // Player 2 owned PLAYER2: 3, // Player 2 owned
PLAYER3: 4, // Player 3 owned
PLAYER4: 5, // Player 4 owned
}; };
/** /**
@@ -80,12 +82,17 @@ class HexCell {
} }
isOwned() { isOwned() {
return this.type === CELL_TYPES.PLAYER1 || this.type === CELL_TYPES.PLAYER2; return this.type === CELL_TYPES.PLAYER1 ||
this.type === CELL_TYPES.PLAYER2 ||
this.type === CELL_TYPES.PLAYER3 ||
this.type === CELL_TYPES.PLAYER4;
} }
getOwner() { getOwner() {
if (this.type === CELL_TYPES.PLAYER1) return 1; if (this.type === CELL_TYPES.PLAYER1) return 1;
if (this.type === CELL_TYPES.PLAYER2) return 2; if (this.type === CELL_TYPES.PLAYER2) return 2;
if (this.type === CELL_TYPES.PLAYER3) return 3;
if (this.type === CELL_TYPES.PLAYER4) return 4;
return 0; return 0;
} }
} }
@@ -157,7 +164,14 @@ class HexMap {
* Get all cells owned by a player * Get all cells owned by a player
*/ */
getPlayerCells(playerId) { getPlayerCells(playerId) {
const targetType = playerId === 1 ? CELL_TYPES.PLAYER1 : CELL_TYPES.PLAYER2; const typeMap = {
1: CELL_TYPES.PLAYER1,
2: CELL_TYPES.PLAYER2,
3: CELL_TYPES.PLAYER3,
4: CELL_TYPES.PLAYER4
};
const targetType = typeMap[playerId];
if (!targetType) return [];
return Array.from(this.cells.values()).filter( return Array.from(this.cells.values()).filter(
cell => cell.type === targetType cell => cell.type === targetType
); );
@@ -265,6 +279,8 @@ class HexMap {
if (cell.type === CELL_TYPES.BLOCKED) return '██'; if (cell.type === CELL_TYPES.BLOCKED) return '██';
if (cell.type === CELL_TYPES.PLAYER1) return 'P1'; if (cell.type === CELL_TYPES.PLAYER1) return 'P1';
if (cell.type === CELL_TYPES.PLAYER2) return 'P2'; if (cell.type === CELL_TYPES.PLAYER2) return 'P2';
if (cell.type === CELL_TYPES.PLAYER3) return 'P3';
if (cell.type === CELL_TYPES.PLAYER4) return 'P4';
if (cell.dice.length > 0) return cell.getStrength().toString().padStart(2, ' '); if (cell.dice.length > 0) return cell.getStrength().toString().padStart(2, ' ');
return ' '; return ' ';
} }
@@ -275,7 +291,13 @@ class HexMap {
setOwner(q, r, playerId) { setOwner(q, r, playerId) {
const cell = this.getCell(q, r); const cell = this.getCell(q, r);
if (cell && cell.isPassable()) { if (cell && cell.isPassable()) {
cell.type = playerId === 1 ? CELL_TYPES.PLAYER1 : CELL_TYPES.PLAYER2; const typeMap = {
1: CELL_TYPES.PLAYER1,
2: CELL_TYPES.PLAYER2,
3: CELL_TYPES.PLAYER3,
4: CELL_TYPES.PLAYER4
};
cell.type = typeMap[playerId] || CELL_TYPES.EMPTY;
return true; return true;
} }
return false; return false;

View File

@@ -14,6 +14,8 @@
--accent-secondary: #00adb5; --accent-secondary: #00adb5;
--player1-color: #4ecca3; --player1-color: #4ecca3;
--player2-color: #e94560; --player2-color: #e94560;
--player3-color: #f9ed69;
--player4-color: #00adb5;
--blocked-color: #2a2a4a; --blocked-color: #2a2a4a;
--empty-color: #3a5a6a; --empty-color: #3a5a6a;
--highlight-color: rgba(255, 255, 255, 0.3); --highlight-color: rgba(255, 255, 255, 0.3);
@@ -225,11 +227,11 @@ body {
} }
.player-4 { .player-4 {
border-left-color: #a8e6cf; border-left-color: #00adb5;
} }
.player-4.active { .player-4.active {
background: linear-gradient(135deg, var(--bg-panel), rgba(168, 230, 207, 0.1)); background: linear-gradient(135deg, var(--bg-panel), rgba(0, 173, 181, 0.1));
} }
.player-card.ai-controlled { .player-card.ai-controlled {

View File

@@ -683,7 +683,8 @@ describe('AIBot', () => {
await bot.playTurn(); await bot.playTurn();
assert.strictEqual(gameUI.executedMoves.length, 1); // AI now makes multiple moves per turn (at least 1)
assert.ok(gameUI.executedMoves.length >= 1, 'Expected at least 1 move to be executed');
assert.strictEqual(gameUI.selectedCell.q, 2); assert.strictEqual(gameUI.selectedCell.q, 2);
assert.strictEqual(gameUI.selectedCell.r, 2); assert.strictEqual(gameUI.selectedCell.r, 2);
}); });