// Shared helpers for Seafarers scenario boards. // // A scenario describes its board as a list of `cells` (one per hex, in hex-id // order) plus a row layout. assembleBoard() turns that into registered topology // + the per-hex state data CatanLogic expects. No Phaser here. import { rowCenters, buildBoard, registerBoard, portsFromEdges, HEX_SIZE, BOARD_CX, BOARD_CY, PORT_BAG, } from '../../CatanBoard.js'; export const SHIP_COST = { wool: 1, lumber: 1 }; // Larger Seafarers boards use a slightly smaller hex so they fit the canvas. export const SEA_HEX_SIZE = 74; export function shuffle(arr) { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } // A cell: { kind: 'land'|'sea'|'gold'|'fog'|'desert', resource?, number? }. // Distribute generic + 2:1 ports onto coastal land edges, spaced around the rim. function autoPorts(geo, hexes, cx, cy) { const isLand = (hid) => { const k = hexes[hid]?.kind; return k === 'land' || k === 'gold' || k === 'desert'; }; // Shore edges: touch exactly one land hex (the other side is water or // off-board), so a settlement on that shore could use the port. const ring = geo.edges.filter((e) => e.hexes.filter(isLand).length === 1); ring.sort((p, q) => { const pm = midOf(geo, p), qm = midOf(geo, q); return Math.atan2(pm.y - cy, pm.x - cx) - Math.atan2(qm.y - cy, qm.x - cx); }); const bag = shuffle(PORT_BAG); const n = Math.min(bag.length, ring.length); const entries = []; const used = new Set(); for (let i = 0; i < n && ring.length; i++) { let idx = Math.round((i * ring.length) / n) % ring.length; while (used.has(idx)) idx = (idx + 1) % ring.length; used.add(idx); entries.push({ edgeId: ring[idx].id, type: bag[i] }); } return portsFromEdges(geo, entries); } function midOf(geo, edge) { const a = geo.nodes[edge.nodes[0]], b = geo.nodes[edge.nodes[1]]; return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }; } // Build a scenario board from cells. Returns the board payload createInitialState // merges into game state. `pirate` is a sea hex id (or null). export function assembleBoard({ id, rows, cells, homeIds = [], pirate = null, winVP = 13, size = SEA_HEX_SIZE, cx = BOARD_CX, cy = BOARD_CY }) { const centers = rowCenters(rows, cx, cy, size); const geo = buildBoard({ centers, size, cx, cy }); registerBoard(id, geo); const hexes = cells.map((c, i) => ({ id: i, kind: c.kind, resource: c.resource ?? null, number: c.number ?? null, hasRobber: false, // Fog hexes carry hidden data that replaces kind/resource/number on reveal. fogData: c.kind === 'fog' && c.reveal ? { kind: c.reveal.kind, resource: c.reveal.resource ?? null, number: c.reveal.number ?? null } : null, })); // Robber starts on the (first) desert, if any. const desert = hexes.find((h) => h.kind === 'desert'); let robberHex = desert ? desert.id : null; if (desert) desert.hasRobber = true; const ports = autoPorts(geo, hexes, cx, cy); return { boardId: id, hexes, ports, robberHex, pirateHex: pirate, homeIds: [...homeIds], winVP }; }