Skip to content

Commit 02f0c49

Browse files
author
DavidQ
committed
Move Asteroids weighted beat timing into a focused helper module - PR_26139_027-asteroids-beat-timing-helper-cleanup
1 parent ed11372 commit 02f0c49

3 files changed

Lines changed: 92 additions & 39 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# PR_26139_027-asteroids-beat-timing-helper-cleanup
2+
3+
## Summary
4+
- Extracted Asteroids weighted beat timing logic from `AsteroidsGameScene.js` into `games/Asteroids/game/asteroidsBeatTiming.js`.
5+
- Preserved the PR_26139_026 weights and cadence behavior:
6+
- large asteroid: `9`
7+
- medium asteroid: `4`
8+
- small asteroid: `1`
9+
- Kept cadence tuning unchanged at the existing `0.18s` fast bound and `0.98s` slow bound.
10+
- Left `AsteroidsGameScene.js` responsible only for baseline lifecycle and applying the resolved beat interval.
11+
12+
## Behavior Preservation
13+
- The targeted PR_26139_026 validation still confirms:
14+
- `8 large = 72`
15+
- `16 medium = 64`
16+
- `32 small = 32`
17+
- inactive asteroid objects do not contribute
18+
- lower weighted total produces faster cadence
19+
20+
## Validation
21+
- PASS: `npm run build:manifest`
22+
- PASS: `npx playwright test tests/playwright/tools/AsteroidsBeatTiming.spec.mjs --project=playwright --workers=1 --reporter=list`
23+
- 1 passed.
24+
- PASS: `git diff --check`
25+
26+
## Coverage
27+
- Playwright impacted: Yes.
28+
- V8 coverage collected the changed Asteroids files:
29+
- `(51%) games/Asteroids/game/AsteroidsGameScene.js - changed JS file with browser V8 coverage`
30+
- `(100%) games/Asteroids/game/asteroidsBeatTiming.js - changed JS file with browser V8 coverage`
31+
- Note: the existing changed-runtime guardrail classifies only `src/`, `tools/`, and `common/` as runtime JS, so Asteroids game files appear under `Changed JS files considered` rather than `Changed runtime JS files covered`.
32+
33+
## Full Samples
34+
- Full samples smoke test was skipped.
35+
- Reason: scope is limited to a focused Asteroids helper extraction with unchanged beat timing behavior covered by the targeted Asteroids Playwright test.
36+
37+
## Manual Validation
38+
1. Launch `games/Asteroids/index.html`.
39+
2. Start a game.
40+
3. Split large asteroids into medium asteroids and confirm the beat speeds up.
41+
4. Split medium asteroids into small asteroids and confirm the beat speeds up again.
42+
5. Confirm cadence remains bounded and no gameplay/rendering behavior changes beyond the PR_26139_026 beat timing behavior.

games/Asteroids/game/AsteroidsGameScene.js

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import AsteroidsAttractAdapter from './AsteroidsAttractAdapter.js';
1414
import AsteroidsHighScoreService from '../systems/AsteroidsHighScoreService.js';
1515
import AsteroidsInitialsEntry from '../systems/AsteroidsInitialsEntry.js';
1616
import { createAsteroidGeometryProfilesFromObjectVectorAssets } from './asteroidObjectGeometry.js';
17+
import {
18+
calculateAsteroidsBeatTiming,
19+
getAsteroidsBeatWeightedTotal
20+
} from './asteroidsBeatTiming.js';
1721
import {
1822
ASTEROIDS_OBJECT_GEOMETRY_IDS,
1923
ASTEROIDS_ASTEROID_SIZE_OBJECT_IDS,
@@ -32,13 +36,6 @@ const SCORE_TWO_X = 824;
3236
const LIFE_SPACING = 22;
3337
const PAUSE_OVERLAY_COLOR = 'rgba(2, 6, 23, 0.58)';
3438
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-
});
4239
const ATTRACT_INPUT_CODES = [
4340
'Digit1',
4441
'Digit2',
@@ -81,31 +78,6 @@ function screenDimensionsFromOptions(options) {
8178
return { width, height };
8279
}
8380

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;
95-
}
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);
107-
}
108-
10981
export default class AsteroidsGameScene extends Scene {
11082
constructor(options = {}) {
11183
super();
@@ -553,13 +525,9 @@ export default class AsteroidsGameScene extends Scene {
553525
}
554526

555527
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-
};
528+
const beatTiming = calculateAsteroidsBeatTiming(this.world?.asteroids, this.beatMaxWeightedTotal);
529+
this.beatMaxWeightedTotal = beatTiming.maxWeightedTotal;
530+
return beatTiming;
563531
}
564532

565533
update(dtSeconds, engine) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const ASTEROID_BEAT_MIN_INTERVAL_SECONDS = 0.18;
2+
const ASTEROID_BEAT_MAX_INTERVAL_SECONDS = 0.98;
3+
const ASTEROID_BEAT_SIZE_WEIGHTS = Object.freeze({
4+
1: 1,
5+
2: 4,
6+
3: 9,
7+
});
8+
9+
function isActiveAsteroid(asteroid) {
10+
return Boolean(asteroid)
11+
&& typeof asteroid === 'object'
12+
&& asteroid.active !== false
13+
&& asteroid.alive !== false
14+
&& asteroid.destroyed !== true;
15+
}
16+
17+
export function getAsteroidsBeatWeightedTotal(asteroids) {
18+
if (!Array.isArray(asteroids)) {
19+
return 0;
20+
}
21+
return asteroids.reduce((total, asteroid) => (
22+
total + (isActiveAsteroid(asteroid) ? ASTEROID_BEAT_SIZE_WEIGHTS[asteroid.size] || 0 : 0)
23+
), 0);
24+
}
25+
26+
function getAsteroidsBeatInterval(weightedTotal, maxWeightedTotal) {
27+
const safeWeightedTotal = Math.max(0, Number.isFinite(weightedTotal) ? weightedTotal : 0);
28+
const safeMaxWeightedTotal = Math.max(1, Number.isFinite(maxWeightedTotal) ? maxWeightedTotal : safeWeightedTotal);
29+
const weightedProgress = Math.min(1, safeWeightedTotal / safeMaxWeightedTotal);
30+
return ASTEROID_BEAT_MIN_INTERVAL_SECONDS
31+
+ ((ASTEROID_BEAT_MAX_INTERVAL_SECONDS - ASTEROID_BEAT_MIN_INTERVAL_SECONDS) * weightedProgress);
32+
}
33+
34+
export function calculateAsteroidsBeatTiming(asteroids, currentMaxWeightedTotal) {
35+
const weightedTotal = getAsteroidsBeatWeightedTotal(asteroids);
36+
const baselineWeightedTotal = Number.isFinite(currentMaxWeightedTotal) ? currentMaxWeightedTotal : 1;
37+
const maxWeightedTotal = Math.max(baselineWeightedTotal, weightedTotal, 1);
38+
return {
39+
intervalSeconds: getAsteroidsBeatInterval(weightedTotal, maxWeightedTotal),
40+
maxWeightedTotal,
41+
weightedTotal,
42+
};
43+
}

0 commit comments

Comments
 (0)