Skip to content

Commit ed11372

Browse files
author
DavidQ
committed
Add weighted Asteroids beat timing based on asteroid size counts - PR_26139_026-asteroids-weighted-beat-timing
1 parent 2c52f56 commit ed11372

3 files changed

Lines changed: 165 additions & 18 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# PR_26139_026-asteroids-weighted-beat-timing
2+
3+
## Summary
4+
- Replaced raw asteroid-count beat cadence with weighted active asteroid totals.
5+
- Weights are large asteroid `9`, medium asteroid `4`, and small asteroid `1`.
6+
- Beat timing now uses only active asteroid objects and preserves the existing cadence bounds of `0.18s` minimum and `0.98s` maximum.
7+
- Added targeted Asteroids Playwright validation for the expected split progression.
8+
9+
## Runtime Behavior
10+
- The active wave establishes the current beat baseline from live asteroid objects; no asteroid-count totals are hardcoded in runtime cadence logic.
11+
- Weighted totals decrease as asteroids split, so cadence speeds up through the expected progression:
12+
- `8 large = 72`
13+
- `16 medium = 64`
14+
- `32 small = 32`
15+
- Inactive asteroid-like objects with `active: false`, `alive: false`, or `destroyed: true` do not contribute to cadence.
16+
17+
## Validation
18+
- PASS: `npx playwright test tests/playwright/tools/AsteroidsBeatTiming.spec.mjs --project=playwright --workers=1 --reporter=list`
19+
- 1 passed.
20+
- PASS: `npm run build:manifest`
21+
- PASS: `npm run test:workspace-v2`
22+
- 58 passed.
23+
- PASS: repeated targeted Asteroids beat timing validation after Workspace V2 for the final coverage report.
24+
- PASS: `git diff --check`
25+
26+
## PR_26139_025 Regression Check
27+
- Workspace V2 suite re-ran the repo discovery/schema reference tests from PR_26139_025.
28+
- Asteroids and AITargetDummy remain discoverable through Workspace Manager V2 Select Repo.
29+
- No unresolved `asset-manager-v2.schema.json` or `palette-manager-v2.schema.json` schema reference regression was observed.
30+
31+
## Playwright Impact
32+
- Playwright impacted: Yes.
33+
- Expected pass behavior: weighted totals are `72`, `64`, `32`, then `0` for inactive-only asteroids; intervals monotonically decrease as totals decrease and stay within `0.18s`/`0.98s`.
34+
- Expected fail behavior: raw object-count timing would make `16 medium` or `32 small` slower, or inactive objects would inflate the weighted total.
35+
36+
## Full Samples
37+
- Full samples smoke test was skipped.
38+
- Reason: scope is limited to Asteroids beat cadence and Workspace V2 schema/game discovery regression was covered by targeted/full Workspace V2 validation.
39+
40+
## Manual Validation
41+
1. Launch `games/Asteroids/index.html`.
42+
2. Start a game and listen for the alternating beat cadence.
43+
3. Split large asteroids into medium asteroids and confirm the beat becomes faster.
44+
4. Split medium asteroids into small asteroids and confirm the beat becomes faster again.
45+
5. Confirm the cadence remains bounded and does not jump outside the existing slow/fast range.

games/Asteroids/game/AsteroidsGameScene.js

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ const SCORE_TWO_X = 824;
3232
const LIFE_SPACING = 22;
3333
const PAUSE_OVERLAY_COLOR = 'rgba(2, 6, 23, 0.58)';
3434
const INITIALS_OVERLAY_COLOR = 'rgba(1, 6, 19, 0.62)';
35+
const ASTEROID_BEAT_MIN_INTERVAL_SECONDS = 0.18;
36+
const ASTEROID_BEAT_MAX_INTERVAL_SECONDS = 0.98;
37+
const ASTEROID_BEAT_SIZE_WEIGHTS = Object.freeze({
38+
1: 1,
39+
2: 4,
40+
3: 9,
41+
});
3542
const ATTRACT_INPUT_CODES = [
3643
'Digit1',
3744
'Digit2',
@@ -74,23 +81,29 @@ function screenDimensionsFromOptions(options) {
7481
return { width, height };
7582
}
7683

77-
function getBeatInterval(asteroidCount) {
78-
if (asteroidCount <= 1) {
79-
return 0.18;
80-
}
81-
if (asteroidCount === 2) {
82-
return 0.28;
83-
}
84-
if (asteroidCount <= 4) {
85-
return 0.42;
86-
}
87-
if (asteroidCount <= 6) {
88-
return 0.58;
89-
}
90-
if (asteroidCount <= 8) {
91-
return 0.78;
84+
function isActiveAsteroid(asteroid) {
85+
return Boolean(asteroid)
86+
&& typeof asteroid === 'object'
87+
&& asteroid.active !== false
88+
&& asteroid.alive !== false
89+
&& asteroid.destroyed !== true;
90+
}
91+
92+
export function getAsteroidsBeatWeightedTotal(asteroids) {
93+
if (!Array.isArray(asteroids)) {
94+
return 0;
9295
}
93-
return 0.98;
96+
return asteroids.reduce((total, asteroid) => (
97+
total + (isActiveAsteroid(asteroid) ? ASTEROID_BEAT_SIZE_WEIGHTS[asteroid.size] || 0 : 0)
98+
), 0);
99+
}
100+
101+
export function getAsteroidsBeatInterval(weightedTotal, maxWeightedTotal) {
102+
const safeWeightedTotal = Math.max(0, Number.isFinite(weightedTotal) ? weightedTotal : 0);
103+
const safeMaxWeightedTotal = Math.max(1, Number.isFinite(maxWeightedTotal) ? maxWeightedTotal : safeWeightedTotal);
104+
const weightedProgress = Math.min(1, safeWeightedTotal / safeMaxWeightedTotal);
105+
return ASTEROID_BEAT_MIN_INTERVAL_SECONDS
106+
+ ((ASTEROID_BEAT_MAX_INTERVAL_SECONDS - ASTEROID_BEAT_MIN_INTERVAL_SECONDS) * weightedProgress);
94107
}
95108

96109
export default class AsteroidsGameScene extends Scene {
@@ -171,6 +184,8 @@ export default class AsteroidsGameScene extends Scene {
171184
this.isPaused = false;
172185
this.beatTimer = 0;
173186
this.nextBeatId = 'beat1';
187+
this.beatMaxWeightedTotal = 1;
188+
this.resetBeatCadenceBaseline();
174189
this.scoreFlashTime = 0;
175190
this.initialsEntry = new AsteroidsInitialsEntry();
176191
this.attractAdapter = new AsteroidsAttractAdapter({ scene: this });
@@ -533,6 +548,20 @@ export default class AsteroidsGameScene extends Scene {
533548
});
534549
}
535550

551+
resetBeatCadenceBaseline() {
552+
this.beatMaxWeightedTotal = Math.max(1, getAsteroidsBeatWeightedTotal(this.world?.asteroids));
553+
}
554+
555+
resolveAsteroidsBeatTiming() {
556+
const weightedTotal = getAsteroidsBeatWeightedTotal(this.world?.asteroids);
557+
this.beatMaxWeightedTotal = Math.max(this.beatMaxWeightedTotal, weightedTotal, 1);
558+
return {
559+
intervalSeconds: getAsteroidsBeatInterval(weightedTotal, this.beatMaxWeightedTotal),
560+
maxWeightedTotal: this.beatMaxWeightedTotal,
561+
weightedTotal,
562+
};
563+
}
564+
536565
update(dtSeconds, engine) {
537566
this.debugFrame += 1;
538567
this.objectVectorPlaybackMs += Math.max(0, Number.isFinite(dtSeconds) ? dtSeconds * 1000 : 0);
@@ -575,12 +604,14 @@ export default class AsteroidsGameScene extends Scene {
575604
if (onePressed && !this.lastOnePressed) {
576605
this.attractController.exitAttract();
577606
this.session.start(1);
607+
this.resetBeatCadenceBaseline();
578608
this.pushDebugEvent('SHIP_SPAWN', { player: 1, wave: this.world.wave });
579609
this.pushDebugEvent('WAVE_STARTED', { wave: this.world.wave, asteroids: this.world.asteroids.length });
580610
}
581611
if (twoPressed && !this.lastTwoPressed) {
582612
this.attractController.exitAttract();
583613
this.session.start(2);
614+
this.resetBeatCadenceBaseline();
584615
this.pushDebugEvent('SHIP_SPAWN', { player: 1, wave: this.world.wave });
585616
this.pushDebugEvent('WAVE_STARTED', { wave: this.world.wave, asteroids: this.world.asteroids.length });
586617
}
@@ -710,12 +741,12 @@ export default class AsteroidsGameScene extends Scene {
710741
this.audio.updateThrust(false);
711742
this.audio.updateUfo(null);
712743
} else {
713-
const beatInterval = getBeatInterval(this.world.asteroids.length);
744+
const beatTiming = this.resolveAsteroidsBeatTiming();
714745
this.beatTimer -= dtSeconds;
715746
if (this.beatTimer <= 0) {
716747
this.audio.play(this.nextBeatId);
717748
this.nextBeatId = this.nextBeatId === 'beat1' ? 'beat2' : 'beat1';
718-
this.beatTimer = beatInterval;
749+
this.beatTimer = beatTiming.intervalSeconds;
719750
}
720751
this.audio.updateThrust(this.world.ship.thrusting && this.session.mode === 'playing');
721752
this.audio.updateUfo(this.world.ufo?.type || null);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { expect, test } from "@playwright/test";
2+
import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs";
3+
import { workspaceV2CoverageReporter as coverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs";
4+
5+
test.afterAll(async () => {
6+
await coverageReporter.writeReport();
7+
});
8+
9+
async function waitForAsteroidsBoot(page) {
10+
await page.waitForFunction(() => window.__asteroidsNewBootStage === "boot-complete");
11+
await page.waitForFunction(() => window.__asteroidsNewEngine?.scene?.world?.asteroids?.length > 0);
12+
}
13+
14+
test("uses weighted active asteroid objects for beat cadence", async ({ page }) => {
15+
const server = await startRepoServer();
16+
const pageErrors = [];
17+
18+
page.on("pageerror", (error) => {
19+
pageErrors.push(error.message);
20+
});
21+
22+
await coverageReporter.start(page);
23+
try {
24+
await page.goto(`${server.baseUrl}/games/Asteroids/index.html`, { waitUntil: "networkidle" });
25+
await waitForAsteroidsBoot(page);
26+
27+
const timing = await page.evaluate(() => {
28+
const scene = window.__asteroidsNewEngine.scene;
29+
const createAsteroids = (count, size) => Array.from({ length: count }, () => ({ size }));
30+
const inactiveAsteroids = [
31+
{ active: false, size: 3 },
32+
{ alive: false, size: 2 },
33+
{ destroyed: true, size: 1 },
34+
];
35+
36+
scene.world.asteroids = [...createAsteroids(8, 3), ...inactiveAsteroids];
37+
scene.resetBeatCadenceBaseline();
38+
const largeAsteroids = scene.resolveAsteroidsBeatTiming();
39+
40+
scene.world.asteroids = [...createAsteroids(16, 2), ...inactiveAsteroids];
41+
const mediumAsteroids = scene.resolveAsteroidsBeatTiming();
42+
43+
scene.world.asteroids = [...createAsteroids(32, 1), ...inactiveAsteroids];
44+
const smallAsteroids = scene.resolveAsteroidsBeatTiming();
45+
46+
scene.world.asteroids = inactiveAsteroids;
47+
const noActiveAsteroids = scene.resolveAsteroidsBeatTiming();
48+
49+
return {
50+
largeAsteroids,
51+
mediumAsteroids,
52+
noActiveAsteroids,
53+
smallAsteroids,
54+
};
55+
});
56+
57+
expect(timing.largeAsteroids.weightedTotal).toBe(72);
58+
expect(timing.mediumAsteroids.weightedTotal).toBe(64);
59+
expect(timing.smallAsteroids.weightedTotal).toBe(32);
60+
expect(timing.noActiveAsteroids.weightedTotal).toBe(0);
61+
expect(timing.largeAsteroids.intervalSeconds).toBeCloseTo(0.98, 5);
62+
expect(timing.noActiveAsteroids.intervalSeconds).toBeCloseTo(0.18, 5);
63+
expect(timing.largeAsteroids.intervalSeconds).toBeGreaterThan(timing.mediumAsteroids.intervalSeconds);
64+
expect(timing.mediumAsteroids.intervalSeconds).toBeGreaterThan(timing.smallAsteroids.intervalSeconds);
65+
expect(timing.smallAsteroids.intervalSeconds).toBeGreaterThan(timing.noActiveAsteroids.intervalSeconds);
66+
expect(pageErrors).toEqual([]);
67+
} finally {
68+
await coverageReporter.stop(page);
69+
await server.close();
70+
}
71+
});

0 commit comments

Comments
 (0)