kgames

KGames — free keyboard games for kids
git clone https://www.keyboard.games/code/kgames.git
Log | Files | Refs | README | LICENSE

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:
Mgames/fire-truck/game.js | 255++++++++-----------------------------------------------------------------------
Mgames/fire-truck/lib.js | 123++-----------------------------------------------------------------------------
Mtests/browser/runner.js | 71+++++++++++------------------------------------------------------------
Mtests/unit/route.test.js | 81+------------------------------------------------------------------------------
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); - }); -}); -