diff --git a/src/engine/systems/ai/AIBuildOrderExecutor.ts b/src/engine/systems/ai/AIBuildOrderExecutor.ts index 6ea508e4..11273316 100644 --- a/src/engine/systems/ai/AIBuildOrderExecutor.ts +++ b/src/engine/systems/ai/AIBuildOrderExecutor.ts @@ -323,12 +323,20 @@ export class AIBuildOrderExecutor { switch (action.type) { case 'train': if (action.targetId) { + // Pre-check resources before attempting to train + if (!this.canAffordUnit(ai, action.targetId)) { + return false; + } return this.tryTrainUnit(ai, action.targetId); } else if (action.options) { - // Weighted random selection - const totalWeight = action.options.reduce((sum: number, opt: { id: string; weight: number }) => sum + opt.weight, 0); + // Filter options to only those we can afford, then do weighted random selection + const affordableOptions = action.options.filter(opt => this.canAffordUnit(ai, opt.id)); + if (affordableOptions.length === 0) { + return false; + } + const totalWeight = affordableOptions.reduce((sum: number, opt: { id: string; weight: number }) => sum + opt.weight, 0); let random = this.coordinator.getRandom().next() * totalWeight; - for (const option of action.options) { + for (const option of affordableOptions) { random -= option.weight; if (random <= 0) { return this.tryTrainUnit(ai, option.id); @@ -624,16 +632,14 @@ export class AIBuildOrderExecutor { const shouldLog = currentTick % 100 === 0; if (ai.minerals < unitDef.mineralCost || ai.vespene < unitDef.vespeneCost) { - // Log resource failures with console.warn to ensure visibility if (shouldLog) { - console.warn(`[AIBuildOrder] ${ai.playerId}: INSUFFICIENT RESOURCES for ${unitType} (need ${unitDef.mineralCost}M/${unitDef.vespeneCost}G, have ${Math.floor(ai.minerals)}M/${Math.floor(ai.vespene)}G)`); + debugAI.warn(`[AIBuildOrder] ${ai.playerId}: INSUFFICIENT RESOURCES for ${unitType} (need ${unitDef.mineralCost}M/${unitDef.vespeneCost}G, have ${Math.floor(ai.minerals)}M/${Math.floor(ai.vespene)}G)`); } return false; } if (ai.supply + unitDef.supplyCost > ai.maxSupply) { - // Log supply failures with console.warn to ensure visibility if (shouldLog) { - console.warn(`[AIBuildOrder] ${ai.playerId}: SUPPLY BLOCKED for ${unitType} (${ai.supply}+${unitDef.supplyCost} > ${ai.maxSupply})`); + debugAI.warn(`[AIBuildOrder] ${ai.playerId}: SUPPLY BLOCKED for ${unitType} (${ai.supply}+${unitDef.supplyCost} > ${ai.maxSupply})`); } return false; } @@ -679,6 +685,23 @@ export class AIBuildOrderExecutor { return false; } + /** + * Check if the AI can afford to train a unit (resources and supply). + * Used as a pre-check before attempting training to reduce log spam. + */ + private canAffordUnit(ai: AIPlayer, unitType: string): boolean { + const unitDef = UNIT_DEFINITIONS[unitType]; + if (!unitDef) return false; + + if (ai.minerals < unitDef.mineralCost || ai.vespene < unitDef.vespeneCost) { + return false; + } + if (ai.supply + unitDef.supplyCost > ai.maxSupply) { + return false; + } + return true; + } + /** * Check if a unit type requires a research module addon. */ diff --git a/src/engine/systems/ai/AICoordinator.ts b/src/engine/systems/ai/AICoordinator.ts index 581cdfe6..d060f9ae 100644 --- a/src/engine/systems/ai/AICoordinator.ts +++ b/src/engine/systems/ai/AICoordinator.ts @@ -260,12 +260,10 @@ export class AICoordinator extends System { ): void { // Idempotency check: prevent duplicate registrations that would reset AI state if (this.aiPlayers.has(playerId)) { - console.log(`[AICoordinator] AI ${playerId} already registered, skipping duplicate registration`); + debugAI.log(`[AICoordinator] AI ${playerId} already registered, skipping duplicate registration`); return; } - // Always log AI registration (bypasses debug settings for diagnostics) - console.log(`[AICoordinator] Registering AI: ${playerId}, faction: ${faction}, difficulty: ${difficulty}`); debugAI.log(`[AICoordinator] Registering AI: ${playerId}, faction: ${faction}, difficulty: ${difficulty}`); const factionConfig = getFactionAIConfig(faction); @@ -650,9 +648,7 @@ export class AICoordinator extends System { this.random.reseed(currentTick * 31337 + 42); if (currentTick === 1) { - // Always log on first tick for diagnostics - console.log(`[AICoordinator] Tick 1: Registered AI players: ${Array.from(this.aiPlayers.keys()).join(', ') || '(none)'}`); - debugAI.log(`[AICoordinator] Registered AI players: ${Array.from(this.aiPlayers.keys()).join(', ')}`); + debugAI.log(`[AICoordinator] Tick 1: Registered AI players: ${Array.from(this.aiPlayers.keys()).join(', ') || '(none)'}`); } for (const [playerId, ai] of this.aiPlayers) { @@ -665,26 +661,23 @@ export class AICoordinator extends System { const totalBuildings = Array.from(ai.buildingCounts.values()).reduce((a, b) => a + b, 0); if (totalBuildings === 0) { - // Critical diagnostic: always log when AI has no buildings (this blocks all AI logic) if (currentTick % 100 === 0) { - console.warn(`[AICoordinator] ${playerId} has NO buildings detected! AI logic SKIPPED. This is likely a bug.`); - debugAI.warn(`[AICoordinator] ${playerId} has no buildings detected! buildingCounts:`, Object.fromEntries(ai.buildingCounts)); + debugAI.warn(`[AICoordinator] ${playerId} has NO buildings detected! AI logic SKIPPED. buildingCounts:`, Object.fromEntries(ai.buildingCounts)); } continue; } - // Periodic status log (always enabled for diagnostics) + // Periodic status log if (currentTick % 200 === 0) { - // Use console.log for critical diagnostics to ensure visibility - console.log(`[AICoordinator] ${playerId}: workers=${ai.workerCount}, buildings=${totalBuildings}, minerals=${Math.floor(ai.minerals)}, vespene=${Math.floor(ai.vespene)}, supply=${ai.supply}/${ai.maxSupply}, buildOrderStep=${ai.buildOrderIndex}/${ai.buildOrder.length}, state=${ai.state}`); + debugAI.log(`[AICoordinator] ${playerId}: workers=${ai.workerCount}, buildings=${totalBuildings}, minerals=${Math.floor(ai.minerals)}, vespene=${Math.floor(ai.vespene)}, supply=${ai.supply}/${ai.maxSupply}, buildOrderStep=${ai.buildOrderIndex}/${ai.buildOrder.length}, state=${ai.state}`); } this.updateMaxSupply(ai); - // Diagnostic: warn if AI is supply blocked with resources available + // Warn if AI is supply blocked with resources available if (ai.supply >= ai.maxSupply && ai.minerals >= 50) { if (currentTick % 100 === 0) { - console.warn(`[AICoordinator] ${playerId} is SUPPLY BLOCKED: supply=${ai.supply}/${ai.maxSupply}, minerals=${Math.floor(ai.minerals)}`); + debugAI.warn(`[AICoordinator] ${playerId} is SUPPLY BLOCKED: supply=${ai.supply}/${ai.maxSupply}, minerals=${Math.floor(ai.minerals)}`); } }