commit 97451ce2d6dd172834d7166bd8575dd67276407f
parent c27e15ff785fe1b01ad4cdd158aa570d5ff2240f
Author: Kyle Barlow <kb@kylebarlow.com>
Date: Sun, 26 Apr 2026 20:36:45 -0700
Revert "feat: Add 5 new features to Fire Truck"
This reverts commit c27e15ff785fe1b01ad4cdd158aa570d5ff2240f.
Diffstat:
4 files changed, 38 insertions(+), 492 deletions(-)
diff --git a/games/fire-truck/game.js b/games/fire-truck/game.js
@@ -6,11 +6,9 @@ const BASE_SPEED = 170;
const MAX_SPEED = 220;
const ACCEL = 260;
const BRAKE = 320;
-const REACTION_TIME_S = 2.0;
-const MIN_PROMPT_DIST = CELL_SIZE * 0.72;
-const STOP_LINE_DIST = Math.round(CELL_SIZE * 0.45);
-const FIRE_TRIGGER_DIST = CELL_SIZE * 0.6;
-const FIRE_EXTINGUISH_S = 3.0;
+const PROMPT_TRIGGER_DIST = CELL_SIZE * 0.72;
+const STOP_LINE_DIST = 6 * SCALE;
+const AUTO_FAIL_GRACE_MS = 4500;
const ROUTE_EXTENSION_COUNT = 36;
const WATER_COLOR = 0x6bc4e8;
@@ -19,12 +17,6 @@ const ASPHALT_COLOR = 0x2a2d34;
const STRIPE_COLOR = 0xffd23f;
const BUILDING_PALETTE = [0xf8d4a5, 0xb8dfe1, 0xf7b4b4, 0xffefb0, 0xcad6f9, 0xdcc7eb];
-function arrowLabel(dir) {
- const ARROWS = { left: '←', right: '→', straight: '↑' };
- const NAMES = { left: 'LEFT', right: 'RIGHT', straight: 'STRAIGHT' };
- return dir ? (ARROWS[dir] || '') + ' ' + (NAMES[dir] || dir.toUpperCase()) : '';
-}
-
class FireTruckScene extends Phaser.Scene {
constructor() {
super({ key: 'FireTruckScene' });
@@ -49,18 +41,10 @@ class FireTruckScene extends Phaser.Scene {
this.successUntil = 0;
this.debugDistance = 0;
this.promptShownAt = 0;
-
- this.fireCell = null;
- this.fireBuildingCell = null;
- this.fireGraphics = null;
- this.waterGraphics = null;
- this.fireExtinguishAccum = 0;
- this.spaceHeld = false;
- this.fireMode = false;
}
create() {
- this.island = FT.buildIsland({ width: 50, height: 50, routeCount: 200, seed: window.__TEST_SEED__ });
+ this.island = FT.buildIsland({ width: 50, height: 50, routeCount: 200, seed: 20260426 });
this.route = this.island.route;
this.cellSize = CELL_SIZE;
@@ -127,7 +111,6 @@ class FireTruckScene extends Phaser.Scene {
this.truck.rotation = this.targetRotation;
this.input.keyboard.on('keydown', this.onKeyDown, this);
- this.input.keyboard.on('keyup', this.onKeyUp, this);
this.scale.on('resize', this.onResize, this);
this.onResize(this.scale.gameSize);
@@ -147,7 +130,6 @@ class FireTruckScene extends Phaser.Scene {
this.events.on('destroy', this.shutdown, this);
window.__FT_SCENE__ = this;
- this._initFireDestination();
this.refreshDebug();
}
@@ -155,148 +137,6 @@ class FireTruckScene extends Phaser.Scene {
return { x: x * CELL_SIZE, y: y * CELL_SIZE };
}
- _initFireDestination() {
- const fireData = FT.pickFireDestination(this.island, FT.createSeededRng(window.__TEST_SEED__ || Date.now()));
- this.fireCell = fireData.roadCell;
- this.fireBuildingCell = fireData.buildingCell;
-
- const startCell = this.island.grid[this.island.routeStart.y][this.island.routeStart.x];
- const targetCell = this.island.grid[this.fireCell.y][this.fireCell.x];
-
- const cellPath = FT.bfsRoadPath(this.island.grid, startCell, targetCell);
- this.route = FT.pathToRouteSteps(this.island.grid, cellPath, 0);
- this.routeIndex = 0;
- this.currentStep = this.route[0];
-
- if (cellPath.length > 1) {
- this.heading = FT.headingFromTo(cellPath[0], cellPath[1]);
- }
-
- if (this.route.length === 0) {
- this.segmentEnd = this.worldPoint(this.fireCell.x, this.fireCell.y);
- this.phase = 'approach';
- this.promptDir = null;
- }
-
- this._createFireGraphics();
- this.fireMode = true;
- }
-
- _createFireGraphics() {
- if (this.fireGraphics) this.fireGraphics.destroy();
- this.fireGraphics = this.add.graphics();
- this.fireGraphics.setDepth(10);
- this.uiCamera.ignore(this.fireGraphics);
- }
-
- _updateFireAnimation(t) {
- if (!this.fireGraphics || !this.fireBuildingCell) return;
- const gfx = this.fireGraphics;
- gfx.clear();
-
- const bx = this.fireBuildingCell.x * CELL_SIZE;
- const by = this.fireBuildingCell.y * CELL_SIZE;
-
- const colors = [0xff6600, 0xff2200, 0xffcc00, 0xff8800];
- for (let i = 0; i < 5; i++) {
- const h = 40 + 20 * Math.sin(t * 0.003 + i * 1.3);
- const w = 20;
- const ox = -30 + i * 15 + 10 * Math.sin(t * 0.005 + i * 0.7);
- const oy = 20;
-
- gfx.fillStyle(colors[i % colors.length], 0.7 + 0.3 * Math.sin(t * 0.01 + i));
- gfx.beginPath();
- gfx.moveTo(bx + ox, by + oy);
- gfx.lineTo(bx + ox + w / 2, by + oy - h);
- gfx.lineTo(bx + ox + w, by + oy);
- gfx.closePath();
- gfx.fillPath();
- }
- }
-
- _updateWaterSpray() {
- if (!this.spaceHeld || !this.fireCell) {
- if (this.waterGraphics) this.waterGraphics.clear();
- return;
- }
- if (!this.waterGraphics) {
- this.waterGraphics = this.add.graphics();
- this.waterGraphics.setDepth(12);
- this.uiCamera.ignore(this.waterGraphics);
- }
-
- const gfx = this.waterGraphics;
- gfx.clear();
- gfx.lineStyle(8 * SCALE, 0x44aaff, 0.75);
-
- const fx = this.fireCell.x * CELL_SIZE;
- const fy = this.fireCell.y * CELL_SIZE;
- const tx = this.truck.x;
- const ty = this.truck.y;
-
- const dist = Math.hypot(fx - tx, fy - ty);
- const midX = (tx + fx) / 2;
- const midY = (ty + fy) / 2;
- const ctrlX = midX - (fy - ty) * 0.2;
- const ctrlY = midY + (fx - tx) * 0.2;
-
- gfx.beginPath();
- gfx.moveTo(tx, ty);
- gfx.quadraticCurveTo(ctrlX, ctrlY, fx, fy);
- gfx.strokePath();
- }
-
- enterFirefighting() {
- this.state = 'firefighting';
- this.targetSpeed = 0;
- this.speed = 0;
- this.fireExtinguishAccum = 0;
- this.spaceHeld = false;
- this.statusText.setText('Hold SPACE to spray water!');
- this.promptText.setText('');
- this.promptDir = null;
- }
-
- _extinguishFire() {
- this.successOverlay.setAlpha(0.6);
- this.successUntil = this.time.now + 600;
- this.playSuccessSound();
-
- if (this.fireGraphics) { this.fireGraphics.destroy(); this.fireGraphics = null; }
- if (this.waterGraphics) { this.waterGraphics.destroy(); this.waterGraphics = null; }
- this.fireCell = null;
- this.fireBuildingCell = null;
- this.spaceHeld = false;
- this.fireExtinguishAccum = 0;
-
- this.time.delayedCall(600, () => {
- const fireData = FT.pickFireDestination(this.island, FT.createSeededRng(window.__TEST_SEED__ || Date.now()));
- this.fireCell = fireData.roadCell;
- this.fireBuildingCell = fireData.buildingCell;
-
- const startCell = this.island.grid[Math.round(this.truck.y / CELL_SIZE)][Math.round(this.truck.x / CELL_SIZE)];
- const targetCell = this.island.grid[this.fireCell.y][this.fireCell.x];
-
- const cellPath = FT.bfsRoadPath(this.island.grid, startCell, targetCell);
- this.route = FT.pathToRouteSteps(this.island.grid, cellPath, 0);
- this.routeIndex = 0;
- this.currentStep = this.route[0];
-
- this.state = 'driving';
- this.targetSpeed = BASE_SPEED * (window.GAME_SPEED_MULTIPLIER ?? 1.0);
-
- if (this.route.length === 0) {
- this.segmentEnd = this.worldPoint(this.fireCell.x, this.fireCell.y);
- this.phase = 'approach';
- this.promptDir = null;
- } else {
- this.advancePhase(); // set up the next step
- }
-
- this._createFireGraphics();
- });
- }
-
renderIsland() {
// Bake the static island into tiled textures so per-frame cost drops to a
// handful of textured quads — and Phaser's camera culling skips offscreen
@@ -432,16 +272,12 @@ class FireTruckScene extends Phaser.Scene {
const m = window.GAME_SPEED_MULTIPLIER ?? 1.0;
if (this.state === 'driving') {
this.targetSpeed = (this.promptResolved ? MAX_SPEED : BASE_SPEED) * m;
- } else if (this.state === 'waiting' || this.state === 'stopped') {
+ } else if (this.state === 'stopped') {
this.targetSpeed = 0;
}
}
onKeyDown(event) {
- if (event.code === 'Space') {
- if (this.state === 'firefighting') this.spaceHeld = true;
- return;
- }
const move = event.key === 'ArrowLeft' ? 'left' : event.key === 'ArrowRight' ? 'right' : event.key === 'ArrowUp' ? 'straight' : null;
if (!move || !this.promptDir) return;
this.initAudio();
@@ -458,13 +294,7 @@ class FireTruckScene extends Phaser.Scene {
this.refreshDebug();
return;
}
- if (this.state === 'driving' || this.state === 'waiting') this.enterStopped();
- }
-
- onKeyUp(event) {
- if (event.code === 'Space') {
- this.spaceHeld = false;
- }
+ if (this.state !== 'stopped') this.enterStopped();
}
initAudio() {
@@ -516,19 +346,11 @@ class FireTruckScene extends Phaser.Scene {
this.failVisible = true;
this.warningUntil = this.time.now + 250;
this.overlay.setFillStyle(0xe63946, 0.18);
- this.statusText.setText('Try the ' + arrowLabel(this.promptDir) + ' arrow.');
+ this.statusText.setText('Try the ' + this.promptDir + ' arrow.');
this.playFailSound();
this.refreshDebug();
}
- enterWaiting() {
- if (this.state === 'stopped' || this.state === 'waiting') return;
- this.state = 'waiting';
- this.targetSpeed = 0;
- this.statusText.setText('Stop! Press ' + arrowLabel(this.promptDir));
- this.refreshDebug();
- }
-
update(_, deltaMs) {
const now = performance.now();
const elapsed = now - this.lastStepTime;
@@ -548,51 +370,32 @@ class FireTruckScene extends Phaser.Scene {
const dist = Math.hypot(dx, dy);
let promptJustSet = false;
- const promptTriggerDist = Math.max(MIN_PROMPT_DIST, this.speed * REACTION_TIME_S);
- if (this.phase === 'approach' && !this.promptDir && dist <= promptTriggerDist) {
+ if (this.phase === 'approach' && !this.promptDir && dist <= PROMPT_TRIGGER_DIST) {
this.promptDir = this.currentStep.move;
this.promptResolved = false;
+ this.state = 'prompting';
this.promptShownAt = this.time.now;
promptJustSet = true;
- this.statusText.setText('Press the ' + arrowLabel(this.promptDir) + ' before the crossing.');
+ this.statusText.setText('Press the ' + this.promptDir + ' arrow before the crossing.');
}
- if (!promptJustSet && this.phase === 'approach' && this.promptDir && !this.promptResolved && dist <= STOP_LINE_DIST + 1.0) {
- this.enterWaiting();
+ if (!promptJustSet && this.phase === 'approach' && this.promptDir && !this.promptResolved && dist <= STOP_LINE_DIST) {
+ if (this.time.now - this.promptShownAt >= AUTO_FAIL_GRACE_MS) {
+ this.enterStopped();
+ } else {
+ this.promptResolved = true;
+ this.state = 'driving';
+ this.targetSpeed = BASE_SPEED * (window.GAME_SPEED_MULTIPLIER ?? 1.0);
+ }
}
if (dist > 0.001 && this.speed > 0) {
- let travel = Math.min(dist, this.speed * dt);
- if (this.phase === 'approach' && this.promptDir && !this.promptResolved) {
- travel = Math.min(travel, Math.max(0, dist - STOP_LINE_DIST));
- }
+ const travel = Math.min(dist, this.speed * dt);
this.truck.x += (dx / dist) * travel;
this.truck.y += (dy / dist) * travel;
this.debugDistance += travel;
}
- if (this.fireMode && this.fireCell && (this.state === 'driving' || this.state === 'waiting')) {
- const fdx = this.fireCell.x * CELL_SIZE - this.truck.x;
- const fdy = this.fireCell.y * CELL_SIZE - this.truck.y;
- if (Math.hypot(fdx, fdy) < FIRE_TRIGGER_DIST) {
- this.enterFirefighting();
- return;
- }
- }
-
- if (this.state === 'firefighting') {
- if (this.spaceHeld) {
- this.fireExtinguishAccum += dt;
- if (this.fireExtinguishAccum >= FIRE_EXTINGUISH_S) this._extinguishFire();
- }
- this._updateFireAnimation(this.time.now);
- this._updateWaterSpray();
- this.refreshDebug();
- return;
- }
-
- if (this.fireGraphics) this._updateFireAnimation(this.time.now);
-
const remaining = Math.hypot(this.segmentEnd.x - this.truck.x, this.segmentEnd.y - this.truck.y);
if (remaining <= 0.8 * SCALE) {
this.truck.setPosition(this.segmentEnd.x, this.segmentEnd.y);
@@ -644,14 +447,7 @@ class FireTruckScene extends Phaser.Scene {
this.routeIndex += 1;
this.extendRouteIfNeeded();
this.currentStep = this.route[this.routeIndex];
- if (!this.currentStep) {
- if (this.fireMode && this.fireCell) {
- this.segmentEnd = { x: this.fireCell.x * CELL_SIZE, y: this.fireCell.y * CELL_SIZE };
- this.phase = 'approach';
- this.promptDir = null;
- }
- return;
- }
+ if (!this.currentStep) return;
this.heading = this.currentStep.headingIn;
this.segmentEnd = this.worldPoint(this.currentStep.x, this.currentStep.y);
this.phase = 'approach';
@@ -664,7 +460,6 @@ class FireTruckScene extends Phaser.Scene {
}
extendRouteIfNeeded() {
- if (this.fireMode) return;
if (this.routeIndex < this.route.length - 12) return;
const extension = FT.extendRouteOnGraph(this.island, this.route, ROUTE_EXTENSION_COUNT);
this.route.push(...extension);
@@ -683,7 +478,7 @@ class FireTruckScene extends Phaser.Scene {
this.debugDistancePx = this.debugDistance;
this.fps = this.game.loop.actualFps;
if (this.fpsText) this.fpsText.setText(`FPS: ${this.fps.toFixed(0)}`);
- this.promptText.setText(arrowLabel(this.promptDir));
+ this.promptText.setText(this.promptDir ? this.promptDir.toUpperCase() : '');
}
cellAt(x, y) {
@@ -700,9 +495,9 @@ class FireTruckScene extends Phaser.Scene {
this.phase = 'approach';
this.promptDir = this.currentStep.move;
this.promptResolved = false;
- this.state = 'driving';
- this.targetSpeed = BASE_SPEED * (window.GAME_SPEED_MULTIPLIER ?? 1.0);
- this.speed = this.targetSpeed;
+ this.state = 'prompting';
+ this.targetSpeed = 0;
+ this.speed = 0;
this.targetRotation = this.rotationForHeading(this.heading);
const pos = this.worldPoint(this.currentStep.approachX, this.currentStep.approachY);
@@ -716,8 +511,6 @@ class FireTruckScene extends Phaser.Scene {
clearInterval(this.fallbackTimer);
this.fallbackTimer = null;
}
- if (this.fireGraphics) { this.fireGraphics.destroy(); this.fireGraphics = null; }
- if (this.waterGraphics) { this.waterGraphics.destroy(); this.waterGraphics = null; }
}
}
diff --git a/games/fire-truck/lib.js b/games/fire-truck/lib.js
@@ -298,14 +298,9 @@
stitchRoadExits(grid, width, height);
validateConnectedRoads(grid, width, height);
- const cells = flattenGrid(grid);
- const roads = cells.filter(c => c.type === CELL_TYPES.ROAD);
-
- const routeStart = (opts && opts.routeStart)
- ? opts.routeStart
- : pickRandomRouteStart(roads, rand);
-
+ const routeStart = { x: 3, y: 2, heading: 'east' };
const route = buildRouteOnGraph(grid, { count: routeCount, startCell: routeStart, startHeading: routeStart.heading }, rand);
+ const cells = flattenGrid(grid);
return {
width,
@@ -459,115 +454,6 @@
}, rng);
}
- function pickRandomRouteStart(roads, rand) {
- const candidates = roads.filter(c => {
- const exits = c.exits;
- let degree = 0;
- for (const k in exits) if (exits[k]) degree++;
- return degree >= 2;
- });
- if (!candidates.length) throw new Error('No valid road cells for route start');
- const cell = randPick(rand, candidates);
- const validExits = ['N', 'E', 'S', 'W'].filter(c => cell.exits[c]);
- const card = randPick(rand, validExits);
- return { x: cell.x, y: cell.y, heading: CARD_TO_HEADING[card] };
- }
-
- function headingFromTo(a, b) {
- const dx = b.x - a.x, dy = b.y - a.y;
- if (dx === 1) return 'east'; if (dx === -1) return 'west';
- if (dy === 1) return 'south'; return 'north';
- }
-
- function bfsRoadPath(grid, startCell, targetCell) {
- if (startCell === targetCell) return [startCell];
- const queue = [[startCell]];
- const seen = new Set([`${startCell.x},${startCell.y}`]);
-
- while (queue.length > 0) {
- const path = queue.shift();
- const cell = path[path.length - 1];
-
- for (const k of ['N', 'E', 'S', 'W']) {
- if (!cell.exits[k]) continue;
- const dir = DIRS[k];
- const nx = cell.x + dir.dx;
- const ny = cell.y + dir.dy;
- const nextCell = grid[ny][nx];
- if (!nextCell || nextCell.type !== CELL_TYPES.ROAD) continue;
-
- const key = `${nx},${ny}`;
- if (!seen.has(key)) {
- seen.add(key);
- const newPath = [...path, nextCell];
- if (nextCell === targetCell) return newPath;
- queue.push(newPath);
- }
- }
- }
- return null;
- }
-
- function pathToRouteSteps(grid, cellPath, startIndex) {
- const route = [];
- if (!cellPath || cellPath.length <= 2) return route;
-
- for (let i = 1; i < cellPath.length - 1; i++) {
- const decision = cellPath[i];
- if (!isDecisionCell(decision)) continue;
-
- const approach = cellPath[i - 1];
- const exitCell = cellPath[i + 1];
- const headingIn = headingFromTo(approach, decision);
- const headingOut = headingFromTo(decision, exitCell);
- const move = getRelativeMove(headingIn, headingOut);
-
- route.push({
- index: startIndex + route.length,
- x: decision.x,
- y: decision.y,
- approachX: approach.x,
- approachY: approach.y,
- headingIn,
- headingOut,
- move,
- exitX: exitCell.x,
- exitY: exitCell.y,
- });
- }
- return route;
- }
-
- function pickFireDestination(island, rng) {
- const candidates = island.roads.filter(cell => {
- for (const k of ['N', 'E', 'S', 'W']) {
- const dir = DIRS[k];
- const nx = cell.x + dir.dx;
- const ny = cell.y + dir.dy;
- const neighbor = island.grid[ny] && island.grid[ny][nx];
- if (neighbor && neighbor.type === CELL_TYPES.BUILDING) return true;
- }
- return false;
- });
-
- if (!candidates.length) throw new Error('No road adjacent to building found');
- const roadCell = randPick(rng, candidates);
-
- let buildingCell = null;
- for (const k of ['N', 'E', 'S', 'W']) {
- const dir = DIRS[k];
- const nx = roadCell.x + dir.dx;
- const ny = roadCell.y + dir.dy;
- const neighbor = island.grid[ny] && island.grid[ny][nx];
- if (neighbor && neighbor.type === CELL_TYPES.BUILDING) {
- buildingCell = neighbor;
- break;
- }
- }
-
- return { roadCell, buildingCell };
- }
-
const api = {
HEADINGS,
CELL_TYPES,
@@ -603,11 +489,6 @@
CARD_TO_HEADING,
HEADING_TO_CARD,
DIRS,
- pickRandomRouteStart,
- headingFromTo,
- bfsRoadPath,
- pathToRouteSteps,
- pickFireDestination,
};
if (typeof module !== 'undefined' && module.exports) module.exports = api;
diff --git a/tests/browser/runner.js b/tests/browser/runner.js
@@ -17,7 +17,6 @@ async function main() {
await page.addInitScript(() => {
window.GAME_SPEED_MULTIPLIER = 8.0;
window.__TEST_MODE__ = true;
- window.__TEST_SEED__ = 20260426;
});
const errors = [];
page.on('console', m => { if (m.type() === 'error') errors.push('[console] ' + m.text()); });
@@ -209,11 +208,15 @@ async function main() {
await test('fire-truck: truck starts moving automatically', async (page, url) => {
await page.goto(`${url}/games/fire-truck/`, { waitUntil: 'domcontentloaded' });
+ await page.waitForTimeout(1200);
const start = await page.evaluate(() => ({
x: window.__FT_SCENE__.truckX,
y: window.__FT_SCENE__.truckY,
+ distance: window.__FT_SCENE__.debugDistancePx || 0,
+ prompt: window.__FT_SCENE__.promptDir,
+ state: window.__FT_SCENE__.state,
}));
- await page.waitForTimeout(600);
+ await page.waitForTimeout(3000);
const end = await page.evaluate(() => ({
x: window.__FT_SCENE__.truckX,
y: window.__FT_SCENE__.truckY,
@@ -221,9 +224,10 @@ async function main() {
prompt: window.__FT_SCENE__.promptDir,
state: window.__FT_SCENE__.state,
}));
- const distanceMoved = end.distance;
- if (distanceMoved < 20) {
- throw new Error(`Truck barely moved: ${distanceMoved.toFixed(1)} pixels (end state: ${end.state})`);
+ const moved = Math.hypot(end.x - start.x, end.y - start.y);
+ const distanceMoved = end.distance - start.distance;
+ if (moved < 20 && distanceMoved < 120) {
+ throw new Error(`Truck moved only ${moved.toFixed(1)} pixels and advanced ${distanceMoved.toFixed(1)} path pixels (start ${start.state}/${start.prompt || 'none'}, end ${end.state}/${end.prompt || 'none'})`);
}
});
@@ -275,7 +279,7 @@ async function main() {
cell11: s.cellAt(1, 1).type,
cell22: s.cellAt(2, 2).type,
routeOutOfBounds: s.route.some(step => step.x < 2 || step.x > 47 || step.y < 2 || step.y > 47 || step.approachX < 2 || step.approachX > 47 || step.approachY < 2 || step.approachY > 47 || step.exitX < 2 || step.exitX > 47 || step.exitY < 2 || step.exitY > 47),
- hasDecisionOnRoute: s.route.some(step => s.island.grid[step.y] && s.island.grid[step.y][step.x] && window.FireTruckLib.isDecisionCell(s.island.grid[step.y][step.x])),
+ hasFourWayOnRoute: s.route.some(step => s.cellAt(step.x, step.y).meta.kind === 'four'),
};
});
if (info.width !== 50) throw new Error(`Expected width 50, got ${info.width}`);
@@ -284,60 +288,7 @@ async function main() {
if (info.cell11 !== 'beach') throw new Error(`Expected cell11 beach, got ${info.cell11}`);
if (info.cell22 !== 'road') throw new Error(`Expected cell22 road, got ${info.cell22}`);
if (info.routeOutOfBounds) throw new Error('Route step out of bounds');
- // If route has length > 0, it must have at least one decision (if it's longer than a straight line). BFS path could be straight, so this check is relaxed.
- });
-
- await test('fire-truck: truck stops at intersection instead of auto-failing', async (page, url) => {
- await page.goto(`${url}/games/fire-truck/`, { waitUntil: 'domcontentloaded' });
- // Find the first decision step
- await page.evaluate(() => { window.__FT_SCENE__.snapTruckToRouteIndex(0); });
- await page.waitForFunction(() => window.__FT_SCENE__ && !!window.__FT_SCENE__.promptDir, null, { timeout: 15000 });
- // Wait for the truck to reach the stop line (state changes to 'waiting')
- await page.waitForFunction(() => window.__FT_SCENE__.state === 'waiting', null, { timeout: 15000 });
- const state = await page.evaluate(() => window.__FT_SCENE__.state);
- if (state !== 'waiting') throw new Error(`Expected waiting, got ${state}`);
- });
-
- await test('fire-truck: correct key from waiting resumes driving', async (page, url) => {
- await page.goto(`${url}/games/fire-truck/`, { waitUntil: 'domcontentloaded' });
- await page.evaluate(() => { window.__FT_SCENE__.snapTruckToRouteIndex(0); });
- await page.waitForFunction(() => window.__FT_SCENE__ && !!window.__FT_SCENE__.promptDir, null, { timeout: 15000 });
- await page.waitForFunction(() => window.__FT_SCENE__.state === 'waiting', null, { timeout: 15000 });
- const promptDir = await page.evaluate(() => window.__FT_SCENE__.promptDir);
- const correctKey = promptDir === 'left' ? 'ArrowLeft' : promptDir === 'right' ? 'ArrowRight' : 'ArrowUp';
- await page.keyboard.press(correctKey);
- await page.waitForFunction(() => window.__FT_SCENE__.state === 'driving', null, { timeout: 5000 });
- const state = await page.evaluate(() => window.__FT_SCENE__.state);
- if (state !== 'driving') throw new Error('Truck did not resume');
- });
-
- await test('fire-truck: fire destination initialized correctly', async (page, url) => {
- await page.goto(`${url}/games/fire-truck/`, { waitUntil: 'domcontentloaded' });
- await page.waitForTimeout(1000);
- const fireSet = await page.evaluate(() => {
- const s = window.__FT_SCENE__;
- return !!(s.fireCell && s.fireBuildingCell && s.fireMode);
- });
- if (!fireSet) throw new Error('Fire destination not set');
- });
-
- await test('fire-truck: spacebar extinguishes fire', async (page, url) => {
- await page.goto(`${url}/games/fire-truck/`, { waitUntil: 'domcontentloaded' });
- await page.waitForTimeout(1000);
- // Teleport truck to fire cell to trigger firefighting
- await page.evaluate(() => {
- const s = window.__FT_SCENE__;
- s.truck.setPosition(s.fireCell.x * s.cellSize, s.fireCell.y * s.cellSize);
- });
- await page.waitForFunction(() => window.__FT_SCENE__.state === 'firefighting', null, { timeout: 5000 });
-
- // Hold spacebar
- await page.keyboard.down('Space');
- await page.waitForFunction(() => window.__FT_SCENE__.fireExtinguishAccum > 0.5, null, { timeout: 5000 });
- await page.keyboard.up('Space');
-
- const accum = await page.evaluate(() => window.__FT_SCENE__.fireExtinguishAccum);
- if (accum <= 0) throw new Error('fireExtinguishAccum did not increase');
+ if (!info.hasFourWayOnRoute) throw new Error('No four-way intersection on route');
});
// ── Summary ───────────────────────────────────────────────────────────────
diff --git a/tests/unit/route.test.js b/tests/unit/route.test.js
@@ -9,14 +9,6 @@ const {
extendRouteOnGraph,
OPPOSITE,
HEADING_TO_CARD,
- CARD_TO_HEADING,
- createSeededRng,
- pickRandomRouteStart,
- bfsRoadPath,
- pathToRouteSteps,
- pickFireDestination,
- headingFromTo,
- isDecisionCell,
} = require('../../games/fire-truck/lib.js');
const fixedRng = { pick: arr => arr[0], next: () => 0.25 };
@@ -88,7 +80,7 @@ describe('buildIsland()', () => {
});
it('route starts correctly', () => {
- const island = buildIsland({ seed: 7, routeCount: 10, routeStart: { x: 3, y: 2, heading: 'east' } });
+ const island = buildIsland({ seed: 7, routeCount: 10 });
assert.deepEqual(island.routeStart, { x: 3, y: 2, heading: 'east' });
assert.equal(island.route[0].headingIn, 'east');
@@ -113,74 +105,3 @@ describe('buildIsland()', () => {
assert.equal(firstExt.headingIn, last.headingOut);
});
});
-
-describe('Feature 1: pickRandomRouteStart', () => {
- it('returns valid road cell with valid exit', () => {
- const island = buildIsland({ seed: 123, routeCount: 10 });
- const rng = createSeededRng(42);
- const start = pickRandomRouteStart(island.roads, rng);
-
- assert.ok(start.x >= 0 && start.y >= 0);
- const cell = island.grid[start.y][start.x];
- assert.equal(cell.type, 'road');
-
- const card = HEADING_TO_CARD[start.heading];
- assert.equal(cell.exits[card], true);
- });
-});
-
-describe('Feature 4: BFS and Fire', () => {
- it('bfsRoadPath works', () => {
- const island = buildIsland({ seed: 10, routeCount: 10 });
- const startCell = island.roads[0];
- const targetCell = island.roads[island.roads.length - 1];
-
- const path = bfsRoadPath(island.grid, startCell, targetCell);
- assert.ok(path.length > 0);
- assert.equal(path[0], startCell);
- assert.equal(path[path.length - 1], targetCell);
-
- for (let i = 0; i < path.length - 1; i++) {
- const c1 = path[i];
- const c2 = path[i+1];
- assert.equal(Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y), 1);
- }
- });
-
- it('bfsRoadPath single cell', () => {
- const island = buildIsland({ seed: 11, routeCount: 10 });
- const startCell = island.roads[0];
- const path = bfsRoadPath(island.grid, startCell, startCell);
- assert.deepEqual(path, [startCell]);
- });
-
- it('pathToRouteSteps converts BFS path correctly', () => {
- const island = buildIsland({ seed: 12, routeCount: 10 });
- const startCell = island.roads[0];
- const targetCell = island.roads[island.roads.length - 1];
- const path = bfsRoadPath(island.grid, startCell, targetCell);
-
- const route = pathToRouteSteps(island.grid, path, 0);
- if (path.length > 2) {
- assert.ok(route.length >= 0);
- for (const step of route) {
- assert.ok(isDecisionCell(island.grid[step.y][step.x]));
- assert.ok(step.headingIn);
- assert.ok(step.headingOut);
- }
- } else {
- assert.equal(route.length, 0);
- }
- });
-
- it('pickFireDestination finds fire adjacent to building', () => {
- const island = buildIsland({ seed: 13, routeCount: 10 });
- const rng = createSeededRng(99);
- const { roadCell, buildingCell } = pickFireDestination(island, rng);
-
- assert.equal(roadCell.type, 'road');
- assert.equal(buildingCell.type, 'building');
- assert.equal(Math.abs(roadCell.x - buildingCell.x) + Math.abs(roadCell.y - buildingCell.y), 1);
- });
-});
-