- packages/games/src/marble: 판 데이터(board.ts, 40칸·가격표·행운카드 20장), 상태 기계(rules.ts), 게임 정의(index.ts). 더블·3더블 무인도, 무인도/탈출권/탈출비, 우주여행·탑승료, 사회복지기금, 통행료(관광지 2곳 이상 2배·면제권), 빚 갚기·파산(채권자에게 매각 대금), 인수·랜드마크·독점 승리·교환 옵션, 라운드 제한과 총자산 순위(동점이면 포인트 비교). 카드 더미 순서는 비공개(남은 장 수만), 시스템 전용 TIMEOUT(굴리기/안 사기/싼 것부터 매각/거절), onLeave = 기권. - 테스트 80개: 문서 §10 체크리스트 전 항목, 1,000판 시드 무작위 대국(포인트 장부·소유 일관성· onTimeout 합법), 엔진 randomPlayout·리플레이 결정성, 시간 초과만으로도 끝나는지. - 화면: SVG 정사각형 판(도시 이름·소유 색·건물 표시·말), 가운데 주사위·결정 프롬프트, 행운카드 공개, 우주여행 칸 고르기와 미리 보기, 빚 갚기 막대·매각 목록, 교환 시트, 진행 기록, PC·휴대폰·다크 모드. 화폐 대신 놀이용 포인트(P) 표기, no-money-wording 검사 대상에 추가. - 등록: GAMES, CATALOG(주사위 땅따먹기), 웹 registry, GAME_ICON, @bg/games/marble/board export. - E2E marble 계획: PC 방장 + 휴대폰 손님, 15라운드·200P로 결과 화면까지 실제 진행. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
40 lines
1.6 KiB
TypeScript
40 lines
1.6 KiB
TypeScript
/**
|
|
* Guard (docs/04 §6, reviewer request): betting-game UI text must never look like real money.
|
|
* Scans the betting games' screens and shared chip/card components for currency-like wording.
|
|
*/
|
|
import { expect, test } from 'bun:test';
|
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const ROOTS = ['games/poker', 'games/seotda', 'games/gostop', 'games/marble', 'components/cards'].map((p) => join(import.meta.dir, p));
|
|
const FORBIDDEN: [RegExp, string][] = [
|
|
[/₩|\$\s?\d|\d\s?\$/, '통화 기호'],
|
|
[/\d[\d,]*\s*(만\s*)?원(?![가-힣])/, '"원" 단위'],
|
|
[/판돈|돈을|돈이|돈은|현금|머니|골드/, '돈을 떠올리는 낱말'],
|
|
[/충전|환전|입금|출금|송금|결제|구매/, '거래 낱말'],
|
|
];
|
|
|
|
function files(dir: string): string[] {
|
|
if (!existsSync(dir)) return [];
|
|
return readdirSync(dir).flatMap((f) => {
|
|
const p = join(dir, f);
|
|
return statSync(p).isDirectory() ? files(p) : /\.(tsx?|css)$/.test(f) ? [p] : [];
|
|
});
|
|
}
|
|
|
|
/** Only string literals / JSX text matter; comments are ignored. */
|
|
function visibleText(src: string): string {
|
|
return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
}
|
|
|
|
test('no currency-like wording in betting game UI', () => {
|
|
const problems: string[] = [];
|
|
for (const f of ROOTS.flatMap(files)) {
|
|
const text = visibleText(readFileSync(f, 'utf8'));
|
|
text.split('\n').forEach((line, i) => {
|
|
for (const [re, why] of FORBIDDEN) if (re.test(line)) problems.push(`${f.split('/src/')[1]}:${i + 1} ${why}: ${line.trim().slice(0, 80)}`);
|
|
});
|
|
}
|
|
expect(problems).toEqual([]);
|
|
});
|