merge: 기본 사령턴 목록과 분류를 Ref에 맞춤

This commit is contained in:
2026-08-16 07:19:03 +00:00
5 changed files with 248 additions and 12 deletions
+70 -1
View File
@@ -10,6 +10,7 @@ import type {
GeneralTurnCommandKey,
MapDefinition,
Nation,
NationTurnCommandKey,
NationTurnCommandSpec,
RequirementKey,
StateView,
@@ -150,6 +151,54 @@ const REF_GENERAL_COMMAND_POSITION = new Map<string, { category: string; index:
)
);
const REF_NATION_COMMAND_GROUPS = [
{
category: '휴식',
commands: ['휴식'],
},
{
category: '인사',
commands: ['che_발령', 'che_포상', 'che_몰수', 'che_부대탈퇴지시'],
},
{
category: '외교',
commands: ['che_물자원조', 'che_불가침제의', 'che_선전포고', 'che_종전제의', 'che_불가침파기제의'],
},
{
category: '특수',
commands: ['che_초토화', 'che_천도', 'che_증축', 'che_감축'],
},
{
category: '전략',
commands: [
'che_필사즉생',
'che_백성동원',
'che_수몰',
'che_허보',
'che_의병모집',
'che_이호경식',
'che_급습',
'che_피장파장',
],
},
{
category: '기타',
commands: ['che_국기변경', 'che_국호변경'],
},
] as const satisfies ReadonlyArray<{
category: string;
commands: ReadonlyArray<NationTurnCommandKey>;
}>;
const REF_NATION_CATEGORY_ORDER = new Map<string, number>(
REF_NATION_COMMAND_GROUPS.map(({ category }, index) => [category, index] as const)
);
const REF_NATION_COMMAND_POSITION = new Map<string, { category: string; index: number }>(
REF_NATION_COMMAND_GROUPS.flatMap(({ category, commands }) =>
commands.map((command, index) => [command, { category, index }] as const)
)
);
const INPUT_REQUIREMENT_KINDS = new Set<RequirementKey['kind']>([
'destGeneral',
'destCity',
@@ -665,6 +714,26 @@ const projectRefGeneralCommandGroups = (entries: CommandEntry[]): CommandEntry[]
)
.map(({ entry }) => entry);
const projectRefNationCommandGroups = (entries: CommandEntry[]): CommandEntry[] =>
entries
.map((entry, profileIndex) => {
const refPosition = REF_NATION_COMMAND_POSITION.get(entry.definition.key as NationTurnCommandKey);
return {
entry: refPosition ? { ...entry, category: refPosition.category } : entry,
categoryIndex:
REF_NATION_CATEGORY_ORDER.get(refPosition?.category ?? entry.category) ?? Number.MAX_SAFE_INTEGER,
commandIndex: refPosition?.index ?? profileIndex,
profileIndex,
};
})
.sort(
(left, right) =>
left.categoryIndex - right.categoryIndex ||
left.commandIndex - right.commandIndex ||
left.profileIndex - right.profileIndex
)
.map(({ entry }) => entry);
export const buildTurnCommandTable = async (options: {
worldState: WorldStateRow;
general: GeneralRow;
@@ -697,7 +766,7 @@ export const buildTurnCommandTable = async (options: {
return {
general: buildGroups(projectRefGeneralCommandGroups(generalEntries), ctx, view),
nation: buildGroups(nationEntries, ctx, view),
nation: buildGroups(projectRefNationCommandGroups(nationEntries), ctx, view),
inputOptions: options.inputOptions ?? {
cities: [],
nations: [],
+31 -1
View File
@@ -100,7 +100,7 @@ const buildNation = (): NationRow =>
}) as unknown as NationRow;
describe('buildTurnCommandTable', () => {
it('projects the main reserved-turn categories and command order from Ref', async () => {
it('projects the general and chief reserved-turn categories and command order from Ref', async () => {
const table = await buildTurnCommandTable({
worldState: buildWorldState(),
general: buildGeneral(),
@@ -140,6 +140,36 @@ describe('buildTurnCommandTable', () => {
: ['che_선동', 'che_탈취', 'che_파괴', 'che_화계'],
: ['che_증여', 'che_헌납', 'che_물자조달', 'che_하야', 'che_거병', 'che_건국', 'che_선양', 'che_해산'],
});
expect(table.nation.map(({ category }) => category)).toEqual(['휴식', '인사', '외교', '특수', '전략', '기타']);
expect(
Object.fromEntries(table.nation.map(({ category, values }) => [category, values.map(({ key }) => key)]))
).toEqual({
: ['휴식'],
: ['che_발령', 'che_포상', 'che_몰수', 'che_부대탈퇴지시'],
: ['che_물자원조', 'che_불가침제의', 'che_선전포고', 'che_종전제의', 'che_불가침파기제의'],
: ['che_초토화', 'che_천도', 'che_증축', 'che_감축'],
: [
'che_필사즉생',
'che_백성동원',
'che_수몰',
'che_허보',
'che_의병모집',
'che_이호경식',
'che_급습',
'che_피장파장',
],
: ['che_국기변경', 'che_국호변경'],
});
expect(
Object.fromEntries(table.nation.map(({ category, values }) => [category, values.map(({ name }) => name)]))
).toEqual({
: ['휴식'],
: ['발령', '포상', '몰수', '부대 탈퇴 지시'],
: ['원조', '불가침 제의', '선전포고', '종전 제의', '불가침 파기 제의'],
: ['초토화', '천도', '증축', '감축'],
: ['필사즉생', '백성동원', '수몰', '허보', '의병모집', '이호경식', '급습', '피장파장'],
: ['국기변경', '국호변경'],
});
});
it('projects the Ref availability boundaries for force move, retirement, and resignation', async () => {
@@ -35,7 +35,32 @@ const GENERAL_REF_EDITOR_ACTIONS = [
] as const;
const GENERAL_REF_STRATEGY_ACTIONS = ['che_선동', 'che_탈취', 'che_파괴', 'che_화계'] as const;
const GENERAL_REF_STRATEGY_ACTION_SET = new Set<string>(GENERAL_REF_STRATEGY_ACTIONS);
const NATION_REF_EDITOR_ACTIONS = ['che_포상', 'che_발령', 'che_증축', 'che_필사즉생'] as const;
const NATION_REF_EDITOR_ACTIONS = [
'휴식',
'che_발령',
'che_포상',
'che_몰수',
'che_부대탈퇴지시',
'che_물자원조',
'che_불가침제의',
'che_선전포고',
'che_종전제의',
'che_불가침파기제의',
'che_초토화',
'che_천도',
'che_증축',
'che_감축',
'che_필사즉생',
'che_백성동원',
'che_수몰',
'che_허보',
'che_의병모집',
'che_이호경식',
'che_급습',
'che_피장파장',
'che_국기변경',
'che_국호변경',
] as const;
describe('default turn command profile AI coverage', () => {
it('loads every action selected directly by the general and nation AI', async () => {
@@ -52,6 +77,6 @@ describe('default turn command profile AI coverage', () => {
expect(profile.general.filter((action) => GENERAL_REF_STRATEGY_ACTION_SET.has(action))).toEqual(
GENERAL_REF_STRATEGY_ACTIONS
);
expect(profile.nation).toEqual(expect.arrayContaining([...NATION_REF_EDITOR_ACTIONS]));
expect(profile.nation).toEqual(NATION_REF_EDITOR_ACTIONS);
});
});
@@ -231,6 +231,66 @@ const commandTable = {
],
inputOptions,
};
const buildSimpleCommand = (key: string, name: string) => ({
key,
name,
reqArg: false,
possible: true,
status: 'available',
inputFields: [],
});
const refChiefCommandTable = {
general: [],
nation: [
{ category: '휴식', values: [buildSimpleCommand('휴식', '휴식')] },
{
category: '인사',
values: [
buildSimpleCommand('che_발령', '발령'),
buildSimpleCommand('che_포상', '포상'),
buildSimpleCommand('che_몰수', '몰수'),
buildSimpleCommand('che_부대탈퇴지시', '부대 탈퇴 지시'),
],
},
{
category: '외교',
values: [
buildSimpleCommand('che_물자원조', '원조'),
buildSimpleCommand('che_불가침제의', '불가침 제의'),
buildSimpleCommand('che_선전포고', '선전포고'),
buildSimpleCommand('che_종전제의', '종전 제의'),
buildSimpleCommand('che_불가침파기제의', '불가침 파기 제의'),
],
},
{
category: '특수',
values: [
buildSimpleCommand('che_초토화', '초토화'),
buildSimpleCommand('che_천도', '천도'),
buildSimpleCommand('che_증축', '증축'),
buildSimpleCommand('che_감축', '감축'),
],
},
{
category: '전략',
values: [
buildSimpleCommand('che_필사즉생', '필사즉생'),
buildSimpleCommand('che_백성동원', '백성동원'),
buildSimpleCommand('che_수몰', '수몰'),
buildSimpleCommand('che_허보', '허보'),
buildSimpleCommand('che_의병모집', '의병모집'),
buildSimpleCommand('che_이호경식', '이호경식'),
buildSimpleCommand('che_급습', '급습'),
buildSimpleCommand('che_피장파장', '피장파장'),
],
},
{
category: '기타',
values: [buildSimpleCommand('che_국기변경', '국기변경'), buildSimpleCommand('che_국호변경', '국호변경')],
},
],
inputOptions,
};
const generalContext = {
general: {
id: 1,
@@ -618,6 +678,51 @@ test('reserves force move, retirement, and resignation from the user command pic
await picker.screenshot({ path: test.info().outputPath('special-user-commands-mobile-500.png') });
});
test('shows every Ref chief command in the exact category and command order', async ({ page }) => {
await install(page, false, refChiefCommandTable);
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('/che/chief-center');
const editor = page.locator('[data-command-scope="nation"]');
await editor.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
const picker = page.getByTestId('command-picker');
const expected = {
: ['휴식'],
: ['발령', '포상', '몰수', '부대 탈퇴 지시'],
: ['원조', '불가침 제의', '선전포고', '종전 제의', '불가침 파기 제의'],
: ['초토화', '천도', '증축', '감축'],
: ['필사즉생', '백성동원', '수몰', '허보', '의병모집', '이호경식', '급습', '피장파장'],
: ['국기변경', '국호변경'],
} as const;
await expect(picker.locator('.category-btn')).toHaveText(Object.keys(expected));
for (const [category, commands] of Object.entries(expected)) {
await picker.locator('.category-btn').filter({ hasText: category }).click();
await expect(picker.locator('.command-grid .command-item')).toHaveText([...commands]);
}
const rename = picker.getByRole('button', { name: '국호변경', exact: true });
await rename.hover();
await rename.focus();
await expect(rename).toBeFocused();
await picker.screenshot({ path: test.info().outputPath('ref-chief-command-list-desktop-1200.png') });
await picker.getByRole('button', { name: '명령 입력 닫기', exact: true }).click();
await page.setViewportSize({ width: 500, height: 900 });
const mobileEditor = page.locator('[data-command-scope="nation"]:visible');
await mobileEditor.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
const mobilePicker = page.locator('[data-testid="command-picker"]:visible');
await expect(mobilePicker.locator('.category-btn')).toHaveText(Object.keys(expected));
const mobileGeometry = await mobilePicker.evaluate((element) => ({
width: element.getBoundingClientRect().width,
horizontalOverflow: element.scrollWidth - element.clientWidth,
categoryColumns: getComputedStyle(element.querySelector<HTMLElement>('.category-list')!).gridTemplateColumns,
}));
expect(mobileGeometry.width).toBeLessThanOrEqual(500);
expect(mobileGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
expect(mobileGeometry.categoryColumns.split(' ')).toHaveLength(3);
await mobilePicker.screenshot({ path: test.info().outputPath('ref-chief-command-list-mobile-500.png') });
});
test('enters general and nation command arguments and sends exact values', async ({ page }) => {
const requests = await install(page);
await page.setViewportSize({ width: 1200, height: 900 });
+15 -8
View File
@@ -46,21 +46,28 @@
],
"nation": [
"휴식",
"che_발령",
"che_포상",
"che_몰수",
"che_부대탈퇴지시",
"che_발령",
"che_물자원조",
"che_불가침제의",
"che_선전포고",
"che_종전제의",
"che_불가침파기제의",
"che_초토화",
"che_천도",
"che_증축",
"che_선전포고",
"che_불가침제의",
"che_불가침파기제의",
"che_의병모집",
"che_허보",
"che_감축",
"che_필사즉생",
"che_백성동원",
"che_이호경식",
"che_수몰",
"che_급습"
"che_허보",
"che_의병모집",
"che_이호경식",
"che_급습",
"che_피장파장",
"che_국기변경",
"che_국호변경"
]
}