import type { PlaneCell } from './cell-event'; export const DEFAULT_PLANE_SIZE = { width: 9, height: 7 } as const; export const MIN_PLANE_SIZE = 4; export const MAX_PLANE_SIZE = 16; export const CELL_TYPE_PRESETS = [ '한잔', '안주', '모두한잔', '두잔', '벌칙1', '벌칙2', '무인도', '역방향', '비행기', ] as const; export type CellEventOption = { id: string | null; label: string; }; export const CELL_EVENT_OPTIONS: CellEventOption[] = [ { id: null, label: '없음' }, { id: 'island', label: '무인도' }, { id: 'reverse', label: '역방향' }, { id: 'airplane', label: '비행기 → 시작' }, ]; /** 저장용 칸 설정 (좌표는 지오메트리에서 합친다) */ export type BoardCellConfig = { type: string; eventId?: string | null; /** 칸 전용 이미지 (data URL 또는 상대 경로) */ imageSrc?: string | null; }; export type BoardConfig = { version: number; width: number; height: number; cells: BoardCellConfig[]; }; const BOARD_STORAGE_KEY = 'drinkmarble.board'; const BOARD_VERSION = 2; export function clampPlaneSize(n: number) { return Math.min(MAX_PLANE_SIZE, Math.max(MIN_PLANE_SIZE, Math.round(n))); } export function perimeterLength(width: number, height: number) { return 2 * (width + height - 2); } function randomPresetType() { const pool = CELL_TYPE_PRESETS.filter( (t) => t !== '무인도' && t !== '역방향' && t !== '비행기', ); return pool[Math.floor(Math.random() * pool.length)]; } /** 시작(좌상)을 제외한 세 모서리 인덱스 */ function cornerIndices(width: number, height: number) { const w = clampPlaneSize(width); const h = clampPlaneSize(height); return { topRight: w - 1, bottomRight: w + h - 2, bottomLeft: 2 * w + h - 3, }; } function defaultSpecials(width: number, height: number): Record { if (perimeterLength(width, height) < 8) return {}; const { topRight, bottomRight, bottomLeft } = cornerIndices(width, height); return { [topRight]: { type: '무인도', eventId: 'island' }, [bottomRight]: { type: '역방향', eventId: 'reverse' }, [bottomLeft]: { type: '비행기', eventId: 'airplane' }, }; } /** 시계방향 둘레 좌표만 만든다. */ export function buildPerimeterGeometry( width: number, height: number, ): { id: number; x: number; y: number }[] { const w = clampPlaneSize(width); const h = clampPlaneSize(height); const cells: { id: number; x: number; y: number }[] = []; let id = 0; for (let x = 0; x < w; x++) { cells.push({ id: id++, x, y: 0 }); } for (let y = 1; y < h; y++) { cells.push({ id: id++, x: w - 1, y }); } for (let x = w - 2; x >= 0; x--) { cells.push({ id: id++, x, y: h - 1 }); } for (let y = h - 2; y >= 1; y--) { cells.push({ id: id++, x: 0, y }); } return cells; } export function createRandomBoardConfig( width: number = DEFAULT_PLANE_SIZE.width, height: number = DEFAULT_PLANE_SIZE.height, ): BoardConfig { const w = clampPlaneSize(width); const h = clampPlaneSize(height); const geo = buildPerimeterGeometry(w, h); const specials = defaultSpecials(w, h); const cells: BoardCellConfig[] = geo.map((_, index) => { if (index === 0) return { type: '시작', eventId: null, imageSrc: null }; if (specials[index]) return { ...specials[index], imageSrc: null }; return { type: randomPresetType(), eventId: null, imageSrc: null }; }); return { version: BOARD_VERSION, width: w, height: h, cells }; } function sanitizeCell(src: BoardCellConfig | undefined, fallback: BoardCellConfig): BoardCellConfig { if (!src || typeof src.type !== 'string' || src.type.trim() === '') { return { ...fallback }; } const eventId = src.eventId === undefined ? null : src.eventId === null || src.eventId === '' ? null : String(src.eventId); const imageSrc = typeof src.imageSrc === 'string' && src.imageSrc.length > 0 ? src.imageSrc : null; return { type: src.type.trim(), eventId, imageSrc }; } export function normalizeBoardConfig(input: BoardConfig | null | undefined): BoardConfig { const width = clampPlaneSize(input?.width ?? DEFAULT_PLANE_SIZE.width); const height = clampPlaneSize(input?.height ?? DEFAULT_PLANE_SIZE.height); const geoLen = perimeterLength(width, height); const base = createRandomBoardConfig(width, height); if (!input?.cells || !Array.isArray(input.cells)) { return base; } const cells: BoardCellConfig[] = []; for (let i = 0; i < geoLen; i++) { cells.push(sanitizeCell(input.cells[i], base.cells[i])); } cells[0] = { type: '시작', eventId: null, imageSrc: cells[0]?.imageSrc ?? null }; return { version: BOARD_VERSION, width, height, cells }; } export function readStoredBoard(): BoardConfig | null { if (typeof window === 'undefined') return null; try { const raw = window.localStorage.getItem(BOARD_STORAGE_KEY); if (!raw) return null; return normalizeBoardConfig(JSON.parse(raw) as BoardConfig); } catch { return null; } } export function writeStoredBoard(config: BoardConfig) { if (typeof window === 'undefined') return; const normalized = normalizeBoardConfig(config); window.localStorage.setItem(BOARD_STORAGE_KEY, JSON.stringify(normalized)); } export function getOrCreateBoardConfig(): BoardConfig { const existing = readStoredBoard(); if (existing) return existing; const created = createRandomBoardConfig(); writeStoredBoard(created); return created; } export function setBoardConfig(config: BoardConfig) { writeStoredBoard(config); } export function clearBoardConfig() { if (typeof window === 'undefined') return; window.localStorage.removeItem(BOARD_STORAGE_KEY); } export function randomizeAndSaveBoard( width?: number, height?: number, current = getOrCreateBoardConfig(), ): BoardConfig { const created = createRandomBoardConfig( width ?? current.width, height ?? current.height, ); writeStoredBoard(created); return created; } /** 판 크기를 바꾸고, 가능한 칸 설정을 비율에 맞게 이어받는다. */ export function resizeBoardConfig( width: number, height: number, current = getOrCreateBoardConfig(), ): BoardConfig { const w = clampPlaneSize(width); const h = clampPlaneSize(height); if (w === current.width && h === current.height) { return normalizeBoardConfig(current); } const nextLen = perimeterLength(w, h); const prev = normalizeBoardConfig(current); const cells: BoardCellConfig[] = []; for (let i = 0; i < nextLen; i++) { if (i === 0) { cells.push({ type: '시작', eventId: null, imageSrc: prev.cells[0]?.imageSrc ?? null, }); continue; } const mapped = prev.cells.length <= 1 ? 0 : Math.min( prev.cells.length - 1, Math.round((i / (nextLen - 1)) * (prev.cells.length - 1)), ); const src = prev.cells[mapped] ?? prev.cells[1]; cells.push({ type: src?.type && src.type !== '시작' ? src.type : randomPresetType(), eventId: src?.eventId ?? null, imageSrc: src?.imageSrc ?? null, }); } const next = { version: BOARD_VERSION, width: w, height: h, cells }; writeStoredBoard(next); return next; } export function updateBoardCell( index: number, patch: Partial, current = getOrCreateBoardConfig(), ): BoardConfig { if (index < 0 || index >= current.cells.length) return current; const cells = current.cells.map((cell, i) => { if (i !== index) return cell; if (i === 0) { return { type: '시작', eventId: null, imageSrc: patch.imageSrc === undefined ? cell.imageSrc ?? null : patch.imageSrc === '' ? null : patch.imageSrc, }; } return { type: patch.type?.trim() || cell.type, eventId: patch.eventId === undefined ? cell.eventId ?? null : patch.eventId === '' ? null : patch.eventId, imageSrc: patch.imageSrc === undefined ? cell.imageSrc ?? null : patch.imageSrc === '' ? null : patch.imageSrc, }; }); const next = { version: BOARD_VERSION, width: current.width, height: current.height, cells, }; writeStoredBoard(next); return next; } export function buildPlaneCells(config: BoardConfig = getOrCreateBoardConfig()): PlaneCell[] { const normalized = normalizeBoardConfig(config); const geo = buildPerimeterGeometry(normalized.width, normalized.height); return geo.map((g, index) => ({ id: g.id, x: g.x, y: g.y, type: normalized.cells[index]?.type ?? (index === 0 ? '시작' : '한잔'), eventId: index === 0 ? null : (normalized.cells[index]?.eventId ?? null), imageSrc: normalized.cells[index]?.imageSrc ?? null, })); } export function cellColorFor(eventId: string | null | undefined, index: number) { if (index === 0) return '#059669'; switch (eventId) { case 'island': return '#0f766e'; case 'reverse': return '#7c3aed'; case 'airplane': return '#0284c7'; default: return '#e11d48'; } } /** 주사위 UI를 안쪽 빈 칸에 배치할 때 쓰는 span (1~2) */ export function diceAreaSpan(width: number, height: number) { return Math.min(2, Math.max(1, width - 2), Math.max(1, height - 2)); } /** 하위 호환: 예전 PLANE_SIZE 참조 */ export const PLANE_SIZE = DEFAULT_PLANE_SIZE;