/** 라스베가스 화면 (docs/games/las-vegas.md §9). Bills are play points of this room only (docs/04 §6). */ import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import type { LasVegasChoice, LasVegasOptions, LasVegasPayoutView, LasVegasView } from '@bg/games/las-vegas'; import type { PublicUser } from '@bg/shared/lite'; import { Avatar, ConfirmSheet } from '../../components/ui'; import { TurnTimer } from '../../components/TurnTimer'; import { haptic, sounds } from '../../lib/feedback'; import type { BoardProps, GameUI, OptionsProps } from '../types'; const ROLL_MS = 650; const HOUSE = 'house'; /** Seat colours (dice), plus white for the neutral "house" dice. */ const COLORS = ['#e5484d', '#2f7cf6', '#1f9d55', '#f2a10c', '#8e5cf0']; const COLOR_NAMES = ['빨강', '파랑', '초록', '노랑', '보라']; /** Play-bill colours per face value (deliberately toy-like). */ const BILL: Record = { 1: ['#fde68a', '#78350f'], 2: ['#bbf7d0', '#14532d'], 3: ['#bae6fd', '#0c4a6e'], 4: ['#fbcfe8', '#831843'], 5: ['#ddd6fe', '#3b0764'], 6: ['#fed7aa', '#7c2d12'], 7: ['#a7f3d0', '#064e3b'], 8: ['#c7d2fe', '#1e1b4b'], 9: ['#fecaca', '#7f1d1d'], }; const nick = (players: Record, id: string) => (id === HOUSE ? '하우스(흰 주사위)' : (players[id]?.nickname ?? '알 수 없음')); const userOf = (players: Record, id: string): PublicUser => players[id] ?? { id, nickname: '알 수 없음', avatar: null, kind: 'guest' }; const reducedMotion = () => typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches; const seatColor = (v: LasVegasView, owner: string) => (owner === HOUSE ? '#ffffff' : (COLORS[v.seats.indexOf(owner)] ?? '#888')); const initial = (players: Record, id: string) => (id === HOUSE ? '흰' : ([...nick(players, id)][0] ?? '?')); const CSS = ` .lv-wrap { width: 100%; max-width: 1100px; display: flex; flex-direction: column; gap: 10px; } .lv-head { display: flex; align-items: center; gap: 8px; justify-content: space-between; flex-wrap: wrap; padding: 10px 14px; } .lv-help { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; } .lv-help span { background: var(--warning-soft); color: var(--text); border: 2px solid var(--warning); border-radius: 999px; padding: 4px 14px; font-weight: 800; font-size: 1.02rem; } .lv-casinos { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; } .lv-casino { position: relative; display: flex; flex-direction: column; gap: 6px; padding: 8px; min-height: 196px; border-radius: var(--radius); border: 3px solid var(--border); background: var(--surface); box-shadow: var(--shadow); text-align: left; color: var(--text); font: inherit; transition: border-color .15s, transform .15s; min-width: 0; } button.lv-casino { cursor: pointer; } button.lv-casino:hover { border-color: var(--primary); } .lv-casino:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } .lv-casino.can { border-style: dashed; border-color: var(--primary); } .lv-casino.sel { border-style: solid; border-color: var(--primary); transform: translateY(-3px); box-shadow: 0 0 0 4px var(--primary-soft), var(--shadow); } .lv-casino.first { border-color: var(--success); box-shadow: 0 0 0 4px var(--success-soft), var(--shadow); } .lv-casino.tie { border-color: var(--warning); box-shadow: 0 0 0 4px var(--warning-soft), var(--shadow); } .lv-ctop { display: flex; align-items: center; gap: 6px; } .lv-cnum { font-size: 1.5rem; font-weight: 900; line-height: 1; } .lv-cname { font-size: .8rem; color: var(--text-sub); font-weight: 700; white-space: nowrap; } .lv-bills { display: flex; flex-wrap: wrap; gap: 4px; min-height: 30px; } .lv-bill { position: relative; display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 28px; padding: 0 6px; border-radius: 6px; font-weight: 900; font-size: 1.05rem; border: 2px dashed rgba(0,0,0,.35); box-shadow: inset 0 0 0 2px rgba(255,255,255,.55); font-variant-numeric: tabular-nums; } .lv-bill::before { content: '★'; font-size: .55rem; position: absolute; left: 3px; top: 1px; opacity: .6; } .lv-bill.sm { min-width: 26px; height: 22px; font-size: .85rem; padding: 0 4px; } .lv-bill.hidden { background: repeating-linear-gradient(45deg, var(--surface-2), var(--surface-2) 4px, var(--border) 4px, var(--border) 8px); color: var(--text-sub); } .lv-nobill { color: var(--text-sub); font-size: .8rem; } .lv-stacks { display: flex; flex-direction: column; gap: 3px; margin-top: auto; } .lv-stack { display: flex; align-items: center; gap: 4px; font-size: .82rem; font-weight: 800; min-width: 0; } .lv-pips { display: flex; flex-wrap: wrap; gap: 2px; min-width: 0; } .lv-dot { width: 13px; height: 13px; border-radius: 3px; border: 1.5px solid rgba(0,0,0,.45); flex: none; } .lv-tag { position: absolute; top: -11px; right: 6px; font-size: .78rem; font-weight: 900; border-radius: 999px; padding: 1px 8px; white-space: nowrap; } .lv-tag.first { background: var(--success); color: #fff; } .lv-tag.tie { background: #facc15; color: #422006; } .lv-tag.award { background: var(--primary); color: var(--primary-text); } .lv-tag.none { background: var(--surface-2); color: var(--text-sub); border: 1px solid var(--border); } @keyframes lv-hit { 0% { transform: scale(1); } 40% { transform: scale(1.05); box-shadow: 0 0 0 6px var(--primary-soft); } 100% { transform: scale(1); } } .lv-casino.hit { animation: lv-hit .55s ease-out; } .lv-lower { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: 10px; align-items: start; } .lv-tray { display: flex; flex-direction: column; gap: 10px; padding: 12px; } .lv-groups { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; } .lv-group { display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 92px; min-height: 64px; padding: 6px 10px; border-radius: 12px; border: 3px solid var(--border); background: var(--surface); color: var(--text); font: inherit; cursor: pointer; } .lv-group:disabled { cursor: default; opacity: 1; } .lv-group[aria-pressed='true'] { border-color: var(--primary); background: var(--primary-soft); } .lv-group:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } .lv-group b { font-size: 1.15rem; } .lv-group small { color: var(--text-sub); font-weight: 700; } .lv-mdice { display: flex; gap: 3px; flex-wrap: wrap; justify-content: center; } .lv-die { width: 28px; height: 28px; border-radius: 7px; border: 2px solid rgba(0,0,0,.4); flex: none; display: inline-block; } .lv-die svg { display: block; width: 100%; height: 100%; } .lv-die.lg { width: 40px; height: 40px; border-radius: 9px; } @keyframes lv-tumble { 0% { transform: translateY(-16px) rotate(-40deg); } 50% { transform: translateY(3px) rotate(190deg); } 100% { transform: translateY(0) rotate(360deg); } } .lv-die.rolling { animation: lv-tumble ${ROLL_MS}ms ease-out both; } .lv-preview { border-radius: 10px; padding: 8px 12px; font-weight: 700; text-align: center; } .lv-preview.first { background: var(--success-soft); color: var(--success); } .lv-preview.tie { background: var(--warning-soft); color: var(--warning); } .lv-preview.award, .lv-preview.none { background: var(--surface-2); } .lv-roll { min-height: 60px; font-size: 1.15rem; } .lv-settle { padding: 12px; display: flex; flex-direction: column; gap: 6px; } .lv-settle h3 { margin: 0; font-size: 1rem; } .lv-prow { display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 6px; align-items: center; font-size: .85rem; padding: 3px 0; border-bottom: 1px solid var(--border); } .lv-prow:last-child { border-bottom: 0; } .lv-pbody { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: center; min-width: 0; } .lv-chip { display: inline-flex; align-items: center; gap: 3px; white-space: nowrap; font-weight: 700; } .lv-tieout { display: inline-flex; align-items: center; gap: 3px; color: var(--warning); font-weight: 800; } .lv-tieout .lv-dot { opacity: .55; } @keyframes lv-poof { 0% { transform: scale(1.4); opacity: 0; } 50% { transform: scale(1.15); opacity: 1; } 100% { transform: scale(1); } } .lv-fresh .lv-tieout, .lv-fresh .lv-chip { animation: lv-poof .6s ease-out both; } .lv-pname { max-width: 84px; overflow: hidden; text-overflow: ellipsis; display: inline-block; vertical-align: bottom; } .lv-final { padding: 12px; } .lv-final table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; } .lv-final th, .lv-final td { padding: 4px 6px; border-bottom: 1px solid var(--border); text-align: center; white-space: nowrap; } .lv-final td.notes { white-space: normal; text-align: left; } @media (max-width: 1099px) { .lv-lower { grid-template-columns: minmax(0, 1fr); } } @media (max-width: 719px) { .lv-casinos { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; } .lv-casino { min-height: 132px; padding: 6px; gap: 4px; } .lv-wide { display: none; } .lv-cnum { font-size: 1.25rem; } .lv-cname { display: none; } .lv-bill { min-width: 26px; height: 24px; font-size: .9rem; padding: 0 4px; } .lv-dot { width: 11px; height: 11px; } .lv-help { gap: 6px; flex-wrap: nowrap; } .lv-help span { font-size: .8rem; padding: 2px 8px; white-space: nowrap; } .lv-group { min-width: 76px; padding: 6px 8px; } .lv-die { width: 22px; height: 22px; border-radius: 6px; } .lv-die.lg { width: 32px; height: 32px; } /* The tray sticks to the bottom of the screen so rolling / placing is always in reach. */ .lv-lower { display: contents; } .lv-tray { position: sticky; bottom: 0; z-index: 4; box-shadow: 0 -4px 14px rgba(0,0,0,.12); } } @media (prefers-reduced-motion: reduce) { .lv-casino, .lv-casino.sel { transition: none; transform: none; } .lv-casino.hit, .lv-die.rolling, .lv-fresh .lv-tieout, .lv-fresh .lv-chip { animation: none; } } `; // ---------------------------------------------------------------- small pieces // prettier-ignore const PIPS: Record = { 1: [[50, 50]], 2: [[28, 28], [72, 72]], 3: [[26, 26], [50, 50], [74, 74]], 4: [[28, 28], [72, 28], [28, 72], [72, 72]], 5: [[26, 26], [74, 26], [50, 50], [26, 74], [74, 74]], 6: [[28, 24], [72, 24], [28, 50], [72, 50], [28, 76], [72, 76]], }; /** Pip colour that reads on the die colour (white dice → dark pips). */ const pipOn = (bg: string) => (['#ffffff', '#fffdf8', '#f2a10c'].includes(bg) ? '#1c1e21' : '#ffffff'); function Die({ value, color, size = '', rolling = false }: { value: number; color: string; size?: '' | 'lg'; rolling?: boolean }) { return ( {PIPS[value]!.map(([x, y], i) => ( ))} ); } function Bill({ note, small }: { note: number | null; small?: boolean }) { if (note === null) return ( ? ); const [bg, fg] = BILL[note] ?? ['#eee', '#111']; return ( {note} ); } function Dot({ color }: { color: string }) { return ; } function OwnerChip({ v, players, owner, me }: { v: LasVegasView; players: Record; owner: string; me: string | null }) { return ( {owner === me ? '나' : owner === HOUSE ? '하우스' : nick(players, owner)} ); } const STANDING_TAG: Record = { first: '1등!', tie: '동점 주의', award: '지폐 받음', none: '못 받아요' }; function previewText(c: LasVegasChoice): string { const head = `${c.value}번 카지노에 ${c.own + c.neutral}개를 놓으면`; switch (c.standing) { case 'first': return `${head} 지금은 1등이에요! (${c.note}점 지폐)`; case 'award': return `${head} 지금은 ${c.note}점 지폐를 받을 수 있어요.`; case 'tie': return `${head} 개수가 같아져서 지금은 둘 다 꽝이에요. 동점 주의!`; default: return c.own === 0 ? `${head} 흰 주사위만 놓여요. 내 주사위는 그대로예요.` : `${head} 지금은 받을 지폐가 없어요.`; } } // ---------------------------------------------------------------- board function LasVegasBoard({ view: v, me, players, act, pending }: BoardProps) { const myTurn = v.phase !== 'finished' && !!me && v.current === me; const [sel, setSel] = useState(null); const [rolling, setRolling] = useState(false); const [flick, setFlick] = useState(null); const [hit, setHit] = useState<{ value: number; key: number } | null>(null); const [announce, setAnnounce] = useState(''); const prevRoll = useRef(v.rollSeq); const prevTurn = useRef(v.turnSeq); const prevRounds = useRef(v.roundResults.length); // New roll: tumble animation + screen-reader text. useLayoutEffect(() => { if (v.rollSeq <= prevRoll.current || !v.lastRoll) { prevRoll.current = v.rollSeq; return; } prevRoll.current = v.rollSeq; setSel(null); const who = v.current === me ? '내가' : `${nick(players, v.current ?? '')}님이`; const all = [...v.lastRoll.own, ...v.lastRoll.neutral].sort((a, b) => a - b); setAnnounce(`${who} 주사위 ${all.length}개를 굴렸어요: ${all.join(', ')}`); if (reducedMotion()) { sounds.stone(); return; } setRolling(true); }, [v.rollSeq]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!rolling || !v.lastRoll) return; const n = v.lastRoll.own.length + v.lastRoll.neutral.length; const t = setInterval(() => setFlick(Array.from({ length: n }, () => 1 + Math.floor(Math.random() * 6))), 70); const end = setTimeout(() => { setRolling(false); setFlick(null); sounds.stone(); }, ROLL_MS); return () => { clearInterval(t); clearTimeout(end); }; }, [rolling]); // eslint-disable-line react-hooks/exhaustive-deps // Turn changes: placement flash, round settlement and my-turn cue. useEffect(() => { if (v.turnSeq === prevTurn.current) return; prevTurn.current = v.turnSeq; setSel(null); const parts: string[] = []; const lp = v.lastPlace; if (lp) { setHit({ value: lp.value, key: v.turnSeq }); const who = lp.player === me ? '내가' : `${nick(players, lp.player)}님이`; parts.push( `${who} ${lp.value}번 카지노에 주사위 ${lp.own + lp.neutral}개를 놓았어요${lp.neutral ? `(흰 ${lp.neutral}개 포함)` : ''}${lp.auto ? ' (시간 초과로 자동)' : ''}.`, ); } if (v.roundResults.length > prevRounds.current) { prevRounds.current = v.roundResults.length; const rr = v.roundResults[v.roundResults.length - 1]!; const ties = rr.payouts.filter((p) => p.removedTies.length > 0).length; parts.push(`${rr.round}라운드 정산이 끝났어요.${ties ? ` 동점 무효가 ${ties}곳에서 나왔어요.` : ''}`); haptic([50, 30, 50]); } if (myTurn) { haptic(40); if (document.visibilityState === 'hidden') sounds.myTurn(); } if (parts.length) setAnnounce(parts.join(' ')); }, [v.turnSeq]); // eslint-disable-line react-hooks/exhaustive-deps const choices = v.phase === 'place' && !rolling ? v.choices : []; const selChoice = choices.find((c) => c.value === sel) ?? null; const canPick = myTurn && v.phase === 'place' && !pending && !rolling; const pick = (value: number) => { if (!canPick || !choices.some((c) => c.value === value)) return; setSel(sel === value ? null : value); }; const place = () => { if (!canPick || sel === null) return; act({ type: 'PLACE', value: sel }); setSel(null); }; const roll = () => { if (myTurn && v.phase === 'roll' && !pending) act({ type: 'ROLL' }); }; const curName = v.current ? (v.current === me ? '내' : `${nick(players, v.current)}님`) : ''; const hand = me ? v.hand[me] : undefined; const lastRR = v.roundResults[v.roundResults.length - 1]; return (
{v.phase === 'finished' ? '게임 끝' : `${v.round} / ${v.rounds} 라운드`} {v.current && ( {curName} 차례 )} {v.phase !== 'finished' && `선 ${v.startPlayer === me ? '나' : nick(players, v.startPlayer)} · `} 남은 지폐 더미 {v.pileCount}장
{v.round === 1 && v.phase !== 'finished' && (
🎲 같은 숫자는 한꺼번에 ⚠️ 개수가 같으면 둘 다 꽝
)} {v.result && lastRR && (
)} {!v.result && (
{v.casinos.map((c) => { const ch = choices.find((x) => x.value === c.value); const isSel = sel === c.value; const owners = Object.entries(c.dice) .filter(([, n]) => (n ?? 0) > 0) .sort((a, b) => b[1]! - a[1]! || v.seats.indexOf(a[0]) - v.seats.indexOf(b[0])); const label = `${c.value}번 카지노. 지폐 ${c.notes.length ? c.notes.map((n) => `${n}점`).join(', ') : '없음'}. 주사위 ${ owners.length ? owners.map(([o, n]) => `${o === me ? '나' : nick(players, o)} ${n}개`).join(', ') : '없음' }${ch && isSel ? `. ${STANDING_TAG[ch.standing]}` : ''}`; const cls = `lv-casino${ch && canPick ? ' can' : ''}${isSel ? ` sel ${ch?.standing === 'first' ? 'first' : ch?.standing === 'tie' ? 'tie' : ''}` : ''}${hit?.value === c.value ? ' hit' : ''}`; const body = ( <>
{c.value} 번 카지노
{c.notes.length ? c.notes.map((n, i) => ) : 지폐 없음}
{owners.map(([o, n]) => (
{Array.from({ length: Math.min(n!, 8) }, (_, i) => ( ))} {n! > 8 ? `${n}` : ''} {initial(players, o)}
))} {ch && isSel && (
{Array.from({ length: ch.own }, (_, i) => ( ))} {Array.from({ length: ch.neutral }, (_, i) => ( ))} +{ch.own + ch.neutral}
)}
{ch && isSel && {STANDING_TAG[ch.standing]}} ); return ch && canPick ? ( ) : (
{body}
); })}
)} {!v.result && (
{v.phase === 'finished' ? (

게임이 끝났어요

) : rolling && v.lastRoll ? (
{[...v.lastRoll.own.map(() => seatColor(v, v.current!)), ...v.lastRoll.neutral.map(() => '#ffffff')].map((col, i) => ( ))}
) : v.phase === 'place' && v.current ? ( <>
{choices.map((c) => ( ))}
{canPick && selChoice &&
{previewText(selChoice)}
} {canPick ? ( ) : (

{myTurn ? '잠시만요…' : `${nick(players, v.current)}님이 놓을 숫자를 고르는 중이에요`}

)} ) : myTurn ? ( ) : (

{v.current ? `${nick(players, v.current)}님이 굴릴 차례예요` : ''}

)} {hand && v.phase !== 'finished' && !v.conceded.includes(me!) && (

내 주사위 {hand.own}개{v.neutralPer > 0 ? ` · 흰 주사위 ${hand.neutral}개` : ''} 남음

)} {v.houseRoll && v.houseRoll.round === v.round && v.phase !== 'finished' && (

라운드 시작: 남는 흰 주사위 {v.houseRoll.values.length}개가 {v.houseRoll.values.map((x) => `${x}번`).join(', ')} 카지노에 놓였어요

)}
{lastRR ? ( ) : (

라운드가 끝나면

카지노마다 주사위가 가장 많은 사람부터 큰 지폐를 가져가요. 개수가 같은 사람끼리는 모두 무효예요.

)}
)}

{announce}

); } function Settlement({ round, payouts, v, me, players, }: { round: number; payouts: LasVegasPayoutView[]; v: LasVegasView; me: string | null; players: Record; }) { const fresh = v.phase !== 'finished' && round === v.round - 1; return (

{round}라운드 정산

{payouts.map((p) => (
{p.casino}번
{p.removedTies.length > 0 && ( (o === me ? '나' : nick(players, o))).join(', ')}`}> 💥 동점 무효 {p.removedTies.map((o) => ( ))} )} {p.awards.map((a, i) => ( {a.note === null && 1장} ))} {p.returned.length > 0 && ↩ 더미 아래로 {p.returned.map((n) => n).join('·')}} {p.awards.length === 0 && p.removedTies.length === 0 && p.returned.length === 0 && 아무도 없었어요}
))}
); } function FinalTable({ v, me, players }: { v: LasVegasView; me: string | null; players: Record }) { const r = v.result!; return (

최종 결과

{v.roundResults.map((rr) => ( ))} {r.ranking.map((row) => ( {(r.roundTotals[row.player] ?? []).map((t, i) => ( ))} ))}
순위 이름 {rr.round}R 합계
{row.rank}등 {row.conceded && (기권)} {t} {row.total}점
{row.notes.length}장
); } // ---------------------------------------------------------------- side panel function LasVegasSide({ view: v, me, players, act, deadline }: BoardProps) { const [ask, setAsk] = useState(false); const total = v.options.turnSeconds ? v.options.turnSeconds * 1000 : 30_000; const mine = me ? v.money[me] : undefined; const playing = v.phase !== 'finished'; return ( <>
{v.seats.map((p, i) => { const turn = v.current === p; const m = v.money[p]!; const h = v.hand[p]!; const out = v.conceded.includes(p); return (
{nick(players, p)} {p === me && (나)} {v.startPlayer === p && playing && ( 선 )}
{out ? '기권' : v.away.includes(p) ? '자리 비움' : playing ? `주사위 ${h.own}${v.neutralPer ? ` · 흰 ${h.neutral}` : ''}` : '끝'}
{m.total !== null ? `${m.total}점` : '?'}
{m.count}장
{turn && playing && }
); })}
{mine && (
내 지폐 {mine.count}장 · {mine.total ?? 0}점
{mine.notes && mine.notes.length ? mine.notes.map((n, i) => ) : 아직 없어요}
{v.options.hideMoney && playing && (

다른 사람에게는 장 수만 보여요.

)}
)} {playing && me && v.money[me] && !v.conceded.includes(me) && ( )} {ask && ( act({ type: 'CONCEDE' })} onClose={() => setAsk(false)} /> )} ); } // ---------------------------------------------------------------- status function isMyTurn(v: LasVegasView, me: string | null): boolean { return v.phase !== 'finished' && !!me && v.current === me; } function statusText(v: LasVegasView, me: string | null, players: Record): string { if (v.phase === 'finished') return v.result?.summary ?? '게임이 끝났어요'; if (me && v.conceded.includes(me)) return '기권했어요. 구경하는 중이에요'; if (isMyTurn(v, me)) return v.phase === 'roll' ? '내 차례예요. 주사위를 굴려 주세요' : '내 차례예요. 놓을 숫자를 골라 주세요'; return v.current ? `${nick(players, v.current)}님 차례예요 · ${v.round}/${v.rounds}라운드` : '잠시만 기다려 주세요'; } // ---------------------------------------------------------------- options function Seg({ value, options, onChange, label }: { value: T; options: [T, string][]; onChange: (v: T) => void; label: string }) { return (
{options.map(([k, l]) => ( ))}
); } function Field({ label, children }: { label: string; children: React.ReactNode }) { return (
{label} {children}
); } function Options({ value: o, onChange }: OptionsProps) { const set = (patch: Partial) => onChange({ ...o, ...patch }); return (
set({ rounds })} /> set({ neutralDice })} /> set({ hideMoney: k === 'hide' })} /> set({ turnSeconds: k === 'none' ? null : (Number(k) as 20 | 30 | 60) })} />

흰 주사위 "켜기"는 5명일 때 쓸 수 없어요(5색을 모두 쓰기 때문이에요).

); } function optionsSummary(o: LasVegasOptions): string { return [ `${o.rounds}라운드`, o.neutralDice === 'auto' ? '흰 주사위 자동' : o.neutralDice === 'on' ? '흰 주사위 켜기' : '흰 주사위 끄기', o.hideMoney ? '금액 숨김' : '금액 공개', o.turnSeconds === null ? '시간 제한 없음' : `${o.turnSeconds}초`, ].join(' · '); } // ---------------------------------------------------------------- rules function Rules({ options }: { options?: LasVegasOptions }) { const rounds = options?.rounds ?? 4; return (

1번부터 6번까지 카지노 6곳에 놀이용 지폐가 놓여 있어요(카지노마다 합이 5점 이상이 될 때까지). 각자 자기 색 주사위 8개를 가지고 시작해요.

내 차례에는 남은 주사위를 모두 굴린 뒤, 나온 숫자 하나를 골라 그 숫자가 나온 주사위를 한꺼번에 같은 번호 카지노에 놓아요. 일부만 놓을 수는 없어요. 주사위를 다 쓴 사람은 차례를 건너뛰어요.

모두 주사위를 다 놓으면 정산해요. 카지노마다 주사위가 가장 많은 사람이 가장 큰 지폐, 다음 사람이 그다음 지폐를 가져가요. 단,{' '} 개수가 같은 사람끼리는 모두 무효(꽝)예요.

가 놓인 카지노에 A 5개 · B 3개 · C 3개 · D 1개 → B와 C는 꽝, A가 8, D가 3을 받아요.

흰 주사위(중립): 2명일 때 기본으로 각자 4개씩 더 받아요. 같은 숫자가 나오면 내 주사위와 함께 놓이고, 흰 주사위는 "하우스"라는 가상의 한 사람으로 쳐요. 하우스와 개수가 같아도 꽝이에요. 3명이 켜고 하면 남는 흰 주사위 2개를 라운드마다 선 플레이어가 굴려 바로 놓아요.

{rounds}라운드가 끝나면 받은 지폐 합이 가장 큰 사람이 이겨요. 같으면 지폐 장 수가 많은 사람이 이겨요. 라운드마다 선은 다음 사람에게 넘어가요.

시간 안에 굴리지 않으면 자동으로 굴리고, 시간 안에 고르지 않으면 개수가 가장 많은 숫자(같으면 지폐가 큰 카지노, 그래도 같으면 작은 숫자)에 자동으로 놓아요.

지폐는 이 방에서만 쓰는 놀이용 점수예요. 게임이 끝나면 사라지고, 실제 화폐나 상품과 바꿀 수 없어요.

); } const ui: GameUI = { Board: LasVegasBoard, Side: LasVegasSide, statusText, isMyTurn, Options, optionsSummary, Rules, }; export default ui;