merge: 원격 국가 수와 합성 재야 교정을 반영한다
This commit is contained in:
@@ -349,6 +349,7 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
|
|||||||
city,
|
city,
|
||||||
nation,
|
nation,
|
||||||
nationGenerals,
|
nationGenerals,
|
||||||
|
realNationCount: nations.filter((entry) => entry.id > 0).length,
|
||||||
inputOptions,
|
inputOptions,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -315,6 +315,31 @@ const resolveOptionalString = (source: Record<string, unknown>, keys: string[]):
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_MAX_NATION = 55;
|
||||||
|
|
||||||
|
const resolveMaxNation = (worldState: WorldStateRow): number => {
|
||||||
|
const config = asRecord(worldState.config);
|
||||||
|
const constValues = asRecord(config.const);
|
||||||
|
const meta = asRecord(worldState.meta);
|
||||||
|
const refGameEnv = asRecord(meta.refGameEnv);
|
||||||
|
const candidates = [
|
||||||
|
refGameEnv.maxnation,
|
||||||
|
refGameEnv.maxNation,
|
||||||
|
config.maxnation,
|
||||||
|
config.maxNation,
|
||||||
|
constValues.defaultMaxNation,
|
||||||
|
constValues.maxNation,
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const parsed =
|
||||||
|
typeof candidate === 'number' ? candidate : typeof candidate === 'string' ? Number(candidate) : Number.NaN;
|
||||||
|
if (Number.isFinite(parsed) && parsed >= 0) {
|
||||||
|
return Math.floor(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DEFAULT_MAX_NATION;
|
||||||
|
};
|
||||||
|
|
||||||
const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => {
|
const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => {
|
||||||
const config = asRecord(worldState.config);
|
const config = asRecord(worldState.config);
|
||||||
const constValues = asRecord(config.const);
|
const constValues = asRecord(config.const);
|
||||||
@@ -630,7 +655,13 @@ const pickAvailability = (lhs: AvailabilityCore, rhs: AvailabilityCore): Availab
|
|||||||
|
|
||||||
type TurnCommandSpec = GeneralTurnCommandSpec | NationTurnCommandSpec;
|
type TurnCommandSpec = GeneralTurnCommandSpec | NationTurnCommandSpec;
|
||||||
|
|
||||||
const buildEntries = (env: CommandEnv, specs: TurnCommandSpec[]): CommandEntry[] => {
|
const FOUNDING_COMMAND_KEYS = new Set(['che_건국', 'cr_건국', 'che_무작위건국']);
|
||||||
|
|
||||||
|
const buildEntries = (
|
||||||
|
env: CommandEnv,
|
||||||
|
specs: TurnCommandSpec[],
|
||||||
|
options: { foundingAvailable?: boolean } = {}
|
||||||
|
): CommandEntry[] => {
|
||||||
const entries: CommandEntry[] = [];
|
const entries: CommandEntry[] = [];
|
||||||
|
|
||||||
for (const spec of specs) {
|
for (const spec of specs) {
|
||||||
@@ -659,6 +690,14 @@ const buildEntries = (env: CommandEnv, specs: TurnCommandSpec[]): CommandEntry[]
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (FOUNDING_COMMAND_KEYS.has(spec.key) && options.foundingAvailable === false) {
|
||||||
|
entry.evaluate = () => ({
|
||||||
|
possible: false,
|
||||||
|
status: 'blocked',
|
||||||
|
reason: '더 이상 건국은 불가능합니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,6 +779,8 @@ export const buildTurnCommandTable = async (options: {
|
|||||||
city: CityRow | null;
|
city: CityRow | null;
|
||||||
nation: NationRow | null;
|
nation: NationRow | null;
|
||||||
nationGenerals: GeneralRow[] | null;
|
nationGenerals: GeneralRow[] | null;
|
||||||
|
/** Ref's nation-table row count. Core callers must exclude synthetic id=0. */
|
||||||
|
realNationCount?: number;
|
||||||
inputOptions?: TurnCommandInputOptions;
|
inputOptions?: TurnCommandInputOptions;
|
||||||
}): Promise<TurnCommandTable> => {
|
}): Promise<TurnCommandTable> => {
|
||||||
// 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다.
|
// 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다.
|
||||||
@@ -761,7 +802,12 @@ export const buildTurnCommandTable = async (options: {
|
|||||||
|
|
||||||
const env = buildCommandEnv(options.worldState);
|
const env = buildCommandEnv(options.worldState);
|
||||||
const { general: generalSpecs, nation: nationSpecs } = await loadTurnCommandSpecs();
|
const { general: generalSpecs, nation: nationSpecs } = await loadTurnCommandSpecs();
|
||||||
const generalEntries = buildEntries(env, generalSpecs);
|
const generalEntries = buildEntries(env, generalSpecs, {
|
||||||
|
foundingAvailable:
|
||||||
|
options.realNationCount === undefined
|
||||||
|
? undefined
|
||||||
|
: options.realNationCount < resolveMaxNation(options.worldState),
|
||||||
|
});
|
||||||
const nationEntries = buildEntries(env, nationSpecs);
|
const nationEntries = buildEntries(env, nationSpecs);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -213,9 +213,10 @@ describe('buildTurnCommandTable', () => {
|
|||||||
for (const field of command.inputFields) {
|
for (const field of command.inputFields) {
|
||||||
expect(supportedKinds.has(field.kind), `${scope}:${command.key}:${field.key}`).toBe(true);
|
expect(supportedKinds.has(field.kind), `${scope}:${command.key}:${field.key}`).toBe(true);
|
||||||
if (field.kind === 'select') {
|
if (field.kind === 'select') {
|
||||||
expect(Boolean(field.options?.length || field.optionSource), `${scope}:${command.key}:${field.key}`).toBe(
|
expect(
|
||||||
true
|
Boolean(field.options?.length || field.optionSource),
|
||||||
);
|
`${scope}:${command.key}:${field.key}`
|
||||||
|
).toBe(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,6 +341,34 @@ describe('buildTurnCommandTable', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides founding at the Ref maxnation boundary without counting Core id=0', async () => {
|
||||||
|
const worldState = buildWorldState();
|
||||||
|
worldState.meta = {
|
||||||
|
...((worldState.meta ?? {}) as Record<string, unknown>),
|
||||||
|
refGameEnv: { maxnation: '2' },
|
||||||
|
};
|
||||||
|
const buildAtCount = (realNationCount: number) =>
|
||||||
|
buildTurnCommandTable({
|
||||||
|
worldState,
|
||||||
|
general: buildGeneral(),
|
||||||
|
city: buildCity(),
|
||||||
|
nation: buildNation(),
|
||||||
|
nationGenerals: null,
|
||||||
|
realNationCount,
|
||||||
|
});
|
||||||
|
const findFounding = (table: Awaited<ReturnType<typeof buildAtCount>>) =>
|
||||||
|
table.general.flatMap((group) => group.values).find((command) => command.key === 'che_건국');
|
||||||
|
|
||||||
|
expect(findFounding(await buildAtCount(1))).not.toMatchObject({
|
||||||
|
reason: '더 이상 건국은 불가능합니다.',
|
||||||
|
});
|
||||||
|
expect(findFounding(await buildAtCount(2))).toMatchObject({
|
||||||
|
possible: false,
|
||||||
|
status: 'blocked',
|
||||||
|
reason: '더 이상 건국은 불가능합니다.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('projects Ref recruitment availability, combat values, descriptions, and adjusted costs', () => {
|
it('projects Ref recruitment availability, combat values, descriptions, and adjusted costs', () => {
|
||||||
const general = buildGeneral();
|
const general = buildGeneral();
|
||||||
general.injury = 3;
|
general.injury = 3;
|
||||||
|
|||||||
@@ -119,7 +119,8 @@ export const createNeutralAuctionRegistrar = async (options: {
|
|||||||
hiddenSeed,
|
hiddenSeed,
|
||||||
seedYear: context.previousYear,
|
seedYear: context.previousYear,
|
||||||
seedMonth: context.previousMonth,
|
seedMonth: context.previousMonth,
|
||||||
nationCount: options.getNationPowerRollCount?.() ?? world.listNations().length,
|
nationCount:
|
||||||
|
options.getNationPowerRollCount?.() ?? world.listNations().filter((nation) => nation.id > 0).length,
|
||||||
consumeTournamentRoll,
|
consumeTournamentRoll,
|
||||||
averageGold: average(eligibleGenerals.map((general) => general.gold)),
|
averageGold: average(eligibleGenerals.map((general) => general.gold)),
|
||||||
averageRice: average(eligibleGenerals.map((general) => general.rice)),
|
averageRice: average(eligibleGenerals.map((general) => general.rice)),
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ export const createScoutBlockHandler = (options: {
|
|||||||
// changing nation or game state. No provided scenario invokes it.
|
// changing nation or game state. No provided scenario invokes it.
|
||||||
throw new Error('update(): at least 3 arguments expected');
|
throw new Error('update(): at least 3 arguments expected');
|
||||||
}
|
}
|
||||||
for (const nation of world.listNations()) {
|
// Ref updates every row in its nation table. Core additionally keeps a
|
||||||
|
// synthetic id=0 neutral row, which is not a Ref nation row and must
|
||||||
|
// remain outside the last-nation scout policy.
|
||||||
|
for (const nation of world.listNations().filter((entry) => entry.id > 0)) {
|
||||||
world.updateNation(nation.id, {
|
world.updateNation(nation.id, {
|
||||||
meta: {
|
meta: {
|
||||||
...nation.meta,
|
...nation.meta,
|
||||||
|
|||||||
@@ -414,7 +414,8 @@ const createMonthlyCalendarRuntime = async (options: {
|
|||||||
clock: Clock;
|
clock: Clock;
|
||||||
}) => {
|
}) => {
|
||||||
const cache: MonthlyRuntimeCache = {
|
const cache: MonthlyRuntimeCache = {
|
||||||
nationPowerRollCount: options.snapshot.nations.length,
|
// Ref's monthly nation query has no Core-only id=0 neutral row.
|
||||||
|
nationPowerRollCount: options.snapshot.nations.filter((nation) => nation.id > 0).length,
|
||||||
tournamentRollConsumed: false,
|
tournamentRollConsumed: false,
|
||||||
};
|
};
|
||||||
const unification = options.calendarHandlerOverride
|
const unification = options.calendarHandlerOverride
|
||||||
|
|||||||
@@ -87,16 +87,18 @@ const buildWorld = (
|
|||||||
describe('monthly scout block actions', () => {
|
describe('monthly scout block actions', () => {
|
||||||
it('blocks joining for all nations and globally locks policy changes', async () => {
|
it('blocks joining for all nations and globally locks policy changes', async () => {
|
||||||
const world = buildWorld(0);
|
const world = buildWorld(0);
|
||||||
|
world.addNation(buildNation(0, 0));
|
||||||
await createScoutBlockHandler({ actionName: 'BlockScoutAction', getWorld: () => world })(
|
await createScoutBlockHandler({ actionName: 'BlockScoutAction', getWorld: () => world })(
|
||||||
[true],
|
[true],
|
||||||
{ year: 200, month: 1, startyear: 190, currentEventID: 1, turnTime: new Date() },
|
{ year: 200, month: 1, startyear: 190, currentEventID: 1, turnTime: new Date() },
|
||||||
event
|
event
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(world.listNations().map((nation) => nation.meta)).toEqual([
|
expect(Object.fromEntries(world.listNations().map((nation) => [nation.id, nation.meta]))).toEqual({
|
||||||
{ scout: 1, marker: 1 },
|
0: { scout: 0, marker: 0 },
|
||||||
{ scout: 1, marker: 2 },
|
1: { scout: 1, marker: 1 },
|
||||||
]);
|
2: { scout: 1, marker: 2 },
|
||||||
|
});
|
||||||
expect(world.getState().meta.block_change_scout).toBe(true);
|
expect(world.getState().meta.block_change_scout).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { TurnEvent, TurnWorldSnapshot, TurnWorldState } from '../src/turn/t
|
|||||||
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
|
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
|
||||||
const integration = describe.skipIf(!databaseUrl);
|
const integration = describe.skipIf(!databaseUrl);
|
||||||
const nationIds = [990_081, 990_082] as const;
|
const nationIds = [990_081, 990_082] as const;
|
||||||
|
const neutralNationId = 0;
|
||||||
|
|
||||||
const buildNation = (id: number): Nation => ({
|
const buildNation = (id: number): Nation => ({
|
||||||
id,
|
id,
|
||||||
@@ -49,7 +50,7 @@ integration('monthly scout block persistence', () => {
|
|||||||
OR: [{ srcNationId: { in: [...nationIds] } }, { destNationId: { in: [...nationIds] } }],
|
OR: [{ srcNationId: { in: [...nationIds] } }, { destNationId: { in: [...nationIds] } }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await db.nation.deleteMany({ where: { id: { in: [...nationIds] } } });
|
await db.nation.deleteMany({ where: { id: { in: [neutralNationId, ...nationIds] } } });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -58,12 +59,13 @@ integration('monthly scout block persistence', () => {
|
|||||||
OR: [{ srcNationId: { in: [...nationIds] } }, { destNationId: { in: [...nationIds] } }],
|
OR: [{ srcNationId: { in: [...nationIds] } }, { destNationId: { in: [...nationIds] } }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await db.nation.deleteMany({ where: { id: { in: [...nationIds] } } });
|
await db.nation.deleteMany({ where: { id: { in: [neutralNationId, ...nationIds] } } });
|
||||||
await closeDb?.();
|
await closeDb?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('persists every nation scout flag and the global policy lock in one flush', async () => {
|
it('persists every nation scout flag and the global policy lock in one flush', async () => {
|
||||||
const nations = nationIds.map(buildNation);
|
const neutralNation = { ...buildNation(neutralNationId), name: '재야', level: 0 };
|
||||||
|
const nations = [neutralNation, ...nationIds.map(buildNation)];
|
||||||
await db.nation.createMany({
|
await db.nation.createMany({
|
||||||
data: nations.map((nation) => ({
|
data: nations.map((nation) => ({
|
||||||
id: nation.id,
|
id: nation.id,
|
||||||
@@ -146,6 +148,9 @@ integration('monthly scout block persistence', () => {
|
|||||||
{ power: 0, scout: 1 },
|
{ power: 0, scout: 1 },
|
||||||
{ power: 0, scout: 1 },
|
{ power: 0, scout: 1 },
|
||||||
]);
|
]);
|
||||||
|
expect(await db.nation.findUniqueOrThrow({ where: { id: neutralNationId } })).toMatchObject({
|
||||||
|
meta: { scout: 0 },
|
||||||
|
});
|
||||||
expect(await db.worldState.findUniqueOrThrow({ where: { id: row.id } })).toMatchObject({
|
expect(await db.worldState.findUniqueOrThrow({ where: { id: row.id } })).toMatchObject({
|
||||||
meta: { block_change_scout: true },
|
meta: { block_change_scout: true },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ const buildSnapshot = (): TurnWorldSnapshot => ({
|
|||||||
buildGeneral(2, 2, 99_999, 99_999),
|
buildGeneral(2, 2, 99_999, 99_999),
|
||||||
],
|
],
|
||||||
cities: [],
|
cities: [],
|
||||||
nations: [1, 2, 3].map((id) => ({
|
// Core persists a synthetic id=0 row that Ref's nation-power loop never
|
||||||
|
// sees. The registrar fallback must therefore still consume three rolls.
|
||||||
|
nations: [0, 1, 2, 3].map((id) => ({
|
||||||
id,
|
id,
|
||||||
name: `Nation_${id}`,
|
name: `Nation_${id}`,
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
@@ -151,7 +153,7 @@ describe('neutral auction monthly registrar', () => {
|
|||||||
loadNeutralAuctionCounts: async () => [],
|
loadNeutralAuctionCounts: async () => [],
|
||||||
});
|
});
|
||||||
const snapshot = buildSnapshot();
|
const snapshot = buildSnapshot();
|
||||||
snapshot.nations = snapshot.nations.slice(0, 2);
|
snapshot.nations = snapshot.nations.slice(1, 3);
|
||||||
snapshot.generals = [buildGeneral(1, 0, 5_000, 7_000), buildGeneral(2, 0, 6_000, 8_000)];
|
snapshot.generals = [buildGeneral(1, 0, 5_000, 7_000), buildGeneral(2, 0, 6_000, 8_000)];
|
||||||
const state: TurnWorldState = {
|
const state: TurnWorldState = {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user