- 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>
103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
/** Trump card drawn in SVG (no image assets). Big corner index for readability on PC and phones. */
|
|
import type { CSSProperties } from 'react';
|
|
|
|
const SUIT_SYMBOL: Record<string, string> = { S: '♠', H: '♥', D: '◆', C: '♣' };
|
|
const SUIT_KO: Record<string, string> = { S: '스페이드', H: '하트', D: '다이아', C: '클로버' };
|
|
const RANK_LABEL: Record<string, string> = { T: '10', J: 'J', Q: 'Q', K: 'K', A: 'A' };
|
|
const FACE_KO: Record<string, string> = { J: '잭', Q: '퀸', K: '킹', A: '에이스' };
|
|
|
|
export type CardSize = 'sm' | 'md' | 'lg';
|
|
const WIDTH: Record<CardSize, number> = { sm: 44, md: 64, lg: 88 };
|
|
|
|
export interface PlayingCardProps {
|
|
/** e.g. "AS", "TD", "X1" (joker). null = face down. */
|
|
card: string | null;
|
|
size?: CardSize;
|
|
selected?: boolean;
|
|
dimmed?: boolean;
|
|
highlight?: boolean;
|
|
onClick?: () => void;
|
|
title?: string;
|
|
style?: CSSProperties;
|
|
/** Colour-blind friendly: 4-colour suits. */
|
|
fourColor?: boolean;
|
|
}
|
|
|
|
export function cardAria(card: string | null): string {
|
|
if (!card) return '뒤집힌 카드';
|
|
if (card.startsWith('X')) return '조커';
|
|
const r = card[0]!;
|
|
return `${SUIT_KO[card[1]!]} ${FACE_KO[r] ?? RANK_LABEL[r] ?? r}`;
|
|
}
|
|
|
|
export function PlayingCard({ card, size = 'md', selected, dimmed, highlight, onClick, title, style, fourColor }: PlayingCardProps) {
|
|
const w = WIDTH[size];
|
|
const h = Math.round(w * 1.4);
|
|
const lift = selected ? -Math.round(w * 0.22) : 0;
|
|
const common: CSSProperties = {
|
|
width: w,
|
|
height: h,
|
|
flex: 'none',
|
|
transform: `translateY(${lift}px)`,
|
|
transition: 'transform 0.12s ease',
|
|
cursor: onClick ? 'pointer' : 'default',
|
|
opacity: dimmed ? 0.45 : 1,
|
|
filter: highlight ? 'drop-shadow(0 0 6px #f59e0b)' : 'drop-shadow(0 1px 2px rgba(0,0,0,0.25))',
|
|
...style,
|
|
};
|
|
const Tag = onClick ? 'button' : 'span';
|
|
const btnReset: CSSProperties = onClick ? { padding: 0, border: 0, background: 'none' } : {};
|
|
if (!card) {
|
|
return (
|
|
<Tag style={{ ...common, ...btnReset }} onClick={onClick} aria-label={title ?? '뒤집힌 카드'} title={title}>
|
|
<svg viewBox="0 0 100 140" width={w} height={h} aria-hidden>
|
|
<rect x="1" y="1" width="98" height="138" rx="9" fill="#1e3a8a" stroke="#fff" strokeWidth="3" />
|
|
<rect x="9" y="9" width="82" height="122" rx="6" fill="none" stroke="#93c5fd" strokeWidth="2" />
|
|
<path d="M50 30 L70 70 L50 110 L30 70 Z" fill="#3b82f6" opacity="0.7" />
|
|
</svg>
|
|
</Tag>
|
|
);
|
|
}
|
|
const joker = card.startsWith('X');
|
|
const suit = card[1]!;
|
|
const rank = card[0]!;
|
|
const color = joker
|
|
? card === 'X1' ? '#111827' : '#dc2626'
|
|
: fourColor
|
|
? { S: '#111827', H: '#dc2626', D: '#2563eb', C: '#15803d' }[suit]!
|
|
: suit === 'H' || suit === 'D'
|
|
? '#dc2626'
|
|
: '#111827';
|
|
const label = joker ? '★' : (RANK_LABEL[rank] ?? rank);
|
|
const sym = joker ? '' : SUIT_SYMBOL[suit];
|
|
return (
|
|
<Tag style={{ ...common, ...btnReset }} onClick={onClick} aria-label={title ?? cardAria(card)} aria-pressed={onClick ? !!selected : undefined} title={title}>
|
|
<svg viewBox="0 0 100 140" width={w} height={h} aria-hidden>
|
|
<rect x="1" y="1" width="98" height="138" rx="9" fill="#fffef9" stroke={selected ? '#2563eb' : '#9ca3af'} strokeWidth={selected ? 5 : 2} />
|
|
<text x="9" y="33" fontSize="30" fontWeight="800" fill={color} fontFamily="Pretendard Variable, sans-serif">
|
|
{label}
|
|
</text>
|
|
<text x="11" y="60" fontSize="26" fill={color}>
|
|
{sym}
|
|
</text>
|
|
{joker ? (
|
|
<text x="50" y="98" fontSize="22" fontWeight="800" textAnchor="middle" fill={color}>
|
|
JOKER
|
|
</text>
|
|
) : 'JQK'.includes(rank) ? (
|
|
<>
|
|
<rect x="30" y="44" width="56" height="78" rx="6" fill={color} opacity="0.1" />
|
|
<text x="58" y="100" fontSize="44" fontWeight="800" textAnchor="middle" fill={color}>
|
|
{rank}
|
|
</text>
|
|
</>
|
|
) : (
|
|
<text x="60" y="108" fontSize="54" textAnchor="middle" fill={color}>
|
|
{sym}
|
|
</text>
|
|
)}
|
|
</svg>
|
|
</Tag>
|
|
);
|
|
}
|