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
10 changes: 5 additions & 5 deletions backend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import rateLimit, { type RateLimitInfo } from "express-rate-limit";
import { AccessToken, TrackSource } from "livekit-server-sdk";
import { z } from "zod";
import {
ARCADE_GAMES,
ARCADE_SCORE_GAMES,
LIMITS,
RATE_LIMITS,
arcadeScoreSchema,
Expand All @@ -12,7 +12,7 @@ import {
credentialsSchema,
liveKitSchema,
reportCreateSchema,
type ArcadeGame,
type ArcadeScoreGame,
type ArcadeLeaderboard,
type ArcadeScoreResult,
type BlockAck,
Expand Down Expand Up @@ -151,8 +151,8 @@ const blockLimiter = rateLimit({
},
});

function isArcadeGame(value: string): value is ArcadeGame {
return (ARCADE_GAMES as readonly string[]).includes(value);
function isArcadeScoreGame(value: string): value is ArcadeScoreGame {
return (ARCADE_SCORE_GAMES as readonly string[]).includes(value);
}

api.post("/signup", authLimiter, async (request, response) => {
Expand Down Expand Up @@ -247,7 +247,7 @@ api.post("/arcade/scores", arcadeScoreLimiter, requireAuth, async (request, resp
api.get("/arcade/scores/:game", arcadeLeaderboardLimiter, requireAuth, async (request, response) => {
const raw = Array.isArray(request.params.game) ? request.params.game[0] : request.params.game;
const game = raw ?? "";
if (!isArcadeGame(game)) {
if (!isArcadeScoreGame(game)) {
response.status(404).json({ error: "unknown-game" });
return;
}
Expand Down
14 changes: 7 additions & 7 deletions backend/test/integration/api.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,21 +506,21 @@ describe("arcade high scores", () => {
const zero = await api(base, "/api/v1/arcade/scores", { token, body: { game: "flappy", score: 0 } });
expect(zero.json.best).toBe(0);
expect(zero.json.newBest).toBe(false);
const first = await api(base, "/api/v1/arcade/scores", { token, body: { game: "snake", score: 12 } });
const first = await api(base, "/api/v1/arcade/scores", { token, body: { game: "snake-v2", score: 12 } });
expect(first.status).toBe(200);
expect(first.json.best).toBe(12);
expect(first.json.newBest).toBe(true);
expect(first.json.game).toBe("snake");
expect(first.json.game).toBe("snake-v2");
// A lower score does not lower the stored best.
const lower = await api(base, "/api/v1/arcade/scores", { token, body: { game: "snake", score: 5 } });
const lower = await api(base, "/api/v1/arcade/scores", { token, body: { game: "snake-v2", score: 5 } });
expect(lower.json.best).toBe(12);
expect(lower.json.newBest).toBe(false);
// Matching the standing best is not an improvement either.
const equal = await api(base, "/api/v1/arcade/scores", { token, body: { game: "snake", score: 12 } });
const equal = await api(base, "/api/v1/arcade/scores", { token, body: { game: "snake-v2", score: 12 } });
expect(equal.json.best).toBe(12);
expect(equal.json.newBest).toBe(false);
// A higher score raises it.
const higher = await api(base, "/api/v1/arcade/scores", { token, body: { game: "snake", score: 20 } });
const higher = await api(base, "/api/v1/arcade/scores", { token, body: { game: "snake-v2", score: 20 } });
expect(higher.json.best).toBe(20);
expect(higher.json.newBest).toBe(true);
// The leaderboard lists this user's best with their username.
Expand All @@ -530,7 +530,7 @@ describe("arcade high scores", () => {

it("atomically identifies only one of two identical first submissions as the new best", async () => {
const { token } = await createPlayer("arc-race");
const body = { game: "snake", score: 9 };
const body = { game: "snake-v2", score: 9 };
const [left, right] = await Promise.all([
api(base, "/api/v1/arcade/scores", { token, body }),
api(base, "/api/v1/arcade/scores", { token, body }),
Expand All @@ -557,7 +557,7 @@ describe("arcade high scores", () => {
it("rejects an unknown game and a malformed score", async () => {
const { token } = await createPlayer("arc3");
expect((await api(base, "/api/v1/arcade/scores/pong", { token })).status).toBe(404);
const bad = await api(base, "/api/v1/arcade/scores", { token, body: { game: "snake", score: -1 } });
const bad = await api(base, "/api/v1/arcade/scores", { token, body: { game: "snake-v2", score: -1 } });
expect(bad.status).toBe(400);
expect(bad.json.error).toBe("invalid-score");
});
Expand Down
32 changes: 19 additions & 13 deletions docs/STATE.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
# 2D Metaverse — State
> **hyprverse**: a private student social world with spatial media, meeting rooms, stage, arcade, and board tables. · Last checkpoint: 2026-08-01
> **hyprverse**: a private student social world with spatial media, meeting rooms, stage, arcade, and board tables. · Last checkpoint: 2026-08-08

## 🚧 In progress / next
- Campus art + HUD branch `art/hostel-room-proof` is reconciled with main after #167 and #168; next is push, open the campus/UI PR, exact-head Codex review, and green CI (incl. E2E) before squash-merge.
- Then resume spec 27 tickets #165–#166 (`docs/specs/27-arcade-2.md`, parent #162). Parked: remaining PRD 25 frontier (#107, #117, #108, #118–#121, #124/#125, #127–#132).
- PR #174 (`feat/restore-white-snake`) merged as `3701d473` from head `21e6dec`; no active work remains from this session.
- Resume spec 27 tickets #165–#166 (`docs/specs/27-arcade-2.md`, parent #162). Parked: remaining PRD 25 frontier (#107, #117, #108, #118–#121, #124/#125, #127–#132).
- Next concrete step: choose the next queued spec-27 ticket or a new user task.

## Status
- PRD 25 batch landed (#138–#160); moderator dashboard (#161); arcade polish (#170); flappy dino-ramp (#171); arcade juice/Snake rework (#167); Stellar Forge merge-drop cabinet (#168).
- Production `main` includes #167 (`f766ec1`) and #168 (`4dd72c0`). Stellar Forge is live on main with pause/fullscreen, authoritative `newBest`, live shake/reduced-motion, and board foley preserved.
- Campus polish (local branch, reconciled): per-district `campus_decor/` decoration, curated LimeZu furniture, decluttered walkways/rooms, clear presenter platform, modern seating, Stellar Forge cabinet at (82,96), deterministic regenerated campus map/geometry.
- HUD polish: compact pixel-paper chat with slash-command discovery, thin themed scrollbar, independently collapsible muted/blocked management, cream speech bubble, wooden noticeboard fullscreen map, bottom-centre controls/help, consistent room/admin/chat surfaces.
- Stage: one presenter-only `Go Live` starts microphone audio; global camera adds/removes video on the same session; status is `LIVE` / `NOT LIVE`.
- Focused validation after main merge: frontend 185/185, backend stage+geometry 18/18, ESLint 0 errors (3 existing ChatBox hook warnings), emoji check, generator double-run deterministic, `git diff --check` clean. Full build/E2E delegated to GitHub CI.
- PRD 25 batch landed (#138–#160); moderator dashboard (#161); arcade polish (#170); flappy dino-ramp (#171); arcade juice/Snake rework (#167); Stellar Forge merge-drop (#168); campus art + board UI + HUD polish (#172); stage mic consent + board error timer (#173).
- Production `main` tip is `3701d473` (squash of #174). Prior: #173 `f317144`, #172 `ae8a803`, #168 `4dd72c0`, #167 `f766ec1` — all squash-merged with green CI (incl. E2E). No force-push; no direct main commits.
- PR #174 merged with green exact-head CI: frontend build, backend tests, Docker integration, production image, and E2E all passed; merge commit `3701d473`.
- Board SFX interest gate: place/outcome cues only for seated (incl. forfeit leaver) or nearby table spectators — campus bystanders stay silent (`boardSound` + App seated/near refs).
- Stage Go Live mic consent: pre-live `micOn` captured; stop/walk-off re-mutes in `finally` when the player had not already unmuted for proximity.
- Snake review fixes: versioned score namespaces avoid migration races; pure frame scheduling handles queued instant turns; reduced-motion disables Snake interpolation/pulses and terminal delay; bonus geometry, tick-based comparable scoring, near-full-board food placement, and full terminal FX timing are covered by focused tests.
- Snake rollout hardening: current clients submit `snake-v2` scores while legacy `snake` remains accepted; scored boards are fixed at 34×18; `npm audit` is clean after the lockfile refresh.
- Parked follow-up (non-blocking Opus note): Go Live's implied unmute still persists to sessionStorage across reload — consider not persisting stage-only unmute.

## Architecture map
- Wire contracts and shared rules -> `shared/src/`
- Backend API, sockets, moderation, authority FSMs -> `backend/src/`
- Frontend React/Phaser/media/UI -> `frontend/src/`
- Arcade reducers -> `frontend/src/game/arcade/`; overlay/renderers -> `frontend/src/ui/arcade/`; board stack -> `shared/src/games/` + `backend/src/boardMatch.ts`/`board-manager.ts`
- Arcade reducers -> `frontend/src/game/arcade/`; overlay/renderers -> `frontend/src/ui/arcade/`
- Board rules -> `shared/src/games/`; match lifecycle -> `backend/src/boardMatch.ts` + `board-manager.ts`; client sound -> `frontend/src/game/boardSound.ts`; HUD -> `frontend/src/ui/board/*` + `BoardTablePanel.tsx`
- Generated campus + geometry -> `frontend/scripts/gen_campus.py`; per-district art -> `frontend/scripts/campus_decor/<district>.py`
- Deploy and production operations -> `.github/workflows/`, `deploy/`

Expand All @@ -30,13 +34,15 @@
- `@metaverse/shared` owns every wire shape; game rules stay pure seeded reducers; managers are side-effect shells (full log: `docs/decisions.md`).
- HUD follows the selected compact pixel-paper reference: integrated chat, attached command tray, wooden noticeboard map, bottom-centre media bar, and help inside Settings.
- Stage has one live session: explicit `Go Live` enables microphone audio; camera joins/leaves through the global control; user-facing state is `LIVE` / `NOT LIVE` (2026-08-01).
- Campus art lives in per-district `campus_decor/` modules; `gen_campus.py` remains the structural author; placement legality is enforced centrally. Stellar Forge cabinet is structural (gen_campus), not decor.
- Full local gates are prohibited on Raja's machine; current-head cloud review plus green GitHub CI (including E2E) are the merge gates.
- Campus art lives in per-district `campus_decor/` modules; `gen_campus.py` remains the structural author; empty floor is intentional; presenter zone stays prop-free.
- Merge gate: exact-head CI green (incl. E2E) plus the current PR's unresolved-review verdict; full local suites prohibited on Raja's machine.

## Gotchas
- A solid prop body is bottom-anchored and extends above its authored tile. Connectivity is not enough: E2E helpers walk straight swept corridors, so map changes require the real E2E job.
- `WorldScene` draws a fallback room table only when no authored solid occupies the seat centroid; preserve that guard.
- Generated campus artifacts must come from `cd frontend && python3 scripts/gen_campus.py`, never hand edits.
- LimeZu art is non-commercial-license only; buy the paid pack before monetisation.
- BootScene furniture key regex in `maps.test.ts` must allow hyphens (`arcade_merge-drop`).
- Entry bundle was 98.9/130 KB gzip at a prior strict-build checkpoint; new arcade code stays lazy-loaded.
- Board `board-update` is space-scoped — always gate SFX on seated/nearby interest (`boardSound` + App refs).
- Codex cloud review limits are normally exhausted; PR #174 was merged after its final pushed head passed the required CI checks.
- Production backend currently reports SHA `09f39f0`; its `snake-v2` acceptance was staged before the #174 frontend switch merged.
18 changes: 18 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,21 @@ lists are empty during the floor phase). `frontend/src/game/maps.test.ts` is the

## 2026-08-01 — Stage broadcast is one explicit live session
**Why:** Separate “Go on air” and “Go Live (video)” paths produced overlapping prompts and two competing transport lifecycles. The presenter now gets one explicit `Go Live` action that enables microphone audio and starts the stage session; the existing global camera control adds or removes video from that same session, and `Stop Live` ends it. User-facing state is `LIVE` / `NOT LIVE`. This supersedes the 2026-07-07 “muted preference wins when going on air” choice: the new explicit Go Live click is itself consent to start audio, while camera consent remains independent.

## 2026-08-01 — Prefer Claude residual review over Codex cloud review
**Why:** Codex cloud PR review limits are exhausted. Merge gate remains exact-head green CI (incl. E2E) plus a Claude residual review for actionable P0–P2; do not burn or invoke `@codex review` until limits reset.

## 2026-08-08 — Preserve Snake leaderboard comparability during the faithful restore
**Why:** The restored Snake changes ordinary food from 1 point to 10 and adds timed bonuses. Migration `007_snake_score_scale.sql` multiplies existing `snake` bests by 10 before the new reducer runs, preserving the public cabinet ID and keeping historical ordinary-food scores on the same unit instead of silently mixing incompatible values.

## 2026-08-08 — Keep instant Snake steering inside the pure frame reducer
**Why:** Renderer-side accumulator forcing could schedule a valid turn behind an illegal reversal while `snakeTick` consumed only the reversal. `snakeFrame` now owns immediate-turn scheduling and uses the pure queued-input preview; the renderer only forwards input and draws the result.

## 2026-08-08 — Version Snake score namespaces instead of rescaling live rows
**Why:** A migration that multiplied `snake` rows could race an independently deployed frontend and backend, double-scaling a new 10-point score. Current Snake therefore submits to `snake-v2`; legacy `snake` remains accepted for old clients, and historical rows are left untouched.

## 2026-08-08 — Stage the backend namespace before the Snake frontend switch
**Why:** Frontend Vercel and backend production deploys run independently. The backend accepting `snake-v2` was deployed at PR head `09f39f0` before the frontend can merge, eliminating the window where new clients would submit to an unknown score namespace and lose runs.

## 2026-08-08 — Keep Snake bonus scoring tick-based and reduced-motion terminal state immediate
**Why:** Difficulty changes movement speed while all Snake modes share one leaderboard, so wall-clock bonus decay made identical movement paths earn different values. Bonus decay now advances once per movement tick, and reduced-motion users see the terminal card immediately because decorative effects are disabled. The alternative of preserving the visual delay would violate the reduced-motion contract.
12 changes: 12 additions & 0 deletions docs/sessions/2026-08-01.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,15 @@
- did: resolved PR #168 against main after #167 (semantic merge of Stellar Forge with juice/Snake/board foley, live shake, authoritative newBest); fixed Codex findings (ARCADE_GAMES derivation, ArcadeBusEvent, terminal events, heat-meter fall spike, pause latch); exact-head Codex clean on `c31e57e`; CI full green incl. E2E 8m46s; squash-merged #168 as `4dd72c0`. Merged `origin/main` into `art/hostel-room-proof`, kept campus_decor architecture + Stellar Forge cabinet at (82,96), regenerated map/geometry (deterministic), fixed BootScene hyphen key guard.
- verified: focused frontend 185/185; backend stage+geometry 18/18; ESLint 0 errors (3 existing ChatBox warnings); emoji check; generator double-run hashes identical.
- next: push campus branch, open ready-for-review PR, exact-head Codex + CI, squash-merge when CLEAN.

## 20:31 — #172 campus + board UI merged; Codex review retired
- did: finished campus PR #172 — ChatBox safeCommandIndex (fixed setState-in-effect lint), board SFX interest gate (seated/wasSeated/nearby vs campus bystander), Claude residual review CLEAN on `8f87e4e`, full CI green (lint/typecheck/unit/E2E + backend), squash-merged #172 as `ae8a803` with `--match-head-commit`. Confirmed earlier #168 already on main as `4dd72c0`. Stopped using `@codex review` (limits exhausted); residual reviews via Claude only after exact-head CI green.
- changed: `frontend/src/ui/ChatBox.tsx`, `game/boardSound.ts` + tests, `App.tsx` nearby/seated refs; remote main now `ae8a803`.
- decided: no more Codex PR review for this project until limits reset; Claude residual review is the merge gate reviewer.
- next: resume spec 27 tickets #165–#166; optional commit of checkpoint docs if desired.

## 21:07 — Opus residual on #172; follow-up #173 merged
- did: Opus 5 residual review of #172 @ ae8a803 found P1 (stage-off-air left world mic hot) + P2 (board-error timer). Opened #173, fixed both + goOffAir finally re-mute; CI green; Opus residual CLEAN (MERGE); squash-merged #173 as `f317144`. No Codex.
- changed: `frontend/src/App.tsx`, `App.test.tsx`; main tip `f317144`.
- decided: stage Go Live unmute is transient consent — restore pre-live mic on stop even if goOffAir rejects.
- next: resume spec 27 tickets #165–#166; optional sessionStorage unmute persistence follow-up; commit checkpoint docs if desired.
27 changes: 27 additions & 0 deletions docs/sessions/2026-08-08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## 13:41 — Resolve Snake PR review findings
- did: fixed all five Codex findings on PR #174, added focused regression coverage, committed as `ee30bc5`, pushed `feat/restore-white-snake`, and requested `@codex review` again.
- changed: Snake score migration, pure turn scheduling, live shake/reduced-motion gating, bonus-cell rendering/food placement, and focused Snake tests.
- verified: focused frontend tests 72/72; targeted ESLint clean; `git diff --check` clean. Full CI is pending on the pushed head.
- decided: rescale historical Snake scores by 10 rather than change the public game ID, preserving the existing leaderboard contract.
- next: wait for exact-head CI and Codex rereview; the user will report any CI failure.

## 14:08 — Fix failed audit and second Codex review
- did: diagnosed the failed CI job as `npm audit` (five high vulnerabilities), replaced the unsafe Snake score migration with `snake-v2` namespace versioning, fixed fixed-size scored boards, refreshed the lockfile, pushed `d584c13`, and requested `@codex review` again.
- changed: shared score namespace contract, backend score validation, frontend score client, Snake board sizing, backend integration expectations, package-lock security updates.
- verified: shared tests 41/41; frontend tests 102/102; backend/frontend typechecks; workspace lint; `npm audit` zero vulnerabilities; diff checks clean. New CI is pending.
- decided: never rescale live score rows across independently deployed frontend/backend versions; version the score namespace instead.
- next: wait for exact-head CI and the new Codex review; the user will report any failure.

## 14:44 — Fix final Snake review findings and stage backend rollout
- did: disabled Snake decorative motion under reduced-motion, aligned overlay hold time with the 830ms death FX, updated timing assertions, committed `09f39f0`, pushed PR #174, deployed the backend from that exact SHA, and requested `@codex review` again.
- changed: `SnakeGame.tsx`, Snake renderer/food animation, `ArcadeOverlay.tsx` and focused tests; production backend release.
- verified: focused frontend tests 103/103, frontend typecheck/lint, diff checks; deployment workflow succeeded and `https://api.space.raja-dev.me/health/ready` reports SHA `09f39f0`. Frontend E2E and Codex rereview remain pending.
- decided: stage backend score-namespace acceptance before merging the independently deployed frontend switch; do not claim merge readiness until exact-head E2E and review are green.
- next: inspect the new Codex review and E2E result; merge only after both gates pass.

## 16:25 — Merge Snake restore and close review loop
- did: fixed comparable Snake bonus decay across difficulties and removed the reduced-motion terminal delay; added focused regression coverage, pushed `21e6dec`, and requested `@codex review`.
- changed: `frontend/src/game/arcade/snake.ts`, `snake.test.ts`, `ArcadeOverlay.tsx`, `ArcadeOverlay.test.tsx`, and `terminalHold.ts`.
- verified: 104 focused frontend tests passed; frontend typecheck passed; lint had only the three pre-existing `ChatBox.tsx` warnings; exact-head CI passed including E2E; PR #174 merged as `3701d473`.
- decided: bonus value decays per movement tick so identical paths remain comparable across difficulty speeds; reduced motion removes decorative terminal delay.
- next: no follow-up for this session; continue with the next queued task.
Loading
Loading