보급 차단 시점 계약을 회귀 테스트로 고정한다
출병 점령 그 순간에는 보급이 즉시 끊기지 않고(che_출병/aftermath는 점령 도시와 긴급천도 신수도에만 supply=1을 쓴다), 차단(切断)은 월 경계의 UpdateCitySupply(pre_month)에서 일어난다는 Ref 계승 계약을 테스트로 고정한다. 월 경계 연감 스냅샷은 Ref #230(d17b2518) 이후 UpdateCitySupply 뒤에 찍히므로 끝난 달의 연감 지도에는 이미 고립이 반영된다. 10월중 점령 -> 11월 경계 차단 -> 10월 연감 반영 순서를 검증한다.
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { City, General, Nation } from '../src/domain/entities.js';
|
||||
import { resolveGeneralAction } from '../src/actions/engine.js';
|
||||
import { ActionDefinition } from '../src/actions/turn/general/che_출병.js';
|
||||
import type { DispatchResolveContext } from '../src/actions/turn/general/che_출병.js';
|
||||
import type { TurnSchedule } from '../src/turn/calendar.js';
|
||||
import type { WarAftermathConfig, WarEngineConfig } from '../src/war/types.js';
|
||||
import type { MapDefinition, UnitSetDefinition } from '../src/world/types.js';
|
||||
|
||||
// Ref 계승 계약: city.supply 재계산은 월 경계의 UpdateCitySupply(pre_month)에서만
|
||||
// 일어나고, 출병 점령 순간에는 점령 도시와 긴급천도 신수도에만 supply=1을 쓴다.
|
||||
// (ref TurnExecutionHelper.php PreMonth→preUpdateMonthly 순서, process_war.php의
|
||||
// 'supply' => 1 쓰기 두 곳) 월중에는 다른 도시의 supply가 절대 0으로 바뀌면 안 된다.
|
||||
|
||||
const buildGeneral = (id: number, nationId: number, cityId: number): General => ({
|
||||
id,
|
||||
name: `General${id}`,
|
||||
nationId,
|
||||
cityId,
|
||||
troopId: 0,
|
||||
stats: { leadership: 70, strength: 70, intelligence: 70 },
|
||||
experience: 100,
|
||||
dedication: 100,
|
||||
officerLevel: 3,
|
||||
role: {
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
items: { horse: null, weapon: null, book: null, item: null },
|
||||
},
|
||||
injury: 0,
|
||||
gold: 1000,
|
||||
rice: 2000,
|
||||
crew: 1500,
|
||||
crewTypeId: 100,
|
||||
train: 80,
|
||||
atmos: 80,
|
||||
age: 25,
|
||||
npcState: 0,
|
||||
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||
meta: { killturn: 24 },
|
||||
});
|
||||
|
||||
const buildCity = (id: number, nationId: number): City => ({
|
||||
id,
|
||||
name: `City${id}`,
|
||||
nationId,
|
||||
level: 2,
|
||||
state: 0,
|
||||
population: 10000,
|
||||
populationMax: 10000,
|
||||
agriculture: 1000,
|
||||
agricultureMax: 1000,
|
||||
commerce: 1000,
|
||||
commerceMax: 1000,
|
||||
security: 1000,
|
||||
securityMax: 1000,
|
||||
supplyState: 1,
|
||||
frontState: 0,
|
||||
defence: 200,
|
||||
defenceMax: 400,
|
||||
wall: 200,
|
||||
wallMax: 400,
|
||||
meta: {},
|
||||
});
|
||||
|
||||
const buildNation = (id: number, capitalCityId: number): Nation => ({
|
||||
id,
|
||||
name: `Nation${id}`,
|
||||
color: '#000000',
|
||||
capitalCityId,
|
||||
chiefGeneralId: null,
|
||||
gold: 5000,
|
||||
rice: 5000,
|
||||
power: 0,
|
||||
level: 1,
|
||||
typeCode: 'test',
|
||||
meta: { tech: 1000 },
|
||||
});
|
||||
|
||||
const unitSet: UnitSetDefinition = {
|
||||
id: 'test',
|
||||
name: 'test',
|
||||
defaultCrewTypeId: 100,
|
||||
crewTypes: [
|
||||
{
|
||||
id: 100,
|
||||
armType: 1,
|
||||
name: 'Infantry',
|
||||
attack: 10,
|
||||
defence: 10,
|
||||
speed: 3,
|
||||
avoid: 5,
|
||||
magicCoef: 0,
|
||||
cost: 0,
|
||||
rice: 1,
|
||||
requirements: [],
|
||||
attackCoef: {},
|
||||
defenceCoef: {},
|
||||
info: [],
|
||||
initSkillTrigger: null,
|
||||
phaseSkillTrigger: null,
|
||||
iActionList: null,
|
||||
},
|
||||
{
|
||||
id: 999,
|
||||
armType: 5,
|
||||
name: 'Castle',
|
||||
attack: 0,
|
||||
defence: 0,
|
||||
speed: 1,
|
||||
avoid: 0,
|
||||
magicCoef: 0,
|
||||
cost: 0,
|
||||
rice: 10,
|
||||
requirements: [{ type: 'Impossible' }],
|
||||
attackCoef: {},
|
||||
defenceCoef: {},
|
||||
info: [],
|
||||
initSkillTrigger: null,
|
||||
phaseSkillTrigger: null,
|
||||
iActionList: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const warConfig: WarEngineConfig = {
|
||||
armPerPhase: 500,
|
||||
maxTrainByCommand: 100,
|
||||
maxAtmosByCommand: 100,
|
||||
maxTrainByWar: 110,
|
||||
maxAtmosByWar: 150,
|
||||
castleCrewTypeId: 999,
|
||||
armTypes: {
|
||||
footman: 1,
|
||||
archer: 2,
|
||||
cavalry: 3,
|
||||
wizard: 4,
|
||||
siege: 5,
|
||||
misc: 6,
|
||||
castle: 5,
|
||||
},
|
||||
};
|
||||
|
||||
const aftermathConfig: WarAftermathConfig = {
|
||||
initialNationGenLimit: 1,
|
||||
techLevelIncYear: 5,
|
||||
initialAllowedTechLevel: 1,
|
||||
maxTechLevel: 12,
|
||||
defaultCityWall: 1000,
|
||||
baseGold: 0,
|
||||
baseRice: 0,
|
||||
castleCrewTypeId: 999,
|
||||
};
|
||||
|
||||
const rng = {
|
||||
nextFloat1: () => 0.1,
|
||||
nextBool: (probability: number) => probability >= 0.1,
|
||||
nextInt: (minInclusive: number, _maxExclusive: number) => minInclusive,
|
||||
};
|
||||
|
||||
const schedule: TurnSchedule = {
|
||||
entries: [{ startMinute: 0, tickMinutes: 60 }],
|
||||
};
|
||||
|
||||
// 수도 21 - 중간도시 20 - 외곽도시 22 사슬에서 20만 국가1 영토였고,
|
||||
// 공격자 국가2의 도시 10이 20과 인접한다. 20을 먹히면 22는 월 경계 이후에
|
||||
// 고립되지만, 점령 그 순간에는 아니다.
|
||||
const mapCities: MapDefinition['cities'] = [
|
||||
{ id: 10, name: 'AggBase', level: 2, region: 1, position: { x: 0, y: 0 }, connections: [20] },
|
||||
{ id: 20, name: 'Mid', level: 2, region: 1, position: { x: 1, y: 0 }, connections: [10, 21, 22] },
|
||||
{ id: 21, name: 'Cap', level: 2, region: 1, position: { x: 2, y: 0 }, connections: [20] },
|
||||
{ id: 22, name: 'Far', level: 2, region: 1, position: { x: 3, y: 0 }, connections: [20] },
|
||||
].map((city) => ({
|
||||
...city,
|
||||
max: { population: 10000, agriculture: 1000, commerce: 1000, security: 1000, defence: 400, wall: 400 },
|
||||
initial: { population: 10000, agriculture: 1000, commerce: 1000, security: 1000, defence: 200, wall: 200 },
|
||||
}));
|
||||
|
||||
describe('출병 점령과 보급切断 시점', () => {
|
||||
it('does not cut any other city supply in the same turn the route city is conquered', () => {
|
||||
const defenderNation = buildNation(1, 21);
|
||||
const attackerNation = buildNation(2, 10);
|
||||
const aggBase = buildCity(10, attackerNation.id);
|
||||
const midCity = buildCity(20, defenderNation.id);
|
||||
const capCity = buildCity(21, defenderNation.id);
|
||||
const farCity = buildCity(22, defenderNation.id);
|
||||
midCity.defence = 0;
|
||||
midCity.wall = 0;
|
||||
const attacker = buildGeneral(1, attackerNation.id, aggBase.id);
|
||||
attacker.turnTime = new Date('2000-01-01T00:00:00Z');
|
||||
|
||||
const context: Omit<DispatchResolveContext, 'addLog'> = {
|
||||
general: attacker,
|
||||
city: aggBase,
|
||||
nation: attackerNation,
|
||||
rng,
|
||||
destCity: midCity,
|
||||
destNation: defenderNation,
|
||||
cities: [aggBase, midCity, capCity, farCity],
|
||||
nations: [attackerNation, defenderNation],
|
||||
generals: [attacker],
|
||||
unitSet,
|
||||
map: { id: 'supply-chain', name: 'supply-chain', cities: mapCities },
|
||||
diplomacy: [
|
||||
{ fromNationId: attackerNation.id, toNationId: defenderNation.id, state: 0, term: 0 },
|
||||
{ fromNationId: defenderNation.id, toNationId: attackerNation.id, state: 0, term: 0 },
|
||||
],
|
||||
time: { year: 191, month: 10, startYear: 180 },
|
||||
seedBase: 'supply-cut-seed',
|
||||
warConfig,
|
||||
aftermathConfig,
|
||||
messageTime: new Date('2000-01-01T00:00:00.000Z'),
|
||||
};
|
||||
|
||||
const resolution = resolveGeneralAction(
|
||||
new ActionDefinition(),
|
||||
context,
|
||||
{ now: new Date('2000-01-01T00:00:00Z'), schedule },
|
||||
{ destCityId: midCity.id }
|
||||
);
|
||||
|
||||
// 점령 자체가 일어나야 테스트 의미가 있다.
|
||||
expect(resolution.completed).toBe(true);
|
||||
|
||||
const cityPatches = resolution.patches?.cities ?? [];
|
||||
|
||||
// 점령 도시는 소유권 이동 + Ref의 'supply' => 1 재설정만 받는다.
|
||||
const midPatch = cityPatches.find((patch) => patch.id === midCity.id);
|
||||
expect(midPatch?.patch.nationId).toBe(attackerNation.id);
|
||||
expect(midPatch?.patch.supplyState).toBe(1);
|
||||
|
||||
// 월중에는 그 어떤 도시도 supply=0으로 패치되지 않는다.
|
||||
// (Ref: process_war.php는 점령 도시와 천도 신수도에만 supply=1을 쓴다.)
|
||||
expect(
|
||||
cityPatches.filter((patch) => 'supplyState' in patch.patch && patch.patch.supplyState === 0)
|
||||
).toEqual([]);
|
||||
|
||||
// 고립될 외곽도시와 수도는 이 명령 처리 중에 손대지 않는다.
|
||||
expect(farCity.supplyState).toBe(1);
|
||||
expect(capCity.supplyState).toBe(1);
|
||||
expect(cityPatches.some((patch) => patch.id === farCity.id && 'supplyState' in patch.patch)).toBe(false);
|
||||
expect(cityPatches.some((patch) => patch.id === capCity.id && 'supplyState' in patch.patch)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user