fertig-classic-games/public/src/games/catan/expansions/scenarios/shared.js

84 lines
3.1 KiB
JavaScript

// 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 };
}