랜덤 임관 기수의 등용 실행을 차단
This commit is contained in:
@@ -595,15 +595,17 @@ describe('buildTurnCommandTable', () => {
|
||||
nationGenerals: null,
|
||||
});
|
||||
|
||||
const appointment = table.general
|
||||
.flatMap((group) => group.values)
|
||||
.find((command) => command.key === 'che_임관');
|
||||
for (const commandKey of ['che_임관', 'che_등용']) {
|
||||
const command = table.general
|
||||
.flatMap((group) => group.values)
|
||||
.find((candidate) => candidate.key === commandKey);
|
||||
|
||||
expect(appointment).toMatchObject({
|
||||
possible: false,
|
||||
status: 'blocked',
|
||||
reason: '랜덤 임관만 가능합니다',
|
||||
});
|
||||
expect(command).toMatchObject({
|
||||
possible: false,
|
||||
status: 'blocked',
|
||||
reason: '랜덤 임관만 가능합니다',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('hides founding at the Ref maxnation boundary without counting Core id=0', async () => {
|
||||
|
||||
@@ -1418,6 +1418,20 @@ describe('appRouter', () => {
|
||||
message: expect.stringContaining('랜덤 임관만 가능합니다'),
|
||||
});
|
||||
expect(randomOnlyWrites).toHaveLength(0);
|
||||
|
||||
await expect(
|
||||
randomOnlyCaller.turns.reserved.setGeneral({
|
||||
generalId: general.id,
|
||||
turnIndex: 0,
|
||||
action: 'che_등용',
|
||||
args: { destGeneralId: 2 },
|
||||
expectedRevision: 0,
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
code: 'PRECONDITION_FAILED',
|
||||
message: expect.stringContaining('랜덤 임관만 가능합니다'),
|
||||
});
|
||||
expect(randomOnlyWrites).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('checks nation treaty-term reservation permission but not full diplomacy state', async () => {
|
||||
|
||||
@@ -268,12 +268,13 @@ export const applyLegacyGeneralProgression = (
|
||||
const resolveConstraintEnv = (
|
||||
world: TurnWorldState,
|
||||
scenarioMeta: ScenarioMeta | undefined,
|
||||
env: TurnCommandEnv
|
||||
env: TurnCommandEnv,
|
||||
worldConfig: Record<string, unknown>
|
||||
): Record<string, unknown> => {
|
||||
const worldMeta = asRecord(world.meta);
|
||||
const startYear = typeof scenarioMeta?.startYear === 'number' ? scenarioMeta.startYear : undefined;
|
||||
const relYear = typeof startYear === 'number' ? world.currentYear - startYear : undefined;
|
||||
const joinModeRaw = worldMeta.join_mode ?? worldMeta.joinMode;
|
||||
const joinModeRaw = worldConfig.join_mode ?? worldConfig.joinMode ?? worldMeta.join_mode ?? worldMeta.joinMode;
|
||||
const joinMode = joinModeRaw === 'onlyRandom' ? 'onlyRandom' : 'full';
|
||||
const killturnRaw = worldMeta.killturn;
|
||||
const killturn =
|
||||
@@ -1037,7 +1038,12 @@ export const createReservedTurnHandler = async (options: {
|
||||
const worldOverlay = worldRef ? createWorldOverlay(worldRef) : null;
|
||||
const worldView = worldOverlay?.view ?? worldRef;
|
||||
const baseConstraintEnv = {
|
||||
...resolveConstraintEnv(context.world, options.scenarioMeta, env),
|
||||
...resolveConstraintEnv(
|
||||
context.world,
|
||||
options.scenarioMeta,
|
||||
env,
|
||||
options.getWorld()?.getWorldConfig() ?? asRecord(options.scenarioConfig)
|
||||
),
|
||||
...(options.map ? { map: options.map } : {}),
|
||||
...(options.unitSet ? { unitSet: options.unitSet } : {}),
|
||||
};
|
||||
@@ -2522,7 +2528,7 @@ export const createImmediateGeneralActionExecutor = async (options: {
|
||||
|
||||
const state = options.world.getState();
|
||||
const constraintEnv = {
|
||||
...resolveConstraintEnv(state, options.scenarioMeta, env),
|
||||
...resolveConstraintEnv(state, options.scenarioMeta, env, options.world.getWorldConfig()),
|
||||
...(options.map ? { map: options.map } : {}),
|
||||
...(options.world.getUnitSet() ? { unitSet: options.world.getUnitSet() } : {}),
|
||||
cities: options.world.listCities(),
|
||||
|
||||
@@ -161,6 +161,7 @@ const buildImmediateActionWorld = (options: {
|
||||
availableInstantRetreat?: boolean;
|
||||
lastTurnTime?: Date;
|
||||
scenarioConst?: Record<string, unknown>;
|
||||
worldConfig?: Record<string, unknown>;
|
||||
}) => {
|
||||
const state: TurnWorldState = {
|
||||
id: 1,
|
||||
@@ -209,6 +210,7 @@ const buildImmediateActionWorld = (options: {
|
||||
},
|
||||
},
|
||||
scenarioMeta,
|
||||
worldConfig: options.worldConfig,
|
||||
map: options.map,
|
||||
};
|
||||
const world = new InMemoryTurnWorld(state, snapshot, { schedule });
|
||||
@@ -694,6 +696,71 @@ describe('my information world commands', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects recruitment-letter acceptance in a random-appointment-only world', async () => {
|
||||
const recipient = buildGeneral({
|
||||
id: 8,
|
||||
userId: 'user-8',
|
||||
name: '재야장수',
|
||||
nationId: 0,
|
||||
cityId: 1,
|
||||
officerLevel: 0,
|
||||
});
|
||||
const recruiter = buildGeneral({
|
||||
id: 9,
|
||||
userId: 'user-9',
|
||||
name: '등용장수',
|
||||
nationId: 2,
|
||||
cityId: 2,
|
||||
});
|
||||
const map = {
|
||||
id: 'test',
|
||||
name: 'test',
|
||||
cities: [buildMapCity(1, [2]), buildMapCity(2, [1])],
|
||||
};
|
||||
const fixture = buildImmediateActionWorld({
|
||||
general: recipient,
|
||||
additionalGenerals: [recruiter],
|
||||
cities: [
|
||||
{ id: 1, name: '낙양', nationId: 0, supplyState: 1, meta: {} },
|
||||
{ id: 2, name: '장안', nationId: 2, supplyState: 1, meta: {} },
|
||||
] as TurnWorldSnapshot['cities'],
|
||||
nations: [
|
||||
{
|
||||
id: 2,
|
||||
name: '등용국',
|
||||
color: '#222222',
|
||||
typeCode: 'che_중립',
|
||||
level: 1,
|
||||
capitalCityId: 2,
|
||||
chiefGeneralId: recruiter.id,
|
||||
gold: 0,
|
||||
rice: 0,
|
||||
power: 0,
|
||||
meta: { gennum: 1 },
|
||||
},
|
||||
] as TurnWorldSnapshot['nations'],
|
||||
map,
|
||||
worldConfig: { joinMode: 'onlyRandom' },
|
||||
});
|
||||
const executor = await createImmediateGeneralActionExecutor({
|
||||
world: fixture.world,
|
||||
reservedTurns: fixture.reservedTurns,
|
||||
scenarioMeta: fixture.scenarioMeta,
|
||||
map,
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute({
|
||||
actionKey: 'che_등용수락',
|
||||
generalId: recipient.id,
|
||||
rng: new RandUtil(new LiteHashDRBG('reject-random-only-recruitment-letter')),
|
||||
args: { destNationId: 2, destGeneralId: recruiter.id },
|
||||
})
|
||||
).resolves.toEqual({ ok: false, reason: '랜덤 임관만 가능합니다 등용수락 실패.' });
|
||||
expect(fixture.world.getGeneralById(recipient.id)?.nationId).toBe(0);
|
||||
expect(fixture.world.peekDirtyState().logs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('accepts a recruitment letter after the recruiter was deleted and preserves the reserved command', async () => {
|
||||
const deletedRecruiterId = 99;
|
||||
const originalLastTurn = { command: '내정 특기 초기화', arg: { phase: 2 } };
|
||||
|
||||
Reference in New Issue
Block a user