'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import Dice from './dice'; import { Chessman } from './chessman'; import ChessmanPiece from './chessman-piece'; import { applyLandEvent, type PlaneCell, } from './cell-event'; const PLANE_SIZE = { width: 9, height: 7 } as const; const STEP_MS = 280; const CELL_TYPES = ['한잔', '안주', '모두한잔', '두잔', '벌칙1', '벌칙2'] as const; /** 특수 칸 배치 (28칸 기준 인덱스) */ const SPECIAL_CELLS: Record = { 7: { type: '무인도', eventId: 'island' }, 14: { type: '역방향', eventId: 'reverse' }, 21: { type: '비행기', eventId: 'airplane' }, // → 시작(0) }; function randomType() { return CELL_TYPES[Math.floor(Math.random() * CELL_TYPES.length)]; } function cellLabel(cell: PlaneCell | undefined, index: number) { if (!cell) return ''; return index === 0 ? '시작' : cell.type; } function cellColor(cell: PlaneCell, index: number) { if (index === 0) return '#059669'; switch (cell.eventId) { case 'island': return '#0f766e'; case 'reverse': return '#7c3aed'; case 'airplane': return '#0284c7'; default: return '#e11d48'; } } /** 시계방향 둘레 칸. 모서리는 한 번만 넣어서 9x7 기준 28칸이 된다. */ function buildPerimeter(width: number, height: number, assignType: () => string): PlaneCell[] { const cells: PlaneCell[] = []; let id = 0; for (let x = 0; x < width; x++) { cells.push({ id: id++, x, y: 0, type: assignType() }); } for (let y = 1; y < height; y++) { cells.push({ id: id++, x: width - 1, y, type: assignType() }); } for (let x = width - 2; x >= 0; x--) { cells.push({ id: id++, x, y: height - 1, type: assignType() }); } for (let y = height - 2; y >= 1; y--) { cells.push({ id: id++, x: 0, y, type: assignType() }); } if (cells[0]) { cells[0].type = '시작'; cells[0].eventId = null; } for (const [indexText, special] of Object.entries(SPECIAL_CELLS)) { const index = Number(indexText); if (cells[index]) { cells[index].type = special.type; cells[index].eventId = special.eventId; } } return cells; } export default function DicePlane() { const [dicePlaneArray, setDicePlaneArray] = useState(() => buildPerimeter(PLANE_SIZE.width, PLANE_SIZE.height, () => 'none'), ); const chessmanRef = useRef(new Chessman(28)); const boardRef = useRef(dicePlaneArray); const [chessmanIndex, setChessmanIndex] = useState(0); const [hopKey, setHopKey] = useState(0); const [moving, setMoving] = useState(false); const [landedLabel, setLandedLabel] = useState(null); const [landedKey, setLandedKey] = useState(0); const [statusText, setStatusText] = useState('시계방향'); const [islandTurnsLeft, setIslandTurnsLeft] = useState(0); const moveTimerRef = useRef | null>(null); useEffect(() => { const cells = buildPerimeter(PLANE_SIZE.width, PLANE_SIZE.height, randomType); chessmanRef.current = new Chessman(cells.length, 0); boardRef.current = cells; setChessmanIndex(0); setHopKey(0); setLandedLabel(null); setStatusText('시계방향'); setIslandTurnsLeft(0); setDicePlaneArray(cells); }, []); useEffect(() => { boardRef.current = dicePlaneArray; }, [dicePlaneArray]); useEffect(() => { return () => { if (moveTimerRef.current) clearTimeout(moveTimerRef.current); }; }, []); const showBanner = useCallback((message: string) => { setLandedLabel(message); setLandedKey((k) => k + 1); }, []); const applyLanding = useCallback( (landIndex: number, roll: number) => { const man = chessmanRef.current; const result = applyLandEvent({ chessman: man, board: boardRef.current, roll, landIndex, }); setStatusText(man.facing === 1 ? '시계방향' : '반시계방향'); setIslandTurnsLeft(man.islandTurnsLeft); if (result?.teleportTo != null) { setChessmanIndex(result.teleportTo); setHopKey((k) => k + 1); showBanner(result.message); return; } showBanner(result?.message ?? cellLabel(boardRef.current[landIndex], landIndex)); }, [showBanner], ); const animateMove = useCallback( (steps: number, roll: number) => { const amount = Math.max(0, Math.trunc(steps)); if (amount === 0) { setMoving(false); return; } setMoving(true); let remaining = amount; const stepOnce = () => { const next = chessmanRef.current.moveOne(); setChessmanIndex(next); setHopKey((k) => k + 1); remaining -= 1; if (remaining > 0) { moveTimerRef.current = setTimeout(stepOnce, STEP_MS); } else { moveTimerRef.current = null; setMoving(false); applyLanding(next, roll); } }; stepOnce(); }, [applyLanding], ); const onDiceRolled = useCallback( (value: number) => { const roll = Math.max(0, Math.trunc(value)); if (roll === 0) return; setLandedLabel(null); const man = chessmanRef.current; if (man.isOnIsland) { const outcome = man.resolveIslandRoll(roll); setIslandTurnsLeft(man.islandTurnsLeft); if (outcome === 'wait') { showBanner( man.isOnIsland ? `무인도 대기 (남은 ${man.islandTurnsLeft}턴)` : '무인도 3턴 종료!', ); return; } // escape with 6 showBanner('무인도 탈출!'); } animateMove(roll, roll); }, [animateMove, showBanner], ); const chessmanCell = dicePlaneArray[chessmanIndex] ?? dicePlaneArray[0]; return (
{dicePlaneArray.map((plane, index) => { const isStart = index === 0; const isLanded = !moving && index === chessmanIndex && landedLabel != null; return (
{isStart ? '시작' : plane.type}
); })} {chessmanCell && ( )}
{statusText} {islandTurnsLeft > 0 ? ` · 무인도 ${islandTurnsLeft}턴` : ''}
{landedLabel && (
{landedLabel}
)}
); }