// src/pages/agent-office/canvas/Pathfinder.js /** * BFS 4방향 경로 탐색 (대각선 없음) * blocked 타일과 벽 타일을 회피하여 최단 경로 반환 */ export class Pathfinder { constructor(cols, rows) { this.cols = cols; this.rows = rows; this.blocked = new Set(); } /** blocked 타일 세팅 (wall + furniture footprint) */ setBlocked(blockedList) { // Do NOT clear — setWalls already added wall tiles for (const [col, row] of blockedList) { this.blocked.add(`${col},${row}`); } } /** wall 타일도 blocked로 추가 (floor 배열에서 0인 셀) */ setWalls(floorGrid) { for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { if (floorGrid[r][c] === 0) { this.blocked.add(`${c},${r}`); } } } } isBlocked(col, row) { if (col < 0 || col >= this.cols || row < 0 || row >= this.rows) return true; return this.blocked.has(`${col},${row}`); } /** * BFS 최단 경로 * @returns {Array<{col, row}>} 시작점 제외, 도착점 포함 경로. 경로 없으면 빈 배열. */ findPath(startCol, startRow, goalCol, goalRow) { if (startCol === goalCol && startRow === goalRow) return []; const key = (c, r) => `${c},${r}`; const startKey = key(startCol, startRow); const goalKey = key(goalCol, goalRow); const queue = [{ col: startCol, row: startRow }]; const visited = new Set([startKey]); const parent = new Map(); const dirs = [ { dc: 0, dr: -1 }, // up { dc: 0, dr: 1 }, // down { dc: -1, dr: 0 }, // left { dc: 1, dr: 0 } // right ]; while (queue.length > 0) { const current = queue.shift(); for (const { dc, dr } of dirs) { const nc = current.col + dc; const nr = current.row + dr; const nk = key(nc, nr); if (visited.has(nk)) continue; // 골 지점은 blocked여도 이동 가능 (에이전트가 자기 자리에 앉으려면) if (nk !== goalKey && this.isBlocked(nc, nr)) continue; visited.add(nk); parent.set(nk, key(current.col, current.row)); queue.push({ col: nc, row: nr }); if (nc === goalCol && nr === goalRow) { return this._reconstructPath(parent, startKey, goalKey); } } } return []; // 경로 없음 } _reconstructPath(parent, startKey, goalKey) { const path = []; let current = goalKey; while (current !== startKey) { const [c, r] = current.split(',').map(Number); path.unshift({ col: c, row: r }); current = parent.get(current); } return path; } /** idle 배회용: start 주변 반경 내 랜덤 walkable 타일 */ getRandomNearbyFloor(col, row, radius = 4) { const candidates = []; for (let dr = -radius; dr <= radius; dr++) { for (let dc = -radius; dc <= radius; dc++) { const nc = col + dc; const nr = row + dr; if (nc === col && nr === row) continue; if (!this.isBlocked(nc, nr)) { candidates.push({ col: nc, row: nr }); } } } if (candidates.length === 0) return null; return candidates[Math.floor(Math.random() * candidates.length)]; } }