Skip to content

Commit 51386b2

Browse files
author
DavidQ
committed
Polish bezel/background system and close remaining Level 10 edge cases
BUILD_PR_LEVEL_10_25_POLISH_AND_EDGE_CASES
1 parent ec256a8 commit 51386b2

9 files changed

Lines changed: 272 additions & 28 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
1-
21
MODEL: GPT-5.4
32
REASONING: high
43

54
COMMAND:
6-
Create `BUILD_PR_LEVEL_10_24_MULTI_GAME_VALIDATION_PASS`
5+
Create `BUILD_PR_LEVEL_10_25_POLISH_AND_EDGE_CASES`
76

8-
1. Validate across:
9-
- Asteroids
10-
- games/_template
11-
- another sample
7+
1. Validate and polish the shared bezel/background system for edge cases:
8+
- bezel missing
9+
- bezel malformed
10+
- no valid transparency window
11+
- fullscreen enter/exit cycles
12+
- fullscreen resize
13+
- malformed or extreme override values
14+
- background missing
15+
- gameplay/non-gameplay transitions
16+
- starfield ordering regressions
1217

13-
2. Verify:
14-
- bezel behavior
15-
- background behavior
16-
- override file creation
18+
2. Keep fixes surgical and minimal
1719

18-
3. Fix only real issues (minimal changes)
20+
3. Confirm shared code remains game-agnostic and works for:
21+
- Asteroids
22+
- games/_template
23+
- additional sample coverage already established
1924

20-
4. Package:
21-
<project folder>/tmp/BUILD_PR_LEVEL_10_24_MULTI_GAME_VALIDATION_PASS.zip
25+
4. Final packaging step is REQUIRED:
26+
<project folder>/tmp/BUILD_PR_LEVEL_10_25_POLISH_AND_EDGE_CASES.zip
2227

2328
Rules:
24-
- no scope expansion
25-
- no unnecessary changes
29+
- no unrelated repo changes
30+
- no redesign
31+
- minimal corrections only

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
Multi-game validation pass for bezel/background system
2-
BUILD_PR_LEVEL_10_24_MULTI_GAME_VALIDATION_PASS
1+
Polish bezel/background system and close remaining Level 10 edge cases
2+
BUILD_PR_LEVEL_10_25_POLISH_AND_EDGE_CASES

docs/dev/NEXT_COMMAND.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
BUILD_PR_LEVEL_10_25_POLISH_AND_EDGE_CASES
1+
BUILD_PR_LEVEL_10_26_LEVEL_10_CLOSEOUT_AND_HANDOFF
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
- Multi-game validation
2-
- Confirmed shared behavior
3-
- Minimal fixes if needed
1+
- Added final Level 10 polish/edge-case PR
2+
- Focused on fullscreen cycles, malformed inputs, fallback behavior, and state transitions
3+
- Limited to surgical fixes only
Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
2-
- Asteroids passes
3-
- Template passes
4-
- Another game passes
5-
- Bezel works
6-
- Background works
7-
- Override file works
1+
- Bezel missing-file case covered
2+
- Invalid transparency-window case covered
3+
- Fullscreen cycle case covered
4+
- Override malformed/extreme case covered
5+
- Background state-transition case covered
6+
- Shared code remains game-agnostic
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# BUILD_PR_LEVEL_10_25_POLISH_AND_EDGE_CASES
2+
3+
## Purpose
4+
Polish the shared bezel/background system and close the remaining Level 10 edge cases before moving beyond this lane.
5+
6+
## Scope
7+
8+
### A. Bezel edge cases
9+
- missing bezel file
10+
- malformed/empty bezel image
11+
- no usable transparency window
12+
- fullscreen enter/exit repeatedly
13+
- window resize while fullscreen
14+
- existing override values that are too small/too large
15+
- override file exists but is malformed
16+
17+
### B. Background edge cases
18+
- missing background file
19+
- background present in non-gameplay states
20+
- gameplay state transitions
21+
- starfield/world ordering regressions
22+
- games with no starfield
23+
24+
### C. Shared-pipeline polish
25+
- no Asteroids-specific coupling in shared code
26+
- conventions work cleanly for template and multiple games
27+
- override behavior remains discoverable for developers
28+
29+
## Fix rule
30+
- only the smallest surgical fixes needed from validation results
31+
- no scope expansion
32+
- no redesign
33+
34+
## Validation outputs
35+
Codex should summarize:
36+
- edge cases tested
37+
- defects found
38+
- fixes applied
39+
- unresolved items, if any
40+
41+
## Packaging
42+
<project folder>/tmp/BUILD_PR_LEVEL_10_25_POLISH_AND_EDGE_CASES.zip
43+
44+
## Implementation Delta
45+
- Added one surgical runtime guard in `src/engine/runtime/fullscreenBezel.js`:
46+
- treat malformed/empty loaded bezel images (`naturalWidth/height` and fallback `width/height` invalid) as unavailable
47+
- keep fullscreen bezel hidden and preserve fallback centered-canvas layout for this case
48+
- Expanded focused edge-case validation in `tests/core/BackgroundImageAndFullscreenBezel.test.mjs` for:
49+
- malformed bezel image handling
50+
- fullscreen enter/exit cycle stability
51+
- fullscreen resize relayout behavior
52+
- malformed and extreme override values
53+
- malformed override file fallback behavior
54+
- gameplay -> non-gameplay background transition gating
55+
56+
## Edge Cases Tested
57+
- Bezel missing file: PASS
58+
- Bezel malformed/empty image: PASS
59+
- No valid transparency window: PASS (fallback layout)
60+
- Fullscreen enter/exit cycles: PASS
61+
- Fullscreen resize while active: PASS
62+
- Override malformed/extreme values: PASS (sanitized/clamped by layout bounds)
63+
- Malformed override file content: PASS (default config returned, file not overwritten)
64+
- Background missing: PASS
65+
- Gameplay/non-gameplay transitions: PASS
66+
- Starfield/world ordering regression guard (Asteroids): PASS
67+
68+
## Defects Found
69+
- One real defect identified: malformed/empty bezel images were previously treated as ready/visible.
70+
71+
## Fixes Applied
72+
- `fullscreenBezel` now marks malformed/empty images as unavailable on `onload`, keeps bezel hidden, and falls back to standard centered canvas layout.
73+
74+
## Unresolved Items
75+
- None in this scoped validation pass.
76+
77+
## Validation Evidence (2026-04-14)
78+
- `node --check src/engine/runtime/fullscreenBezel.js` PASS
79+
- `node --check tests/core/BackgroundImageAndFullscreenBezel.test.mjs` PASS
80+
- `BackgroundImageAndFullscreenBezel` focused validation PASS
81+
- `AsteroidsPresentation` regression PASS
82+
- `SpaceInvadersScene` regression PASS
83+
- `GamesTemplateContractEnforcement` PASS
-5.75 KB
Binary file not shown.

src/engine/runtime/fullscreenBezel.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,14 @@ export default class fullscreenBezel {
671671
applyBezelStyles(element);
672672
element.onload = () => {
673673
this.imageSize = getImageSize(element);
674+
if (!this.imageSize) {
675+
this.transparentWindowRect = null;
676+
this.ready = false;
677+
this.missing = true;
678+
element.style.display = "none";
679+
this.applyCanvasFallbackLayout();
680+
return;
681+
}
674682
this.transparentWindowRect = this.imageSize
675683
? inspectTransparentWindowRect(element, this.documentRef, this.alphaInspector, this.imageSize)
676684
: null;

tests/core/BackgroundImageAndFullscreenBezel.test.mjs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@ function testBackgroundGameplayGatingAndOrder() {
199199
assert.equal(gameplayResult.drawn, true);
200200
assert.equal(gameplayResult.path, "games/Asteroids/assets/images/background.png");
201201
assert.deepEqual(order, ["background:/games/Asteroids/assets/images/background.png"]);
202+
203+
order.length = 0;
204+
const backToMenuResult = layer.render(renderer, { scene: { session: { mode: "menu" } } });
205+
assert.equal(backToMenuResult.drawn, false);
206+
assert.equal(backToMenuResult.reason, "non-gameplay-state");
207+
assert.deepEqual(order, []);
202208
}
203209

204210
function testGameImageConventionsAreGameAgnostic() {
@@ -318,6 +324,29 @@ function testNoOpWhenBezelMissing() {
318324
assert.equal(bezel.element.style.display, "none");
319325
}
320326

327+
function testMalformedBezelImageIsTreatedAsUnavailable() {
328+
const documentRef = createDocumentStub("/games/Asteroids/index.html");
329+
const host = createElement("div", documentRef);
330+
const canvas = createElement("canvas", documentRef);
331+
canvas.width = 960;
332+
canvas.height = 720;
333+
host.appendChild(canvas);
334+
documentRef.body.appendChild(host);
335+
336+
const bezel = new fullscreenBezel({ canvas, documentRef });
337+
bezel.attach();
338+
bezel.element.naturalWidth = 0;
339+
bezel.element.naturalHeight = 0;
340+
bezel.element.width = 0;
341+
bezel.element.height = 0;
342+
bezel.element.onload?.();
343+
344+
const result = bezel.sync({ fullscreenActive: true, fullscreenElement: host });
345+
assert.equal(result.visible, false);
346+
assert.equal(bezel.getState().visible, false);
347+
assert.equal(bezel.getState().canvasLayoutMode, "fallback");
348+
}
349+
321350
function testTransparentWindowDetectionAndAspectFit() {
322351
const width = 6;
323352
const height = 5;
@@ -461,6 +490,100 @@ function testFullscreenBezelSharedStretchAffectsAllSides() {
461490
assertNear(stretchedCenterY, baselineCenterY, 0.51);
462491
}
463492

493+
function testFullscreenBezelCyclesAndResizeKeepLayoutStable() {
494+
const documentRef = createDocumentStub("/games/Asteroids/index.html");
495+
const host = createElement("div", documentRef);
496+
host.clientWidth = 1600;
497+
host.clientHeight = 900;
498+
host.offsetWidth = 1600;
499+
host.offsetHeight = 900;
500+
const canvas = createElement("canvas", documentRef);
501+
canvas.width = 960;
502+
canvas.height = 720;
503+
host.appendChild(canvas);
504+
documentRef.body.appendChild(host);
505+
506+
const bezel = new fullscreenBezel({
507+
canvas,
508+
documentRef,
509+
alphaInspector() {
510+
return { x: 460, y: 220, width: 1000, height: 500 };
511+
}
512+
});
513+
bezel.attach();
514+
bezel.element.naturalWidth = 1920;
515+
bezel.element.naturalHeight = 1080;
516+
bezel.element.onload?.();
517+
518+
let result = bezel.sync({ fullscreenActive: true, fullscreenElement: host });
519+
assert.equal(result.visible, true);
520+
assert.equal(result.canvasLayoutMode, "transparent-window-fit");
521+
const firstWidth = parseFloat(canvas.style.width);
522+
const firstHeight = parseFloat(canvas.style.height);
523+
524+
result = bezel.sync({ fullscreenActive: false, fullscreenElement: host });
525+
assert.equal(result.visible, false);
526+
assert.equal(result.canvasLayoutMode, "fallback");
527+
assert.equal(canvas.style.width, "960px");
528+
assert.equal(canvas.style.height, "720px");
529+
530+
host.clientWidth = 1920;
531+
host.clientHeight = 1080;
532+
host.offsetWidth = 1920;
533+
host.offsetHeight = 1080;
534+
result = bezel.sync({ fullscreenActive: true, fullscreenElement: host });
535+
assert.equal(result.visible, true);
536+
assert.equal(result.canvasLayoutMode, "transparent-window-fit");
537+
const resizedWidth = parseFloat(canvas.style.width);
538+
const resizedHeight = parseFloat(canvas.style.height);
539+
assert.equal(resizedWidth > firstWidth, true);
540+
assert.equal(resizedHeight > firstHeight, true);
541+
}
542+
543+
function testMalformedAndExtremeStretchConfigValuesAreSafe() {
544+
const documentRef = createDocumentStub("/games/Asteroids/index.html");
545+
const host = createElement("div", documentRef);
546+
host.clientWidth = 1600;
547+
host.clientHeight = 900;
548+
host.offsetWidth = 1600;
549+
host.offsetHeight = 900;
550+
const canvas = createElement("canvas", documentRef);
551+
canvas.width = 960;
552+
canvas.height = 720;
553+
host.appendChild(canvas);
554+
documentRef.body.appendChild(host);
555+
556+
const malformed = new fullscreenBezel({
557+
canvas,
558+
documentRef,
559+
alphaInspector: () => ({ x: 460, y: 220, width: 1000, height: 500 }),
560+
stretchConfigProvider: () => ({ uniformEdgeStretchPx: "abc" })
561+
});
562+
malformed.attach();
563+
malformed.element.naturalWidth = 1920;
564+
malformed.element.naturalHeight = 1080;
565+
malformed.element.onload?.();
566+
malformed.sync({ fullscreenActive: true, fullscreenElement: host });
567+
assert.equal(malformed.getState().uniformEdgeStretchPx, 0);
568+
malformed.detach();
569+
570+
const extreme = new fullscreenBezel({
571+
canvas,
572+
documentRef,
573+
alphaInspector: () => ({ x: 460, y: 220, width: 1000, height: 500 }),
574+
stretchConfigProvider: () => ({ uniformEdgeStretchPx: Number.MAX_SAFE_INTEGER })
575+
});
576+
extreme.attach();
577+
extreme.element.naturalWidth = 1920;
578+
extreme.element.naturalHeight = 1080;
579+
extreme.element.onload?.();
580+
const result = extreme.sync({ fullscreenActive: true, fullscreenElement: host });
581+
assert.equal(result.visible, true);
582+
assert.equal(extreme.getState().uniformEdgeStretchPx > 0, true);
583+
assert.equal(parseFloat(canvas.style.width) <= 1600.01, true);
584+
assert.equal(parseFloat(canvas.style.height) <= 900.01, true);
585+
}
586+
464587
async function testBezelStretchConfigAutoCreate() {
465588
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "bezel-stretch-config-"));
466589
const configPath = "games/Asteroids/assets/images/bezel.stretch.override.json";
@@ -581,6 +704,27 @@ async function testBezelDetectionDoesNotOverwriteExistingStretchConfig() {
581704
}
582705
}
583706

707+
async function testMalformedStretchConfigFileFallsBackWithoutOverwrite() {
708+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "bezel-malformed-config-"));
709+
const configPath = path.resolve(tempRoot, "games/Asteroids/assets/images/bezel.stretch.override.json");
710+
try {
711+
await fs.mkdir(path.dirname(configPath), { recursive: true });
712+
const malformedContent = "{not-valid-json";
713+
await fs.writeFile(configPath, malformedContent, "utf8");
714+
715+
const loaded = await ensureBezelStretchConfigFile("games/Asteroids/assets/images/bezel.stretch.override.json", {
716+
cwd: tempRoot,
717+
fsModule: fs,
718+
pathModule: path
719+
});
720+
const savedAfterLoad = await fs.readFile(configPath, "utf8");
721+
assert.deepEqual(loaded, { uniformEdgeStretchPx: 0 });
722+
assert.equal(savedAfterLoad, malformedContent);
723+
} finally {
724+
await fs.rm(tempRoot, { recursive: true, force: true });
725+
}
726+
}
727+
584728
async function testSampleGameBezelDetectionCreatesStretchConfig() {
585729
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "bezel-detected-spaceinvaders-config-"));
586730
const documentRef = createDocumentStub("/games/SpaceInvaders/index.html");
@@ -761,12 +905,16 @@ export async function run() {
761905
testSampleGameBackgroundAndBezelNoOpWhenMissing();
762906
testFullscreenBezelVisibilityAndHtmlAttachment();
763907
testNoOpWhenBezelMissing();
908+
testMalformedBezelImageIsTreatedAsUnavailable();
764909
testTransparentWindowDetectionAndAspectFit();
765910
testFullscreenBezelTransparentWindowCanvasFit();
766911
testFullscreenBezelSharedStretchAffectsAllSides();
912+
testFullscreenBezelCyclesAndResizeKeepLayoutStable();
913+
testMalformedAndExtremeStretchConfigValuesAreSafe();
767914
await testBezelStretchConfigAutoCreate();
768915
await testBezelDetectionTriggersStretchConfigAutoCreate();
769916
await testBezelDetectionDoesNotOverwriteExistingStretchConfig();
917+
await testMalformedStretchConfigFileFallsBackWithoutOverwrite();
770918
await testSampleGameBezelDetectionCreatesStretchConfig();
771919
testEngineRuntimeIntegration();
772920
}

0 commit comments

Comments
 (0)