- 보석 상인 → 스플렌더, 벽 쌓기 길찾기 → 쿼리도, 숫자 타일 → 루미큐브, 주사위 땅따먹기 → 부루마불 - deploy/docker-compose.proxy.yml: Caddy 없이 앱만(.5:2970), HTTPS·헤더는 NPMplus - deploy/npmplus-advanced.conf: Caddyfile과 같은 보안 헤더 - 문서: 2026-10-06 결정, 배포 7절 (e2e 구십구 0 카드 방향 버튼 반영 포함) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
3.7 KiB
TypeScript
77 lines
3.7 KiB
TypeScript
/**
|
|
* 스플렌더 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);
|