200 lines
9.1 KiB
JavaScript
200 lines
9.1 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import * as logic from '../../packages/logic/dist/index.js';
|
|
import * as common from '../../packages/common/dist/index.js';
|
|
import { CommandResolver } from '../../packages/logic/dist/src/actions/turn/general/che_징병.js';
|
|
import { CommandResolver as VolunteerResolver } from '../../packages/logic/dist/src/actions/turn/nation/che_의병모집.js';
|
|
import { economicTools, lifecycleProfiles, nationPlans, strategyPlans } from './economy.mjs';
|
|
import { makePayload, observe } from './design.mjs';
|
|
import { buildFollowupPlan } from './followup.mjs';
|
|
|
|
const unitSet = logic.parseUnitSetDefinition(
|
|
JSON.parse(await readFile(new URL('../../resources/unitset/unitset_che.json', import.meta.url), 'utf8'))
|
|
);
|
|
const items = await logic.loadItemModules([...logic.ITEM_KEYS]);
|
|
const war = await logic.loadWarTraitModules([...logic.WAR_TRAIT_KEYS]);
|
|
const nations = await logic.loadNationTraitModules([...logic.NATION_TRAIT_KEYS]);
|
|
const economics = economicTools(logic, CommandResolver, items, war, nations);
|
|
const cell = { unit: 1100, opponent: 1100, build: 'martial', role: 'attack', mode: 'field' };
|
|
const catalog = { items: items.filter((item) => item.unique), war, nations };
|
|
|
|
test('conditional design schedules every loaded unique and war trait against both regional opponents', () => {
|
|
const plan = buildFollowupPlan(catalog, 'conditional');
|
|
for (const module of [...catalog.items, ...catalog.war]) {
|
|
const rows = plan.filter((row) => row.key === module.key);
|
|
assert.equal(rows.length, 20);
|
|
assert.deepEqual([...new Set(rows.map((row) => row.opponent))].sort(), [1104, 1407]);
|
|
}
|
|
});
|
|
|
|
test('premium control equips the same slot on the subject in both roles', () => {
|
|
const plan = buildFollowupPlan(catalog, 'premium');
|
|
for (const cell of plan) {
|
|
const payload = makePayload(unitSet, cell, 'test', false);
|
|
const subject = cell.role === 'attack' ? payload.attackerGeneral : payload.defenderGenerals[0];
|
|
for (const [slot, key] of Object.entries(cell.controlPatch)) assert.equal(subject[slot], key);
|
|
}
|
|
assert.ok(plan.some((row) => row.baseline === 'che_명마_06_흑색마'));
|
|
assert.ok(plan.some((row) => row.baseline === 'che_훈련_청주'));
|
|
});
|
|
|
|
test('regional effect is inactive on basic troops and changes the real regional battle', () => {
|
|
const run = (opponent, treatment) => {
|
|
const payload = makePayload(
|
|
unitSet,
|
|
{ ...cell, opponent, generalPatch: { special2: 'che_척사' } },
|
|
'regional-test',
|
|
treatment
|
|
);
|
|
let result;
|
|
logic.processBattleSimJob(payload, {
|
|
onBattleResolved: (outcome) => {
|
|
result = observe(outcome, payload, 'attack');
|
|
},
|
|
});
|
|
return result;
|
|
};
|
|
assert.deepEqual(run(1100, true), run(1100, false));
|
|
assert.ok(run(1104, true).exchange > run(1104, false).exchange);
|
|
});
|
|
|
|
test('production recruitment caps money and increases capacity/readiness without population for recruiting trait', () => {
|
|
const payload = makePayload(unitSet, cell, 'test');
|
|
const unit = unitSet.crewTypes.find((entry) => entry.id === 1100);
|
|
const base = economics.quote(payload.attackerGeneral, payload.attackerNation, payload.time, unit);
|
|
const trait = economics.quote(
|
|
{ ...payload.attackerGeneral, special2: 'che_징병' },
|
|
payload.attackerNation,
|
|
payload.time,
|
|
unit
|
|
);
|
|
assert.equal(base.capacity, 9000);
|
|
assert.equal(trait.capacity, 11200); // Production integer stat truncation, not 11250.
|
|
assert.equal(base.train, 40);
|
|
assert.equal(trait.train, 70);
|
|
assert.equal(base.population, base.crew);
|
|
assert.equal(trait.population, 0);
|
|
assert.equal(trait.crew, 11200);
|
|
const poor = economics.quote(payload.attackerGeneral, payload.attackerNation, payload.time, unit, { gold: 100 });
|
|
assert.ok(poor.gold <= 100 && poor.crew < base.crew);
|
|
const empty = economics.quote(payload.attackerGeneral, payload.attackerNation, payload.time, unit, { gold: 0 });
|
|
assert.equal(empty.crew, 0);
|
|
});
|
|
|
|
test('recruited battle inputs pay quote costs and separate fresh from trained troops', () => {
|
|
const design = {
|
|
...cell,
|
|
budget: 'recruited',
|
|
wallet: 1500,
|
|
quality: 'fresh',
|
|
generalPatch: { special2: 'che_징병' },
|
|
};
|
|
const payload = makePayload(unitSet, design, 'test');
|
|
const cost = economics.recruit(payload, design);
|
|
assert.equal(payload.attackerGeneral.gold + cost.gold, 1500);
|
|
assert.equal(payload.attackerGeneral.rice + cost.rice, 5000);
|
|
assert.equal(payload.attackerGeneral.crew, cost.crew);
|
|
assert.equal(payload.attackerGeneral.train, 70);
|
|
assert.equal(payload.defenderGenerals[0].train, 40);
|
|
});
|
|
|
|
test('lifecycle probes observe sale resources and healing allies and enemies', () => {
|
|
const payload = makePayload(unitSet, cell, 'test');
|
|
const rows = lifecycleProfiles(
|
|
logic,
|
|
common,
|
|
payload,
|
|
items.filter((item) => ['che_보물_도기', 'che_의술_청낭서'].includes(item.key)),
|
|
'lifecycle-test',
|
|
128
|
|
);
|
|
const sale = rows.find((row) => row.kind === 'sale-bonus' && row.year === 200);
|
|
assert.equal(sale.personalGain, 30000);
|
|
assert.equal(sale.nationGain, 30000);
|
|
const heal = rows.find((row) => row.kind === 'pre-turn-heal');
|
|
assert.equal(heal.self, 1);
|
|
assert.ok(heal.ally > 0 && heal.ally < 1 && heal.enemy > 0 && heal.enemy < 1);
|
|
});
|
|
|
|
test('national planning exposes labour cap and individual strategy cooldown bottleneck', () => {
|
|
const profile = {
|
|
key: 'neutral',
|
|
domestic: Object.fromEntries(
|
|
['농업', '상업', '기술', '수비', '성벽', '치안', '민심', '인구'].map((action) => [
|
|
action,
|
|
{ score: 100, cost: 100 },
|
|
])
|
|
),
|
|
income: { gold: 100, rice: 100, pop: 100 },
|
|
};
|
|
const plans = nationPlans([profile]).filter((row) => row.role === 'development');
|
|
assert.deepEqual(
|
|
plans.map((row) => row.turns),
|
|
[6, 12, 12]
|
|
);
|
|
const payload = makePayload(unitSet, cell, 'test');
|
|
const strategy = strategyPlans(VolunteerResolver, payload, nations, items);
|
|
const neutral = strategy.find((row) => row.key === 'che_중립' && row.generals === 10 && !row.equipped);
|
|
const diplomat = strategy.find((row) => row.key === 'che_종횡가' && row.generals === 10 && !row.equipped);
|
|
assert.equal(neutral.delay, 100);
|
|
assert.equal(diplomat.delay, 75);
|
|
assert.equal(diplomat.usesWithResources, 4);
|
|
assert.equal(neutral.usesWithResources, 3);
|
|
});
|
|
|
|
test('attrition starts at the same capacity fraction, preserves full payment, and leaves opponents full', () => {
|
|
const plan = buildFollowupPlan(catalog, 'attrition');
|
|
assert.equal(plan.length, 240);
|
|
for (const role of ['attack', 'defend']) {
|
|
const cell = plan.find(
|
|
(row) =>
|
|
row.key === 'che_명마_12_옥란백용구' &&
|
|
row.unit === 1501 &&
|
|
row.crewFraction === 0.25 &&
|
|
row.role === role
|
|
);
|
|
const payload = makePayload(unitSet, cell, 'attrition-test');
|
|
const report = economics.recruit(payload, cell);
|
|
const subject = role === 'attack' ? payload.attackerGeneral : payload.defenderGenerals[0];
|
|
const opponent = role === 'attack' ? payload.defenderGenerals[0] : payload.attackerGeneral;
|
|
assert.equal(report.crew, 16200);
|
|
assert.equal(subject.crew, 4050);
|
|
assert.equal(report.remainingBeforeBattle, 4050);
|
|
assert.equal(opponent.crew, 9000);
|
|
assert.equal(subject.gold + report.gold, cell.wallet);
|
|
assert.equal(subject.train, 100);
|
|
}
|
|
});
|
|
|
|
test('counter design patches only the opposing trait and suppression is a true no-op without it', () => {
|
|
const plan = buildFollowupPlan(catalog, 'counter');
|
|
assert.equal(plan.length, 120);
|
|
for (const role of ['attack', 'defend']) {
|
|
const cell = plan.find(
|
|
(row) => row.unit === 1400 && row.opponent === 1400 && row.role === role && row.condition === 'che_격노'
|
|
);
|
|
const payload = makePayload(unitSet, cell, 'counter-test');
|
|
const subject = role === 'attack' ? payload.attackerGeneral : payload.defenderGenerals[0];
|
|
const opponent = role === 'attack' ? payload.defenderGenerals[0] : payload.attackerGeneral;
|
|
assert.equal(subject.item, 'che_진압_박혁론');
|
|
assert.equal(subject.special2, null);
|
|
assert.equal(opponent.special2, 'che_격노');
|
|
}
|
|
const control = plan.find(
|
|
(row) =>
|
|
row.unit === 1400 && row.opponent === 1400 && row.role === 'attack' && row.condition === 'no-opponent-trait'
|
|
);
|
|
const outcomes = [true, false].map((treatment) => {
|
|
const payload = makePayload(unitSet, control, 'counter-test', treatment);
|
|
let result;
|
|
logic.processBattleSimJob(payload, {
|
|
onBattleResolved: (outcome) => {
|
|
result = observe(outcome, payload, 'attack');
|
|
},
|
|
});
|
|
return result;
|
|
});
|
|
assert.deepEqual(outcomes[0], outcomes[1]);
|
|
});
|