feat(battle-sim): implement battle simulation logic and metrics tracking

- Added WarBattleMetrics interface to track attacker and defender skills during battles.
- Enhanced WarUnit class with method to log activated skills.
- Introduced environment setup for battle simulation, including unit set loading and configuration resolution.
- Developed in-memory and Redis transport layers for handling battle simulation requests and results.
- Created types and interfaces for battle simulation payloads and responses.
- Implemented battle simulation processing logic, including logging and skill tracking.
- Added tests for battle simulation processor to ensure functionality and correctness.
This commit is contained in:
2025-12-30 11:17:28 +00:00
parent 97439f64a9
commit 56e662a7db
20 changed files with 1549 additions and 7 deletions
+80 -6
View File
@@ -143,7 +143,7 @@ const isSupplyCity = (city: City): boolean => {
return city.supplyState > 0;
};
const extractBattleOrder = (
export const computeBattleOrder = (
defender: WarUnit,
attacker: WarUnitGeneral
): number => {
@@ -300,7 +300,7 @@ export const resolveWarBattle = <
defenderLogger,
createPipeline(defender)
);
if (extractBattleOrder(unit, attackerUnit) <= 0) {
if (computeBattleOrder(unit, attackerUnit) <= 0) {
continue;
}
defenderUnits.push(unit);
@@ -309,15 +309,15 @@ export const resolveWarBattle = <
if (
defenderGenerals.length > 0 &&
extractBattleOrder(cityUnit, attackerUnit) > 0
computeBattleOrder(cityUnit, attackerUnit) > 0
) {
defenderUnits.push(cityUnit);
}
defenderUnits.sort(
(lhs, rhs) =>
extractBattleOrder(rhs, attackerUnit) -
extractBattleOrder(lhs, attackerUnit)
computeBattleOrder(rhs, attackerUnit) -
computeBattleOrder(lhs, attackerUnit)
);
const iter = defenderUnits.values();
@@ -335,7 +335,7 @@ export const resolveWarBattle = <
return null;
}
const candidate = next.value as WarUnit<TriggerState>;
if (extractBattleOrder(candidate, attackerUnit) <= 0) {
if (computeBattleOrder(candidate, attackerUnit) <= 0) {
return null;
}
return candidate;
@@ -641,5 +641,79 @@ export const resolveWarBattle = <
logs,
conquered: conquerCity,
reports,
metrics: {
attackerPhase: attackerUnit.getPhase(),
attackerActivatedSkills: attackerUnit.getActivatedSkillLog(),
defenderActivatedSkills: defenderUnits.map((unit) =>
unit.getActivatedSkillLog()
),
},
};
};
export const resolveDefenderOrder = <
TriggerState extends GeneralTriggerState = GeneralTriggerState
>(
input: WarBattleInput<TriggerState>
): number[] => {
const rng =
input.rng ??
new RandUtil(
LiteHashDRBG.build(input.seed ?? '')
);
const loggerFactory = input.loggerFactory ?? defaultLoggerFactory;
const crewTypeIndex = buildWarCrewTypeIndex(input.unitSet);
const attackerPipeline = createPipeline(input.attacker);
const attackerLogger =
input.attacker.logger ??
loggerFactory({
generalId: input.attacker.general.id,
nationId: input.attacker.general.nationId,
});
const attackerUnit = new WarUnitGeneral(
rng,
input.config,
input.attacker.general,
input.attacker.city,
input.attacker.nation,
true,
resolveCrewType(crewTypeIndex, input.attacker.general.crewTypeId),
attackerLogger,
attackerPipeline
);
const defenderUnits: WarUnitGeneral<TriggerState>[] = [];
for (const defender of input.defenders) {
const defenderLogger =
defender.logger ??
loggerFactory({
generalId: defender.general.id,
nationId: defender.general.nationId,
});
const unit = new WarUnitGeneral(
rng,
input.config,
defender.general,
input.defenderCity,
defender.nation,
false,
resolveCrewType(crewTypeIndex, defender.general.crewTypeId),
defenderLogger,
createPipeline(defender)
);
if (computeBattleOrder(unit, attackerUnit) <= 0) {
continue;
}
defenderUnits.push(unit);
}
defenderUnits.sort(
(lhs, rhs) =>
computeBattleOrder(rhs, attackerUnit) -
computeBattleOrder(lhs, attackerUnit)
);
return defenderUnits.map((unit) => unit.getGeneral().id);
};
+7
View File
@@ -73,6 +73,12 @@ export interface WarUnitReport {
dead: number;
}
export interface WarBattleMetrics {
attackerPhase: number;
attackerActivatedSkills: Record<string, number>;
defenderActivatedSkills: Array<Record<string, number>>;
}
export interface WarBattleOutcome<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
@@ -82,6 +88,7 @@ export interface WarBattleOutcome<
logs: LogEntryDraft[];
conquered: boolean;
reports: WarUnitReport[];
metrics?: WarBattleMetrics;
}
export interface WarAftermathConfig {
+11
View File
@@ -237,6 +237,17 @@ export abstract class WarUnit<
(this.hasActivatedSkill(skillName) ? 1 : 0);
}
public getActivatedSkillLog(): Record<string, number> {
const snapshot: Record<string, number> = { ...this.logActivatedSkill };
for (const [skillName, active] of Object.entries(this.activatedSkill)) {
if (!active) {
continue;
}
snapshot[skillName] = (snapshot[skillName] ?? 0) + 1;
}
return snapshot;
}
public activateSkill(...skillNames: string[]): void {
for (const skillName of skillNames) {
this.activatedSkill[skillName] = true;