// CatanBoard.js — pure geometry + static data for Settlers of Catan. // No Phaser imports. Shared by CatanLogic, CatanAI, and CatanGame so they all // agree on one node/edge/hex coordinate model. // ── Layout constants (pixel space, 1920×1080 canvas) ──────────────────────── // Board sits in the centre-right; left strip is reserved for opponent portraits // and the bottom strip for the human's hand. export const BOARD_CX = 1000; export const BOARD_CY = 470; export const HEX_SIZE = 92; // centre-to-corner radius (pointy-top) const SQRT3 = Math.sqrt(3); export const HEX_W = SQRT3 * HEX_SIZE; // flat-to-flat width / in-row spacing const ROW_V = 1.5 * HEX_SIZE; // vertical spacing between rows export const HEX_ROWS = [3, 4, 5, 4, 3]; // ── Resource / chit / port / cost / dev-deck definitions ──────────────────── export const RESOURCE_INFO = { brick: { label: 'Brick', tile: 'Hills', color: 0xc1502e, swatch: 0xc1502e }, lumber: { label: 'Lumber', tile: 'Forest', color: 0x2e7d32, swatch: 0x2e7d32 }, wool: { label: 'Wool', tile: 'Pasture', color: 0x8bc34a, swatch: 0x8bc34a }, grain: { label: 'Grain', tile: 'Fields', color: 0xf0c419, swatch: 0xe0b000 }, ore: { label: 'Ore', tile: 'Mountains', color: 0x9aa3ab, swatch: 0x9aa3ab }, }; export const RESOURCE_TYPES = ['brick', 'lumber', 'wool', 'grain', 'ore']; export const DESERT_COLOR = 0xe3cf94; // 19 hexes: 4 lumber, 3 brick, 4 wool, 4 grain, 3 ore, 1 desert. export const RESOURCE_BAG = [ 'lumber', 'lumber', 'lumber', 'lumber', 'brick', 'brick', 'brick', 'wool', 'wool', 'wool', 'wool', 'grain', 'grain', 'grain', 'grain', 'ore', 'ore', 'ore', 'desert', ]; // Fixed hex resources for the standard Catan beginner layout. // Indexed by hex ID (rows [3,4,5,4,3], left-to-right top-to-bottom). export const STANDARD_RESOURCES = [ 'ore', 'wool', 'lumber', // row 0: hex 0-2 'grain', 'brick', 'wool', 'brick', // row 1: hex 3-6 'lumber', 'grain', 'desert','grain', 'ore', // row 2: hex 7-11 'lumber', 'ore', 'grain', 'wool', // row 3: hex 12-15 'brick', 'wool', 'lumber', // row 4: hex 16-18 ]; // 18 number chits for the 18 non-desert hexes. export const CHIT_BAG = [2, 3, 3, 4, 4, 5, 5, 6, 6, 8, 8, 9, 9, 10, 10, 11, 11, 12]; // Hex IDs visited in the standard Catan clockwise spiral (outer ring → inner ring → centre). // Rows [3,4,5,4,3] assign IDs left-to-right, top-to-bottom: 0-2, 3-6, 7-11, 12-15, 16-18. export const CHIT_SPIRAL = [0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 7, 3, 4, 5, 10, 14, 13, 8, 9]; // Standard chit values A–R placed in spiral order; desert hex is skipped when assigning. export const CHIT_SEQUENCE = [5, 2, 6, 3, 8, 10, 9, 12, 11, 4, 8, 10, 9, 4, 5, 6, 3, 11]; // 9 ports: 4 generic 3:1, one 2:1 per resource. export const PORT_BAG = ['any', 'any', 'any', 'any', 'brick', 'lumber', 'wool', 'grain', 'ore']; export const COSTS = { road: { brick: 1, lumber: 1 }, settlement: { brick: 1, lumber: 1, wool: 1, grain: 1 }, city: { grain: 2, ore: 3 }, devCard: { wool: 1, grain: 1, ore: 1 }, }; // 25-card development deck. export const DEV_DECK = [ ...Array(14).fill('knight'), ...Array(5).fill('vp'), ...Array(2).fill('roadBuilding'), ...Array(2).fill('yearOfPlenty'), ...Array(2).fill('monopoly'), ]; export const DEV_INFO = { knight: { label: 'Knight', short: 'Knight' }, vp: { label: 'Victory Point', short: 'VP' }, roadBuilding: { label: 'Road Building', short: 'Roads' }, yearOfPlenty: { label: 'Year of Plenty', short: 'Plenty' }, monopoly: { label: 'Monopoly', short: 'Monop.' }, }; export const PLAYER_COLORS = [ { key: 'blue', hex: 0x2d6cdf, hexDark: 0x1c4490, name: 'Blue' }, { key: 'red', hex: 0xd23b3b, hexDark: 0x8f2424, name: 'Red' }, { key: 'orange', hex: 0xe08a1e, hexDark: 0x9c5d10, name: 'Orange' }, { key: 'white', hex: 0xe8e4d8, hexDark: 0x9c9684, name: 'White' }, ]; export const WIN_VP = 10; // Probability pips for a number chit: 2/12→1 … 6/8→5. export function pipCount(n) { return 6 - Math.abs(7 - n); } // ── Geometry generation ───────────────────────────────────────────────────── // Pointy-top corner: top & bottom vertices, angle = 60*i - 90 degrees. function hexCorners(cx, cy, size) { const pts = []; for (let i = 0; i < 6; i++) { const a = (Math.PI / 180) * (60 * i - 90); pts.push({ x: cx + size * Math.cos(a), y: cy + size * Math.sin(a) }); } return pts; } function buildGeometry(cx, cy, size) { const hexes = []; // Hex centres, rows of [3,4,5,4,3], each row horizontally centred. let id = 0; for (let r = 0; r < HEX_ROWS.length; r++) { const count = HEX_ROWS[r]; const rowY = cy + (r - 2) * ROW_V; const startX = cx - ((count - 1) / 2) * HEX_W; for (let c = 0; c < count; c++) { hexes.push({ id: id++, cx: startX + c * HEX_W, cy: rowY, row: r, col: c }); } } // Nodes: dedup hex corners by rounded pixel key. const nodeKey = (p) => `${Math.round(p.x)}_${Math.round(p.y)}`; const nodeMap = new Map(); // key -> node const nodes = []; const ensureNode = (p) => { const k = nodeKey(p); let n = nodeMap.get(k); if (!n) { n = { id: nodes.length, x: Math.round(p.x), y: Math.round(p.y), hexes: [], adj: [] }; nodeMap.set(k, n); nodes.push(n); } return n; }; // Edges: dedup by sorted node-id pair. const edgeMap = new Map(); const edges = []; const ensureEdge = (a, b) => { const lo = Math.min(a, b), hi = Math.max(a, b); const k = `${lo}_${hi}`; let e = edgeMap.get(k); if (!e) { e = { id: edges.length, nodes: [lo, hi], hexes: [] }; edgeMap.set(k, e); edges.push(e); } return e; }; for (const hex of hexes) { const corners = hexCorners(hex.cx, hex.cy, size).map(ensureNode); hex.corners = corners.map((n) => n.id); for (const n of corners) if (!n.hexes.includes(hex.id)) n.hexes.push(hex.id); for (let i = 0; i < 6; i++) { const a = corners[i].id; const b = corners[(i + 1) % 6].id; ensureEdge(a, b); } } // Node adjacency + edge→hex membership. for (const e of edges) { const [a, b] = e.nodes; if (!nodes[a].adj.includes(b)) nodes[a].adj.push(b); if (!nodes[b].adj.includes(a)) nodes[b].adj.push(a); e.hexes = nodes[a].hexes.filter((h) => nodes[b].hexes.includes(h)); } // Port slots: coastal edges (touch exactly 1 hex), 9 spaced around the rim. const coastal = edges.filter((e) => e.hexes.length === 1); coastal.sort((p, q) => { const pm = midpoint(nodes, p), qm = midpoint(nodes, q); return Math.atan2(pm.y - cy, pm.x - cx) - Math.atan2(qm.y - cy, qm.x - cx); }); const portSlots = []; for (let i = 0; i < 9; i++) { const e = coastal[Math.round((i * coastal.length) / 9) % coastal.length]; const m = midpoint(nodes, e); portSlots.push({ edgeId: e.id, nodes: [...e.nodes], x: m.x, y: m.y, // outward direction (board centre → edge midpoint), for drawing the marker offshore angle: Math.atan2(m.y - cy, m.x - cx), }); } return { hexes, nodes, edges, portSlots }; } function midpoint(nodes, edge) { const a = nodes[edge.nodes[0]], b = nodes[edge.nodes[1]]; return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }; } // Default geometry baked at module load — shared by every consumer. export const GEOMETRY = buildGeometry(BOARD_CX, BOARD_CY, HEX_SIZE); // Convenience accessors. export const HEXES = GEOMETRY.hexes; export const NODES = GEOMETRY.nodes; export const EDGES = GEOMETRY.edges; export const PORT_SLOTS = GEOMETRY.portSlots; // Edge id between two adjacent node ids, or -1. export function edgeBetween(a, b) { const lo = Math.min(a, b), hi = Math.max(a, b); const e = EDGES.find((x) => x.nodes[0] === lo && x.nodes[1] === hi); return e ? e.id : -1; }