From cda88738faeb019863acb3c8dac6acf66c0a6f57 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 16 Aug 2026 07:09:03 +0000 Subject: [PATCH] =?UTF-8?q?fix(turn):=20=EA=B8=B0=EB=B3=B8=20=EC=82=AC?= =?UTF-8?q?=EB=A0=B9=ED=84=B4=20=EB=AA=A9=EB=A1=9D=EA=B3=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A5=98=EB=A5=BC=20Ref=EC=97=90=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref 사령부 조작기의 24개 공개 명령을 기본 프로필에 모두 포함하고 카테고리와 내부 순서를 그대로 투영한다.\n\n35개 국가 명령 차등 실행과 조건·로그 비교, 실제 Chromium 목록 검사를 추가한다. --- app/game-api/src/turns/commandTable.ts | 71 +++++++++++- app/game-api/test/commandTable.test.ts | 32 +++++- .../defaultCommandProfileAiCoverage.test.ts | 29 ++++- .../e2e/commandArguments.spec.ts | 105 ++++++++++++++++++ resources/turn-commands/default.json | 23 ++-- 5 files changed, 248 insertions(+), 12 deletions(-) diff --git a/app/game-api/src/turns/commandTable.ts b/app/game-api/src/turns/commandTable.ts index d70174c1..5e5c32ef 100644 --- a/app/game-api/src/turns/commandTable.ts +++ b/app/game-api/src/turns/commandTable.ts @@ -10,6 +10,7 @@ import type { GeneralTurnCommandKey, MapDefinition, Nation, + NationTurnCommandKey, NationTurnCommandSpec, RequirementKey, StateView, @@ -150,6 +151,54 @@ const REF_GENERAL_COMMAND_POSITION = new Map; +}>; + +const REF_NATION_CATEGORY_ORDER = new Map( + REF_NATION_COMMAND_GROUPS.map(({ category }, index) => [category, index] as const) +); +const REF_NATION_COMMAND_POSITION = new Map( + REF_NATION_COMMAND_GROUPS.flatMap(({ category, commands }) => + commands.map((command, index) => [command, { category, index }] as const) + ) +); + const INPUT_REQUIREMENT_KINDS = new Set([ '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: [], diff --git a/app/game-api/test/commandTable.test.ts b/app/game-api/test/commandTable.test.ts index 96699fee..15ce9ce8 100644 --- a/app/game-api/test/commandTable.test.ts +++ b/app/game-api/test/commandTable.test.ts @@ -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 () => { diff --git a/app/game-engine/test/defaultCommandProfileAiCoverage.test.ts b/app/game-engine/test/defaultCommandProfileAiCoverage.test.ts index f71197ee..31f30b3c 100644 --- a/app/game-engine/test/defaultCommandProfileAiCoverage.test.ts +++ b/app/game-engine/test/defaultCommandProfileAiCoverage.test.ts @@ -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(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); }); }); diff --git a/app/game-frontend/e2e/commandArguments.spec.ts b/app/game-frontend/e2e/commandArguments.spec.ts index 3b1c4a0f..dde3ebb1 100644 --- a/app/game-frontend/e2e/commandArguments.spec.ts +++ b/app/game-frontend/e2e/commandArguments.spec.ts @@ -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('.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 }); diff --git a/resources/turn-commands/default.json b/resources/turn-commands/default.json index e754e261..29a6a649 100644 --- a/resources/turn-commands/default.json +++ b/resources/turn-commands/default.json @@ -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_국호변경" ] }