109 lines
5.3 KiB
JavaScript
109 lines
5.3 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { BUILDS, makePayload, buildPlan, summarize, observe } from './design.mjs';
|
|
|
|
const unitSet = JSON.parse(
|
|
await readFile(new URL('../../resources/unitset/unitset_che.json', import.meta.url), 'utf8')
|
|
);
|
|
const cell = {
|
|
unit: 1100,
|
|
opponent: 1200,
|
|
build: 'martial',
|
|
role: 'attack',
|
|
mode: 'field',
|
|
generalPatch: { weapon: 'test-item' },
|
|
};
|
|
|
|
test('allocation preserves total stat budget and caps troops by leadership', () => {
|
|
for (const [build, stats] of Object.entries(BUILDS)) {
|
|
assert.equal(
|
|
Object.values(stats).reduce((a, b) => a + b),
|
|
210
|
|
);
|
|
const payload = makePayload(unitSet, { ...cell, build }, 's');
|
|
assert.ok(payload.attackerGeneral.crew <= stats.leadership * 100);
|
|
}
|
|
});
|
|
test('treatment changes only the subject and paired control removes it in both roles', () => {
|
|
for (const role of ['attack', 'defend']) {
|
|
const treatment = makePayload(unitSet, { ...cell, role, nationTrait: 'che_병가' }, 's');
|
|
const control = makePayload(unitSet, { ...cell, role, nationTrait: 'che_병가' }, 's', false);
|
|
const own = role === 'attack' ? treatment.attackerGeneral : treatment.defenderGenerals[0];
|
|
const enemy = role === 'attack' ? treatment.defenderGenerals[0] : treatment.attackerGeneral;
|
|
assert.equal(own.crewtype, cell.unit);
|
|
assert.equal(enemy.crewtype, cell.opponent);
|
|
assert.equal(own.weapon, 'test-item');
|
|
assert.equal(enemy.weapon, null);
|
|
own.weapon = null;
|
|
treatment[role === 'attack' ? 'attackerNation' : 'defenderNation'].type = 'che_중립';
|
|
assert.deepEqual(treatment, control);
|
|
}
|
|
});
|
|
test('field and siege are separate and economic budget respects expensive units', () => {
|
|
assert.equal(makePayload(unitSet, cell, 's').defenderCity.wall, 0);
|
|
const siege = makePayload(unitSet, { ...cell, mode: 'siege' }, 's');
|
|
assert.equal(siege.defenderGenerals.length, 0);
|
|
assert.equal(siege.defenderCity.wall, 1000);
|
|
const expensive = makePayload(unitSet, { ...cell, unit: 1503, budget: 'equal-gold' }, 's');
|
|
assert.ok(expensive.attackerGeneral.crew < 7000);
|
|
assert.throws(() => makePayload(unitSet, { ...cell, mode: 'siege', role: 'defend' }, 's'));
|
|
});
|
|
test('observer excludes wall damage from field exchange and preserves unknown siege exchange', () => {
|
|
const payload = makePayload(unitSet, cell, 's');
|
|
const outcome = {
|
|
attacker: { crew: 6000, rice: 99900, injury: 0 },
|
|
defenders: [{ crew: 5000 }],
|
|
conquered: false,
|
|
reports: [
|
|
{ id: 1, type: 'general', dead: 1000, killed: 9000 },
|
|
{ id: 2, type: 'general', dead: 2000 },
|
|
{ id: null, type: 'city', dead: 7000 },
|
|
],
|
|
};
|
|
assert.equal(observe(outcome, payload, 'attack').fieldKills, 2000);
|
|
assert.equal(observe(outcome, payload, 'attack').exchange, 1000 / 7000);
|
|
const siege = makePayload(unitSet, { ...cell, mode: 'siege' }, 's');
|
|
assert.equal(observe({ ...outcome, defenders: [] }, siege, 'attack').exchange, null);
|
|
});
|
|
test('statistics expose n, interval and tails; invalid samples fail closed', () => {
|
|
assert.deepEqual(summarize([2, 2]).ci95, [2, 2]);
|
|
assert.equal(summarize([1, 2, 3]).mean, 2);
|
|
assert.equal(summarize([1]).ci95, null);
|
|
assert.throws(() => summarize([]));
|
|
assert.throws(() => summarize([NaN]));
|
|
});
|
|
test('design covers every registered unit, both budgets, four builds, roles and siege', () => {
|
|
const catalog = { units: unitSet.crewTypes.filter((unit) => unit.armType > 0) };
|
|
const plan = buildPlan(catalog, 'units');
|
|
assert.equal(plan.length, catalog.units.length * 4 * 2 * 12);
|
|
assert.equal(new Set(plan.map((cell) => cell.id)).size, plan.length);
|
|
assert.throws(() => buildPlan(catalog, 'no-such-suite'));
|
|
});
|
|
|
|
test('native family and tier comparisons use matched total stats and directly compare tier-three units', () => {
|
|
const catalog = { units: unitSet.crewTypes.filter((unit) => unit.armType > 0) };
|
|
const families = buildPlan(catalog, 'families');
|
|
assert.equal(families.length, 5 * 5 * 3 * 2);
|
|
const pair = families.find((row) => row.unit === 1400 && row.opponent === 1100 && row.role === 'attack');
|
|
const payload = makePayload(unitSet, pair, 's');
|
|
assert.equal(payload.attackerGeneral.intel, 90);
|
|
assert.equal(payload.defenderGenerals[0].strength, 90);
|
|
const capacity = makePayload(unitSet, { ...pair, unit: 1500, build: 'command', budget: 'capacity' }, 's');
|
|
assert.equal(capacity.attackerGeneral.crew, 15000);
|
|
const tiers = buildPlan(catalog, 'tiers');
|
|
assert.ok(tiers.some((row) => row.unit === 1502 && row.opponent === 1503));
|
|
assert.ok(tiers.every((row) => ![1100, 1306].includes(row.unit)));
|
|
});
|
|
|
|
test('depleted line encounters preserve defender ownership and vary only subject readiness', () => {
|
|
const payload = makePayload(unitSet, { ...cell, condition: 'depleted', mode: 'line' }, 's');
|
|
assert.equal(payload.attackerGeneral.crew, 3500);
|
|
assert.equal(payload.attackerGeneral.train, 70);
|
|
assert.deepEqual(
|
|
payload.defenderGenerals.map((row) => row.no),
|
|
[2, 3, 4]
|
|
);
|
|
assert.ok(payload.defenderGenerals.every((row) => row.nation === 2 && row.city === 2 && row.train === 100));
|
|
});
|