refactor Catan setup: use deterministic chit placement and randomize player order

- Replace the random hex chit assignment with a fixed spiral-based sequence (`CHIT_SPIRAL` and `CHIT_SEQUENCE`) to ensure consistent board layouts.
- Randomize the initial player turn order using a shuffled seat array instead of the static snake pattern.
- Update the build costs legend UI to display colored resource swatches in a bottom panel.
This commit is contained in:
Brian Fertig 2026-05-23 10:25:55 -06:00
parent 19620c10d4
commit 4b6593b074
3 changed files with 54 additions and 41 deletions

View File

@ -39,6 +39,12 @@ export const RESOURCE_BAG = [
// 18 number chits for the 18 non-desert hexes. // 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]; 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. // 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 PORT_BAG = ['any', 'any', 'any', 'any', 'brick', 'lumber', 'wool', 'grain', 'ore'];

View File

@ -254,7 +254,7 @@ export default class CatanGame extends Phaser.Scene {
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex,
}).setOrigin(0, 0.5).setDepth(D.hud); }).setOrigin(0, 0.5).setDepth(D.hud);
// cost legend (right, above buttons) // cost legend (bottom bar, right of dice)
this.buildCostLegend(); this.buildCostLegend();
// action buttons (vertical column, right) // action buttons (vertical column, right)
@ -273,19 +273,45 @@ export default class CatanGame extends Phaser.Scene {
} }
buildCostLegend() { buildCostLegend() {
const x = 1600, y = 230; const panelRight = 1900;
const panelW = 320;
const cx = panelRight - panelW / 2;
const bgCy = 980, bgH = 164;
const panel = this.add.container(0, 0).setDepth(D.hud); const panel = this.add.container(0, 0).setDepth(D.hud);
panel.add(this.add.rectangle(x, y + 80, 180, 200, 0x000000, 0.3).setStrokeStyle(1, COLORS.accent, 0.5)); panel.add(this.add.rectangle(cx, bgCy, panelW, bgH, 0x000000, 0.3).setStrokeStyle(1, COLORS.accent, 0.5));
panel.add(this.add.text(x, y - 4, 'Build Costs', { fontFamily: 'Righteous', fontSize: '16px', color: COLORS.goldHex }).setOrigin(0.5)); panel.add(this.add.text(cx, bgCy - bgH / 2 + 7, 'Build Costs', {
const lines = [ fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex,
['Road', 'Brick Lumber'], }).setOrigin(0.5, 0));
['Settle', 'Br Lu Wo Gr'],
['City', '2 Grain 3 Ore'], const rows = [
['Dev', 'Wool Grain Ore'], { name: 'Road', resources: ['brick', 'lumber'] },
{ name: 'Settlement', resources: ['brick', 'lumber', 'wool', 'grain'] },
{ name: 'City', resources: ['grain', 'grain', 'ore', 'ore', 'ore'] },
{ name: 'Dev Card', resources: ['wool', 'grain', 'ore'] },
]; ];
lines.forEach((ln, i) => {
panel.add(this.add.text(x - 78, y + 30 + i * 38, ln[0], { fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.textHex }).setOrigin(0, 0.5)); const lx = panelRight - panelW + 14;
panel.add(this.add.text(x - 78, y + 48 + i * 38, ln[1], { fontFamily: '"Julius Sans One"', fontSize: '12px', color: COLORS.mutedHex }).setOrigin(0, 0.5)); const rx = panelRight - 14;
const rowY0 = bgCy - bgH / 2 + 44;
const rowStep = (bgH - 44 - 14) / (rows.length - 1);
const SW = 16, SH = 16, SG = 4, SR = 3; // swatch w/h/gap/radius
const g = this.add.graphics();
panel.add(g);
rows.forEach(({ name, resources }, i) => {
const ry = rowY0 + i * rowStep;
panel.add(this.add.text(lx, ry, name, {
fontFamily: '"Julius Sans One"', fontSize: '19px', color: COLORS.textHex,
}).setOrigin(0, 0.5));
const totalW = resources.length * SW + (resources.length - 1) * SG;
let sx = rx - totalW;
for (const r of resources) {
g.fillStyle(RESOURCE_INFO[r].swatch, 1);
g.fillRoundedRect(sx, ry - SH / 2, SW, SH, SR);
sx += SW + SG;
}
}); });
} }

View File

@ -3,7 +3,8 @@
import { import {
NODES, EDGES, HEXES, PORT_SLOTS, edgeBetween, NODES, EDGES, HEXES, PORT_SLOTS, edgeBetween,
RESOURCE_BAG, CHIT_BAG, PORT_BAG, COSTS, DEV_DECK, RESOURCE_TYPES, WIN_VP, RESOURCE_BAG, PORT_BAG, COSTS, DEV_DECK, RESOURCE_TYPES, WIN_VP,
CHIT_SPIRAL, CHIT_SEQUENCE,
} from './CatanBoard.js'; } from './CatanBoard.js';
// ── small utilities ───────────────────────────────────────────────────────── // ── small utilities ─────────────────────────────────────────────────────────
@ -26,16 +27,6 @@ export function handSize(player) {
return RESOURCE_TYPES.reduce((s, r) => s + player.resources[r], 0); return RESOURCE_TYPES.reduce((s, r) => s + player.resources[r], 0);
} }
// Hex adjacency (share an edge → share 2 corner nodes).
const HEX_NEIGHBORS = HEXES.map(() => []);
for (const e of EDGES) {
if (e.hexes.length === 2) {
const [h1, h2] = e.hexes;
if (!HEX_NEIGHBORS[h1].includes(h2)) HEX_NEIGHBORS[h1].push(h2);
if (!HEX_NEIGHBORS[h2].includes(h1)) HEX_NEIGHBORS[h2].push(h1);
}
}
// ── occupancy queries ──────────────────────────────────────────────────────── // ── occupancy queries ────────────────────────────────────────────────────────
export function nodeBuilding(state, nodeId) { export function nodeBuilding(state, nodeId) {
for (const p of state.players) { for (const p of state.players) {
@ -65,21 +56,12 @@ export function createInitialState(playerCount = 3) {
const desertHex = hexes.find((h) => h.resource === 'desert'); const desertHex = hexes.find((h) => h.resource === 'desert');
desertHex.hasRobber = true; desertHex.hasRobber = true;
// Number chits onto non-desert hexes, 6/8 never adjacent. // Number chits: walk the standard spiral, skip desert, assign fixed sequence AR.
const nonDesert = hexes.filter((h) => h.resource !== 'desert'); let chitIdx = 0;
for (let attempt = 0; attempt < 500; attempt++) { for (const hexId of CHIT_SPIRAL) {
const chits = shuffle(CHIT_BAG); if (hexes[hexId].resource !== 'desert') {
nonDesert.forEach((h, i) => { h.number = chits[i]; }); hexes[hexId].number = CHIT_SEQUENCE[chitIdx++];
let ok = true;
for (const h of nonDesert) {
if (h.number !== 6 && h.number !== 8) continue;
for (const nb of HEX_NEIGHBORS[h.id]) {
const other = hexes[nb];
if (other.number === 6 || other.number === 8) { ok = false; break; }
} }
if (!ok) break;
}
if (ok) break;
} }
// Port types onto fixed slots. // Port types onto fixed slots.
@ -109,10 +91,9 @@ export function createInitialState(playerCount = 3) {
}); });
} }
// Snake setup order: 0..n-1 then n-1..0. // Snake setup order: randomized forward then reverse.
const order = []; const seats = shuffle([...Array(n).keys()]);
for (let s = 0; s < n; s++) order.push(s); const order = [...seats, ...[...seats].reverse()];
for (let s = n - 1; s >= 0; s--) order.push(s);
return { return {
playerCount: n, playerCount: n,