건국 커맨드 구현 및 테스트 추가
This commit is contained in:
@@ -4,6 +4,7 @@ import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn
|
||||
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
||||
import { InMemoryReservedTurnStore } from '../src/turn/reservedTurnStore.js';
|
||||
import { createReservedTurnHandler } from '../src/turn/reservedTurnHandler.js';
|
||||
import { InMemoryTurnProcessor } from '../src/turn/inMemoryTurnProcessor.js';
|
||||
// Inline MINIMAL_MAP to avoid cross-package relative import issues
|
||||
const MINIMAL_MAP = {
|
||||
id: 'minimal_map',
|
||||
@@ -320,4 +321,306 @@ describe('Reserved Turn Execution Integration', () => {
|
||||
// Turn 2 exec -> Shift -1
|
||||
// Turns should indeed be empty/default now.
|
||||
});
|
||||
|
||||
describe('Uprising and Founding Execution', () => {
|
||||
it('should execute uprising, fail founding in wrong city, fail domestic in wandering nation, then succeed founding and domestic', async () => {
|
||||
const mockDate = new Date('0189-01-01T00:00:00Z');
|
||||
// 1. Setup World Data
|
||||
const generals: TurnGeneral[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'General_Leader',
|
||||
nationId: 0,
|
||||
cityId: 1,
|
||||
troopId: 0,
|
||||
stats: { leadership: 80, strength: 80, intelligence: 80 },
|
||||
turnTime: mockDate,
|
||||
role: {
|
||||
items: { horse: null, weapon: null, book: null, item: null },
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
},
|
||||
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||
meta: {},
|
||||
officerLevel: 5,
|
||||
experience: 0,
|
||||
dedication: 0,
|
||||
injury: 0,
|
||||
gold: 2000,
|
||||
rice: 2000,
|
||||
crew: 0,
|
||||
crewTypeId: 0,
|
||||
train: 0,
|
||||
atmos: 0,
|
||||
age: 30,
|
||||
npcState: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'General_Sub',
|
||||
nationId: 0,
|
||||
cityId: 1,
|
||||
troopId: 0,
|
||||
stats: { leadership: 70, strength: 70, intelligence: 70 },
|
||||
turnTime: mockDate,
|
||||
role: {
|
||||
items: { horse: null, weapon: null, book: null, item: null },
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
},
|
||||
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||
meta: {},
|
||||
officerLevel: 5,
|
||||
experience: 0,
|
||||
dedication: 0,
|
||||
injury: 0,
|
||||
gold: 2000,
|
||||
rice: 2000,
|
||||
crew: 0,
|
||||
crewTypeId: 0,
|
||||
train: 0,
|
||||
atmos: 0,
|
||||
age: 30,
|
||||
npcState: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const cities = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Small_City',
|
||||
nationId: 0,
|
||||
viewName: 'Small_City',
|
||||
agriculture: 100,
|
||||
agricultureMax: 2000,
|
||||
commerce: 100,
|
||||
commerceMax: 2000,
|
||||
security: 100,
|
||||
securityMax: 100,
|
||||
def: 100,
|
||||
defMax: 100,
|
||||
wall: 100,
|
||||
wallMax: 100,
|
||||
pop: 10000,
|
||||
popMax: 50000,
|
||||
trust: 50,
|
||||
supplyState: 1,
|
||||
frontState: 0,
|
||||
tradepoint: 0,
|
||||
level: 1, // Invalid for founding
|
||||
meta: {},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Big_City',
|
||||
nationId: 0,
|
||||
viewName: 'Big_City',
|
||||
agriculture: 100,
|
||||
agricultureMax: 3000,
|
||||
commerce: 100,
|
||||
commerceMax: 3000,
|
||||
security: 100,
|
||||
securityMax: 100,
|
||||
def: 100,
|
||||
defMax: 100,
|
||||
wall: 100,
|
||||
wallMax: 100,
|
||||
pop: 20000,
|
||||
popMax: 80000,
|
||||
trust: 50,
|
||||
supplyState: 1,
|
||||
frontState: 0,
|
||||
tradepoint: 0,
|
||||
level: 5, // Valid for founding
|
||||
meta: {},
|
||||
},
|
||||
];
|
||||
|
||||
const localMap = {
|
||||
id: 'local_map',
|
||||
name: 'TestMap',
|
||||
cities: [
|
||||
{ id: 1, connections: [2] },
|
||||
{ id: 2, connections: [1] },
|
||||
] as any,
|
||||
};
|
||||
|
||||
const snapshot: TurnWorldSnapshot = {
|
||||
generals: generals as any,
|
||||
cities: cities as any,
|
||||
nations: [],
|
||||
troops: [],
|
||||
diplomacy: [],
|
||||
events: [],
|
||||
initialEvents: [],
|
||||
map: localMap,
|
||||
scenarioConfig: {
|
||||
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
||||
iconPath: '',
|
||||
map: {},
|
||||
const: { openingPartYear: 9999 },
|
||||
environment: { mapName: 'local', unitSet: 'default' },
|
||||
},
|
||||
scenarioMeta: {
|
||||
startYear: 189,
|
||||
} as any,
|
||||
unitSet: {} as any,
|
||||
};
|
||||
|
||||
const state: TurnWorldState = {
|
||||
id: 1,
|
||||
currentYear: 189,
|
||||
currentMonth: 1,
|
||||
tickSeconds: 600,
|
||||
lastTurnTime: mockDate,
|
||||
meta: {},
|
||||
};
|
||||
|
||||
const initialRows = [
|
||||
// Gen 1: Uprising -> Founding (Fail) -> Move -> Founding (Success)
|
||||
{ generalId: 1, turnIdx: 0, actionCode: 'che_거병', arg: {} },
|
||||
{
|
||||
generalId: 1,
|
||||
turnIdx: 1,
|
||||
actionCode: 'che_건국',
|
||||
arg: { nationName: 'NewEmpire', nationType: 'che_def', colorType: 0 },
|
||||
},
|
||||
{ generalId: 1, turnIdx: 2, actionCode: 'che_이동', arg: { destCityId: 2 } },
|
||||
{
|
||||
generalId: 1,
|
||||
turnIdx: 3,
|
||||
actionCode: 'che_건국',
|
||||
arg: { nationName: 'NewEmpire', nationType: 'che_def', colorType: 0 },
|
||||
},
|
||||
|
||||
// Gen 2: Rest -> Domestic (Fail) -> Rest -> Rest -> Domestic (Success)
|
||||
{ generalId: 2, turnIdx: 0, actionCode: '휴식', arg: {} },
|
||||
{ generalId: 2, turnIdx: 1, actionCode: 'che_농지개간', arg: {} },
|
||||
{ generalId: 2, turnIdx: 2, actionCode: '휴식', arg: {} },
|
||||
{ generalId: 2, turnIdx: 3, actionCode: '휴식', arg: {} },
|
||||
{ generalId: 2, turnIdx: 4, actionCode: 'che_농지개간', arg: {} },
|
||||
];
|
||||
|
||||
const mockPrisma = createMockPrisma(initialRows);
|
||||
const reservedTurnStore = new InMemoryReservedTurnStore(mockPrisma as any, {
|
||||
maxGeneralTurns: 10,
|
||||
maxNationTurns: 10,
|
||||
});
|
||||
await reservedTurnStore.loadAll();
|
||||
|
||||
const wrapper = { world: null as InMemoryTurnWorld | null };
|
||||
|
||||
const handler = await createReservedTurnHandler({
|
||||
reservedTurns: reservedTurnStore,
|
||||
scenarioConfig: snapshot.scenarioConfig,
|
||||
scenarioMeta: snapshot.scenarioMeta,
|
||||
map: localMap,
|
||||
unitSet: snapshot.unitSet,
|
||||
getWorld: () => wrapper.world,
|
||||
});
|
||||
|
||||
const world = new InMemoryTurnWorld(state, snapshot, {
|
||||
schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] },
|
||||
generalTurnHandler: handler,
|
||||
});
|
||||
wrapper.world = world;
|
||||
|
||||
const processor = new InMemoryTurnProcessor(world, { tickMinutes: 10 });
|
||||
|
||||
// Execution Loop
|
||||
// Turn 0: Uprising (Gen 1)
|
||||
await processor.run(new Date(mockDate.getTime() + 10 * 60 * 1000), {
|
||||
budgetMs: 1000,
|
||||
maxGenerals: 100,
|
||||
catchUpCap: 10,
|
||||
});
|
||||
await reservedTurnStore.flushChanges();
|
||||
|
||||
// Verify Uprising
|
||||
const gen1 = world.getGeneralById(1)!;
|
||||
expect(gen1.nationId).toBeGreaterThan(0);
|
||||
const nation = world.getNationById(gen1.nationId)!;
|
||||
expect(nation.level).toBe(0); // Wandering
|
||||
expect(gen1.officerLevel).toBe(12); // Monarch
|
||||
|
||||
// Cheat: Join Gen 2 to Gen 1's nation
|
||||
// We can treat this as "Gen 2 was convinced by Gen 1" or manually updated
|
||||
world.updateGeneral(2, { nationId: gen1.nationId });
|
||||
|
||||
// Turn 1: Founding (Fail) and Domestic (Fail)
|
||||
await processor.run(new Date(mockDate.getTime() + 2 * 10 * 60 * 1000), {
|
||||
// Skip enough time for turn
|
||||
budgetMs: 1000,
|
||||
maxGenerals: 100,
|
||||
catchUpCap: 10,
|
||||
});
|
||||
await reservedTurnStore.flushChanges();
|
||||
|
||||
// Verify Failures
|
||||
// Founding should fail (Wrong City), so Nation Level should still be 0
|
||||
const nationAfterFail = world.getNationById(gen1.nationId)!;
|
||||
expect(nationAfterFail.level).toBe(0);
|
||||
|
||||
// Domestic should fail (Wandering Nation)
|
||||
// Domestic effect is increasing agriculture. Gen 2 in City 1.
|
||||
const city1 = world.getCityById(1)!;
|
||||
expect(city1.agriculture).toBe(100); // No change
|
||||
|
||||
// Turn 2: Move (Gen 1)
|
||||
await processor.run(new Date(mockDate.getTime() + 3 * 10 * 60 * 1000), {
|
||||
budgetMs: 1000,
|
||||
maxGenerals: 100,
|
||||
catchUpCap: 10,
|
||||
});
|
||||
await reservedTurnStore.flushChanges();
|
||||
|
||||
const gen1AfterMove = world.getGeneralById(1)!;
|
||||
expect(gen1AfterMove.cityId).toBe(2);
|
||||
|
||||
// Turn 3: Founding (Success in City 2)
|
||||
await processor.run(new Date(mockDate.getTime() + 4 * 10 * 60 * 1000), {
|
||||
budgetMs: 1000,
|
||||
maxGenerals: 100,
|
||||
catchUpCap: 10,
|
||||
});
|
||||
await reservedTurnStore.flushChanges();
|
||||
|
||||
// Verify Founding
|
||||
const nationFinal = world.getNationById(gen1.nationId)!;
|
||||
expect(nationFinal.level).toBe(1); // Normal Nation
|
||||
expect(nationFinal.name).toBe('NewEmpire');
|
||||
expect(nationFinal.capitalCityId).toBe(2);
|
||||
|
||||
// Turn 4: Domestic (Success, Gen 2 in City 1)
|
||||
// Wait, Gen 2 is in City 1. City 1 belongs to nation?
|
||||
// Founding sets Capital City 2 to belongs to nation.
|
||||
// City 1 is still Neutral?
|
||||
// If City 1 is neutral, Domestic (Agriculture) might still fail due to "OccupiedCity" or ownership?
|
||||
// NotWanderingNation passes.
|
||||
// occupiedCity(): requires city.nationId === general.nationId?
|
||||
// Let's check occupiedCity constraint.
|
||||
// If occupiedCity fails, then Domestic fails.
|
||||
|
||||
// To make Domestic SUCCESS, Gen 2 must be in a city owned by the nation.
|
||||
// But Gen 2 is in City 1.
|
||||
// Gen 1 founded in City 2.
|
||||
// So Gen 2 should move to City 2 OR we own City 1.
|
||||
// Let's assume Gen 2 moves to City 2.
|
||||
// But I didn't schedule Move for Gen 2.
|
||||
// I'll manually move Gen 2 to City 2 before Turn 4.
|
||||
world.updateGeneral(2, { cityId: 2 });
|
||||
|
||||
await processor.run(new Date(mockDate.getTime() + 5 * 10 * 60 * 1000), {
|
||||
budgetMs: 1000,
|
||||
maxGenerals: 100,
|
||||
catchUpCap: 10,
|
||||
});
|
||||
await reservedTurnStore.flushChanges();
|
||||
|
||||
const city2 = world.getCityById(2)!;
|
||||
expect(city2.agriculture).toBeGreaterThan(100); // Success
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user