// 로그라이크 맵 절차 생성 — StS식 경로 걷기. // ⚠️ 전투 규칙과 마찬가지로 tools/deck/gen-slaydeck.mjs 의 Lua(GenerateMap)와 로직 동기화 유지할 것. // (Lua는 math.random, 여기는 주입 rng — 수치 동일성이 아니라 구조 규칙 동일성이 대상) export const ROWS = 6; // 걷는 행 1..6, 보스는 row 7 (depth 최대 7) export const COLS = 4; export const PATHS = 4; // 행별 타입 가중치 (설계 문서 표와 동일) function rowWeights(row) { if (row === ROWS) { return [['rest', 50], ['combat', 25], ['shop', 10], ['elite', 8], ['treasure', 7]]; } if (row >= 4) { return [['combat', 45], ['elite', 16], ['shop', 12], ['rest', 12], ['treasure', 15]]; } return [['combat', 45], ['shop', 12], ['rest', 12]]; } // rng: () => [0,1) 균등 난수 (mulberry32 등) export function generateMap(rng) { const randInt = (lo, hi) => lo + Math.floor(rng() * (hi - lo + 1)); // [lo,hi] const nodes = {}; const start = []; const nodeId = (r, c) => `r${r}c${c}`; const ensureNode = (r, c) => { const id = nodeId(r, c); if (!nodes[id]) nodes[id] = { type: 'combat', row: r, col: c, next: [] }; return id; }; const addNext = (id, nid) => { if (!nodes[id].next.includes(nid)) nodes[id].next.push(nid); }; nodes.boss = { type: 'boss', row: ROWS + 1, col: 0, next: [] }; // 시작열: 셔플 앞 2개(상이 보장) + 랜덤 2개 const cols = [1, 2, 3, 4]; for (let i = cols.length - 1; i >= 1; i--) { const j = randInt(0, i); [cols[i], cols[j]] = [cols[j], cols[i]]; } const starts = [cols[0], cols[1], randInt(1, COLS), randInt(1, COLS)]; for (let p = 0; p < PATHS; p++) { let c = starts[p]; const sid = ensureNode(1, c); if (!start.includes(sid)) start.push(sid); for (let r = 1; r < ROWS; r++) { let nc = c + randInt(-1, 1); if (nc < 1) nc = 1; if (nc > COLS) nc = COLS; ensureNode(r + 1, nc); addNext(nodeId(r, c), nodeId(r + 1, nc)); c = nc; } addNext(nodeId(ROWS, c), 'boss'); } // 타입 배정 — 행 오름차순 (1~2행은 combat 고정) for (let r = 3; r <= ROWS; r++) { for (let c = 1; c <= COLS; c++) { const id = nodeId(r, c); const node = nodes[id]; if (!node) continue; // 부모 노드 타입 수집 (rest/shop/elite 는 부모와 같은 타입 연속 금지) const parentTypes = new Set(); for (const pn of Object.values(nodes)) { if (pn.row === r - 1 && pn.next.includes(id)) parentTypes.add(pn.type); } const NO_REPEAT = new Set(['rest', 'shop', 'elite']); const w = rowWeights(r).map(([t, wt]) => [t, NO_REPEAT.has(t) && parentTypes.has(t) ? 0 : wt]); const total = w.reduce((s, [, wt]) => s + wt, 0); const roll = rng() * total; let acc = 0; for (const [t, wt] of w) { acc += wt; if (roll <= acc) { node.type = t; break; } } } } return { nodes, start }; }