M9: 게임 26종 30초 요약 카드·[?] 지금 할 수 있는 것 내용

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-06 13:35:21 +09:00
parent 32e3e89d8d
commit 68edd9b434
102 changed files with 6555 additions and 1 deletions

View File

@@ -0,0 +1,72 @@
import { expect, test } from 'bun:test';
import { applyAction, setupGame, usersOf } from '../help-kit.test-util';
import { GAMES } from '@bg/games';
import { helpNow } from './help';
const users = usersOf(['A', 'B']);
const view = (s: unknown, who: string | null) => GAMES.janggi!.view(s, who) as any;
const mv = (f1: number, r1: number, f2: number, r2: number) => ({ type: 'move', from: { f: f1, r: r1 }, to: { f: f2, r: r2 } });
/** A = 초, B = 한; both 상차림 chosen (한 first), 초 to move. */
function started(o: Record<string, unknown> = {}) {
const { state } = setupGame('janggi', 2, { sideAssignment: 'hostCho', setupOrder: 'hanFirst', ...o });
const s = applyAction('janggi', state, 'B', { type: 'chooseSetup', arrangement: 'MSSM' });
return applyAction('janggi', s, 'A', { type: 'chooseSetup', arrangement: 'SMMS' });
}
/** Custom position in play (kept consistent with the rule module: 장군 flag from the real board). */
function custom(pieces: [string, string, number, number][], turn: 'cho' | 'han', inCheck: 'cho' | 'han' | null) {
const s = started();
s.board = new Array(90).fill(null);
for (const [side, kind, f, r] of pieces) s.board[r * 9 + f] = { side, kind };
s.startBoard = s.board.slice();
s.turn = turn;
s.inCheck = inCheck;
s.positionCounts = {};
return s;
}
test('장기: 상차림 단계 (한 먼저 고르기)', () => {
const { state } = setupGame('janggi', 2, { sideAssignment: 'hostCho', setupOrder: 'hanFirst' });
expect(helpNow(view(state, 'B'), 'B', users)).toContain('상차림을 골라 주세요(나는 한)');
const cho = helpNow(view(state, 'A'), 'A', users);
expect(cho).toContain('한이 먼저 상차림을 골라요');
expect(cho).toContain('고양이님이 상차림을 고르는 중');
expect(helpNow(view(state, null), null, users)).toContain('고양이님이 상차림');
const s = applyAction('janggi', state, 'B', { type: 'chooseSetup', arrangement: 'MSSM' });
expect(helpNow(view(s, 'A'), 'A', users)).toContain('한이 안상차림을 골랐어요');
});
test('장기: 내 차례 / 상대 차례 / 관전 (점수 포함)', () => {
const s = started();
const mine = helpNow(view(s, 'A'), 'A', users);
expect(mine).toContain('내 차례예요(초)');
expect(mine).toContain('초록 점');
expect(mine).toContain('[⏸ 한수쉼]');
expect(mine).toContain('초 72점, 한 73.5점');
expect(helpNow(view(s, 'B'), 'B', users)).toContain('나는 한이에요');
expect(helpNow(view(s, null), null, users)).toContain('호랑이님(초) 차례예요');
});
test('장기: 장군을 받으면 한수쉼 없이 궁을 지키라고 안내', () => {
const s = custom([['cho', 'king', 3, 0], ['han', 'king', 4, 9], ['cho', 'cha', 0, 9]], 'han', 'han');
const t = helpNow(view(s, 'B'), 'B', users);
expect(t).toContain('장군이에요!');
expect(t).not.toContain('[⏸ 한수쉼]');
expect(helpNow(view(s, 'A'), 'A', users)).toContain('내가 장군을 불렀어요');
expect(helpNow(view(s, null), null, users)).toContain('한 궁이 장군을 받았어요');
});
test('장기: 빅장을 받은 쪽 안내 (점수제/비점수제)', () => {
for (const scoring of [true, false]) {
const s0 = started({ bikjang: 'kja30', scoring });
const s = custom([['cho', 'king', 4, 1], ['cho', 'jol', 4, 4], ['cho', 'cha', 0, 0], ['han', 'king', 4, 8], ['han', 'jol', 0, 6], ['han', 'cha', 8, 5], ['han', 'sa', 3, 9]], 'cho', null);
s.options = s0.options;
const after = applyAction('janggi', s, 'A', mv(4, 4, 3, 4));
expect(after.bikjang?.calledBy).toBe('cho');
const t = helpNow(view(after, 'B'), 'B', users);
expect(t).toContain('빅장이에요!');
expect(t).toContain(scoring ? '점수로 승부를 가려요' : '무승부로 끝나요');
expect(helpNow(view(after, null), null, users)).toContain('초가 빅장을 불렀어요');
}
});