Files
evolution/src/lib/neatArena/fitness.ts
2026-01-14 11:13:33 +11:00

117 lines
4.0 KiB
TypeScript

import type { SimulationState } from './types';
import { hasLineOfSight } from './sensors';
/**
* Fitness calculation for NEAT Arena.
*
* Fitness rewards:
* - +10 per hit on opponent
* - -10 per being hit
* - -0.002 per tick (time penalty to encourage aggression)
* - -0.2 per shot fired (ammo management)
* - +0.01 per tick when aiming well at visible opponent
*/
export const FITNESS_CONFIG = {
HIT_REWARD: 10.0, // Kill
DAMAGE_REWARD: 4.0, // Per hit dealt (High reward for hitting)
HIT_PENALTY: 1.0, // Per hit taken (Reduced to 1.0 to encourage aggression/trading)
TIME_PENALTY: 0.002, // Per tick
SHOT_PENALTY: 0.0, // REMOVED: Free shooting encourages exploration
AIM_REWARD: 0.01, // Increased: Stronger guide signal
MOVE_REWARD: 0.001, // Per tick moving
};
export interface FitnessTracker {
agentId: number;
fitness: number;
// For incremental calculation
lastKills: number;
lastHitsTaken: number;
lastHitsDealt: number;
shotsFired: number;
}
/**
* Create a new fitness tracker
*/
export function createFitnessTracker(agentId: number): FitnessTracker {
return {
agentId,
fitness: 0,
lastKills: 0,
lastHitsTaken: 0,
lastHitsDealt: 0,
shotsFired: 0,
};
}
/**
* Update fitness based on current simulation state
*/
export function updateFitness(tracker: FitnessTracker, state: SimulationState): FitnessTracker {
const agent = state.agents.find(a => a.id === tracker.agentId)!;
const opponent = state.agents.find(a => a.id !== tracker.agentId)!;
const newTracker = { ...tracker };
// Reward for new kills
const newKills = agent.kills - tracker.lastKills;
newTracker.fitness += newKills * FITNESS_CONFIG.HIT_REWARD;
newTracker.lastKills = agent.kills;
// Reward for HITS DEALT (Direct Damage)
// We infer hits dealt by checking opponent's hit counter increase
const currentHitsDealt = opponent.hits; // Assuming opponent.hits tracks times they were hit
const newHitsDealt = currentHitsDealt - tracker.lastHitsDealt;
if (newHitsDealt > 0) {
// +2.0 per hit. 5 hits = 10 pts (Kill equivalent).
// Makes shooting visibly rewarding immediately.
newTracker.fitness += newHitsDealt * FITNESS_CONFIG.DAMAGE_REWARD;
}
newTracker.lastHitsDealt = currentHitsDealt;
// Penalty for being hit (Hits Taken)
const newHitsTaken = agent.hits - tracker.lastHitsTaken;
newTracker.fitness -= newHitsTaken * FITNESS_CONFIG.HIT_PENALTY;
newTracker.lastHitsTaken = agent.hits;
// Time penalty (encourages finishing quickly)
newTracker.fitness -= FITNESS_CONFIG.TIME_PENALTY;
// Check if agent fired this tick
if (agent.fireCooldown === 10) {
newTracker.shotsFired++;
newTracker.fitness -= FITNESS_CONFIG.SHOT_PENALTY; // Tiny penalty just to prevent spamming empty space
}
// Reward for aiming at visible opponent (Guide Signal ONLY)
if (hasLineOfSight(agent, opponent, state.map.walls)) {
const dx = opponent.position.x - agent.position.x;
const dy = opponent.position.y - agent.position.y;
const angleToOpponent = Math.atan2(dy, dx);
let angleDiff = angleToOpponent - agent.aimAngle;
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
const cosAngleDiff = Math.cos(angleDiff);
// Reduced from 0.05 to 0.005.
// Max total aim points = 1.5.
// One bullet hit (2.0) is worth more than perfect aiming all match.
newTracker.fitness += ((cosAngleDiff + 1) * 0.5) * FITNESS_CONFIG.AIM_REWARD;
}
// Small reward for movement
const speed = Math.sqrt(agent.velocity.x**2 + agent.velocity.y**2);
if (speed > 0.1) {
newTracker.fitness += FITNESS_CONFIG.MOVE_REWARD;
}
return newTracker;
}