From f3be577a32c62c0dbc9f3f8b58843347cb14552f Mon Sep 17 00:00:00 2001 From: sokol Date: Sat, 21 Feb 2026 21:25:22 +0300 Subject: [PATCH] Fix AI multiple moves and player colors --- public/ai-bot.js | 98 +++++++++++++++++++++++++++++++-------------- public/game.js | 10 ++--- public/map.js | 28 +++++++++++-- public/styles.css | 6 ++- test/ai-bot.test.js | 3 +- 5 files changed, 105 insertions(+), 40 deletions(-) diff --git a/public/ai-bot.js b/public/ai-bot.js index f652188..e27abd0 100644 --- a/public/ai-bot.js +++ b/public/ai-bot.js @@ -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() { console.log(`[AI-BOT P${this.playerId}] === Turn started ===`); @@ -22,7 +23,7 @@ export class AIBot { try { // 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`); @@ -33,40 +34,79 @@ export class AIBot { return; } - // Find all possible moves - const moves = this.findPossibleMoves(playerCells); + let moveCount = 0; + 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) { - // No moves available, end turn - console.log(`[AI-BOT P${this.playerId}] No valid moves, ending turn`); + if (playerCells.length === 0) { + console.log(`[AI-BOT P${this.playerId}] No cells remaining, 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); - 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) - 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 + // End turn after all moves are executed + console.log(`[AI-BOT P${this.playerId}] Total moves this turn: ${moveCount}`); console.log(`[AI-BOT P${this.playerId}] Calling endTurn()`); this.gameUI.endTurn(); - + console.log(`[AI-BOT P${this.playerId}] === Turn completed ===`); } catch (error) { console.error(`[AI-BOT P${this.playerId}] Error during turn:`, error); diff --git a/public/game.js b/public/game.js index 1322ab3..834feb1 100644 --- a/public/game.js +++ b/public/game.js @@ -13,10 +13,10 @@ const ANIMATION_DURATION = 300; // Player colors const PLAYER_COLORS = { - 1: '#4ecca3', - 2: '#e94560', - 3: '#f9ed69', - 4: '#a8e6cf' + 1: '#4ecca3', // teal/green + 2: '#e94560', // red/pink + 3: '#f9ed69', // yellow + 4: '#00adb5' // cyan/blue }; // Colors @@ -26,7 +26,7 @@ const COLORS = { player1: '#4ecca3', player2: '#e94560', player3: '#f9ed69', - player4: '#a8e6cf', + player4: '#00adb5', highlight: 'rgba(255, 255, 255, 0.3)', selected: 'rgba(233, 69, 96, 0.6)', target: 'rgba(78, 204, 163, 0.5)', diff --git a/public/map.js b/public/map.js index d096f8e..7eac38b 100644 --- a/public/map.js +++ b/public/map.js @@ -14,6 +14,8 @@ const CELL_TYPES = { BLOCKED: 1, // Impassable terrain PLAYER1: 2, // Player 1 owned PLAYER2: 3, // Player 2 owned + PLAYER3: 4, // Player 3 owned + PLAYER4: 5, // Player 4 owned }; /** @@ -80,12 +82,17 @@ class HexCell { } 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() { if (this.type === CELL_TYPES.PLAYER1) return 1; 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; } } @@ -157,7 +164,14 @@ class HexMap { * Get all cells owned by a player */ 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( cell => cell.type === targetType ); @@ -265,6 +279,8 @@ class HexMap { if (cell.type === CELL_TYPES.BLOCKED) return '██'; if (cell.type === CELL_TYPES.PLAYER1) return 'P1'; 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, ' '); return ' '; } @@ -275,7 +291,13 @@ class HexMap { setOwner(q, r, playerId) { const cell = this.getCell(q, r); 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 false; diff --git a/public/styles.css b/public/styles.css index 58a759c..30ae29b 100644 --- a/public/styles.css +++ b/public/styles.css @@ -14,6 +14,8 @@ --accent-secondary: #00adb5; --player1-color: #4ecca3; --player2-color: #e94560; + --player3-color: #f9ed69; + --player4-color: #00adb5; --blocked-color: #2a2a4a; --empty-color: #3a5a6a; --highlight-color: rgba(255, 255, 255, 0.3); @@ -225,11 +227,11 @@ body { } .player-4 { - border-left-color: #a8e6cf; + border-left-color: #00adb5; } .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 { diff --git a/test/ai-bot.test.js b/test/ai-bot.test.js index f970a9e..226e796 100644 --- a/test/ai-bot.test.js +++ b/test/ai-bot.test.js @@ -683,7 +683,8 @@ describe('AIBot', () => { 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.r, 2); });