/** * AI Bot for Hexo game * Controls computer-controlled players */ import { CELL_TYPES } from './map.js'; export class AIBot { constructor(playerId, map, gameUI) { this.playerId = playerId; this.map = map; this.gameUI = gameUI; this.thinkingTime = 1000; // ms delay between moves } /** * 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 ===`); console.log(`[AI-BOT P${this.playerId}] Thinking...`); try { // Get all player cells let 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; } 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 // 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 (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); // 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(); } } // 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); // Still end turn on error to prevent game from getting stuck this.gameUI.endTurn(); throw error; // Re-throw so caller knows there was an error } } /** * Find all possible moves for AI */ findPossibleMoves(playerCells) { const moves = []; for (const cell of playerCells) { if (cell.getStrength() <= 1) continue; const neighbors = this.map.getNeighbors(cell.q, cell.r); for (const neighbor of neighbors) { // Skip own cells if (neighbor.getOwner() === this.playerId) continue; const attackStrength = cell.getStrength() - 1; const defenseStrength = neighbor.getStrength(); moves.push({ from: cell, to: neighbor, attackStrength, defenseStrength, type: neighbor.type === CELL_TYPES.EMPTY ? 'expand' : 'attack' }); } } return moves; } /** * Calculate move priority (higher = better) */ movePriority(move) { let priority = 0; // Prefer attacks on weak enemies if (move.type === 'attack') { if (move.attackStrength > move.defenseStrength) { priority += 100; // Likely to win priority += move.attackStrength - move.defenseStrength; } else { priority -= 50; // Risky attack } } // Prefer expanding to empty cells if (move.type === 'expand') { priority += 50; priority += move.attackStrength; // Stronger placement = better } // Prefer moves that create strong positions priority += move.attackStrength * 0.5; return priority; } /** * Execute a move */ executeMove(move) { this.gameUI.selectedCell = move.from; this.gameUI.currentTarget = move.to; this.gameUI.executeAttack(); } /** * Wait for specified time */ wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } }