From 4644b8e3af25e6fc734631e973b57549b7836c0b Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Thu, 21 May 2026 20:52:00 -0600 Subject: [PATCH] refactor: implement dynamic track layout for overflowing trains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace fixed `TRACK_PITCH` tiling with `computeTrackLayout()` to dynamically fit tiles within track bounds - Add `tileHalfW()` helper to correctly calculate width for double vs. single tiles - Add truncation marks ("+N«") when the tile count exceeds visible track space - Update marker placement, flash effects, and next-tile positioning to use the new dynamic layout coordinates - Adjust score badge positioning to align relative to portrait radius --- .../games/mexicantrain/MexicanTrainGame.js | 84 ++++++++++++++----- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/public/src/games/mexicantrain/MexicanTrainGame.js b/public/src/games/mexicantrain/MexicanTrainGame.js index 15e479d..5e9b57f 100644 --- a/public/src/games/mexicantrain/MexicanTrainGame.js +++ b/public/src/games/mexicantrain/MexicanTrainGame.js @@ -277,30 +277,62 @@ export default class MexicanTrainGame extends Phaser.Scene { } } + // Returns the horizontal half-width a tile occupies when painted. + tileHalfW(tile) { + return tile.left === tile.right ? TRACK_HALF / 2 : TRACK_HALF; + } + + // Returns { start, positions[] } for the visible slice of a train. + // Positions are the x-centres of each visible tile (index start..end). + computeTrackLayout(key) { + const tiles = this.gs.trains[key].tiles; + if (tiles.length === 0) return { start: 0, positions: [] }; + const GAP = 6; + const TRUNC = 16; // px reserved for the "+N«" truncation mark + const avail = TRACKS_RIGHT - TRACKS_LEFT; + + // Walk backwards to find the first tile that still fits. + let accumulated = 0; + let start = tiles.length; + for (let t = tiles.length - 1; t >= 0; t--) { + const addW = (start < tiles.length ? GAP : 0) + this.tileHalfW(tiles[t]) * 2; + if (accumulated + addW > avail - (t > 0 ? TRUNC : 0)) break; + accumulated += addW; + start = t; + } + + // Build centre-x positions for each visible tile. + const positions = []; + let x = TRACKS_LEFT + (start > 0 ? TRUNC : 0) + this.tileHalfW(tiles[start]); + for (let t = start; t < tiles.length; t++) { + positions.push(x); + if (t < tiles.length - 1) { + x += this.tileHalfW(tiles[t]) + GAP + this.tileHalfW(tiles[t + 1]); + } + } + return { start, positions }; + } + paintTracks() { const g = this.trackGfx; g.clear(); const rows = this.gs.players.length + 1; - const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH); for (let i = 0; i < rows; i++) { const key = this.rowKeyFor(i); const y = this.rowY(i); const tiles = this.gs.trains[key].tiles; - const start = Math.max(0, tiles.length - maxTiles); - let drawX = TRACKS_LEFT + TRACK_HALF; - if (start > 0) { - this.maybeTruncMark(i, start); - drawX += 8; - } else { - this.clearTruncMark(i); - } - for (let t = start; t < tiles.length; t++) { + if (tiles.length === 0) { this.clearTruncMark(i); continue; } + + const { start, positions } = this.computeTrackLayout(key); + if (start > 0) this.maybeTruncMark(i, start); else this.clearTruncMark(i); + + for (let ti = 0; ti < positions.length; ti++) { + const t = start + ti; const tile = tiles[t]; const isDouble = tile.left === tile.right; const isOpenDouble = this.gs.openDouble?.train === key && t === tiles.length - 1; const border = isOpenDouble ? COLORS.danger : (isDouble ? COLORS.gold : COLORS.accent); - this.paintDomino(g, drawX, y, TRACK_HALF, tile.left, tile.right, isDouble, border); - drawX += TRACK_PITCH; + this.paintDomino(g, positions[ti], y, TRACK_HALF, tile.left, tile.right, isDouble, border); } } } @@ -331,7 +363,10 @@ export default class MexicanTrainGame extends Phaser.Scene { if (!this.gs.trains[key].marker) continue; const y = this.rowY(i); const tiles = this.gs.trains[key].tiles; - const x = TRACKS_LEFT + TRACK_HALF + Math.min(tiles.length, Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH)) * TRACK_PITCH + 6; + const { positions } = this.computeTrackLayout(key); + const lastPos = positions.length ? positions[positions.length - 1] : TRACKS_LEFT; + const lastHW = tiles.length ? this.tileHalfW(tiles[tiles.length - 1]) : TRACK_HALF; + const x = lastPos + lastHW + 6; // little open-train flag g.fillStyle(COLORS.danger, 1); g.fillCircle(x, y, 9); @@ -419,8 +454,8 @@ export default class MexicanTrainGame extends Phaser.Scene { for (let i = 0; i < this.gs.players.length; i++) { const score = scores[i]; - const x = PORTRAIT_X; - const y = this.rowY(i) - PORTRAIT_R - 20; + const x = PORTRAIT_X + Math.round(PORTRAIT_R * 0.85); + const y = this.rowY(i) - Math.round(PORTRAIT_R * 0.85); const borderColor = score > 75 ? COLORS.danger : score > 50 ? 0xe07030 : score > 25 ? COLORS.accent @@ -676,14 +711,14 @@ export default class MexicanTrainGame extends Phaser.Scene { // ─── Effects ────────────────────────────────────────────────────────── flashTrainEnd(key) { const rows = this.gs.players.length + 1; - let i = key === MEXICAN ? rows - 1 : Number(key); + const i = key === MEXICAN ? rows - 1 : Number(key); const y = this.rowY(i); const tiles = this.gs.trains[key].tiles; - const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH); - const shown = Math.min(tiles.length, maxTiles); - const x = TRACKS_LEFT + TRACK_HALF + (shown - 1) * TRACK_PITCH; + if (!tiles.length) return; + const { positions } = this.computeTrackLayout(key); + const x = positions[positions.length - 1]; const lastTile = tiles[tiles.length - 1]; - const isLastDouble = lastTile && lastTile.left === lastTile.right; + const isLastDouble = lastTile.left === lastTile.right; const fw = isLastDouble ? TRACK_HALF : TRACK_TILE_W; const fh = isLastDouble ? TRACK_TILE_W : TRACK_HALF; const fx = this.add.graphics().setDepth(DEPTH.tileFx); @@ -697,9 +732,12 @@ export default class MexicanTrainGame extends Phaser.Scene { const i = key === MEXICAN ? rows - 1 : Number(key); const y = this.rowY(i); const tiles = this.gs.trains[key].tiles; - const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH); - const newIdx = Math.min(tiles.length, maxTiles - 1); - return { x: TRACKS_LEFT + TRACK_HALF + newIdx * TRACK_PITCH, y }; + if (tiles.length === 0) return { x: TRACKS_LEFT + TRACK_HALF, y }; + const { positions } = this.computeTrackLayout(key); + const lastX = positions[positions.length - 1]; + const lastHW = this.tileHalfW(tiles[tiles.length - 1]); + const nextX = Math.min(lastX + lastHW + 6 + TRACK_HALF, TRACKS_RIGHT - TRACK_HALF); + return { x: nextX, y }; } animateTilePlay(fromX, fromY, toX, toY, tileA, tileB, onComplete) {