Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions src/workers/ai-decisions.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ interface ThreatInfo {
}

// State
let config: AIWorkerConfig = {};
let _config: AIWorkerConfig = {};
let unitPriorities: Record<string, number> = DEFAULT_UNIT_PRIORITY;
let threatWeights = THREAT_WEIGHTS;
let focusFireThreshold = 0.3;
Expand All @@ -139,7 +139,7 @@ let initialized = false;
*/
function init(workerConfig: AIWorkerConfig): boolean {
try {
config = workerConfig;
_config = workerConfig;
unitPriorities = workerConfig.unitPriorities || DEFAULT_UNIT_PRIORITY;
threatWeights = workerConfig.threatWeights || THREAT_WEIGHTS;
focusFireThreshold = workerConfig.focusFireThreshold ?? 0.3;
Expand Down Expand Up @@ -282,8 +282,17 @@ function calculateKitePosition(
// Random direction if too close
const angle = Math.random() * Math.PI * 2;
return {
x: Math.max(2, Math.min(mapWidth - 2, unit.x + Math.cos(angle) * unit.attackRange * kiteDistanceMultiplier)),
y: Math.max(2, Math.min(mapHeight - 2, unit.y + Math.sin(angle) * unit.attackRange * kiteDistanceMultiplier)),
x: Math.max(
2,
Math.min(mapWidth - 2, unit.x + Math.cos(angle) * unit.attackRange * kiteDistanceMultiplier)
),
y: Math.max(
2,
Math.min(
mapHeight - 2,
unit.y + Math.sin(angle) * unit.attackRange * kiteDistanceMultiplier
)
),
};
}

Expand Down Expand Up @@ -360,15 +369,23 @@ function shouldTransform(
if (nearbyGroundEnemies > 0 && nearbyAirEnemies === 0) {
return { shouldTransform: true, targetMode: 'assault' };
}
if (nearbyGroundEnemies >= 3 && nearbyAirEnemies <= 1 && groundThreatScore > airThreatScore * 2) {
if (
nearbyGroundEnemies >= 3 &&
nearbyAirEnemies <= 1 &&
groundThreatScore > airThreatScore * 2
) {
return { shouldTransform: true, targetMode: 'assault' };
}
} else {
// Assault mode (ground-only attacks) -> Transform to Fighter if air enemies and no ground
if (nearbyAirEnemies > 0 && nearbyGroundEnemies === 0) {
return { shouldTransform: true, targetMode: 'fighter' };
}
if (nearbyAirEnemies >= 2 && nearbyGroundEnemies <= 1 && airThreatScore > groundThreatScore * 2) {
if (
nearbyAirEnemies >= 2 &&
nearbyGroundEnemies <= 1 &&
airThreatScore > groundThreatScore * 2
) {
return { shouldTransform: true, targetMode: 'fighter' };
}
}
Expand All @@ -389,7 +406,7 @@ function shouldSwitchTarget(
if (unit.targetEntityId === bestTargetId) return false;

// Find current target
const currentTarget = enemies.find(e => e.id === unit.targetEntityId);
const currentTarget = enemies.find((e) => e.id === unit.targetEntityId);
if (!currentTarget || currentTarget.health <= 0) return true;

const currentHealthPercent = currentTarget.health / currentTarget.maxHealth;
Expand All @@ -398,7 +415,7 @@ function shouldSwitchTarget(
if (currentHealthPercent < focusFireThreshold) return false;

// Find better target
const betterTarget = enemies.find(e => e.id === bestTargetId);
const betterTarget = enemies.find((e) => e.id === bestTargetId);
if (!betterTarget) return false;

const betterHealthPercent = betterTarget.health / betterTarget.maxHealth;
Expand Down Expand Up @@ -465,7 +482,13 @@ function evaluateMicro(
// Check if should kite
const kite = shouldKite(unit, enemyUnits);
if (kite.shouldKite && kite.kiteFromX !== undefined && kite.kiteFromY !== undefined) {
const kitePos = calculateKitePosition(unit, kite.kiteFromX, kite.kiteFromY, mapWidth, mapHeight);
const kitePos = calculateKitePosition(
unit,
kite.kiteFromX,
kite.kiteFromY,
mapWidth,
mapHeight
);
decisions.push({
unitId: unit.id,
action: 'kite',
Expand Down
24 changes: 11 additions & 13 deletions tests/data/aiConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function createTestState(overrides: Partial<AIStateSnapshot> = {}): AIStateSnaps
hard: { targetWorkers: 60 },
very_hard: { targetWorkers: 70 },
insane: { targetWorkers: 80 },
} as any,
} as FactionAIConfig['difficultyConfig'],
};

return {
Expand Down Expand Up @@ -236,7 +236,11 @@ describe('evaluateCondition', () => {
describe('edge cases', () => {
it('returns false for unknown condition type', () => {
const state = createTestState();
const condition = { type: 'unknownType', operator: '>', value: 0 } as unknown as RuleCondition;
const condition = {
type: 'unknownType',
operator: '>',
value: 0,
} as unknown as RuleCondition;
expect(evaluateCondition(condition, state)).toBe(false);
});

Expand Down Expand Up @@ -431,9 +435,7 @@ describe('calculateUtilityScore', () => {
const state = createTestState({ minerals: 500 });
const utility: UtilityScore = {
baseScore: 100,
conditions: [
{ type: 'minerals', operator: '>', value: 400, multiplier: 2 },
],
conditions: [{ type: 'minerals', operator: '>', value: 400, multiplier: 2 }],
};

expect(calculateUtilityScore(utility, state)).toBe(200);
Expand All @@ -443,9 +445,7 @@ describe('calculateUtilityScore', () => {
const state = createTestState({ minerals: 100 });
const utility: UtilityScore = {
baseScore: 100,
conditions: [
{ type: 'minerals', operator: '>', value: 400, multiplier: 2 },
],
conditions: [{ type: 'minerals', operator: '>', value: 400, multiplier: 2 }],
};

expect(calculateUtilityScore(utility, state)).toBe(100);
Expand All @@ -468,9 +468,7 @@ describe('calculateUtilityScore', () => {
const state = createTestState({ enemyAirUnits: 5 });
const utility: UtilityScore = {
baseScore: 100,
conditions: [
{ type: 'enemyAirUnits', operator: '>', value: 0, multiplier: 0.5 },
],
conditions: [{ type: 'enemyAirUnits', operator: '>', value: 0, multiplier: 0.5 }],
};

expect(calculateUtilityScore(utility, state)).toBe(50);
Expand Down Expand Up @@ -520,14 +518,14 @@ describe('dominion AI macro rules', () => {

const supplyCost = BUILDING_DEFINITIONS.supply_cache.mineralCost;
const supplyRules = config!.macroRules.filter(
rule => rule.action.type === 'build' && rule.action.targetId === 'supply_cache'
(rule) => rule.action.type === 'build' && rule.action.targetId === 'supply_cache'
);

expect(supplyRules.length).toBeGreaterThan(0);

for (const rule of supplyRules) {
const mineralCondition = rule.conditions.find(
condition => condition.type === 'minerals' && condition.operator === '>='
(condition) => condition.type === 'minerals' && condition.operator === '>='
);
expect(mineralCondition).toBeDefined();
expect(mineralCondition!.value).toBeGreaterThanOrEqual(supplyCost);
Expand Down
12 changes: 9 additions & 3 deletions tests/engine/definitions/definitionValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe('DefinitionValidator', () => {

it('rejects manifest with missing id', () => {
const manifest = createValidFactionManifest();
delete (manifest as any).id;
delete (manifest as Record<string, unknown>).id;

const result = validator.validateFactionManifest(manifest);
expect(result.valid).toBe(false);
Expand Down Expand Up @@ -822,7 +822,11 @@ describe('DefinitionValidator', () => {
it('warns for invalid effect target reference', () => {
const { units, buildings, research, abilities } = createTestData();
research.research1.effects = [
{ type: 'damage_bonus', value: 1, targets: ['nonexistent_target'] } as any,
{
type: 'damage_bonus',
value: 1,
targets: ['nonexistent_target'],
} as ResearchDefinition['effects'][number],
];

const result = validator.validateReferences(units, buildings, research, abilities);
Expand Down Expand Up @@ -874,7 +878,9 @@ describe('DefinitionValidator', () => {

it('handles effects without targets', () => {
const { units, buildings, research, abilities } = createTestData();
research.research1.effects = [{ type: 'damage_bonus', value: 1 } as any];
research.research1.effects = [
{ type: 'damage_bonus', value: 1 } as ResearchDefinition['effects'][number],
];

const result = validator.validateReferences(units, buildings, research, abilities);
expect(result.valid).toBe(true);
Expand Down
Loading