Skip to content

Add Party and Classic mode selection#150

Open
tim4724 wants to merge 1 commit into
mainfrom
feat/classic-party-modes
Open

Add Party and Classic mode selection#150
tim4724 wants to merge 1 commit into
mainfrom
feat/classic-party-modes

Conversation

@tim4724
Copy link
Copy Markdown
Owner

@tim4724 tim4724 commented May 23, 2026

Summary

  • add a root mode selector with trailer access on the first screen
  • send Party selection directly into the QR lobby and keep /party as a direct lobby route
  • add Classic solo keyboard mode with local high scores and full-piece gameplay
  • add friendly Party piece bag support and counterclockwise keyboard input plumbing

Verification

  • npm test
  • npm run test:e2e
  • npm run test:e2e:airconsole
  • browser smoke: root mode screen, Party direct lobby, Classic menu/gameplay

@claude
Copy link
Copy Markdown

claude Bot commented May 23, 2026

Claude finished @tim4724's task in 7m 42s —— View job


Code Review — Add Party and Classic mode selection

  • Gather context and understand the request
  • Read changed files and analyze code (3 independent finder angles + verification)
  • Post review findings

Findings

🔴 Bug — False "Score saved locally" when score doesn't make the top 10

public/display/DisplayClassic.js, lines 98–101 + line 175

recordClassicScore writes only the top-10 slice to localStorage:

scores = scores.slice(0, CLASSIC_MAX_SCORES);  // entry excluded if rank > 10
writeClassicScores(scores);
return rank > 0 && rank <= CLASSIC_MAX_SCORES ? rank : null;

When the leaderboard already has 10 entries and the new score ranks 11th, rank is null and the score is not persisted. But the caller displays:

classicResultPanel.textContent = rank
  ? t('classic_new_score', { rank: rank })
  : t('classic_score_saved');   // ← shows "Score saved locally" even though it wasn't

Fix options:

  1. Save all scores unconditionally (unlimited writes, cap only the display/sort at 10) — this makes the existing classic_score_saved message accurate in all cases.
  2. Or add a distinct i18n key for the not-ranked outcome, e.g. classic_score_not_ranked ("Keep playing to reach the top 10").

Fix this →


🟡 Inconsistency — endClassicToMenu bypasses setRoomState

public/display/DisplayClassic.js, line 199

if (roomState !== ROOM_STATE.LOBBY) {
  roomState = ROOM_STATE.LOBBY;  // direct assignment, bypasses VALID_TRANSITIONS guard
}

Every other state transition in the codebase uses setRoomState(). The direct assignment works today because all callers reach this from states where LOBBY is a valid transition (PLAYING→LOBBY, RESULTS→LOBBY), but the bypass is inconsistent and could silently corrupt state if a new caller is added.

// preferred
setRoomState(ROOM_STATE.LOBBY);

Fix this →


🟡 Possible design oversight — PARTY_PIECE_TYPES missing 'p'

server/constants.js, line 54

const PIECE_TYPES       = ['I', 'O', 'S', 'Z', 'q', 'p', 'L', 'J'];  // classic — includes both q and p
const PARTY_PIECE_TYPES = ['I', 'O', 'q', 'J', 'i3', 'v3'];           // party — has q but not its mirror p

q and p are described in Piece.js as hex analogs of the S/Z pair (chevron ribbon + right/left stem respectively). Classic includes both; party includes only q. This makes the party bag asymmetric — players can fill right-overhang gaps but not left-overhang gaps with the chevron+stem shape.

If the omission is intentional (design decision to simplify), it's worth a one-line comment. If it's accidental, adding 'p' would restore symmetry.


🟢 Minor — classicResultPanel.textContent = clobbers child nodes

public/display/DisplayClassic.js, line 175

Setting .textContent replaces all children of the element with a text node. Currently the panel only holds text, so this is harmless. But classList.remove('hidden') and classList.add('hidden') are applied elsewhere, suggesting it's treated as a container — using .textContent would silently destroy any future child elements. Prefer .textContent only on leaf-text elements, or use a dedicated <span> inside the panel for the message.


🟢 Test gap — i3/v3 not included in rotation cache-key uniqueness test

tests/hex-board.test.js (the rotation-uniqueness test iterates over PIECE_TYPES only, not PARTY_PIECE_TYPES)

i3 and v3 are 3-cell pieces on a 6-rotation hex grid; straight 3-chains have 3-fold symmetry. The ghost-preview cache uses piece._rotId (strictly monotonic) as the key, so no functional bug exists — but the test never exercises party-piece ghost cache hits. Worth adding i3 and v3 to the existing rotation uniqueness loop.


Summary

One confirmed bug (false score-saved confirmation), one inconsistency (direct roomState mutation), one possible design oversight (asymmetric party bag), and two minor code quality notes. Everything else — the Game / Randomizer / PlayerBoard options wiring, the INPUT.ROTATE_CCW plumbing, _absoluteBlocksFast handling 3-cell pieces, and the server-side relay being purely a file server — looks correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant