194 lines
6.0 KiB
JavaScript
194 lines
6.0 KiB
JavaScript
/**
|
|
* 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));
|
|
}
|
|
}
|