M3 기반: 공용 베팅 엔진, 카드 정의, 카드·칩 화면 컴포넌트, 돈 표현 금지 검사

- packages/engine/betting: 한국식(체크·삥·콜·따당·쿼터·하프·풀·다이, 라운드당 1회·첫 행동에서만 레이즈),
  노리밋·팟리밋(최소 레이즈, 불완전 올인 레이즈는 재오픈 안 함, BB 옵션), 앤티·블라인드, 올인,
  받지 않은 베팅 반환, 사이드 팟 분할, 동점 분배(홀수 칩 순서), 시간 초과 자동 행동
- 테스트 24개: 포커·섯다 계획서 계산 예시, 행동별 상태 전이, 사이드 팟 예시, 무작위 베팅 1,500판
  (칩 총량 보존·라운드 종료·라운드 끝 금액 일치·팟 합계=판 전체)
- packages/engine/cards: 트럼프 52+조커, 화투 48(+보너스)·섯다 20장 정의와 테스트
- 웹: 직접 그린 SVG 트럼프·화투 카드, 칩 표시("1,250 칩"), 놀이용 칩 안내문
- apps/web/src/no-money-wording.test.ts: 베팅 게임 화면에 통화 기호·"원"·판돈·충전·환전 등 금지

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-04 05:45:13 +09:00
parent a10139b51a
commit 9519b09be8
10 changed files with 1370 additions and 3 deletions

View File

@@ -0,0 +1,39 @@
/**
* 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', '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([]);
});