714 lines
22 KiB
JavaScript
714 lines
22 KiB
JavaScript
/**
|
|
* Hexo Game UI - Canvas Rendering and Interactions
|
|
* Supports 2-4 players with AI bots
|
|
*/
|
|
|
|
import { HexMap, CELL_TYPES } from './map.js';
|
|
import { AIBot } from './ai-bot.js';
|
|
|
|
// Game constants
|
|
const HEX_SIZE = 14;
|
|
const DEFAULT_MAP_SIZE = 20;
|
|
const ANIMATION_DURATION = 300;
|
|
|
|
// Player colors
|
|
const PLAYER_COLORS = {
|
|
1: '#4ecca3', // teal/green
|
|
2: '#e94560', // red/pink
|
|
3: '#f9ed69', // yellow
|
|
4: '#00adb5' // cyan/blue
|
|
};
|
|
|
|
// Colors
|
|
const COLORS = {
|
|
blocked: '#2a2a4a',
|
|
empty: '#3a5a6a',
|
|
player1: '#4ecca3',
|
|
player2: '#e94560',
|
|
player3: '#f9ed69',
|
|
player4: '#00adb5',
|
|
highlight: 'rgba(255, 255, 255, 0.3)',
|
|
selected: 'rgba(233, 69, 96, 0.6)',
|
|
target: 'rgba(78, 204, 163, 0.5)',
|
|
stroke: '#1a1a2e',
|
|
text: '#eeeeee',
|
|
textDark: '#1a1a2e',
|
|
dice: '#ffffff',
|
|
diceBorder: '#333333'
|
|
};
|
|
|
|
/**
|
|
* Game UI Controller
|
|
*/
|
|
class GameUI {
|
|
constructor() {
|
|
this.canvas = document.getElementById('game-canvas');
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.map = null;
|
|
this.mapSize = DEFAULT_MAP_SIZE;
|
|
this.selectedCell = null;
|
|
this.currentTarget = null;
|
|
this.currentPlayer = 1;
|
|
this.playerCount = 2;
|
|
this.playerTypes = {}; // 1: 'human', 2: 'ai', etc.
|
|
this.aiBots = {};
|
|
this.gamePhase = 'movement';
|
|
this.hasMoved = false;
|
|
this.isAIThinking = false;
|
|
this.isProcessingTurn = false; // Prevent re-entrancy during turn processing
|
|
|
|
this.offsetX = 0;
|
|
this.offsetY = 0;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.setupStartScreen();
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
setupStartScreen() {
|
|
const playerCountSelect = document.getElementById('player-count');
|
|
const playerTypeRows = document.querySelectorAll('.player-type-row');
|
|
|
|
// Initialize player type rows visibility based on default selection
|
|
const initialCount = parseInt(playerCountSelect.value);
|
|
playerTypeRows.forEach((row, index) => {
|
|
row.style.display = index < initialCount ? 'flex' : 'none';
|
|
});
|
|
|
|
playerCountSelect.addEventListener('change', (e) => {
|
|
const count = parseInt(e.target.value);
|
|
playerTypeRows.forEach((row, index) => {
|
|
row.style.display = index < count ? 'flex' : 'none';
|
|
});
|
|
});
|
|
|
|
document.getElementById('start-game-btn').addEventListener('click', () => {
|
|
this.startGame();
|
|
});
|
|
|
|
document.getElementById('back-menu-btn').addEventListener('click', () => {
|
|
this.showStartScreen();
|
|
});
|
|
}
|
|
|
|
showStartScreen() {
|
|
document.getElementById('start-screen').style.display = 'flex';
|
|
document.getElementById('game-screen').style.display = 'none';
|
|
}
|
|
|
|
startGame() {
|
|
// Get settings
|
|
this.playerCount = parseInt(document.getElementById('player-count').value);
|
|
this.mapSize = parseInt(document.getElementById('map-size').value);
|
|
|
|
for (let i = 1; i <= this.playerCount; i++) {
|
|
const typeSelect = document.getElementById(`player${i}-type`);
|
|
this.playerTypes[i] = typeSelect.value;
|
|
}
|
|
|
|
// Show game screen
|
|
document.getElementById('start-screen').style.display = 'none';
|
|
document.getElementById('game-screen').style.display = 'flex';
|
|
|
|
this.newGame();
|
|
}
|
|
|
|
newGame() {
|
|
this.map = new HexMap(this.mapSize);
|
|
this.selectedCell = null;
|
|
this.currentTarget = null;
|
|
this.currentPlayer = 1;
|
|
this.gamePhase = 'movement';
|
|
this.hasMoved = false;
|
|
this.isAIThinking = false;
|
|
this.isProcessingTurn = false;
|
|
|
|
// Initialize AI bots AFTER map is created
|
|
this.aiBots = {};
|
|
for (let i = 1; i <= this.playerCount; i++) {
|
|
if (this.playerTypes[i] === 'ai') {
|
|
this.aiBots[i] = new AIBot(i, this.map, this);
|
|
}
|
|
}
|
|
|
|
// Initialize starting positions for all players
|
|
this.initializePlayers();
|
|
|
|
this.centerMap();
|
|
this.render();
|
|
this.createPlayerCards();
|
|
this.updateUI();
|
|
this.log(`New game started with ${this.playerCount} players!`);
|
|
console.log(`[GAME] New game started with ${this.playerCount} players`);
|
|
|
|
// Start first player's turn (AI if needed)
|
|
this.checkAndRunAITurn();
|
|
}
|
|
|
|
createPlayerCards() {
|
|
const container = document.getElementById('players-container');
|
|
container.innerHTML = '';
|
|
|
|
for (let i = 1; i <= this.playerCount; i++) {
|
|
const card = document.createElement('div');
|
|
card.className = `player-card player-${i}${i === 1 ? ' active' : ''}${this.playerTypes[i] === 'ai' ? ' ai-controlled' : ''}`;
|
|
card.id = `player${i}-card`;
|
|
|
|
card.innerHTML = `
|
|
<h3>Player ${i}${this.playerTypes[i] === 'ai' ? ' (AI)' : ''}</h3>
|
|
<div class="player-stats">
|
|
<div class="stat">
|
|
<span class="stat-label">Cells:</span>
|
|
<span class="stat-value" id="player${i}-cells">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-label">Supply:</span>
|
|
<span class="stat-value" id="player${i}-supply">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-label">Strength:</span>
|
|
<span class="stat-value" id="player${i}-strength">0</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.appendChild(card);
|
|
}
|
|
}
|
|
|
|
initializePlayers() {
|
|
// Place starting units for each player at fixed positions that are always passable
|
|
// Use corners of the map to ensure they don't overlap
|
|
// Positions scale with map size
|
|
const offset = Math.max(1, Math.floor(this.mapSize / 10));
|
|
const positions = [
|
|
{ q: offset, r: offset }, // Player 1 - top-left
|
|
{ q: this.mapSize - 1 - offset, r: this.mapSize - 1 - offset }, // Player 2 - bottom-right
|
|
{ q: offset, r: this.mapSize - 1 - offset }, // Player 3 - bottom-left
|
|
{ q: this.mapSize - 1 - offset, r: offset } // Player 4 - top-right
|
|
];
|
|
|
|
for (let i = 1; i <= this.playerCount; i++) {
|
|
const pos = positions[i - 1];
|
|
if (!pos) continue;
|
|
|
|
// Force the cell to be passable and set ownership
|
|
const cell = this.map.getCell(pos.q, pos.r);
|
|
if (cell) {
|
|
// Make sure cell is passable
|
|
if (cell.type === CELL_TYPES.BLOCKED) {
|
|
cell.type = CELL_TYPES.EMPTY;
|
|
}
|
|
this.map.setOwner(pos.q, pos.r, i);
|
|
cell.setStrength(8);
|
|
console.log(`[GAME] Player ${i} placed at (${pos.q}, ${pos.r})`);
|
|
} else {
|
|
console.error(`[GAME] Failed to place Player ${i} at (${pos.q}, ${pos.r})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
centerMap() {
|
|
const canvasWidth = this.canvas.width;
|
|
const canvasHeight = this.canvas.height;
|
|
|
|
const sqrt3 = Math.sqrt(3);
|
|
|
|
// Calculate actual map bounds based on dynamic map size
|
|
// For pointy-top hex grid:
|
|
// - Width spans from q=0 to q=mapSize-1, with r offset
|
|
// - Rightmost point: (q=mapSize-1, r=mapSize-1) at x = HEX_SIZE * sqrt3 * (mapSize - 1 + (mapSize - 1) / 2)
|
|
// - Height spans from r=0 to r=mapSize-1
|
|
// - Bottommost point: (any q, r=mapSize-1) at y = HEX_SIZE * 1.5 * (mapSize - 1)
|
|
|
|
const mapWidth = HEX_SIZE * sqrt3 * ((this.mapSize - 1) + (this.mapSize - 1) / 2);
|
|
const mapHeight = HEX_SIZE * 1.5 * (this.mapSize - 1);
|
|
|
|
// Add padding for hex radius (hex extends beyond center point)
|
|
const hexRadius = HEX_SIZE;
|
|
const totalWidth = mapWidth + 2 * hexRadius;
|
|
const totalHeight = mapHeight + 2 * hexRadius;
|
|
|
|
// Center the map on canvas
|
|
this.offsetX = (canvasWidth - totalWidth) / 2 + hexRadius;
|
|
this.offsetY = (canvasHeight - totalHeight) / 2 + hexRadius;
|
|
|
|
console.log(`[GAME] Map: canvas=${canvasWidth}x${canvasHeight}, map=${mapWidth.toFixed(0)}x${mapHeight.toFixed(0)}, total=${totalWidth.toFixed(0)}x${totalHeight.toFixed(0)}`);
|
|
console.log(`[GAME] Offset: x=${this.offsetX.toFixed(1)}, y=${this.offsetY.toFixed(1)}`);
|
|
}
|
|
|
|
setupEventListeners() {
|
|
this.canvas.addEventListener('click', (e) => this.handleClick(e));
|
|
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
|
|
|
document.getElementById('end-turn-btn').addEventListener('click', () => this.endTurn());
|
|
document.getElementById('cancel-btn').addEventListener('click', () => this.cancelSelection());
|
|
}
|
|
|
|
hexToPixel(q, r) {
|
|
const sqrt3 = Math.sqrt(3);
|
|
const x = this.offsetX + HEX_SIZE * sqrt3 * (q + r/2);
|
|
const y = this.offsetY + HEX_SIZE * 1.5 * r;
|
|
return { x, y };
|
|
}
|
|
|
|
pixelToHex(x, y) {
|
|
const adjX = x - this.offsetX;
|
|
const adjY = y - this.offsetY;
|
|
|
|
const sqrt3 = Math.sqrt(3);
|
|
const q = (adjX / (HEX_SIZE * sqrt3) - adjY / (HEX_SIZE * 1.5) / 2);
|
|
const r = adjY / (HEX_SIZE * 1.5);
|
|
|
|
const qi = Math.round(q);
|
|
const ri = Math.round(r);
|
|
|
|
if (qi >= 0 && qi < this.mapSize && ri >= 0 && ri < this.mapSize) {
|
|
return { q: qi, r: ri };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getValidTargets(q, r) {
|
|
const directions = [
|
|
[+1, -1], // north-east
|
|
[0, -1], // north-west
|
|
[-1, 0], // west
|
|
[-1, +1], // south-west
|
|
[0, +1], // south-east
|
|
[+1, 0] // east
|
|
];
|
|
|
|
const targets = [];
|
|
for (const [dq, dr] of directions) {
|
|
const nq = q + dq;
|
|
const nr = r + dr;
|
|
if (nq >= 0 && nq < this.mapSize && nr >= 0 && nr < this.mapSize) {
|
|
const cell = this.map.getCell(nq, nr);
|
|
if (cell && cell.isPassable()) {
|
|
targets.push(cell);
|
|
}
|
|
}
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
drawHex(q, r, fillStyle, strokeStyle = COLORS.stroke, lineWidth = 2) {
|
|
const { x, y } = this.hexToPixel(q, r);
|
|
const size = HEX_SIZE * 0.98;
|
|
|
|
const vertices = [];
|
|
for (let i = 0; i < 6; i++) {
|
|
const angle = Math.PI / 6 + (Math.PI / 3) * i;
|
|
vertices.push({
|
|
x: x + size * Math.cos(angle),
|
|
y: y + size * Math.sin(angle)
|
|
});
|
|
}
|
|
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let i = 1; i < vertices.length; i++) {
|
|
this.ctx.lineTo(vertices[i].x, vertices[i].y);
|
|
}
|
|
this.ctx.closePath();
|
|
|
|
this.ctx.fillStyle = fillStyle;
|
|
this.ctx.fill();
|
|
|
|
this.ctx.strokeStyle = strokeStyle;
|
|
this.ctx.lineWidth = lineWidth;
|
|
this.ctx.stroke();
|
|
}
|
|
|
|
drawDice(cell) {
|
|
if (cell.dice.length === 0) return;
|
|
|
|
const { x, y } = this.hexToPixel(cell.q, cell.r);
|
|
const strength = cell.getStrength();
|
|
|
|
this.ctx.fillStyle = COLORS.text;
|
|
this.ctx.font = 'bold 11px Arial';
|
|
this.ctx.textAlign = 'center';
|
|
this.ctx.textBaseline = 'middle';
|
|
this.ctx.fillText(strength.toString(), x, y - 3);
|
|
|
|
if (cell.dice.length > 1) {
|
|
const dotY = y + 8;
|
|
for (let i = 0; i < Math.min(cell.dice.length, 5); i++) {
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(x - 6 + i * 4, dotY, 1.5, 0, Math.PI * 2);
|
|
this.ctx.fillStyle = COLORS.dice;
|
|
this.ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
drawOwnerIndicator(cell) {
|
|
if (!cell.isOwned()) return;
|
|
|
|
const { x, y } = this.hexToPixel(cell.q, cell.r);
|
|
const ownerColor = PLAYER_COLORS[cell.getOwner()] || COLORS.player1;
|
|
const size = HEX_SIZE * 0.98 - 3;
|
|
|
|
const vertices = [];
|
|
for (let i = 0; i < 6; i++) {
|
|
const angle = Math.PI / 6 + (Math.PI / 3) * i;
|
|
vertices.push({
|
|
x: x + size * Math.cos(angle),
|
|
y: y + size * Math.sin(angle)
|
|
});
|
|
}
|
|
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let i = 1; i < vertices.length; i++) {
|
|
this.ctx.lineTo(vertices[i].x, vertices[i].y);
|
|
}
|
|
this.ctx.closePath();
|
|
this.ctx.strokeStyle = ownerColor;
|
|
this.ctx.lineWidth = 3;
|
|
this.ctx.stroke();
|
|
}
|
|
|
|
render() {
|
|
this.ctx.fillStyle = COLORS.stroke;
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
if (!this.map) return;
|
|
|
|
for (let r = 0; r < this.mapSize; r++) {
|
|
for (let q = 0; q < this.mapSize; q++) {
|
|
const cell = this.map.getCell(q, r);
|
|
let color;
|
|
|
|
if (cell.type === CELL_TYPES.BLOCKED) {
|
|
color = COLORS.blocked;
|
|
} else if (cell.type === CELL_TYPES.PLAYER1) {
|
|
color = COLORS.player1;
|
|
} else if (cell.type === CELL_TYPES.PLAYER2) {
|
|
color = COLORS.player2;
|
|
} else if (cell.type === CELL_TYPES.PLAYER3) {
|
|
color = COLORS.player3;
|
|
} else if (cell.type === CELL_TYPES.PLAYER4) {
|
|
color = COLORS.player4;
|
|
} else {
|
|
color = COLORS.empty;
|
|
}
|
|
|
|
if (!cell.isOwned()) {
|
|
color = this.hexToRgba(color, 0.6);
|
|
}
|
|
|
|
this.drawHex(q, r, color);
|
|
this.drawOwnerIndicator(cell);
|
|
this.drawDice(cell);
|
|
}
|
|
}
|
|
|
|
if (this.selectedCell) {
|
|
this.drawHex(
|
|
this.selectedCell.q,
|
|
this.selectedCell.r,
|
|
COLORS.selected,
|
|
COLORS.text,
|
|
3
|
|
);
|
|
|
|
const targets = this.getValidTargets(this.selectedCell.q, this.selectedCell.r);
|
|
for (const target of targets) {
|
|
if (target.getOwner() !== this.currentPlayer) {
|
|
this.drawHex(
|
|
target.q,
|
|
target.r,
|
|
COLORS.target,
|
|
PLAYER_COLORS[this.currentPlayer] || COLORS.player1,
|
|
2
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
hexToRgba(hex, alpha) {
|
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
const b = parseInt(hex.slice(5, 7), 16);
|
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
}
|
|
|
|
handleClick(e) {
|
|
if (this.gamePhase !== 'movement') return;
|
|
if (this.playerTypes[this.currentPlayer] === 'ai') return;
|
|
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
const hexPos = this.pixelToHex(x, y);
|
|
if (!hexPos) return;
|
|
|
|
const cell = this.map.getCell(hexPos.q, hexPos.r);
|
|
if (!cell || !cell.isPassable()) return;
|
|
|
|
if (this.selectedCell === null) {
|
|
if (cell.getOwner() === this.currentPlayer && cell.getStrength() > 1) {
|
|
this.selectedCell = cell;
|
|
this.updateUI();
|
|
this.render();
|
|
}
|
|
} else {
|
|
if (cell === this.selectedCell) {
|
|
this.cancelSelection();
|
|
return;
|
|
}
|
|
|
|
const targets = this.getValidTargets(this.selectedCell.q, this.selectedCell.r);
|
|
const isValidTarget = targets.some(n => n.q === cell.q && n.r === cell.r);
|
|
|
|
if (cell.getOwner() === this.currentPlayer) {
|
|
if (cell.getStrength() > 1) {
|
|
this.selectedCell = cell;
|
|
this.updateUI();
|
|
this.render();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isValidTarget) {
|
|
this.currentTarget = cell;
|
|
this.executeAttack();
|
|
} else {
|
|
this.cancelSelection();
|
|
}
|
|
}
|
|
}
|
|
|
|
handleMouseMove(e) {
|
|
// Hover effects could be added here
|
|
}
|
|
|
|
executeAttack() {
|
|
if (!this.selectedCell || !this.currentTarget) return;
|
|
|
|
const attacker = this.selectedCell;
|
|
const defender = this.currentTarget;
|
|
const attackStrength = attacker.getStrength() - 1;
|
|
|
|
if (defender.type === CELL_TYPES.EMPTY || defender.getOwner() !== this.currentPlayer) {
|
|
let defenseStrength = defender.getStrength();
|
|
|
|
if (defenseStrength > 0) {
|
|
const attackRoll = Math.floor(Math.random() * attackStrength) + 1;
|
|
const defenseRoll = Math.floor(Math.random() * defenseStrength) + 1;
|
|
|
|
this.log(`P${this.currentPlayer} Attack: ${attackStrength} vs ${defenseStrength} | Roll: ${attackRoll} vs ${defenseRoll}`);
|
|
|
|
if (attackRoll > defenseRoll) {
|
|
const remainingStrength = attackRoll - 1;
|
|
attacker.setStrength(1);
|
|
|
|
if (remainingStrength > 0) {
|
|
defender.setStrength(remainingStrength);
|
|
this.map.setOwner(defender.q, defender.r, this.currentPlayer);
|
|
this.log(`Victory! Captured with strength ${remainingStrength}`, 'victory');
|
|
}
|
|
} else {
|
|
const remainingDefense = defenseRoll - attackRoll;
|
|
defender.setStrength(Math.max(1, remainingDefense));
|
|
attacker.setStrength(1);
|
|
this.log(`Attack repelled! Defender has ${Math.max(1, remainingDefense)}`, 'defeat');
|
|
}
|
|
} else {
|
|
attacker.setStrength(1);
|
|
defender.setStrength(attackStrength);
|
|
this.map.setOwner(defender.q, defender.r, this.currentPlayer);
|
|
this.log(`Captured empty cell with strength ${attackStrength}`);
|
|
}
|
|
}
|
|
|
|
this.hasMoved = true;
|
|
this.cancelSelection();
|
|
this.render();
|
|
this.updateUI();
|
|
}
|
|
|
|
cancelSelection() {
|
|
this.selectedCell = null;
|
|
this.currentTarget = null;
|
|
this.updateUI();
|
|
this.render();
|
|
}
|
|
|
|
async endTurn() {
|
|
if (this.gamePhase !== 'movement') {
|
|
console.log(`[GAME] endTurn() called but gamePhase is '${this.gamePhase}', ignoring`);
|
|
return;
|
|
}
|
|
|
|
// Prevent re-entrancy - only one turn processing at a time
|
|
if (this.isProcessingTurn) {
|
|
console.log(`[GAME] endTurn() called but isProcessingTurn is true, ignoring`);
|
|
return;
|
|
}
|
|
|
|
this.isProcessingTurn = true;
|
|
console.log(`[GAME] endTurn() - Player ${this.currentPlayer} ending turn`);
|
|
|
|
try {
|
|
// Apply supply
|
|
const supply = this.map.calculateSupply(this.currentPlayer);
|
|
this.distributeSupply(supply);
|
|
this.log(`Player ${this.currentPlayer} received ${supply} supply`);
|
|
console.log(`[GAME] Player ${this.currentPlayer} received ${supply} supply`);
|
|
|
|
// Next player
|
|
const previousPlayer = this.currentPlayer;
|
|
this.currentPlayer = (this.currentPlayer % this.playerCount) + 1;
|
|
this.hasMoved = false;
|
|
|
|
console.log(`[GAME] Turn transition: P${previousPlayer} -> P${this.currentPlayer}`);
|
|
|
|
this.cancelSelection();
|
|
this.updateUI();
|
|
this.render();
|
|
|
|
this.log(`Player ${this.currentPlayer}'s turn`);
|
|
console.log(`[GAME] Player ${this.currentPlayer}'s turn started (${this.playerTypes[this.currentPlayer]})`);
|
|
|
|
// Reset isProcessingTurn BEFORE awaiting AI turn
|
|
// This allows the next AI's endTurn() call to proceed
|
|
this.isProcessingTurn = false;
|
|
|
|
// Check if next player is AI and await completion
|
|
await this.checkAndRunAITurn();
|
|
} catch (error) {
|
|
console.error(`[GAME] Error in endTurn():`, error);
|
|
this.log(`Error during turn transition: ${error.message}`, 'error');
|
|
this.isProcessingTurn = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if current player is AI and run their turn
|
|
* @returns {Promise<boolean>} - true if AI turn was run, false if human turn
|
|
*/
|
|
async checkAndRunAITurn() {
|
|
if (this.playerTypes[this.currentPlayer] === 'ai' && this.aiBots[this.currentPlayer]) {
|
|
console.log(`[AI] Player ${this.currentPlayer} is AI, starting turn`);
|
|
this.isAIThinking = true;
|
|
this.updateUI();
|
|
|
|
try {
|
|
// Run AI turn and wait for it to complete
|
|
await this.aiBots[this.currentPlayer].playTurn();
|
|
console.log(`[AI] Player ${this.currentPlayer} AI turn completed`);
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`[AI] Error during Player ${this.currentPlayer} AI turn:`, error);
|
|
this.log(`AI error: ${error.message}`, 'error');
|
|
this.isAIThinking = false;
|
|
this.updateUI();
|
|
// Still advance to next player even on error
|
|
return true;
|
|
} finally {
|
|
// Always reset the flag when AI turn completes (success or error)
|
|
this.isAIThinking = false;
|
|
console.log(`[AI] Player ${this.currentPlayer} isAIThinking reset to false`);
|
|
}
|
|
}
|
|
console.log(`[AI] Player ${this.currentPlayer} is human, no AI turn needed`);
|
|
return false;
|
|
}
|
|
|
|
distributeSupply(supply) {
|
|
const playerCells = this.map.getPlayerCells(this.currentPlayer);
|
|
const eligibleCells = playerCells.filter(cell => !cell.isMaxStrength());
|
|
|
|
if (eligibleCells.length === 0 || supply === 0) return;
|
|
|
|
let remainingSupply = supply;
|
|
while (remainingSupply > 0 && eligibleCells.length > 0) {
|
|
const randomCell = eligibleCells[Math.floor(Math.random() * eligibleCells.length)];
|
|
const currentStrength = randomCell.getStrength();
|
|
|
|
if (currentStrength < 48) {
|
|
randomCell.setStrength(currentStrength + 1);
|
|
remainingSupply--;
|
|
}
|
|
|
|
if (randomCell.isMaxStrength()) {
|
|
eligibleCells.splice(eligibleCells.indexOf(randomCell), 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
updateUI() {
|
|
// Update all player cards
|
|
for (let i = 1; i <= this.playerCount; i++) {
|
|
const playerCells = this.map.getPlayerCells(i);
|
|
const playerStrength = playerCells.reduce((sum, c) => sum + c.getStrength(), 0);
|
|
const playerSupply = this.map.calculateSupply(i);
|
|
|
|
document.getElementById(`player${i}-cells`).textContent = playerCells.length;
|
|
document.getElementById(`player${i}-supply`).textContent = playerSupply;
|
|
document.getElementById(`player${i}-strength`).textContent = playerStrength;
|
|
|
|
const card = document.getElementById(`player${i}-card`);
|
|
card.classList.toggle('active', i === this.currentPlayer);
|
|
}
|
|
|
|
document.getElementById('current-turn').textContent = this.currentPlayer;
|
|
document.getElementById('game-phase').textContent = this.gamePhase;
|
|
|
|
// Update map size display
|
|
document.getElementById('map-size-display').textContent = `${this.mapSize}x${this.mapSize}`;
|
|
|
|
const instruction = document.getElementById('action-instruction');
|
|
if (this.selectedCell) {
|
|
instruction.textContent = `Select target (strength: ${this.selectedCell.getStrength()})`;
|
|
} else if (this.playerTypes[this.currentPlayer] === 'ai') {
|
|
instruction.textContent = 'AI is thinking...';
|
|
} else {
|
|
instruction.textContent = 'Select a cell with dice to move';
|
|
}
|
|
|
|
const cellInfo = document.getElementById('selected-cell-info');
|
|
cellInfo.style.display = 'block';
|
|
|
|
if (this.selectedCell) {
|
|
document.getElementById('cell-strength').textContent = this.selectedCell.getStrength();
|
|
document.getElementById('cell-dice').textContent = this.selectedCell.dice.length;
|
|
} else {
|
|
document.getElementById('cell-strength').textContent = '-';
|
|
document.getElementById('cell-dice').textContent = '-';
|
|
}
|
|
|
|
const cancelBtn = document.getElementById('cancel-btn');
|
|
const endTurnBtn = document.getElementById('end-turn-btn');
|
|
|
|
cancelBtn.disabled = !this.selectedCell || this.playerTypes[this.currentPlayer] === 'ai';
|
|
endTurnBtn.disabled = this.playerTypes[this.currentPlayer] === 'ai';
|
|
}
|
|
|
|
log(message, type = '') {
|
|
const logContainer = document.getElementById('battle-log');
|
|
const entry = document.createElement('div');
|
|
entry.className = `log-entry ${type}`;
|
|
entry.textContent = `[P${this.currentPlayer}] ${message}`;
|
|
logContainer.insertBefore(entry, logContainer.firstChild);
|
|
|
|
while (logContainer.children.length > 50) {
|
|
logContainer.removeChild(logContainer.lastChild);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize game when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.game = new GameUI();
|
|
});
|