Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions challenges/glitch-invaders/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ browser ES modules.

## How steps advance

The game shows a `glitch-state v1` line, and the headless monitor reports
The game keeps a `glitch-state v1` line, visually hidden but present in the
page's accessibility snapshot, and the headless monitor reports
`launch-game`, `fix-weapon`, `fix-shield`, and `fix-boss` from it, as
[monitor/SKILL.md](monitor/SKILL.md) describes. The game also saves
`glitch-progress.json` in the workspace so Pi can see which bug and stage
`glitch-progress.json` in the workspace (`setup` seeds it with an unplayed run,
so Pi's first read succeeds) so Pi can see which bug and stage
(`unseen`, `encountered`, `investigated`, `repaired`) the participant is on and
emit non-PII milestone events. Once all three bugs are `repaired`, Pi asks
which clue helped most and reports `debrief`.
Expand Down
8 changes: 4 additions & 4 deletions challenges/glitch-invaders/challenge.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,25 @@
"id": "launch-game",
"title": "Launch your patch drone",
"hint": "Press Start in the game, then shoot the first glitches.",
"done_when": "The page shows the line \"glitch-state v1 …\" containing \"launched=yes\"."
"done_when": "The page's accessibility snapshot holds the line \"glitch-state v1 …\" containing \"launched=yes\"."
},
{
"id": "fix-weapon",
"title": "Fix the jammed weapon",
"hint": "Grab the gold upgrade and fire. Then trace the error in the Incident Console.",
"done_when": "The page shows the line \"glitch-state v1 …\" containing \"weapon=repaired\"."
"done_when": "The page's accessibility snapshot holds the line \"glitch-state v1 …\" containing \"weapon=repaired\"."
},
{
"id": "fix-shield",
"title": "Fix the shield that hurts",
"hint": "Grab the blue shield and take a hit. Read the breadcrumbs, then patch it.",
"done_when": "The page shows the line \"glitch-state v1 …\" containing \"shield=repaired\"."
"done_when": "The page's accessibility snapshot holds the line \"glitch-state v1 …\" containing \"shield=repaired\"."
},
{
"id": "fix-boss",
"title": "Fix the slow boss spawn",
"hint": "Clear the wave so the boss spawns. Find the bottleneck in the trace.",
"done_when": "The page shows the line \"glitch-state v1 …\" containing \"boss=repaired\"."
"done_when": "The page's accessibility snapshot holds the line \"glitch-state v1 …\" containing \"boss=repaired\"."
},
{
"id": "debrief",
Expand Down
8 changes: 5 additions & 3 deletions challenges/glitch-invaders/game/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ <h1 class="brand">
<li>SQUASHED <b id="stat-kills">0</b></li>
<li>SCORE <b id="stat-score">0</b></li>
</ul>
<span id="mode-badge" class="badge" title="Demo mode: the diagnostics are realistic simulations.">DEMO · simulated evidence</span>
<button type="button" class="topbar-reset" id="reset-run" title="Reset and start a fresh run">Reset ↺</button>
<div class="topbar-end">
<span id="mode-badge" class="badge" title="Demo mode: the diagnostics are realistic simulations.">DEMO · simulated evidence</span>
<button type="button" class="topbar-reset" id="reset-run" title="Reset and start a fresh run">Reset ↺</button>
</div>
</header>

<main class="stage-wrap" id="stage-wrap">
Expand Down Expand Up @@ -63,7 +65,7 @@ <h2 class="card-title" id="controls-title">Controls</h2>
<div id="overlay-root" class="overlay-root" hidden></div>
</div>

<p id="boothstrap-state" class="state-line">glitch-state v1 launched=no weapon=unseen shield=unseen boss=unseen completed=no mode=demo</p>
<p id="boothstrap-state" class="visually-hidden">glitch-state v1 launched=no weapon=unseen shield=unseen boss=unseen completed=no mode=demo</p>
<p id="save-error" class="save-error" role="alert" hidden>Progress can't be saved; reload the page.</p>
<div id="announcer" class="visually-hidden" role="status" aria-live="polite"></div>
<noscript>Glitch Invaders needs JavaScript. Open this page in Chrome.</noscript>
Expand Down
7 changes: 5 additions & 2 deletions challenges/glitch-invaders/game/src/app/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { GameLoop } from './game-loop.mjs';
import { canonicalEncounter, codeForProgress, firstOpenBug, waveForBug } from './resume.mjs';

/** How long the banner after a success or patch stays up. */
const BANNER_MS = 6_000;
const BANNER_MS = 2_500;

/** The pause after the last repair, so the participant sees the result before the recap. */
const COMPLETION_DELAY_MS = 1_800;
Expand Down Expand Up @@ -349,7 +349,7 @@ export class GameApp {
this.overlays.show(this.incidentConsole.element);
const cached = this.evidenceByBug.get(bug);
if (cached !== undefined) {
this.incidentConsole.showEvidence(cached);
this.deliverEvidence(cached);

return;
}
Expand All @@ -364,6 +364,9 @@ export class GameApp {
/** Remember the evidence and show it if its console is still open. */
deliverEvidence(evidence) {
this.evidenceByBug.set(evidence.bug, evidence);
if (this.live !== null) {
this.hud.showEvidenceSource(evidence);
}
this.incidentConsole.showEvidence(evidence);
}

Expand Down
112 changes: 100 additions & 12 deletions challenges/glitch-invaders/game/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ body {
display: grid;
flex: 1 1 auto;
grid-template-rows: auto minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1fr);
min-height: 0;
}

Expand Down Expand Up @@ -127,6 +128,7 @@ body {
display: flex;
flex: 1;
gap: 18px;
white-space: nowrap;
color: var(--muted);
font-size: 13px;
letter-spacing: 0.04em;
Expand All @@ -142,6 +144,13 @@ body {
color: var(--accent);
}

.topbar-end {
display: flex;
align-items: center;
gap: 20px;
margin-left: auto;
}

.badge {
padding: 2px 10px;
border: 2px solid var(--mlh-yellow);
Expand Down Expand Up @@ -201,9 +210,10 @@ body {

.banner {
position: absolute;
top: 10px;
right: 10px;
bottom: 10px;
left: 10px;
pointer-events: none;
padding: 10px 14px;
border: 2px solid var(--accent);
background: var(--surface);
Expand Down Expand Up @@ -426,11 +436,16 @@ body {
gap: 12px;
}

/* The overlay spans the rows below the topbar, and its single track is bounded so a card's
`max-height: 100%` resolves against the space actually left on screen. */
.overlay-root {
position: absolute;
inset: 0;
z-index: 10;
display: grid;
grid-row: 2 / -1;
grid-column: 1;
grid-template: minmax(0, 1fr) / minmax(0, 1fr);
place-items: center;
padding: 16px;
background: rgb(21 23 28 / 0.88);
Expand All @@ -453,6 +468,11 @@ body {
font-weight: 700;
}

.last-step {
color: var(--mlh-yellow);
font-weight: 700;
}

.hero em {
color: var(--mlh-yellow);
font-style: normal;
Expand Down Expand Up @@ -629,11 +649,21 @@ kbd {
flex: 1;
flex-direction: column;
min-height: 0;
padding: 12px 2px 2px;
padding-top: 12px;
}

/* The bottom padding sits under the fade, so the last line is fully visible once scrolled to. */
.pane-scroll {
flex: 1 1 auto;
min-height: 0;
padding: 2px 2px 20px;
overflow: auto;
scroll-padding-bottom: 20px;
scrollbar-color: var(--border) transparent;
scrollbar-width: thin;
mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent);
}

.pane > * + *,
.evidence > * + *,
.diagnose > * + *,
.patches > * + * {
Expand Down Expand Up @@ -923,15 +953,20 @@ kbd {

.pane-actions {
display: flex;
flex: none;
flex-wrap: wrap;
gap: 12px;
gap: 10px 12px;
align-items: center;
margin-top: auto;
padding-top: 6px;
padding-top: 10px;
border-top: 1px solid var(--border);
}

.spacer {
flex: 1;
.pane-actions > .feedback {
flex: 1 0 100%;
}

.pane-actions > .button-primary {
margin-left: auto;
}

.text-link {
Expand Down Expand Up @@ -1093,6 +1128,46 @@ kbd {
font: 12px / 1.35 var(--mono);
}

/* Compact topbar: the badge wraps inside itself so the topbar stays one row down to 800 px. */
@media (max-width: 1100px) {
.topbar {
gap: 12px;
padding: 6px 12px;
}

.brand {
font-size: 16px;
}

.brand-sub {
display: none;
}

.stats {
gap: 10px;
font-size: 12px;
}

.topbar-end {
gap: 12px;
}

.badge {
font-size: 11px;
line-height: 1.15;
text-align: center;
white-space: normal;
}
}

/* Below the compact range the stats, badge and Reset wrap onto further rows. */
@media (max-width: 760px) {
.topbar {
flex-wrap: wrap;
gap: 6px 12px;
}
}

@media (max-width: 720px) {
.dock {
grid-template-columns: 1fr;
Expand All @@ -1106,16 +1181,29 @@ kbd {
.stats {
gap: 10px;
}

.badge {
display: none;
}
}

@media (max-height: 620px) {
.objective-detail {
display: none;
}

.overlay-root {
padding: 8px 16px;
}

.overlay-card {
padding: 20px 24px 14px;
}

.overlay-card > * + * {
margin-top: 10px;
}

.overlay-card .button-large {
min-height: 44px;
padding: 8px 20px;
}
}

@media (prefers-reduced-motion: reduce) {
Expand Down
6 changes: 3 additions & 3 deletions challenges/glitch-invaders/game/src/ui/boss-loading.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { ALL_LOAD_TASKS } from '../boss/boss-loader.mjs';
import { BOSS_SPAWN_BUDGET_MS } from '../engine/constants.mjs';

import { h, setText } from './dom.mjs';
import { h, setData, setStyle, setText } from './dom.mjs';

/** Draws the loading panel from the world. */
export class BossLoadingView {
Expand Down Expand Up @@ -48,7 +48,7 @@ export class BossLoadingView {
? `Buffering… over the ${(BOSS_SPAWN_BUDGET_MS / 1_000).toFixed(1)} s budget`
: 'Loading resources',
);
this.note.dataset.over = String(elapsed > BOSS_SPAWN_BUDGET_MS);
setData(this.note, 'over', String(elapsed > BOSS_SPAWN_BUDGET_MS));
}
for (const task of load.tasks) {
const row = this.rows.get(task.name);
Expand All @@ -61,7 +61,7 @@ export class BossLoadingView {
: task.startMs === null
? 0
: Math.min(1, (elapsed - task.startMs) / row.durationMs);
row.fill.style.width = `${(fraction * 100).toFixed(1)}%`;
setStyle(row.fill, 'width', `${(fraction * 100).toFixed(1)}%`);
setText(
row.status,
task.endMs !== null ? '✓ done' : task.startMs === null ? 'waiting' : 'loading',
Expand Down
26 changes: 26 additions & 0 deletions challenges/glitch-invaders/game/src/ui/dom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ export function setText(element, text) {
}
}

/** Set a `data-` attribute only when it changed; a same-value write still queues a DOM mutation. */
export function setData(element, name, value) {
if (element.dataset[name] !== value) {
element.dataset[name] = value;
}
}

/** The inline style values {@link setStyle} last wrote, per element and property. */
const writtenStyles = new WeakMap();

/**
* Set one inline style property only when it changed, so per-frame updates do not churn the DOM.
* It compares with the value it last wrote, not `element.style`, because the browser reads a value
* back normalized: `100.0%` returns as `100%`.
*/
export function setStyle(element, property, value) {
const written = writtenStyles.get(element) ?? new Map();
if (written.get(property) === value) {
return;
}

element.style[property] = value;
written.set(property, value);
writtenStyles.set(element, written);
}

/** Add a click handler and return the element, for chaining inside {@link h} calls. */
export function onClick(element, handler) {
element.addEventListener('click', handler);
Expand Down
Loading
Loading