Fix AI multiple moves and player colors
This commit is contained in:
@@ -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,17 +34,48 @@ export class AIBot {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all possible moves
|
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);
|
const moves = this.findPossibleMoves(playerCells);
|
||||||
|
|
||||||
console.log(`[AI-BOT P${this.playerId}] Found ${moves.length} possible moves`);
|
console.log(`[AI-BOT P${this.playerId}] Found ${moves.length} possible moves (move #${moveCount + 1})`);
|
||||||
|
|
||||||
if (moves.length === 0) {
|
if (moves.length === 0) {
|
||||||
// No moves available, end turn
|
// No moves available this iteration
|
||||||
console.log(`[AI-BOT P${this.playerId}] No valid moves, ending turn`);
|
consecutiveNoMoves++;
|
||||||
await this.wait(this.thinkingTime);
|
console.log(`[AI-BOT P${this.playerId}] No valid moves this iteration (${consecutiveNoMoves}/${maxConsecutiveNoMoves})`);
|
||||||
this.gameUI.endTurn();
|
|
||||||
return;
|
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)
|
// Sort moves by priority (attack > expand > reinforce)
|
||||||
@@ -53,7 +85,7 @@ export class AIBot {
|
|||||||
const bestMove = moves[0];
|
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}`);
|
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
|
// Wait for thinking time between moves
|
||||||
await this.wait(this.thinkingTime);
|
await this.wait(this.thinkingTime);
|
||||||
|
|
||||||
// Execute the move
|
// Execute the move
|
||||||
@@ -61,9 +93,17 @@ export class AIBot {
|
|||||||
this.gameUI.currentTarget = bestMove.to;
|
this.gameUI.currentTarget = bestMove.to;
|
||||||
this.gameUI.executeAttack();
|
this.gameUI.executeAttack();
|
||||||
|
|
||||||
console.log(`[AI-BOT P${this.playerId}] Move executed`);
|
moveCount++;
|
||||||
|
console.log(`[AI-BOT P${this.playerId}] Move #${moveCount} executed`);
|
||||||
|
|
||||||
// End turn after executing move
|
// 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()`);
|
console.log(`[AI-BOT P${this.playerId}] Calling endTurn()`);
|
||||||
this.gameUI.endTurn();
|
this.gameUI.endTurn();
|
||||||
|
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user