M7: 보석 상인 규칙·화면·테스트
- 규칙 모듈 packages/games/src/splendor: 카드 90장·귀족 10장 데이터 파일 분리(cards.ts), 보석 가져오기(3색/같은 색 2개, allowFewerTokens), 예약(공개/더미, 황금), 구매(자동·수동 지불), 10개 한도 반납, 귀족 방문·선택, 라운드 끝까지 진행 후 종료, 동점은 카드 적은 쪽, 넘기기와 모두 넘기면 종료, 기권, 시간 초과 자동 행동(3번 연속이면 자리 비움), view는 더미·블라인드 예약·제외 귀족을 숨기고 살 수 있는 카드·지불 미리보기를 계산 - 테스트: 문서 10절 시나리오 42개 + 무작위 대국 1,000판(보존·정보 유출·onTimeout 합법·결정성) + 엔진 randomPlayout - 화면 apps/web/src/games/splendor: 자체 카드·귀족·보석 그림(색+모양+글자), 3단계 시장과 더미, 보석 은행, 내 보너스·보석·예약, 상대 요약, 살 수 있는 카드 강조, 반납·귀족 선택, PC/휴대폰·다크 모드 - 등록: GAMES, 카탈로그, 화면 레지스트리, 아이콘 - E2E: PC 방장 + 휴대폰 친구가 10점 판을 결과까지 진행 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
76
packages/games/src/splendor/cards.ts
Normal file
76
packages/games/src/splendor/cards.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 보석 상인 component data (docs/games/splendor.md §3.1, §3.2). Kept in a separate data file so the card
|
||||
* list can be swapped for an own list with the same structure (§12). Encoding: `<id>:<points>:<W U G R K cost digits>`.
|
||||
*/
|
||||
export const COLORS = ['W', 'U', 'G', 'R', 'K'] as const;
|
||||
export type Color = (typeof COLORS)[number];
|
||||
export type TokenColor = Color | 'gold';
|
||||
export const TOKEN_COLORS: readonly TokenColor[] = [...COLORS, 'gold'];
|
||||
export type Gems = Record<Color, number>;
|
||||
export type Tokens = Record<TokenColor, number>;
|
||||
export type Tier = 1 | 2 | 3;
|
||||
export const TIERS: readonly Tier[] = [1, 2, 3];
|
||||
|
||||
export interface DevCard {
|
||||
id: string;
|
||||
tier: Tier;
|
||||
bonus: Color;
|
||||
points: number;
|
||||
cost: Gems;
|
||||
}
|
||||
export interface Noble {
|
||||
id: string;
|
||||
points: 3;
|
||||
req: Gems;
|
||||
}
|
||||
|
||||
const RAW_CARDS = [
|
||||
// tier 1
|
||||
'1W01:0:01111', '1W02:0:01211', '1W03:0:02201', '1W04:0:31001', '1W05:0:00021', '1W06:0:02002', '1W07:0:03000', '1W08:1:00400',
|
||||
'1U01:0:10111', '1U02:0:10121', '1U03:0:10220', '1U04:0:01310', '1U05:0:10002', '1U06:0:00202', '1U07:0:00003', '1U08:1:00040',
|
||||
'1G01:0:11011', '1G02:0:11012', '1G03:0:01022', '1G04:0:13100', '1G05:0:21000', '1G06:0:02020', '1G07:0:00030', '1G08:1:00004',
|
||||
'1R01:0:11101', '1R02:0:21101', '1R03:0:20102', '1R04:0:10013', '1R05:0:02100', '1R06:0:20020', '1R07:0:30000', '1R08:1:40000',
|
||||
'1K01:0:11110', '1K02:0:12110', '1K03:0:22010', '1K04:0:00131', '1K05:0:00210', '1K06:0:20200', '1K07:0:00300', '1K08:1:04000',
|
||||
// tier 2
|
||||
'2W01:1:00322', '2W02:1:23030', '2W03:2:00142', '2W04:2:00053', '2W05:2:00050', '2W06:3:60000', '2U01:1:02230', '2U02:1:02303',
|
||||
'2U03:2:53000', '2U04:2:20014', '2U05:2:05000', '2U06:3:06000', '2G01:1:30230', '2G02:1:23002', '2G03:2:42001', '2G04:2:05300',
|
||||
'2G05:2:00500', '2G06:3:00600', '2R01:1:20023', '2R02:1:03023', '2R03:2:14200', '2R04:2:30005', '2R05:2:00005', '2R06:3:00060',
|
||||
'2K01:1:32200', '2K02:1:30302', '2K03:2:01420', '2K04:2:00530', '2K05:2:50000', '2K06:3:00006',
|
||||
// tier 3
|
||||
'3W01:3:03353', '3W02:4:00007', '3W03:4:30036', '3W04:5:30007', '3U01:3:30335', '3U02:4:70000', '3U03:4:63003', '3U04:5:73000',
|
||||
'3G01:3:53033', '3G02:4:07000', '3G03:4:36300', '3G04:5:07300', '3R01:3:35303', '3R02:4:00700', '3R03:4:03630', '3R04:5:00730',
|
||||
'3K01:3:33530', '3K02:4:00070', '3K03:4:00363', '3K04:5:00073',
|
||||
];
|
||||
|
||||
const RAW_NOBLES = [
|
||||
'N01:00440', 'N02:04400', 'N03:44000', 'N04:40004', 'N05:00044', 'N06:03330', 'N07:33300', 'N08:33003', 'N09:30033', 'N10:00333',
|
||||
];
|
||||
|
||||
const gems = (digits: string): Gems => ({ W: +digits[0]!, U: +digits[1]!, G: +digits[2]!, R: +digits[3]!, K: +digits[4]! });
|
||||
|
||||
export const CARDS: readonly DevCard[] = RAW_CARDS.map((raw) => {
|
||||
const [id, points, cost] = raw.split(':') as [string, string, string];
|
||||
return { id, tier: Number(id[0]) as Tier, bonus: id[1] as Color, points: Number(points), cost: gems(cost) };
|
||||
});
|
||||
export const NOBLES: readonly Noble[] = RAW_NOBLES.map((raw) => {
|
||||
const [id, req] = raw.split(':') as [string, string];
|
||||
return { id, points: 3, req: gems(req) };
|
||||
});
|
||||
|
||||
const CARD_BY_ID = new Map(CARDS.map((c) => [c.id, c]));
|
||||
const NOBLE_BY_ID = new Map(NOBLES.map((n) => [n.id, n]));
|
||||
export function cardOf(id: string): DevCard {
|
||||
const c = CARD_BY_ID.get(id);
|
||||
if (!c) throw new Error(`unknown card ${id}`);
|
||||
return c;
|
||||
}
|
||||
export function nobleOf(id: string): Noble {
|
||||
const n = NOBLE_BY_ID.get(id);
|
||||
if (!n) throw new Error(`unknown noble ${id}`);
|
||||
return n;
|
||||
}
|
||||
export const isCardId = (id: string): boolean => CARD_BY_ID.has(id);
|
||||
|
||||
export const emptyGems = (): Gems => ({ W: 0, U: 0, G: 0, R: 0, K: 0 });
|
||||
export const emptyTokens = (): Tokens => ({ W: 0, U: 0, G: 0, R: 0, K: 0, gold: 0 });
|
||||
export const sumTokens = (t: Partial<Record<TokenColor, number>>): number => TOKEN_COLORS.reduce((a, c) => a + (t[c] ?? 0), 0);
|
||||
Reference in New Issue
Block a user