fertig-classic-games/public/src/games/catan/CatanBoard.js

214 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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