fix nation diplomacy proposal parity
This commit is contained in:
@@ -20,15 +20,9 @@ import { z } from 'zod';
|
|||||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||||
|
|
||||||
const ARGS_SCHEMA = z.object({
|
const ARGS_SCHEMA = z.object({
|
||||||
destNationId: z.preprocess(
|
destNationId: z.number().int().positive(),
|
||||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
year: z.number().int().min(0),
|
||||||
z.number().int().positive()
|
month: z.number().int().min(1).max(12),
|
||||||
),
|
|
||||||
year: z.preprocess((value) => (typeof value === 'number' ? Math.floor(value) : value), z.number().int().min(0)),
|
|
||||||
month: z.preprocess(
|
|
||||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
|
||||||
z.number().int().min(1).max(12)
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
export type NonAggressionProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
export type NonAggressionProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||||
|
|
||||||
@@ -52,12 +46,14 @@ const reqMinimumTreatyTerm = (minMonths: number): Constraint => ({
|
|||||||
{ kind: 'arg', key: 'month' },
|
{ kind: 'arg', key: 'month' },
|
||||||
{ kind: 'env', key: 'year' },
|
{ kind: 'env', key: 'year' },
|
||||||
{ kind: 'env', key: 'month' },
|
{ kind: 'env', key: 'month' },
|
||||||
|
{ kind: 'env', key: 'startYear' },
|
||||||
],
|
],
|
||||||
test: (ctx) => {
|
test: (ctx) => {
|
||||||
const yearValue = typeof ctx.args.year === 'number' ? ctx.args.year : null;
|
const yearValue = typeof ctx.args.year === 'number' ? ctx.args.year : null;
|
||||||
const monthValue = typeof ctx.args.month === 'number' ? ctx.args.month : null;
|
const monthValue = typeof ctx.args.month === 'number' ? ctx.args.month : null;
|
||||||
const envYearValue = typeof ctx.env.year === 'number' ? ctx.env.year : null;
|
const envYearValue = typeof ctx.env.year === 'number' ? ctx.env.year : null;
|
||||||
const envMonthValue = typeof ctx.env.month === 'number' ? ctx.env.month : null;
|
const envMonthValue = typeof ctx.env.month === 'number' ? ctx.env.month : null;
|
||||||
|
const startYearValue = typeof ctx.env.startYear === 'number' ? ctx.env.startYear : null;
|
||||||
const missing = [];
|
const missing = [];
|
||||||
|
|
||||||
if (yearValue === null) {
|
if (yearValue === null) {
|
||||||
@@ -72,17 +68,27 @@ const reqMinimumTreatyTerm = (minMonths: number): Constraint => ({
|
|||||||
if (envMonthValue === null) {
|
if (envMonthValue === null) {
|
||||||
missing.push({ kind: 'env', key: 'month' } as const);
|
missing.push({ kind: 'env', key: 'month' } as const);
|
||||||
}
|
}
|
||||||
|
if (startYearValue === null) {
|
||||||
|
missing.push({ kind: 'env', key: 'startYear' } as const);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
missing.length > 0 ||
|
missing.length > 0 ||
|
||||||
yearValue === null ||
|
yearValue === null ||
|
||||||
monthValue === null ||
|
monthValue === null ||
|
||||||
envYearValue === null ||
|
envYearValue === null ||
|
||||||
envMonthValue === null
|
envMonthValue === null ||
|
||||||
|
startYearValue === null
|
||||||
) {
|
) {
|
||||||
return unknownOrDeny(ctx, missing, '기한 정보가 없습니다.');
|
return unknownOrDeny(ctx, missing, '기한 정보가 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (yearValue < startYearValue) {
|
||||||
|
return {
|
||||||
|
kind: 'deny',
|
||||||
|
reason: '시작 연도보다 이전의 기한은 지정할 수 없습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
const currentMonth = resolveMonthIndex(envYearValue, envMonthValue);
|
const currentMonth = resolveMonthIndex(envYearValue, envMonthValue);
|
||||||
const targetMonth = resolveMonthIndex(yearValue, monthValue);
|
const targetMonth = resolveMonthIndex(yearValue, monthValue);
|
||||||
if (targetMonth < currentMonth + minMonths) {
|
if (targetMonth < currentMonth + minMonths) {
|
||||||
@@ -137,7 +143,7 @@ export class ActionDefinition<
|
|||||||
return { effects: [createLogEffect('국가 정보가 없습니다.')] };
|
return { effects: [createLogEffect('국가 정보가 없습니다.')] };
|
||||||
}
|
}
|
||||||
const destNationName = destNation.name;
|
const destNationName = destNation.name;
|
||||||
const josaRo = JosaUtil.pick(destNationName, '로');
|
const josaRo = JosaUtil.pick(nation.name, '로');
|
||||||
const josaWa = JosaUtil.pick(nation.name, '와');
|
const josaWa = JosaUtil.pick(nation.name, '와');
|
||||||
const validUntil = new Date(context.messageTime.getTime() + context.messageValidMinutes * 60_000);
|
const validUntil = new Date(context.messageTime.getTime() + context.messageValidMinutes * 60_000);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ import { z } from 'zod';
|
|||||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||||
|
|
||||||
const ARGS_SCHEMA = z.object({
|
const ARGS_SCHEMA = z.object({
|
||||||
destNationId: z.preprocess(
|
destNationId: z.number().int().positive(),
|
||||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
|
||||||
z.number().int().positive()
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
export type NonAggressionCancelProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
export type NonAggressionCancelProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type {
|
|||||||
GeneralActionOutcome,
|
GeneralActionOutcome,
|
||||||
GeneralActionResolveContext,
|
GeneralActionResolveContext,
|
||||||
} from '@sammo-ts/logic/actions/engine.js';
|
} from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
import { createDiplomacyPatchEffect, createLogEffect, createMessageEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
@@ -25,14 +25,18 @@ import { z } from 'zod';
|
|||||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||||
|
|
||||||
const ARGS_SCHEMA = z.object({
|
const ARGS_SCHEMA = z.object({
|
||||||
destNationId: z.preprocess(
|
destNationId: z.number().int().positive(),
|
||||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
|
||||||
z.number().int().positive()
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
export type DeclareWarArgs = z.infer<typeof ARGS_SCHEMA>;
|
export type DeclareWarArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||||
|
|
||||||
// DeclareWarResolveContext is not used anymore as it was replaced by inline type in ActionDefinition
|
interface DeclareWarResolveContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
destNation: Nation;
|
||||||
|
currentYear: number;
|
||||||
|
currentMonth: number;
|
||||||
|
messageTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const ACTION_NAME = '선전포고';
|
const ACTION_NAME = '선전포고';
|
||||||
// legacy 규칙: 선전포고 상태는 24턴 유지.
|
// legacy 규칙: 선전포고 상태는 24턴 유지.
|
||||||
@@ -41,11 +45,7 @@ const DECLARE_TERM = 24;
|
|||||||
|
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
> implements GeneralActionDefinition<
|
> implements GeneralActionDefinition<TriggerState, DeclareWarArgs, DeclareWarResolveContext<TriggerState>> {
|
||||||
TriggerState,
|
|
||||||
DeclareWarArgs,
|
|
||||||
GeneralActionResolveContext<TriggerState> & { destNation: Nation }
|
|
||||||
> {
|
|
||||||
public readonly key = 'che_선전포고';
|
public readonly key = 'che_선전포고';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
@@ -98,10 +98,7 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(context: DeclareWarResolveContext<TriggerState>, args: DeclareWarArgs): GeneralActionOutcome<TriggerState> {
|
||||||
context: GeneralActionResolveContext<TriggerState> & { destNation: Nation },
|
|
||||||
args: DeclareWarArgs
|
|
||||||
): GeneralActionOutcome<TriggerState> {
|
|
||||||
const nationId = context.nation?.id;
|
const nationId = context.nation?.id;
|
||||||
if (nationId === undefined || nationId <= 0 || !context.destNation) {
|
if (nationId === undefined || nationId <= 0 || !context.destNation) {
|
||||||
return {
|
return {
|
||||||
@@ -157,27 +154,46 @@ export class ActionDefinition<
|
|||||||
format: LogFormat.YEAR_MONTH,
|
format: LogFormat.YEAR_MONTH,
|
||||||
}),
|
}),
|
||||||
// Global Action Log
|
// Global Action Log
|
||||||
createLogEffect(`<Y>${generalName}</>${josaYiGeneral} <D><b>${destNationName}</b></>에 <M>선전 포고</> 하였습니다.`, {
|
|
||||||
scope: LogScope.SYSTEM,
|
|
||||||
category: LogCategory.ACTION,
|
|
||||||
format: LogFormat.PLAIN,
|
|
||||||
}),
|
|
||||||
// Global History Log
|
|
||||||
createLogEffect(`<R><b>【선포】</b></><D><b>${nationName}</b></>${josaYiNation} <D><b>${destNationName}</b></>에 선전 포고 하였습니다.`, {
|
|
||||||
scope: LogScope.SYSTEM,
|
|
||||||
category: LogCategory.HISTORY,
|
|
||||||
format: LogFormat.YEAR_MONTH,
|
|
||||||
}),
|
|
||||||
// National Message (국메)
|
|
||||||
createLogEffect(
|
createLogEffect(
|
||||||
`【국메】<Y>${generalName}</>${josaYiGeneral} <Y>${destNationName}</>에게 <R>${ACTION_NAME}</>하였습니다!`,
|
`<Y>${generalName}</>${josaYiGeneral} <D><b>${destNationName}</b></>에 <M>선전 포고</> 하였습니다.`,
|
||||||
{
|
{
|
||||||
scope: LogScope.NATION,
|
scope: LogScope.SYSTEM,
|
||||||
nationId: nationId,
|
category: LogCategory.SUMMARY,
|
||||||
category: LogCategory.ACTION,
|
format: LogFormat.MONTH,
|
||||||
format: LogFormat.PLAIN,
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
// Global History Log
|
||||||
|
createLogEffect(
|
||||||
|
`<R><b>【선포】</b></><D><b>${nationName}</b></>${josaYiNation} <D><b>${destNationName}</b></>에 선전 포고 하였습니다.`,
|
||||||
|
{
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
createMessageEffect({
|
||||||
|
msgType: 'national',
|
||||||
|
src: {
|
||||||
|
generalId: context.general.id,
|
||||||
|
generalName,
|
||||||
|
nationId,
|
||||||
|
nationName,
|
||||||
|
color: context.nation?.color ?? '',
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
dest: {
|
||||||
|
generalId: 0,
|
||||||
|
generalName: '',
|
||||||
|
nationId: args.destNationId,
|
||||||
|
nationName: destNationName,
|
||||||
|
color: context.destNation.color,
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
text: `【외교】${context.currentYear}년 ${context.currentMonth}월:${nationName}에서 ${destNationName}에 선전포고`,
|
||||||
|
time: context.messageTime,
|
||||||
|
validUntil: new Date('9999-12-31T00:00:00.000Z'),
|
||||||
|
option: {},
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
return { effects };
|
return { effects };
|
||||||
@@ -201,6 +217,9 @@ export const actionContextBuilder: ActionContextBuilder<DeclareWarArgs> = (base,
|
|||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
destNation,
|
destNation,
|
||||||
|
currentYear: options.world.currentYear,
|
||||||
|
currentMonth: options.world.currentMonth,
|
||||||
|
messageTime: base.general.turnTime,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ import { z } from 'zod';
|
|||||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||||
|
|
||||||
const ARGS_SCHEMA = z.object({
|
const ARGS_SCHEMA = z.object({
|
||||||
destNationId: z.preprocess(
|
destNationId: z.number().int().positive(),
|
||||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
|
||||||
z.number().int().positive()
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
export type StopWarProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
export type StopWarProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import type { City, General, Nation } from '../../../src/domain/entities.js';
|
import type { City, General, Nation } from '../../../src/domain/entities.js';
|
||||||
import { resolveGeneralAction } from '../../../src/actions/engine.js';
|
import { resolveGeneralAction } from '../../../src/actions/engine.js';
|
||||||
import { ActionDefinition as DeclareWarAction } from '../../../src/actions/turn/nation/che_선전포고.js';
|
import { ActionDefinition as DeclareWarAction } from '../../../src/actions/turn/nation/che_선전포고.js';
|
||||||
|
import { ActionDefinition as NonAggressionProposalAction } from '../../../src/actions/turn/nation/che_불가침제의.js';
|
||||||
|
import { ActionDefinition as StopWarProposalAction } from '../../../src/actions/turn/nation/che_종전제의.js';
|
||||||
|
import { ActionDefinition as NonAggressionCancelProposalAction } from '../../../src/actions/turn/nation/che_불가침파기제의.js';
|
||||||
import { ActionDefinition as MoveCapitalAction } from '../../../src/actions/turn/nation/che_천도.js';
|
import { ActionDefinition as MoveCapitalAction } from '../../../src/actions/turn/nation/che_천도.js';
|
||||||
import {
|
import {
|
||||||
ActionDefinition as ChangeNationNameAction,
|
ActionDefinition as ChangeNationNameAction,
|
||||||
@@ -125,6 +128,9 @@ describe('Nation Actions', () => {
|
|||||||
destNation: nation2,
|
destNation: nation2,
|
||||||
cities: [city1, city2],
|
cities: [city1, city2],
|
||||||
nations: [nation1, nation2],
|
nations: [nation1, nation2],
|
||||||
|
currentYear: 190,
|
||||||
|
currentMonth: 1,
|
||||||
|
messageTime: new Date('2026-01-01T00:00:00Z'),
|
||||||
rng: {} as any,
|
rng: {} as any,
|
||||||
addLog: () => {},
|
addLog: () => {},
|
||||||
};
|
};
|
||||||
@@ -144,10 +150,19 @@ describe('Nation Actions', () => {
|
|||||||
patch: expect.objectContaining({ state: 1 }),
|
patch: expect.objectContaining({ state: 1 }),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(resolution.logs.some((l) => l.scope === LogScope.SYSTEM && l.category === LogCategory.ACTION)).toBe(
|
expect(resolution.logs.some((l) => l.scope === LogScope.SYSTEM && l.category === LogCategory.SUMMARY)).toBe(
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
expect(resolution.logs.some((l) => l.text.includes('【국메】'))).toBe(true);
|
expect(resolution.effects).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'message:add',
|
||||||
|
draft: expect.objectContaining({
|
||||||
|
msgType: 'national',
|
||||||
|
text: '【외교】190년 1월:Nation1에서 Nation2에 선전포고',
|
||||||
|
option: {},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails if not neighbor', () => {
|
it('fails if not neighbor', () => {
|
||||||
@@ -234,6 +249,23 @@ describe('Nation Actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('diplomacy proposal argument boundaries', () => {
|
||||||
|
it.each([
|
||||||
|
['che_선전포고', new DeclareWarAction(), { destNationId: 2.9 }],
|
||||||
|
[
|
||||||
|
'che_불가침제의 destination',
|
||||||
|
new NonAggressionProposalAction(),
|
||||||
|
{ destNationId: 2.9, year: 190, month: 7 },
|
||||||
|
],
|
||||||
|
['che_불가침제의 year', new NonAggressionProposalAction(), { destNationId: 2, year: 190.9, month: 7 }],
|
||||||
|
['che_불가침제의 month', new NonAggressionProposalAction(), { destNationId: 2, year: 190, month: 7.9 }],
|
||||||
|
['che_종전제의', new StopWarProposalAction(), { destNationId: 2.9 }],
|
||||||
|
['che_불가침파기제의', new NonAggressionCancelProposalAction(), { destNationId: 2.9 }],
|
||||||
|
])('%s rejects fractional numeric arguments', (_name, definition, args) => {
|
||||||
|
expect(definition.parseArgs(args)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('che_필사즉생 (Last Stand)', () => {
|
describe('che_필사즉생 (Last Stand)', () => {
|
||||||
it('applies the legacy three-turn gains and global delay', () => {
|
it('applies the legacy three-turn gains and global delay', () => {
|
||||||
const nation = buildNation(1);
|
const nation = buildNation(1);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const NPC_SEIZURE_MESSAGE_TEXT = '몰수를 하다니... 이것이 윗사람이
|
|||||||
const normalizeStoredLogText = (value: unknown): string =>
|
const normalizeStoredLogText = (value: unknown): string =>
|
||||||
String(value)
|
String(value)
|
||||||
.replace(/^(?:<C>●<\/>|<S>◆<\/>|<R>★<\/>)(?:(?:\d+년 )?\d+월:|\d+년:)?/, '')
|
.replace(/^(?:<C>●<\/>|<S>◆<\/>|<R>★<\/>)(?:(?:\d+년 )?\d+월:|\d+년:)?/, '')
|
||||||
.replace(/ <1>\d{2}:\d{2}<\/>$/, '');
|
.replace(/ ?<1>\d{2}:\d{2}<\/>$/, '');
|
||||||
|
|
||||||
const semanticLogSignatures = (logs: Array<Record<string, unknown>>): string[] =>
|
const semanticLogSignatures = (logs: Array<Record<string, unknown>>): string[] =>
|
||||||
logs
|
logs
|
||||||
@@ -875,6 +875,309 @@ integration('nation flag boundary parity', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type DiplomacyProposalAction = 'che_선전포고' | 'che_불가침제의' | 'che_종전제의' | 'che_불가침파기제의';
|
||||||
|
|
||||||
|
interface DiplomacyProposalBoundaryCase {
|
||||||
|
name: string;
|
||||||
|
action: DiplomacyProposalAction;
|
||||||
|
args?: Record<string, unknown>;
|
||||||
|
fixturePatches?: FixturePatches;
|
||||||
|
completed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diplomacyProposalBoundaryCases: DiplomacyProposalBoundaryCase[] = [
|
||||||
|
...(['che_선전포고', 'che_불가침제의', 'che_종전제의', 'che_불가침파기제의'] as const).flatMap((action) => [
|
||||||
|
{
|
||||||
|
name: `${action} rejects a numeric-string destination`,
|
||||||
|
action,
|
||||||
|
args: action === 'che_불가침제의' ? { destNationID: '2', year: 190, month: 7 } : { destNationID: '2' },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${action} rejects a fractional destination instead of truncating it`,
|
||||||
|
action,
|
||||||
|
args: action === 'che_불가침제의' ? { destNationID: 2.9, year: 190, month: 7 } : { destNationID: 2.9 },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${action} rejects a missing destination nation`,
|
||||||
|
action,
|
||||||
|
args: action === 'che_불가침제의' ? { destNationID: 9, year: 190, month: 7 } : { destNationID: 9 },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${action} rejects a neutral actor`,
|
||||||
|
action,
|
||||||
|
args: action === 'che_불가침제의' ? { destNationID: 2, year: 190, month: 7 } : { destNationID: 2 },
|
||||||
|
fixturePatches: {
|
||||||
|
generals: { 1: { nationId: 0 } },
|
||||||
|
cities: { 3: { nationId: 0 } },
|
||||||
|
},
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${action} rejects an actor below chief`,
|
||||||
|
action,
|
||||||
|
args: action === 'che_불가침제의' ? { destNationID: 2, year: 190, month: 7 } : { destNationID: 2 },
|
||||||
|
fixturePatches: { generals: { 1: { officerLevel: 4 } } },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
name: 'declare-war rejects the opening year',
|
||||||
|
action: 'che_선전포고',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { world: { year: 180 } },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'declare-war accepts the exact start-year plus one boundary',
|
||||||
|
action: 'che_선전포고',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { world: { year: 181 } },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'declare-war rejects an actor city occupied by another nation',
|
||||||
|
action: 'che_선전포고',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { cities: { 3: { nationId: 2 } } },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'declare-war rejects an unsupplied actor city',
|
||||||
|
action: 'che_선전포고',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { cities: { 3: { supplyState: 0 } } },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'declare-war rejects forward war state',
|
||||||
|
action: 'che_선전포고',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { diplomacy: { '1:2': { state: 0 }, '2:1': { state: 3 } } },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'declare-war ignores the reverse diplomacy state',
|
||||||
|
action: 'che_선전포고',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { diplomacy: { '1:2': { state: 3 }, '2:1': { state: 0 } } },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-aggression rejects a fractional year',
|
||||||
|
action: 'che_불가침제의',
|
||||||
|
args: { destNationID: 2, year: 190.9, month: 7 },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-aggression rejects a fractional month',
|
||||||
|
action: 'che_불가침제의',
|
||||||
|
args: { destNationID: 2, year: 190, month: 7.9 },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-aggression rejects a year before the scenario start even when six months ahead',
|
||||||
|
action: 'che_불가침제의',
|
||||||
|
args: { destNationID: 2, year: 179, month: 7 },
|
||||||
|
fixturePatches: { world: { year: 179, month: 1 } },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-aggression rejects five months ahead',
|
||||||
|
action: 'che_불가침제의',
|
||||||
|
args: { destNationID: 2, year: 190, month: 6 },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-aggression accepts exactly six months ahead without city ownership or supply',
|
||||||
|
action: 'che_불가침제의',
|
||||||
|
args: { destNationID: 2, year: 190, month: 7 },
|
||||||
|
fixturePatches: {
|
||||||
|
nations: { 1: { name: '위' }, 2: { name: '탑' } },
|
||||||
|
cities: { 3: { nationId: 2, supplyState: 0 } },
|
||||||
|
},
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-aggression rejects a forward war state',
|
||||||
|
action: 'che_불가침제의',
|
||||||
|
args: { destNationID: 2, year: 190, month: 7 },
|
||||||
|
fixturePatches: { diplomacy: { '1:2': { state: 0 }, '2:1': { state: 3 } } },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-aggression ignores the reverse war state',
|
||||||
|
action: 'che_불가침제의',
|
||||||
|
args: { destNationID: 2, year: 190, month: 7 },
|
||||||
|
fixturePatches: { diplomacy: { '1:2': { state: 3 }, '2:1': { state: 0 } } },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stop-war accepts forward declaration state zero',
|
||||||
|
action: 'che_종전제의',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { diplomacy: { '1:2': { state: 0 }, '2:1': { state: 3 } } },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stop-war accepts forward war state one',
|
||||||
|
action: 'che_종전제의',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { diplomacy: { '1:2': { state: 1 }, '2:1': { state: 3 } } },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stop-war rejects an unrelated forward state',
|
||||||
|
action: 'che_종전제의',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stop-war rejects an unsupplied actor city',
|
||||||
|
action: 'che_종전제의',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { cities: { 3: { supplyState: 0 } }, diplomacy: { '1:2': { state: 0 } } },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-aggression cancellation accepts forward state seven',
|
||||||
|
action: 'che_불가침파기제의',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { diplomacy: { '1:2': { state: 7 }, '2:1': { state: 3 } } },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-aggression cancellation rejects an unrelated forward state',
|
||||||
|
action: 'che_불가침파기제의',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-aggression cancellation ignores a reverse-only state seven',
|
||||||
|
action: 'che_불가침파기제의',
|
||||||
|
args: { destNationID: 2 },
|
||||||
|
fixturePatches: { diplomacy: { '1:2': { state: 3 }, '2:1': { state: 7 } } },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expectedDiplomacyMessage = (
|
||||||
|
action: DiplomacyProposalAction,
|
||||||
|
sourceName: string,
|
||||||
|
destinationName: string,
|
||||||
|
year: number,
|
||||||
|
month: number
|
||||||
|
): { type: 'national' | 'diplomacy'; text: string; option: Record<string, unknown> } => {
|
||||||
|
switch (action) {
|
||||||
|
case 'che_선전포고':
|
||||||
|
return {
|
||||||
|
type: 'national',
|
||||||
|
text: `【외교】${year}년 ${month}월:${sourceName}에서 ${destinationName}에 선전포고`,
|
||||||
|
option: {},
|
||||||
|
};
|
||||||
|
case 'che_불가침제의':
|
||||||
|
return {
|
||||||
|
type: 'diplomacy',
|
||||||
|
text: `${sourceName}${sourceName === '위' ? '와' : '과'} ${year}년 ${month + 6}월까지 불가침 제의 서신`,
|
||||||
|
option: { action: 'noAggression', year, month: month + 6 },
|
||||||
|
};
|
||||||
|
case 'che_종전제의':
|
||||||
|
return {
|
||||||
|
type: 'diplomacy',
|
||||||
|
text: `${sourceName}의 종전 제의 서신`,
|
||||||
|
option: { action: 'stopWar', deletable: false },
|
||||||
|
};
|
||||||
|
case 'che_불가침파기제의':
|
||||||
|
return {
|
||||||
|
type: 'diplomacy',
|
||||||
|
text: `${sourceName}의 불가침 파기 제의 서신`,
|
||||||
|
option: { action: 'cancelNA', deletable: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
integration('nation diplomacy proposal boundary and message parity', () => {
|
||||||
|
it.each(diplomacyProposalBoundaryCases)(
|
||||||
|
'$name',
|
||||||
|
async ({ action, args, fixturePatches, completed }) => {
|
||||||
|
const request = buildRequest(action, args, fixturePatches);
|
||||||
|
request.observe!.includeNationHistoryLogs = true;
|
||||||
|
request.observe!.includeGlobalHistoryLogs = true;
|
||||||
|
const reference = runReferenceTurnCommandTraceRequest(
|
||||||
|
workspaceRoot!,
|
||||||
|
request as unknown as Record<string, unknown>
|
||||||
|
);
|
||||||
|
const core = await runCoreTurnCommandTrace(request, reference.before);
|
||||||
|
|
||||||
|
expect(reference.execution.outcome).toMatchObject({ completed });
|
||||||
|
if (
|
||||||
|
fixturePatches?.generals?.[1]?.nationId === 0 ||
|
||||||
|
(typeof fixturePatches?.generals?.[1]?.officerLevel === 'number' &&
|
||||||
|
fixturePatches.generals[1].officerLevel < 5)
|
||||||
|
) {
|
||||||
|
expect(core.execution.outcome).toBeUndefined();
|
||||||
|
} else {
|
||||||
|
expect(core.execution.outcome).toMatchObject({
|
||||||
|
requestedAction: action,
|
||||||
|
actionKey: completed ? action : '휴식',
|
||||||
|
usedFallback: !completed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
expect(core.rng).toEqual(reference.rng);
|
||||||
|
expect(
|
||||||
|
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||||
|
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||||
|
})
|
||||||
|
).toEqual([]);
|
||||||
|
|
||||||
|
const referenceMessages = reference.after.messages.slice(reference.before.messages.length);
|
||||||
|
if (!completed) {
|
||||||
|
expect(referenceMessages).toEqual([]);
|
||||||
|
expect(core.after.messages).toEqual([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceNation = reference.after.nations.find((entry) => entry.id === 1);
|
||||||
|
const destinationNation = reference.after.nations.find((entry) => entry.id === 2);
|
||||||
|
const expected = expectedDiplomacyMessage(
|
||||||
|
action,
|
||||||
|
String(sourceNation?.name),
|
||||||
|
String(destinationNation?.name),
|
||||||
|
Number(reference.after.world.year),
|
||||||
|
Number(reference.after.world.month)
|
||||||
|
);
|
||||||
|
expect(referenceMessages).toHaveLength(2);
|
||||||
|
expect(referenceMessages.find((entry) => entry.mailbox === 9002)).toMatchObject({
|
||||||
|
type: expected.type,
|
||||||
|
sourceId: 9001,
|
||||||
|
destinationId: 9002,
|
||||||
|
payload: {
|
||||||
|
src: { nation_id: 1, nation: sourceNation?.name },
|
||||||
|
dest: { nation_id: 2, nation: destinationNation?.name },
|
||||||
|
text: expected.text,
|
||||||
|
option: expected.option,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(core.after.messages).toHaveLength(1);
|
||||||
|
expect(core.after.messages[0]).toMatchObject({
|
||||||
|
payload: {
|
||||||
|
msgType: expected.type,
|
||||||
|
src: { nationId: 1, nationName: sourceNation?.name },
|
||||||
|
dest: { nationId: 2, nationName: destinationNation?.name },
|
||||||
|
text: expected.text,
|
||||||
|
option: expected.option,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(semanticLogSignatures(nationCommandLogs(core.after.logs))).toEqual(
|
||||||
|
semanticLogSignatures(addedReferenceLogs(reference.before, reference.after.logs))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
120_000
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
type VolunteerRecruitOutcome = 'fallback' | 'intermediate' | 'completed';
|
type VolunteerRecruitOutcome = 'fallback' | 'intermediate' | 'completed';
|
||||||
|
|
||||||
const volunteerRecruitBoundaryCases: Array<{
|
const volunteerRecruitBoundaryCases: Array<{
|
||||||
|
|||||||
Reference in New Issue
Block a user