Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ precheck-results/

.vscode/
.vite/
.opencode/
.specify/**
!.specify/memory/**
121 changes: 121 additions & 0 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<!--
## Sync Impact Report

**Version change**: 0.1.0 → 0.2.0
**Modified principles**:
- I. TypeScript-First — strengthened with strict mode requirement

**Added sections**: None
**Removed sections**: None

**Templates requiring updates**:
- `.specify/templates/plan-template.md` — ✅ updated (generic template, no constitution-specific references)
- `.specify/templates/spec-template.md` — ✅ updated (generic template, no constitution-specific references)
- `.specify/templates/tasks-template.md` — ✅ updated (generic template, no constitution-specific references)
- `.specify/templates/checklist-template.md` — ✅ updated (generic template, no constitution-specific references)
- `.specify/templates/constitution-template.md` — ✅ updated (this file)

**Follow-up TODOs**: None — all placeholders filled.
-->

# Scribble Constitution

## Core Principles

### I. TypeScript-First (Strict Mode)

TypeScript strict mode MUST be enabled (`strict: true` in tsconfig). All code
MUST be fully typed. Avoid `any`; use `unknown` for truly dynamic types. Every
new file and refactor MUST include proper type annotations.

Rationale: Strict mode catches null/undefined errors, implicit-any leaks, and
unchecked callbacks at compile time — critical for a multiplayer game where
multiple clients interact through a shared API.

### II. Extend, Don't Rewrite

All changes MUST extend the existing starter codebase, never replace it.
Preserve established directory structure, naming conventions, component
boundaries, and data flow patterns.

Rationale: The lab evaluates incremental brownfield enhancement. Rewriting
defeats the purpose and creates drift between spec, plan, and implementation.

### III. Spec-Driven Development

Spec Kit artifacts MUST be produced before implementation in this order:
discovery → spec → plan → tasks. Each feature MUST have documented acceptance
criteria using Given/When/Then scenarios. Implementation MUST satisfy all
acceptance criteria before progressing to the next feature.

Rationale: Ensures traceability from requirements to code and prevents scope
creep.

### IV. Deterministic Game Logic

All game rules MUST be deterministic. No randomness in word selection, drawer
assignment, or scoring. Scoring is flat: 100 points for a correct guess, 0 for
incorrect. Round duration is turn-based (no timers). Word selection uses a
deterministic function of round number against the fixed seed list.

Rationale: Deterministic behavior ensures consistent results across clients and
enables reproducible testing.

### V. Minimal Dependencies

No new state-management or routing libraries beyond what the starter ships.
No WebSockets, databases, or authentication modules. No top-level dependencies
without explicit justification in the feature specification.

Rationale: Every dependency is a maintenance burden and an attack surface. The
starter's dependency set (Express, Zod, React, React Router, Vite) is
sufficient for all in-scope features.

## Technical Constraints

- **Sync mechanism**: HTTP polling only. WebSockets, Socket.io, or any
real-time push protocol is strictly forbidden.
- **Storage**: In-memory only. No SQL, NoSQL, SQLite, or any database.
- **Authentication**: None. No accounts, sessions, JWT, or OAuth.
- **Validation**: Use Zod for all backend request payload and response
validation.
- **Error handling**: Backend MUST use centralized error handlers. Frontend
MUST gracefully handle API failures without crashing.
- **Imports**: ES modules with standard relative paths. Backend follows NodeNext
module resolution.
- **Immutability**: Prefer immutable data structures and pure functions.
- **Frontend components**: MUST use functional components with hooks. Class
components are forbidden.
- **Test coverage**: Vitest MUST be used for all tests. Line coverage MUST
meet a minimum of 80%. Coverage thresholds are enforced — dropping below
blocks the build.

## Development Workflow

1. **Discovery first**: Read relevant starter files and document gaps and
assumptions before writing any code.
2. **Specify**: Write feature specification with acceptance criteria using
Given/When/Then format.
3. **Clarify**: Resolve ambiguity before planning — never guess requirements.
4. **Plan**: Update state model, data flow, and file-level changes.
5. **Task**: Decompose the plan into ordered, testable work items.
6. **Implement**: Complete one meaningful slice at a time. Commit after each
slice.
7. **Validate**: Verify acceptance criteria with two browser tabs before
moving on.
8. **Build check**: Run `npm run build` in both `backend/` and `frontend/`
before handing off changes.

## Governance

- This constitution supersedes all other practices and workflow documentation.
- Amendments MUST be documented with version, date, and rationale.
- All PRs and reviews MUST verify compliance with these principles.
- Complexity MUST be justified: any deviation from these principles requires
explicit documentation in the relevant spec or plan artifact.
- Versioning follows semantic versioning:
- MAJOR: backward-incompatible governance or principle removals/redefinitions
- MINOR: new principle or section added, materially expanded guidance
- PATCH: clarifications, wording, typo fixes, non-semantic refinements

**Version**: 0.2.0 | **Ratified**: 2026-06-26 | **Last Amended**: 2026-06-26
105 changes: 105 additions & 0 deletions .specify/memory/discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Discovery Notes — Scribble Assignment


## Missing Features (Gaps)

### 1. Host tracking & host-only permissions
The backend creates rooms (`POST /rooms`) but does not mark any participant as the **host**. There is no endpoint or logic that enforces host-only actions (e.g., starting the game). The `Participant` model in `backend/src/models/game.ts` has no `isHost` flag.

**Relevant files:** `backend/src/models/game.ts`, `backend/src/api/rooms.ts`, `backend/src/services/roomStore.ts`

### 2. Player name validation
Player names are accepted as-is with no trimming and no rejection of empty or whitespace-only input. The `createRoomSchema` and `joinRoomSchema` in `backend/src/api/schemas.ts` only check that `playerName` is a string.

**Relevant files:** `backend/src/api/schemas.ts`, `frontend/src/pages/CreateRoomPage.tsx`, `frontend/src/pages/JoinRoomPage.tsx`

### 3. Automatic lobby polling
The Lobby page has a manual "Refresh Room" button but no automatic polling mechanism. Scenario 1 requires the lobby to refresh automatically at ~2s intervals.

**Relevant files:** `frontend/src/pages/LobbyPage.tsx`, `frontend/src/state/roomStore.ts`

### 4. Start game flow
There is no backend endpoint to transition a room from `"lobby"` status to an active game state. The `RoomStatus` type in `backend/src/models/game.ts` only defines `"lobby"`. No drawer assignment, word selection, or round initialization exists.

**Relevant files:** `backend/src/api/rooms.ts`, `backend/src/models/game.ts`, `backend/src/services/roomStore.ts`, `frontend/src/pages/LobbyPage.tsx`

### 5. Drawing interaction & clear canvas
The Game page shows a placeholder canvas area but no interactive drawing. There is no clear canvas action.

**Relevant files:** `frontend/src/pages/GamePage.tsx`

### 6. Guess submission & history
No backend endpoint exists for submitting guesses. No guess history is tracked on the room state or synced to players via polling.

**Relevant files:** `backend/src/api/rooms.ts`, `backend/src/models/game.ts`, `backend/src/services/roomStore.ts`, `frontend/src/pages/GamePage.tsx`

### 7. Scoring
No scoring logic exists. Scenario 3 requires correct guesses to award 100 points.

**Relevant files:** `backend/src/models/game.ts`, `backend/src/services/roomStore.ts`

### 8. Result state & restart
No result display after a round ends. No restart flow to return players to the lobby with round state cleared.

**Relevant files:** `backend/src/api/rooms.ts`, `backend/src/models/game.ts`, `frontend/src/pages/GamePage.tsx`

### 9. Secret word visibility rules
There is no mechanism to ensure the secret word is visible only to the drawer. All players would currently see the same data.

**Relevant files:** `backend/src/api/rooms.ts`, `backend/src/models/game.ts`, `frontend/src/pages/GamePage.tsx`

### 10. Round lifecycle management
No round state (current round number, round active flag, round end logic). The `Room` model has no concept of rounds.

**Relevant files:** `backend/src/models/game.ts`, `backend/src/services/roomStore.ts`

## Assumptions

### A1. Polling-based sync (no WebSockets)
The README and AGENTS.md explicitly forbid WebSockets. All real-time sync between players is assumed to use HTTP polling at ~2s intervals. This affects lobby refresh, guess history sync, and game state updates.

### A2. First player (creator) is host
Since there is no explicit host assignment yet, the assumed convention is that the room creator (first player to join) is the host. The host is the only player who can start the game.

### A3. Deterministic word selection
Words are selected from the fixed seed list (`rocket`, `pizza`, `castle`, `guitar`, `sunflower`) using a deterministic method (e.g., based on round number), not random. This ensures consistent behavior across clients.

### A4. Flat scoring (100 / 0)
Correct guesses score exactly 100 points. Incorrect guesses add 0. No speed bonuses, streak bonuses, or partial credit. This keeps scoring simple and deterministic.

### A5. Single round only (no rotation)
Multiple rounds, drawer rotation, timers, and countdowns are explicitly out of scope. The implementation covers exactly one round per game session, then a restart back to lobby.

### A6. In-memory only (no persistence)
All room data is stored in memory via `Map` in `roomStore.ts`. Restarting the backend clears all rooms. No database of any kind is used.

### A7. No authentication
Anyone can create or join a room without accounts, login, or sessions. Room codes are the only access mechanism. No rate limiting or security measures are in scope.

### A8. Case-insensitive guess comparison
Guesses are compared case-insensitively against the secret word. Input is trimmed before comparison. Empty guesses are rejected.

## Out of Scope (Confirmed from README)

### Technical
- WebSockets / real-time sync
- Databases / persistent storage
- Authentication / accounts / sessions
- Deployment / hosting / CI pipelines
- Docker / containerization
- New state-management or routing libraries (beyond what the starter ships)

### Game features
- Multiple rounds
- Drawer rotation
- Round timers / countdowns
- Speed or drawer bonuses
- Custom or random word packs
- Spectator mode
- Room moderation (kick / mute)
- Room passwords or invite links

### Process
- Rewriting the starter from scratch — extend, don't replace
- Adding top-level dependencies your spec doesn't justify
- Refactoring unrelated code
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ You are working on a monolithic repository for a multiplayer drawing game ("Scri
- Give concise, direct answers.
- Do not output large blocks of code if a small change suffices.
- When creating or editing files, ensure consistency with the existing directory structure detailed above.

<!-- SPECKIT START -->
Current plan: specs/004-result-restart-validation/plan.md
<!-- SPECKIT END -->
99 changes: 99 additions & 0 deletions REFLECTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
## What the Starter App Already Had

The starter was a **skeleton** with room setup infrastructure and stub pages:

**Backend:**
- In-memory `Room` store with `createRoom`, `joinRoom`, `getRoom`, `saveRoom`
- `RoomSnapshot` + `Participant` data models (no `hostId`, no game state)
- API routes: `POST /rooms`, `POST /rooms/:code/join`, `GET /rooms/:code`
- Zod validation for basic schemas
- `STARTER_WORDS` and `STARTER_ROLES` seed data

**Frontend:**
- Custom class-based `RoomStore` with `useSyncExternalStore` + Context
- API client with `createRoom`, `joinRoom`, `fetchRoom`
- Pages: `StartPage`, `CreateRoomPage`, `JoinRoomPage`, `LobbyPage` (stub), `GamePage` (stub)
- Components: `Card`, `GuessForm` (stub), `Scoreboard` (stub), `ResultPanel` (stub)
- React Router v6 routing, CSS styling

**What was missing:** No game mechanics — no drawing, guessing, scoring, rounds, disconnect detection, skip, restart, or leave. No polling, session persistence, or host-only controls.

---

## Phase 1 — Room Setup & Lobby (Spec 001)

### What it does

Players create rooms and share a 4-character code. The room creator becomes the host. Others join by code. Everyone sees the participant list update in real time. The host sees a "Start Game" button that stays disabled until 2+ players have joined. The game starts when the host clicks it.

### Technical additions

- **Data model:** `hostId` added to `Room` and `RoomSnapshot`; `joinRoom` now rejects non-lobby rooms
- **API:** `POST /rooms/:code/start` — host-only, validates 2+ participants, transitions status to `"playing"`, initializes first round/drawer/word/scores
- **Frontend:** Auto-polling (2s with exponential backoff to 8s), session persistence via `sessionStorage`, host-only "Start Game" button with dynamic label, auto-navigate to `/game` on status change
- **Store:** `startGame()`, `startPolling()`, `stopPolling()`, `clearSession()`, `getSavedSession()`, `restoreSession()` methods added
- **Tests:** 10 backend tests (room lifecycle, host validation, join rejection, start game), 2 frontend tests (API client)

---

## Phase 2 — Game Start & Drawer Flow (Spec 002)

### What it does

When the game starts, one player is assigned as the drawer and sees the secret word. Everyone else sees a "waiting for drawer" view. The drawer's identity is visible to all. The secret word is never revealed to guessers.

### Technical additions

- **Data model:** `drawerId` and `secretWord` on `Room`; `secretWord` only included in snapshots when `viewerParticipantId === drawerId`
- **Frontend:** Session restoration on page load, navigation guard (`/game` → `/lobby` if status reverts), `CanvasPlaceholder` for non-drawers
- **No new API routes** — reuses existing `GET /rooms/:code` with conditional `secretWord`

---

## Phase 3 — Drawing, Guessing, Scoring & Rounds (Spec 003)

### What it does

The drawer draws freehand on a canvas with a color picker and clear button. Guessers type guesses and get instant correct/incorrect feedback. A correct guess awards 100 points and immediately advances the round. Scores (round + cumulative) are visible to everyone. The drawer rotates each round, cycling through all participants. When a round ends, a persistent overlay shows the correct word, drawer name, full guess history (timestamps + correct/incorrect badges), and final scores. Each player clicks "Continue" individually to dismiss it — no auto-dismiss timer.

### Technical additions

- **Data model:** `Point`, `DrawingStroke`, `GuessEntry`, `ParticipantScore`, `RoundResult` types added; `strokes`, `guessHistory`, `scores`, `lastRoundResult`, `lastRoundResultSetAt` on `Room`
- **API:** `POST /rooms/:code/guess` — case-insensitive, 100pts, auto-advances on correct; `POST /rooms/:code/draw` — drawer-only stroke addition; `DELETE /rooms/:code/draw` — drawer-only canvas clear
- **Frontend:** `Canvas.tsx` (pen, color picker, clear, survives poll redraws), `GuessHistory.tsx` (timestamped list), `Scoreboard.tsx` (round + cumulative), `RoundResultCard.tsx` (overlay with Continue/Restart/Exit)
- **Store:** `addStrokes()`, `clearCanvas()`, `submitGuess()`, `dismissLastRoundResult()` methods; local tracking of `lastRoundResult`/`resultDismissed`/`dismissedRoundNumber`
- **Tests:** 13 backend tests (guess validation, scoring, round advancement, stroke add/clear), 8 frontend tests (Canvas, GuessForm, GuessHistory, Scoreboard, RoundResultCard)

---

## Phase 4 — Disconnect Handling, Skip, Restart, Exit & Game Over (Spec 004)

### What it does

**Host disconnect** — If the host's browser goes idle for 30+ seconds, all remaining players see a "Host disconnected" popup with a "Claim Host" button. Whoever clicks it becomes the new host.

**Drawer disconnect & skip** — If the drawer disconnects, non-host players see a "Notify Host" button. Clicking it sets a flag that the host sees as a badge on their "Skip Round" button. The host can advance the round without a correct guess.

**Restart** — From the result overlay, the host can restart the game. Everyone returns to the lobby with participants preserved and all game state cleared.

**Exit & game over** — Any player can leave a game in progress. If all remaining participants have disconnected browsers (stale beyond 30 seconds), the game auto-ends with a final result card showing just an "Exit Game" button. The last player to leave deletes the room entirely.

**Name validation** — Player names are capped at 20 characters on both API and UI.

### Technical additions

- **Data model:** `lastSeenAt` on `Participant` (heartbeat timestamp); `skipSuggested`, `hostDisconnected`, `drawerDisconnected` on `Room`/`RoomSnapshot`; `"finished"` status variant
- **API:** `POST /rooms/:code/restart` — host-only, resets to lobby; `POST /rooms/:code/skip-round` — host-only, checks no correct guess exists; `POST /rooms/:code/suggest-skip` — any participant sets flag; `POST /rooms/:code/claim-host` — any participant; `DELETE /rooms/:code/leave` — removes participant, triggers game-over logic
- **Disconnect detection:** `touchParticipant()` called on every poll updates `lastSeenAt`; `isDisconnected()` checks 30-second threshold server-side; computed in `toRoomSnapshot()`
- **Frontend:** `HostDisconnectedBanner.tsx`, `DrawerDisconnectedBanner.tsx`; "Skip Round" button with pulse badge on `skipSuggested`; `leaveRoom()` method; finished status shows RoundResultCard with Exit only
- **Design decisions:** End game when host leaves and no active participants remain (prevents false "Host Disconnected" popup). `HostDisconnectedBanner` guarded with `room.status !== "finished"` in LobbyPage.
- **Tests:** 21 backend tests (skip round, suggest skip, claim host, remove participant, disconnect flags in snapshot), 8 frontend tests (banners, round result card, API client)

---

## Growth Summary

- Backend tests: **0 → 54**
- Frontend tests: **0 → 18**
- API routes: **3 → 12**
- Frontend components: **4 stubs → 10 working**
Loading
Loading