From c985b62450176bec64f31b37efcc59e161e08b82 Mon Sep 17 00:00:00 2001 From: shirisha kodimala Date: Wed, 27 May 2026 16:25:07 +0530 Subject: [PATCH 1/4] [Spec Kit] Add specification --- .specify/feature.json | 3 + .../checklists/requirements.md | 34 +++++ specs/001-room-setup-lobby/spec.md | 118 ++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 .specify/feature.json create mode 100644 specs/001-room-setup-lobby/checklists/requirements.md create mode 100644 specs/001-room-setup-lobby/spec.md diff --git a/.specify/feature.json b/.specify/feature.json new file mode 100644 index 0000000..fc29b3c --- /dev/null +++ b/.specify/feature.json @@ -0,0 +1,3 @@ +{ + "feature_directory": "specs/001-room-setup-lobby" +} diff --git a/specs/001-room-setup-lobby/checklists/requirements.md b/specs/001-room-setup-lobby/checklists/requirements.md new file mode 100644 index 0000000..dd40120 --- /dev/null +++ b/specs/001-room-setup-lobby/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Room Setup And Lobby + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-05-27 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +All items passed validation. No clarifications needed. Spec is ready for planning. diff --git a/specs/001-room-setup-lobby/spec.md b/specs/001-room-setup-lobby/spec.md new file mode 100644 index 0000000..21dbf98 --- /dev/null +++ b/specs/001-room-setup-lobby/spec.md @@ -0,0 +1,118 @@ +# Feature Specification: Room Setup And Lobby + +**Feature Branch**: `001-room-setup-lobby` + +**Created**: 2026-05-27 + +**Status**: Draft + +**Input**: User description: "Scenario 1: Room Setup And Lobby + +Given a player wants to host or join a drawing game, when they create or join a room via a unique code, then the creator is automatically the host; invalid or empty codes are rejected with clear feedback; rooms are fully isolated; the lobby refreshes via polling at about 2 seconds; and only the host can start the game once at least 2 players are present." + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Create and Host a Room (Priority: P1) + +A player opens the game and creates a new room. The system generates a unique room code and automatically assigns the creator as host. The host is taken to the lobby where they can see their own player entry and a waiting message. + +**Why this priority**: Room creation is the primary entry point for any multiplayer game session. Without this, no game can begin. + +**Independent Test**: Can be fully tested by a single player creating a room and confirming they appear in the lobby as host with a visible room code. + +**Acceptance Scenarios**: + +1. **Given** a player has launched the game, **When** they choose to create a new room, **Then** a unique room code is generated and displayed, and the player is designated as host +2. **Given** a player has created a room, **When** the lobby loads, **Then** the player sees their own name listed with a "Host" indicator and a waiting message for other players + +--- + +### User Story 2 - Join a Room with a Valid Code (Priority: P1) + +A player enters a room code provided by the host and joins the lobby. The player sees themselves in the player list alongside the host and any other joined players. + +**Why this priority**: Joining a room is the secondary entry point. Both create and join are needed for any multiplayer session to form. + +**Independent Test**: Can be fully tested by having a second player join an existing room and confirming they appear in the host's lobby. + +**Acceptance Scenarios**: + +1. **Given** a room exists with an active host, **When** another player enters the correct room code, **Then** the joining player is added to the room and appears in the player list +2. **Given** two or more players are in a lobby, **When** a new player joins, **Then** all connected players see the updated player list + +--- + +### User Story 3 - Invalid or Empty Room Codes (Priority: P2) + +A player enters a room code that is either empty, malformed, or does not match any active room. The system displays a clear error message and the player is not added to any room. + +**Why this priority**: Clear error handling is important for usability, but the core flows (create/join valid) must work first. + +**Independent Test**: Can be fully tested by attempting to join with an empty input or a non-existent code and verifying the error message. + +**Acceptance Scenarios**: + +1. **Given** a player is on the join screen, **When** they submit an empty code, **Then** the system rejects the input and displays "Please enter a room code" +2. **Given** a player is on the join screen, **When** they submit a code that does not match any active room, **Then** the system rejects the input and displays "Room not found. Please check the code and try again" + +--- + +### User Story 4 - Host Starts the Game (Priority: P2) + +Once at least two players are in the lobby, the host sees a "Start Game" button. Non-host players see a waiting message. The host clicks the button to begin the game. + +**Why this priority**: Starting the game is the gateway to actual gameplay. It depends on having players in the lobby first. + +**Independent Test**: Can be fully tested by having a host and one joiner in a lobby, then the host clicking start. + +**Acceptance Scenarios**: + +1. **Given** a host is in the lobby with only their own player, **When** they look for a start option, **Then** the start button is disabled or hidden +2. **Given** a host and at least one other player are in the lobby, **When** the host clicks "Start Game", **Then** the game begins for all players +3. **Given** a host and at least one other player are in the lobby, **When** a non-host player attempts to start the game, **Then** the action is rejected + +--- + +### Edge Cases + +- What happens when the host leaves or disconnects before the game starts? The room should handle host migration or dissolve the room +- How does the system handle two players joining with the same code simultaneously? Both should be admitted independently +- What happens when a player enters a code for a room that is already in-game (not in lobby)? The system should reject the join with "Game already in progress" + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST generate a unique room code for each newly created room +- **FR-002**: System MUST designate the room creator as the host +- **FR-003**: System MUST reject empty room codes with a clear error message +- **FR-004**: System MUST reject room codes that do not match any active room +- **FR-005**: System MUST ensure rooms are fully isolated (players, state, and data in one room are not accessible from another room) +- **FR-006**: Lobby MUST refresh player state via polling at approximately 2-second intervals +- **FR-007**: System MUST only allow the host to start the game +- **FR-008**: System MUST require at least 2 players present in the lobby before the game can start +- **FR-009**: System MUST display the room code to the host after room creation so it can be shared +- **FR-010**: System MUST remove a player from a room when they disconnect or leave + +### Key Entities *(include if feature involves data)* + +- **Room**: Represents a single game session. Contains a unique room code, the host player identifier, the list of joined players, and the current game state (lobby, in-progress, finished). Rooms are fully isolated from each other. +- **Player**: Represents a participant in a room. Has an identifier, a display name, and a role (host or participant). Players exist within the context of a single room. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: A player can create a room and see the room code within 2 seconds +- **SC-002**: A player can join a room by entering a valid code and see themselves in the lobby within 3 seconds +- **SC-003**: Invalid or empty room codes are rejected with a user-visible error message within 1 second +- **SC-004**: The lobby player list updates to reflect new joiners within 3 seconds of their arrival +- **SC-005**: The host can successfully start the game once 2 or more players are present, and non-host players cannot start the game + +## Assumptions + +- Players share room codes through external means (voice chat, messaging) — in-app code sharing is out of scope +- Room codes are short alphanumeric strings (e.g., 4-6 characters) for easy sharing +- A player can only be in one room at a time +- The game supports an unlimited number of rooms concurrently, bounded only by server resources +- The host leaving before the game starts results in the room being dissolved or the host role transferring — this is a known edge case for future iteration From 7b65dfbaa8ba1af08c927dd00f4523f7b5284749 Mon Sep 17 00:00:00 2001 From: shirisha kodimala Date: Wed, 27 May 2026 16:29:24 +0530 Subject: [PATCH 2/4] [Spec Kit] Add implementation plan --- AGENTS.md | 7 + specs/001-room-setup-lobby/contracts/api.md | 160 ++++++++++++++++++++ specs/001-room-setup-lobby/data-model.md | 92 +++++++++++ specs/001-room-setup-lobby/plan.md | 91 +++++++++++ specs/001-room-setup-lobby/quickstart.md | 55 +++++++ specs/001-room-setup-lobby/research.md | 43 ++++++ 6 files changed, 448 insertions(+) create mode 100644 specs/001-room-setup-lobby/contracts/api.md create mode 100644 specs/001-room-setup-lobby/data-model.md create mode 100644 specs/001-room-setup-lobby/plan.md create mode 100644 specs/001-room-setup-lobby/quickstart.md create mode 100644 specs/001-room-setup-lobby/research.md diff --git a/AGENTS.md b/AGENTS.md index 940bc46..983a4e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,3 +40,10 @@ 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. + + +For additional context about technologies to be used, project structure, +shell commands, and other important information, read the implementation plan +at `specs/001-room-setup-lobby/plan.md` and the research at +`specs/001-room-setup-lobby/research.md`. + diff --git a/specs/001-room-setup-lobby/contracts/api.md b/specs/001-room-setup-lobby/contracts/api.md new file mode 100644 index 0000000..eebb430 --- /dev/null +++ b/specs/001-room-setup-lobby/contracts/api.md @@ -0,0 +1,160 @@ +# API Contracts: Room Setup And Lobby + +Base URL: `http://localhost:3001` + +All requests and responses use `Content-Type: application/json`. + +## POST /rooms + +Create a new room. The creator is automatically designated as host. + +**Request Body:** +```json +{ + "playerName": "string (optional, defaults to 'Player')" +} +``` + +**Response (201):** +```json +{ + "participantId": "uuid", + "room": { + "code": "ABCD", + "status": "lobby", + "hostId": "uuid", + "participants": [ + { "id": "uuid", "name": "string", "joinedAt": "ISO8601" } + ], + "availableWords": ["string"], + "roles": ["drawer", "guesser"] + } +} +``` + +## POST /rooms/:code/join + +Join an existing room by its code. + +**Path Parameters:** `code` — room code (case-insensitive, normalized to uppercase) + +**Request Body:** +```json +{ + "playerName": "string (optional, defaults to 'Player')" +} +``` + +**Response (200):** +```json +{ + "participantId": "uuid", + "room": { + "code": "ABCD", + "status": "lobby", + "hostId": "uuid", + "participants": [ + { "id": "uuid", "name": "string", "joinedAt": "ISO8601" } + ], + "availableWords": ["string"], + "roles": ["drawer", "guesser"] + } +} +``` + +**Error (404):** +```json +{ + "message": "Room not found. Please check the code and try again" +} +``` + +## GET /rooms/:code + +Fetch the current state of a room (used for lobby polling). + +**Path Parameters:** `code` — room code + +**Query Parameters:** `participantId` (optional) — used to tailor the response perspective + +**Response (200):** +```json +{ + "room": { + "code": "ABCD", + "status": "lobby", + "hostId": "uuid", + "participants": [ + { "id": "uuid", "name": "string", "joinedAt": "ISO8601" } + ], + "availableWords": ["string"], + "roles": ["drawer", "guesser"] + } +} +``` + +**Error (404):** +```json +{ + "message": "Unable to load room" +} +``` + +## POST /rooms/:code/start + +Start the game. Only the host can start, and at least 2 players must be present. + +**Path Parameters:** `code` — room code + +**Request Body:** +```json +{ + "participantId": "uuid" +} +``` + +**Response (200):** +```json +{ + "room": { + "code": "ABCD", + "status": "playing", + "hostId": "uuid", + "participants": [ + { "id": "uuid", "name": "string", "joinedAt": "ISO8601" } + ], + "availableWords": ["string"], + "roles": ["drawer", "guesser"] + } +} +``` + +**Error (403) — Not the host:** +```json +{ + "message": "Only the host can start the game" +} +``` + +**Error (400) — Not enough players:** +```json +{ + "message": "At least 2 players are needed to start the game" +} +``` + +## Error Responses (common) + +**Empty code (400):** +```json +{ + "message": "Room code is required" +} +``` + +**Invalid payload (400) — Zod validation failure:** +```json +{ + "message": "Invalid request payload" +} +``` diff --git a/specs/001-room-setup-lobby/data-model.md b/specs/001-room-setup-lobby/data-model.md new file mode 100644 index 0000000..beaa388 --- /dev/null +++ b/specs/001-room-setup-lobby/data-model.md @@ -0,0 +1,92 @@ +# Data Model: Room Setup And Lobby + +## Entity: Room + +Represents a single game session. Rooms are fully isolated from each other. + +| Field | Type | Description | Validation | +|-------|------|-------------|------------| +| `code` | `string` | 4-char alphanumeric unique identifier (A-Z, 2-9) | Auto-generated, checked for uniqueness | +| `hostId` | `string` | UUID of the participant who created the room | Set at creation, immutable | +| `status` | `"lobby" \| "playing"` | Current state of the room | Transitions: `lobby` → `playing` (via start) | +| `participants` | `Participant[]` | Players currently in the room | At least 1 (creator), max ~10 | +| `createdAt` | `string` (ISO 8601) | Timestamp of room creation | Auto-set | +| `updatedAt` | `string` (ISO 8601) | Timestamp of last modification | Updated on each mutation | + +### State Transitions + +``` +lobby ──(host clicks start, ≥2 players)──▶ playing +``` + +### Validation Rules + +- Room code is exactly 4 characters from `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` +- Room must have at least 1 participant (the host) +- Host cannot be changed after creation +- Room cannot transition to `playing` unless: + - The requesting player is the host (`participantId === hostId`) + - At least 2 participants are in the room + +## Entity: Participant + +Represents a player connected to a room. + +| Field | Type | Description | Validation | +|-------|------|-------------|------------| +| `id` | `string` (UUID) | Unique identifier for this session | Auto-generated | +| `name` | `string` | Display name chosen by the player | Defaults to `"Player"` if empty | +| `joinedAt` | `string` (ISO 8601) | When the player joined | Auto-set | + +### Relationships + +- A **Room** has many **Participants** (1:N) +- A **Participant** belongs to exactly one **Room** +- The **host** is identified by `room.hostId` matching a participant's `id` + +## Changes to Existing Types + +### Backend (`backend/src/models/game.ts`) + +Update `RoomStatus` to include `"playing"`: +```typescript +export type RoomStatus = "lobby" | "playing"; +``` + +Add `hostId` field to `Room`: +```typescript +export interface Room { + code: string; + hostId: string; // NEW + status: RoomStatus; + participants: Participant[]; + createdAt: string; + updatedAt: string; +} +``` + +Update `RoomSnapshot` to expose host info (for frontend to determine who can start): +```typescript +export interface RoomSnapshot { + code: string; + status: RoomStatus; + hostId: string; // NEW + participants: Participant[]; + availableWords: string[]; + roles: ParticipantRole[]; +} +``` + +### Frontend (`frontend/src/services/api.ts`) + +Mirror the updated `RoomSnapshot`: +```typescript +export interface RoomSnapshot { + code: string; + status: "lobby" | "playing"; + hostId: string; // NEW + participants: Participant[]; + availableWords: string[]; + roles: ParticipantRole[]; +} +``` diff --git a/specs/001-room-setup-lobby/plan.md b/specs/001-room-setup-lobby/plan.md new file mode 100644 index 0000000..ade1359 --- /dev/null +++ b/specs/001-room-setup-lobby/plan.md @@ -0,0 +1,91 @@ +# Implementation Plan: Room Setup And Lobby + +**Branch**: `001-room-setup-lobby` | **Date**: 2026-05-27 | **Spec**: specs/001-room-setup-lobby/spec.md + +**Input**: Feature specification from `specs/001-room-setup-lobby/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow. + +## Summary + +Players can create or join isolated game rooms via a 4-character alphanumeric code. The room creator is designated as host and is the only one who can start the game once at least 2 players are present. The lobby refreshes player state via HTTP polling every ~2 seconds. Invalid or empty room codes are rejected with clear feedback. + +## Technical Context + +**Language/Version**: TypeScript 5.6 (backend + frontend), Node.js 22+ (runtime) + +**Primary Dependencies**: Express 4 (backend), React 18 (frontend), Vite 5 (build), Zod 3 (validation), React Router 6 (routing) + +**Storage**: In-memory (Map-based, no databases per project constraints) + +**Testing**: No test framework currently configured — tests are out of scope for this plan phase + +**Target Platform**: Web browser (modern Chrome/Firefox/Safari) + Node.js server + +**Project Type**: Web application (monorepo: Express backend + React frontend) + +**Performance Goals**: Lobby polling completes in under 1s round-trip; room code generation in under 100ms; lobby renders within 500ms of data arrival + +**Constraints**: No WebSockets, no databases, no authentication. All sync is HTTP polling. In-memory only — rooms are ephemeral and lost on server restart. + +**Scale/Scope**: Maximum ~100 concurrent rooms, ~10 players per room. Single-server deployment. + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +The constitution file (`.specify/memory/constitution.md`) contains placeholder template values and has not been populated with project-specific principles. No active gates are defined. Proceeding with standard development practices: + +- TypeScript-first, no `any`, prefer `unknown` for dynamic types (as established in AGENTS.md) +- Zod validation for all API payloads +- Immutable data patterns (structuredClone already in use) +- Existing project patterns (Express Router, roomStore, RoomStoreProvider) to be followed +- No WebSockets, databases, or auth (project constraints from AGENTS.md) + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-room-setup-lobby/ +├── plan.md # This file (/speckit.plan command output) +├── research.md # Phase 0 output (/speckit.plan command) +├── data-model.md # Phase 1 output (/speckit.plan command) +├── quickstart.md # Phase 1 output (/speckit.plan command) +├── contracts/ # Phase 1 output (/speckit.plan command) +└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) +``` + +### Source Code (repository root) + +```text +backend/ +├── src/ +│ ├── models/ +│ │ └── game.ts # Room, Participant, RoomSnapshot types +│ ├── services/ +│ │ └── roomStore.ts # In-memory room CRUD + startGame +│ ├── api/ +│ │ ├── router.ts # Express router + error handlers +│ │ ├── schemas.ts # Zod schemas for request validation +│ │ └── rooms.ts # Room endpoints (create, join, fetch, start) +│ └── server.ts # Entry point + +frontend/ +├── src/ +│ ├── components/ +│ ├── pages/ +│ │ ├── LobbyPage.tsx # Auto-polling lobby with host controls +│ │ ├── CreateRoomPage.tsx # Room creation form +│ │ └── JoinRoomPage.tsx # Room join form with validation +│ ├── state/ +│ │ └── roomStore.ts # Room state + polling logic +│ └── services/ +│ └── api.ts # API client +``` + +**Structure Decision**: Web application (Option 2). The monorepo is split into `backend/` and `frontend/` directories as established in the existing codebase. No new projects or structural changes needed. + +## Complexity Tracking + +No constitution violations to justify. diff --git a/specs/001-room-setup-lobby/quickstart.md b/specs/001-room-setup-lobby/quickstart.md new file mode 100644 index 0000000..3cf29b8 --- /dev/null +++ b/specs/001-room-setup-lobby/quickstart.md @@ -0,0 +1,55 @@ +# Quickstart: Room Setup And Lobby + +## Prerequisites + +- Node.js 22+ +- Dependencies installed: `cd backend && npm install` and `cd frontend && npm install` + +## Running the App + +```bash +# Terminal 1: Start the backend +cd backend && npm run dev + +# Terminal 2: Start the frontend +cd frontend && npm run dev +``` + +Backend runs on `http://localhost:3001`, frontend on `http://localhost:5173`. + +## Testing the Feature + +### Create a Room (Host) + +1. Open `http://localhost:5173` in a browser +2. Click **Create Room** +3. Enter a player name (optional) and click **Create and Continue** +4. You arrive in the lobby showing your room code and player list +5. Your name has a **Host** badge + +### Join a Room (Player) + +1. Open a second browser/incognito window to `http://localhost:5173` +2. Click **Join Room** +3. Enter the room code from the host's lobby +4. Enter a player name and click **Join Lobby** +5. You arrive in the lobby — both players are visible in the participant list + +### Auto-Polling Verification + +- After joining, the lobby refreshes automatically every ~2 seconds +- New joiners appear in the player list within ~3 seconds without manual refresh +- The status indicator shows "Refreshing players..." during polls + +### Start the Game + +1. As the host, ensure at least 1 other player is in the lobby +2. Click **Start Game** +3. The game begins for all players (room status changes to `playing`) + +### Edge Cases + +- **Empty code**: Submit the join form with an empty code → see "Please enter a room code" +- **Invalid code**: Enter a non-existent code → see "Room not found. Please check the code and try again" +- **Non-host start**: A non-host player tries to start → request is rejected with 403 +- **Solo start**: Host tries to start with only 1 player → start button is disabled diff --git a/specs/001-room-setup-lobby/research.md b/specs/001-room-setup-lobby/research.md new file mode 100644 index 0000000..c57344d --- /dev/null +++ b/specs/001-room-setup-lobby/research.md @@ -0,0 +1,43 @@ +# Research: Room Setup And Lobby + +## Overview + +No NEEDS CLARIFICATION markers existed in the spec. All technical context was resolved by examining the existing codebase. This research documents the key architectural decisions for the feature. + +## Architecture Decisions + +### Decision 1: Host Designation via Participant ID +- **Decision**: The room creator's participant ID is stored as `hostId` on the Room model +- **Rationale**: Participant IDs are UUIDs generated server-side, making them unforgeable. The host check is done by comparing the request's `participantId` against the stored `hostId`. No authentication or session mechanism needed (per project constraints). +- **Alternatives considered**: Storing host as a boolean on the Participant — rejected because it conflates identity with role and makes host transfer harder. + +### Decision 2: Auto-Polling in the Frontend +- **Decision**: `LobbyPage` uses `setInterval` at 2000ms to call `roomStore.fetchRoom()` when the component mounts, clearing the interval on unmount +- **Rationale**: Matches the spec's "~2 second" polling requirement. Uses existing `fetchRoom` API. Cleanup via `useEffect` return prevents leaks. +- **Alternatives considered**: WebSockets — rejected (per project constraints). Server-Sent Events — rejected (over-engineered for polling requirement). Manual refresh only — rejected (spec requires auto-refresh). + +### Decision 3: Empty Code Validation +- **Decision**: Client-side validation blocks empty codes before submission with "Please enter a room code"; server-side also rejects empty strings with 400 status +- **Rationale**: Dual validation provides immediate UX feedback and defense-in-depth. Client catches it before network round-trip; server catches it in case of direct API calls. +- **Alternatives considered**: Server-only validation — rejected (worse UX, unnecessary delay). + +### Decision 4: Start Game Endpoint +- **Decision**: New `POST /rooms/:code/start` endpoint that accepts `participantId` in body; checks that the participant is the host and at least 2 players are present; transitions room status from `lobby` to an active game state +- **Rationale**: Dedicated endpoint ensures the start action is authorized and validated server-side. The host check prevents non-hosts from starting. +- **Alternatives considered**: PATCH to room status — rejected (less explicit, harder to authorize). + +### Decision 5: Room Status Expansion +- **Decision**: Add `"playing"` to the `RoomStatus` type (currently only `"lobby"`) +- **Rationale**: The start game action needs to transition the room out of lobby state. Future game phases will use additional statuses. +- **Alternatives considered**: Keeping only `"lobby"` and tracking game state separately — rejected (unnecessary indirection). + +## Existing Patterns Confirmed + +| Pattern | Source | Status | +|---------|--------|--------| +| Express Router with try/catch + next(error) | `rooms.ts` | Follow | +| Zod schema parsing in route handler | `schemas.ts` | Follow | +| Room snapshot pattern (toRoomSnapshot) | `roomStore.ts` | Extend | +| RoomStore class + Context + useSyncExternalStore | `roomStore.tsx` | Extend | +| API client with typed request function | `api.ts` | Extend | +| CSS class naming (BEM-like) | Components | Follow | From c6db32c551b0b70d4043c59773663ca0552627c8 Mon Sep 17 00:00:00 2001 From: shirisha kodimala Date: Wed, 27 May 2026 16:33:40 +0530 Subject: [PATCH 3/4] [Spec Kit] Add tasks --- specs/001-room-setup-lobby/tasks.md | 233 ++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 specs/001-room-setup-lobby/tasks.md diff --git a/specs/001-room-setup-lobby/tasks.md b/specs/001-room-setup-lobby/tasks.md new file mode 100644 index 0000000..44e3d8e --- /dev/null +++ b/specs/001-room-setup-lobby/tasks.md @@ -0,0 +1,233 @@ +--- + +description: "Task list for Room Setup And Lobby feature implementation" + +--- + +# Tasks: Room Setup And Lobby + +**Input**: Design documents from `specs/001-room-setup-lobby/` + +**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ + +**Tests**: No test tasks included — testing framework not configured per plan.md. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions + +## Path Conventions + +- **Backend**: `backend/src/models/`, `backend/src/services/`, `backend/src/api/` +- **Frontend**: `frontend/src/pages/`, `frontend/src/state/`, `frontend/src/services/` + +--- + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Project initialization and basic structure + +No setup tasks needed — project scaffolding, dependencies, and build tooling are already in place. + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented + +**⚠️ CRITICAL**: No user story work can begin until this phase is complete + +- [ ] T001 Add `hostId` field to `Room` interface and `RoomSnapshot` interface in `backend/src/models/game.ts` +- [ ] T002 [P] Extend `RoomStatus` type to include `"playing"` in `backend/src/models/game.ts` +- [ ] T003 Update `createRoom` in `backend/src/services/roomStore.ts` to set the creator's participant ID as `hostId` on the new room +- [ ] T004 [P] Update `toRoomSnapshot` in `backend/src/services/roomStore.ts` to include `hostId` in the snapshot +- [ ] T005 [P] Mirror updated `RoomSnapshot` type (add `hostId`, extend status to `"lobby" | "playing"`) in `frontend/src/services/api.ts` + +**Checkpoint**: Foundation ready — user story implementation can now begin in parallel + +--- + +## Phase 3: User Story 1 - Create and Host a Room (Priority: P1) 🎯 MVP + +**Goal**: A player creates a room, gets a unique code, is designated as host, and sees a host badge in the lobby. + +**Independent Test**: Single player creates a room and confirms they appear in the lobby as host with a visible room code. + +### Implementation for User Story 1 + +- [ ] T006 [P] [US1] Add `participantId` to request body validation in `createRoomSchema` in `backend/src/api/schemas.ts` [NEEDS CLARIFICATION: how does the participant ID get sent if not yet created?] + +*Wait, that doesn't make sense. The participant ID is generated on the server. The hostId is simply the participant ID of the first participant.* + +- [ ] T006 [P] [US1] Display "Host" badge next to the host's name in the participant list in `frontend/src/pages/LobbyPage.tsx` by comparing `participantId` with `room.hostId` +- [ ] T007 [P] [US1] Add room code display (already present via `RoomCodeBadge`) — verify it renders correctly after the model changes in `frontend/src/pages/LobbyPage.tsx` + +*Actually, T006 already works because RoomCodeBadge and participant list already exist. Let me re-scope US1 tasks properly:* + +- [ ] T006 [US1] Update `LobbyPage.tsx` in `frontend/src/pages/LobbyPage.tsx` to show "Host" label next to the room creator's name in the participant list (compare `participantId` from store with `room.hostId`) +- [ ] T007 [US1] Verify the lobby header renders the room code correctly — the code is already displayed via `RoomCodeBadge` component in `frontend/src/pages/LobbyPage.tsx` + +**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently + +--- + +## Phase 4: User Story 2 - Join a Room with a Valid Code (Priority: P1) + +**Goal**: A second player joins an existing room using the room code. All connected players see the updated player list. + +**Independent Test**: Open two browser windows. Create a room in one, join with the same code in the other. Both lobbies show the full participant list. + +### Implementation for User Story 2 + +- [ ] T008 [P] [US2] Add auto-polling via `setInterval(2000)` in a `useEffect` in `frontend/src/pages/LobbyPage.tsx` — call `roomStore.fetchRoom()` every 2 seconds, clear interval on unmount +- [ ] T009 [P] [US2] Show all participants with host indicator in `frontend/src/pages/LobbyPage.tsx` — the list already renders, just needs host label from T006 +- [ ] T010 [US2] Add visual loading/refresh indicator during polling in `frontend/src/pages/LobbyPage.tsx` (already partially implemented with isLoading state) + +**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently + +--- + +## Phase 5: User Story 3 - Invalid or Empty Room Codes (Priority: P2) + +**Goal**: Empty or invalid room codes are rejected with clear, user-friendly error messages. + +**Independent Test**: Submit the join form with an empty code → see "Please enter a room code". Submit with a non-existent code → see "Room not found. Please check the code and try again". + +### Implementation for User Story 3 + +- [ ] T011 [P] [US3] Add client-side validation in `frontend/src/pages/JoinRoomPage.tsx` — block submission when room code is empty, display "Please enter a room code" +- [ ] T012 [P] [US3] Improve server-side error message in `backend/src/api/rooms.ts` — change "Unable to join room" to "Room not found. Please check the code and try again" when room code doesn't match any active room +- [ ] T013 [P] [US3] Update `roomCodeParamsSchema` in `backend/src/api/schemas.ts` to validate that the room code is a non-empty string (Zod `.min(1)`) + +**Checkpoint**: Error handling for room codes is robust on both client and server + +--- + +## Phase 6: User Story 4 - Host Starts the Game (Priority: P2) + +**Goal**: Once at least 2 players are in the lobby, the host can start the game. Non-hosts cannot start. + +**Independent Test**: Host and one joiner in lobby → host clicks start → game begins. Non-host tries to start → rejected. + +### Implementation for User Story 4 + +- [ ] T014 [P] [US4] Implement `startGame` service function in `backend/src/services/roomStore.ts` — validate that `participantId` matches `hostId` and that `participants.length >= 2`, then set `status = "playing"` and return updated room +- [ ] T015 [P] [US4] Add `startRoomSchema` Zod schema in `backend/src/api/schemas.ts` — validate `{ participantId: z.string() }` body +- [ ] T016 [US4] Add `POST /rooms/:code/start` endpoint in `backend/src/api/rooms.ts` — parse params/body, call `startGame`, return updated snapshot; handle 403 (not host) and 400 (not enough players) errors +- [ ] T017 [US4] Add `startGame(code, participantId)` API method to the frontend API client in `frontend/src/services/api.ts` +- [ ] T018 [P] [US4] Add `startGame` method to the `RoomStore` class in `frontend/src/state/roomStore.ts` — call API, update store with new room state +- [ ] T019 [US4] Update `frontend/src/pages/LobbyPage.tsx` — disable "Start Game" button when user is not the host OR when `room.participants.length < 2`; show appropriate message ("Waiting for host to start" for non-hosts, "Need at least 2 players" if host but solo) + +**Checkpoint**: All user stories should now be independently functional + +--- + +## Phase 7: Polish & Cross-Cutting Concerns + +**Purpose**: Improvements that affect multiple user stories + +- [ ] T020 [P] Update `frontend/src/pages/LobbyPage.tsx` to navigate away if room status transitions to `"playing"` (redirect to `/game`) +- [ ] T021 Update the existing manual "Refresh Room" button in `frontend/src/pages/LobbyPage.tsx` to work alongside auto-polling (remove manual refresh or keep as fallback) +- [ ] T022 Run `quickstart.md` validation — manually test all scenarios documented in `specs/001-room-setup-lobby/quickstart.md` + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — already complete +- **Foundational (Phase 2)**: No external dependencies — BLOCKS all user stories +- **User Stories (Phase 3-6)**: All depend on Foundational phase completion + - US1 (P1) and US2 (P1) are independent of each other and can be done in parallel + - US3 (P2) is independent of US4 + - US4 (P2) has a minor dependency: T016 calls `startGame` from T014; T019 updates lobby page from T006 +- **Polish (Final Phase)**: Depends on all desired user stories being complete + +### User Story Dependencies + +- **User Story 1 (P1) MVP**: Can start after Phase 2 — depends on T001-T005 (model/snapshot changes) +- **User Story 2 (P1)**: Can start after Phase 2 — independent of US1 +- **User Story 3 (P2)**: Can start after Phase 2 — independent of all other stories +- **User Story 4 (P2)**: Can start after Phase 2 — depends on US1's hostId being in snapshot + +### Within Each User Story + +- Backend model changes before services +- Services before endpoints +- API methods before frontend store +- Store before UI components +- Story complete before moving to next priority + +### Parallel Opportunities + +- T001-T005 can all run in parallel (different files, no cross-dependencies) +- T006 (US1 UI) and T008-T010 (US2 polling) can run in parallel once Phase 2 is done +- T011-T013 (US3 validation) can run in parallel +- T014-T016 (US4 backend) can run in parallel with T017-T019 (US4 frontend) once US1 is done providing hostId +- US1, US2, and US3 can all be worked on simultaneously after Phase 2 + +--- + +## Parallel Example: Foundational Phase (Phase 2) + +```bash +Task: "Add hostId to Room/RoomSnapshot in backend/src/models/game.ts and update frontend type in frontend/src/services/api.ts" +Task: "Extend RoomStatus to include playing in backend/src/models/game.ts" +Task: "Update createRoom and toRoomSnapshot in backend/src/services/roomStore.ts" +``` + +## Parallel Example: User Story 4 + +```bash +Task: "Implement startGame service + schema + endpoint in backend/src/services/roomStore.ts, backend/src/api/schemas.ts, backend/src/api/rooms.ts" +Task: "Add startGame to frontend API client and RoomStore in frontend/src/services/api.ts, frontend/src/state/roomStore.ts" +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 2: Foundational +2. Complete Phase 3: User Story 1 (Create and Host) +3. **STOP and VALIDATE**: Test US1 independently +4. Deploy/demo if ready + +### Incremental Delivery + +1. Complete Foundational → Foundation ready +2. Add US1 (Create/Host) → MVP: single player can create room & see host badge +3. Add US2 (Join/Polling) → Two players can see each other in lobby +4. Add US3 (Validation) → Error handling for bad codes +5. Add US4 (Start Game) → Full lobby flow complete +6. Stories can be added in any order after US1 (US2, US3, US4 are independent of each other) + +### Parallel Team Strategy + +With multiple developers: + +1. Team completes Foundational together (Phase 2) +2. Once Foundational is done: + - Developer A: US1 (host badge in lobby) + US4 (start game — depends on hostId) + - Developer B: US2 (auto-polling) + - Developer C: US3 (empty/invalid code validation) +3. Developer B and C can start before Developer A is done +4. Developer A needs US1 first, then US4 builds on it + +--- + +## Notes + +- [P] tasks = different files, no dependencies +- [Story] label maps task to specific user story for traceability +- Each user story should be independently completable and testable +- No test framework is configured — test tasks are excluded per plan.md +- The existing codebase already has working create/join/fetch endpoints — tasks focus on the new host/start/polling/validation behavior +- Commit after each task or logical group +- Stop at any checkpoint to validate story independently From 1927696ecb51c553a439707687c8c049932c46f6 Mon Sep 17 00:00:00 2001 From: shirisha kodimala Date: Wed, 27 May 2026 16:39:51 +0530 Subject: [PATCH 4/4] [Spec Kit] Implementation progress --- backend/src/api/rooms.ts | 36 ++++++++++++++++-- backend/src/api/schemas.ts | 6 ++- backend/src/models/game.ts | 4 +- backend/src/services/roomStore.ts | 26 +++++++++++++ frontend/package-lock.json | 6 --- frontend/src/pages/JoinRoomPage.tsx | 10 ++++- frontend/src/pages/LobbyPage.tsx | 57 ++++++++++++++++++++++++++--- frontend/src/services/api.ts | 9 ++++- frontend/src/state/roomStore.ts | 12 ++++++ specs/001-room-setup-lobby/tasks.md | 38 +++++++++---------- 10 files changed, 166 insertions(+), 38 deletions(-) diff --git a/backend/src/api/rooms.ts b/backend/src/api/rooms.ts index 8a6c6c9..0f28e41 100644 --- a/backend/src/api/rooms.ts +++ b/backend/src/api/rooms.ts @@ -4,9 +4,10 @@ import { HttpError, joinRoomSchema, roomCodeParamsSchema, - roomViewerQuerySchema + roomViewerQuerySchema, + startRoomSchema } from "./schemas.js"; -import { createRoom, getRoom, joinRoom, toRoomSnapshot } from "../services/roomStore.js"; +import { createRoom, getRoom, joinRoom, startGame, toRoomSnapshot } from "../services/roomStore.js"; export function createRoomsRouter() { const router = Router(); @@ -32,7 +33,7 @@ export function createRoomsRouter() { const result = joinRoom(code.toUpperCase(), playerName); if (!result) { - throw new HttpError(404, "Unable to join room"); + throw new HttpError(404, "Room not found. Please check the code and try again"); } response.json({ @@ -44,6 +45,35 @@ export function createRoomsRouter() { } }); + router.post("/:code/start", (request, response, next) => { + try { + const { code } = roomCodeParamsSchema.parse(request.params); + const { participantId } = startRoomSchema.parse(request.body); + + const result = startGame(code.toUpperCase(), participantId); + + if (!result) { + throw new HttpError(404, "Room not found"); + } + + response.json({ + room: toRoomSnapshot(result.room, participantId) + }); + } catch (error) { + if (error instanceof Error && error.message === "Only the host can start the game") { + next(new HttpError(403, error.message)); + return; + } + + if (error instanceof Error && error.message === "At least 2 players are needed to start the game") { + next(new HttpError(400, error.message)); + return; + } + + next(error); + } + }); + router.get("/:code", (request, response, next) => { try { const { code } = roomCodeParamsSchema.parse(request.params); diff --git a/backend/src/api/schemas.ts b/backend/src/api/schemas.ts index bfebba0..8c1d7f4 100644 --- a/backend/src/api/schemas.ts +++ b/backend/src/api/schemas.ts @@ -9,7 +9,11 @@ export const joinRoomSchema = z.object({ }); export const roomCodeParamsSchema = z.object({ - code: z.string() + code: z.string().min(1, "Room code is required") +}); + +export const startRoomSchema = z.object({ + participantId: z.string() }); export const roomViewerQuerySchema = z.object({ diff --git a/backend/src/models/game.ts b/backend/src/models/game.ts index 88ce946..d182c2b 100644 --- a/backend/src/models/game.ts +++ b/backend/src/models/game.ts @@ -1,5 +1,5 @@ export type ParticipantRole = "drawer" | "guesser"; -export type RoomStatus = "lobby"; +export type RoomStatus = "lobby" | "playing"; export interface Participant { id: string; @@ -9,6 +9,7 @@ export interface Participant { export interface Room { code: string; + hostId: string; status: RoomStatus; participants: Participant[]; createdAt: string; @@ -17,6 +18,7 @@ export interface Room { export interface RoomSnapshot { code: string; + hostId: string; status: RoomStatus; participants: Participant[]; availableWords: string[]; diff --git a/backend/src/services/roomStore.ts b/backend/src/services/roomStore.ts index e53987a..d3f4603 100644 --- a/backend/src/services/roomStore.ts +++ b/backend/src/services/roomStore.ts @@ -53,6 +53,7 @@ export function createRoom(playerName?: string) { const participant = createParticipant(playerName); const room: Room = { code: generateUniqueCode(), + hostId: participant.id, status: "lobby", participants: [participant], createdAt: now(), @@ -96,11 +97,36 @@ export function saveRoom(room: Room) { return getRoom(room.code); } +export function startGame(code: string, participantId: string) { + const room = rooms.get(code); + + if (!room) { + return null; + } + + if (room.hostId !== participantId) { + throw new Error("Only the host can start the game"); + } + + if (room.participants.length < 2) { + throw new Error("At least 2 players are needed to start the game"); + } + + room.status = "playing"; + room.updatedAt = now(); + rooms.set(room.code, cloneRoom(room)); + + return { + room: getRoom(room.code)! + }; +} + export function toRoomSnapshot(room: Room, viewerParticipantId?: string): RoomSnapshot { void viewerParticipantId; return { code: room.code, + hostId: room.hostId, status: room.status, participants: room.participants.map((participant) => ({ ...participant })), availableWords: listWords(), diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 94782cf..b950f47 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -51,7 +51,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1175,7 +1174,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1245,7 +1243,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1522,7 +1519,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -1535,7 +1531,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -1711,7 +1706,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/frontend/src/pages/JoinRoomPage.tsx b/frontend/src/pages/JoinRoomPage.tsx index db4f530..c854424 100644 --- a/frontend/src/pages/JoinRoomPage.tsx +++ b/frontend/src/pages/JoinRoomPage.tsx @@ -12,10 +12,16 @@ export function JoinRoomPage() { async function handleSubmit(event: React.FormEvent) { event.preventDefault(); + setError(null); + + const trimmedCode = roomCode.trim(); + if (!trimmedCode) { + setError("Please enter a room code"); + return; + } try { - setError(null); - await roomStore.joinRoom(roomCode.toUpperCase(), playerName); + await roomStore.joinRoom(trimmedCode.toUpperCase(), playerName); navigate("/lobby"); } catch (caughtError) { setError(caughtError instanceof Error ? caughtError.message : "Unable to join room"); diff --git a/frontend/src/pages/LobbyPage.tsx b/frontend/src/pages/LobbyPage.tsx index 1c99bd2..030365b 100644 --- a/frontend/src/pages/LobbyPage.tsx +++ b/frontend/src/pages/LobbyPage.tsx @@ -8,7 +8,7 @@ import { useRoomState, useRoomStore } from "../state/roomStore"; export function LobbyPage() { const navigate = useNavigate(); const roomStore = useRoomStore(); - const { room, error, isLoading } = useRoomState(); + const { room, error, isLoading, participantId } = useRoomState(); const [refreshError, setRefreshError] = useState(null); useEffect(() => { @@ -17,6 +17,29 @@ export function LobbyPage() { } }, [navigate, room]); + useEffect(() => { + if (room?.status === "playing") { + navigate("/game"); + } + }, [navigate, room?.status]); + + useEffect(() => { + if (!room) { + return; + } + + const interval = setInterval(async () => { + try { + setRefreshError(null); + await roomStore.fetchRoom(); + } catch (caughtError) { + setRefreshError(caughtError instanceof Error ? caughtError.message : "Unable to refresh room"); + } + }, 2000); + + return () => clearInterval(interval); + }, [room, roomStore]); + async function handleRefresh() { try { setRefreshError(null); @@ -30,6 +53,20 @@ export function LobbyPage() { return null; } + const isHost = participantId === room.hostId; + const hasEnoughPlayers = room.participants.length >= 2; + const canStart = isHost && hasEnoughPlayers && !isLoading; + + async function handleStartGame() { + try { + setRefreshError(null); + await roomStore.startGame(); + navigate("/game"); + } catch (caughtError) { + setRefreshError(caughtError instanceof Error ? caughtError.message : "Unable to start game"); + } + } + return (
@@ -50,7 +87,11 @@ export function LobbyPage() { {room.participants.map((participant) => (
  • {participant.name} - joined + {participant.id === room.hostId ? ( + Host + ) : ( + joined + )}
  • ))} @@ -61,7 +102,13 @@ export function LobbyPage() {

    {isLoading ? "Refreshing players..." : "Ready to play"}

    -

    {error ?? refreshError ?? "Waiting for the host to start the game."}

    +

    + {error ?? refreshError ?? ( + isHost + ? (hasEnoughPlayers ? "Click Start Game to begin" : "Need at least 2 players to start") + : "Waiting for the host to start the game." + )} +

    @@ -69,8 +116,8 @@ export function LobbyPage() { -
    diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 26e8bf5..29dd808 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -8,7 +8,8 @@ export interface Participant { export interface RoomSnapshot { code: string; - status: "lobby"; + hostId: string; + status: "lobby" | "playing"; participants: Participant[]; availableWords: string[]; roles: ParticipantRole[]; @@ -57,5 +58,11 @@ export const api = { fetchRoom(code: string, participantId?: string) { const query = participantId ? `?participantId=${encodeURIComponent(participantId)}` : ""; return request<{ room: RoomSnapshot }>(`/rooms/${encodeURIComponent(code)}${query}`); + }, + startGame(code: string, participantId: string) { + return request<{ room: RoomSnapshot }>(`/rooms/${encodeURIComponent(code)}/start`, { + method: "POST", + body: JSON.stringify({ participantId }) + }); } }; diff --git a/frontend/src/state/roomStore.ts b/frontend/src/state/roomStore.ts index aefd373..d58e301 100644 --- a/frontend/src/state/roomStore.ts +++ b/frontend/src/state/roomStore.ts @@ -98,6 +98,18 @@ class RoomStore { this.setRoomSnapshot(response.room); return response.room; } + + async startGame() { + if (!this.state.room || !this.state.participantId) { + throw new Error("No active room session"); + } + + const response = await this.withLoading(() => + api.startGame(this.state.room!.code, this.state.participantId!) + ); + this.setRoomSnapshot(response.room); + return response.room; + } } const RoomStoreContext = createContext(null); diff --git a/specs/001-room-setup-lobby/tasks.md b/specs/001-room-setup-lobby/tasks.md index 44e3d8e..6f6d0fa 100644 --- a/specs/001-room-setup-lobby/tasks.md +++ b/specs/001-room-setup-lobby/tasks.md @@ -41,11 +41,11 @@ No setup tasks needed — project scaffolding, dependencies, and build tooling a **⚠️ CRITICAL**: No user story work can begin until this phase is complete -- [ ] T001 Add `hostId` field to `Room` interface and `RoomSnapshot` interface in `backend/src/models/game.ts` -- [ ] T002 [P] Extend `RoomStatus` type to include `"playing"` in `backend/src/models/game.ts` -- [ ] T003 Update `createRoom` in `backend/src/services/roomStore.ts` to set the creator's participant ID as `hostId` on the new room -- [ ] T004 [P] Update `toRoomSnapshot` in `backend/src/services/roomStore.ts` to include `hostId` in the snapshot -- [ ] T005 [P] Mirror updated `RoomSnapshot` type (add `hostId`, extend status to `"lobby" | "playing"`) in `frontend/src/services/api.ts` +- [X] T001 Add `hostId` field to `Room` interface and `RoomSnapshot` interface in `backend/src/models/game.ts` +- [X] T002 [P] Extend `RoomStatus` type to include `"playing"` in `backend/src/models/game.ts` +- [X] T003 Update `createRoom` in `backend/src/services/roomStore.ts` to set the creator's participant ID as `hostId` on the new room +- [X] T004 [P] Update `toRoomSnapshot` in `backend/src/services/roomStore.ts` to include `hostId` in the snapshot +- [X] T005 [P] Mirror updated `RoomSnapshot` type (add `hostId`, extend status to `"lobby" | "playing"`) in `frontend/src/services/api.ts` **Checkpoint**: Foundation ready — user story implementation can now begin in parallel @@ -68,8 +68,8 @@ No setup tasks needed — project scaffolding, dependencies, and build tooling a *Actually, T006 already works because RoomCodeBadge and participant list already exist. Let me re-scope US1 tasks properly:* -- [ ] T006 [US1] Update `LobbyPage.tsx` in `frontend/src/pages/LobbyPage.tsx` to show "Host" label next to the room creator's name in the participant list (compare `participantId` from store with `room.hostId`) -- [ ] T007 [US1] Verify the lobby header renders the room code correctly — the code is already displayed via `RoomCodeBadge` component in `frontend/src/pages/LobbyPage.tsx` +- [X] T006 [US1] Update `LobbyPage.tsx` in `frontend/src/pages/LobbyPage.tsx` to show "Host" label next to the room creator's name in the participant list (compare `participantId` from store with `room.hostId`) +- [X] T007 [US1] Verify the lobby header renders the room code correctly — the code is already displayed via `RoomCodeBadge` component in `frontend/src/pages/LobbyPage.tsx` **Checkpoint**: At this point, User Story 1 should be fully functional and testable independently @@ -83,9 +83,9 @@ No setup tasks needed — project scaffolding, dependencies, and build tooling a ### Implementation for User Story 2 -- [ ] T008 [P] [US2] Add auto-polling via `setInterval(2000)` in a `useEffect` in `frontend/src/pages/LobbyPage.tsx` — call `roomStore.fetchRoom()` every 2 seconds, clear interval on unmount -- [ ] T009 [P] [US2] Show all participants with host indicator in `frontend/src/pages/LobbyPage.tsx` — the list already renders, just needs host label from T006 -- [ ] T010 [US2] Add visual loading/refresh indicator during polling in `frontend/src/pages/LobbyPage.tsx` (already partially implemented with isLoading state) +- [X] T008 [P] [US2] Add auto-polling via `setInterval(2000)` in a `useEffect` in `frontend/src/pages/LobbyPage.tsx` — call `roomStore.fetchRoom()` every 2 seconds, clear interval on unmount +- [X] T009 [P] [US2] Show all participants with host indicator in `frontend/src/pages/LobbyPage.tsx` — the list already renders, just needs host label from T006 +- [X] T010 [US2] Add visual loading/refresh indicator during polling in `frontend/src/pages/LobbyPage.tsx` (already partially implemented with isLoading state) **Checkpoint**: At this point, User Stories 1 AND 2 should both work independently @@ -115,12 +115,12 @@ No setup tasks needed — project scaffolding, dependencies, and build tooling a ### Implementation for User Story 4 -- [ ] T014 [P] [US4] Implement `startGame` service function in `backend/src/services/roomStore.ts` — validate that `participantId` matches `hostId` and that `participants.length >= 2`, then set `status = "playing"` and return updated room -- [ ] T015 [P] [US4] Add `startRoomSchema` Zod schema in `backend/src/api/schemas.ts` — validate `{ participantId: z.string() }` body -- [ ] T016 [US4] Add `POST /rooms/:code/start` endpoint in `backend/src/api/rooms.ts` — parse params/body, call `startGame`, return updated snapshot; handle 403 (not host) and 400 (not enough players) errors -- [ ] T017 [US4] Add `startGame(code, participantId)` API method to the frontend API client in `frontend/src/services/api.ts` -- [ ] T018 [P] [US4] Add `startGame` method to the `RoomStore` class in `frontend/src/state/roomStore.ts` — call API, update store with new room state -- [ ] T019 [US4] Update `frontend/src/pages/LobbyPage.tsx` — disable "Start Game" button when user is not the host OR when `room.participants.length < 2`; show appropriate message ("Waiting for host to start" for non-hosts, "Need at least 2 players" if host but solo) +- [X] T014 [P] [US4] Implement `startGame` service function in `backend/src/services/roomStore.ts` — validate that `participantId` matches `hostId` and that `participants.length >= 2`, then set `status = "playing"` and return updated room +- [X] T015 [P] [US4] Add `startRoomSchema` Zod schema in `backend/src/api/schemas.ts` — validate `{ participantId: z.string() }` body +- [X] T016 [US4] Add `POST /rooms/:code/start` endpoint in `backend/src/api/rooms.ts` — parse params/body, call `startGame`, return updated snapshot; handle 403 (not host) and 400 (not enough players) errors +- [X] T017 [US4] Add `startGame(code, participantId)` API method to the frontend API client in `frontend/src/services/api.ts` +- [X] T018 [P] [US4] Add `startGame` method to the `RoomStore` class in `frontend/src/state/roomStore.ts` — call API, update store with new room state +- [X] T019 [US4] Update `frontend/src/pages/LobbyPage.tsx` — disable "Start Game" button when user is not the host OR when `room.participants.length < 2`; show appropriate message ("Waiting for host to start" for non-hosts, "Need at least 2 players" if host but solo) **Checkpoint**: All user stories should now be independently functional @@ -130,9 +130,9 @@ No setup tasks needed — project scaffolding, dependencies, and build tooling a **Purpose**: Improvements that affect multiple user stories -- [ ] T020 [P] Update `frontend/src/pages/LobbyPage.tsx` to navigate away if room status transitions to `"playing"` (redirect to `/game`) -- [ ] T021 Update the existing manual "Refresh Room" button in `frontend/src/pages/LobbyPage.tsx` to work alongside auto-polling (remove manual refresh or keep as fallback) -- [ ] T022 Run `quickstart.md` validation — manually test all scenarios documented in `specs/001-room-setup-lobby/quickstart.md` +- [X] T020 [P] Update `frontend/src/pages/LobbyPage.tsx` to navigate away if room status transitions to `"playing"` (redirect to `/game`) +- [X] T021 Update the existing manual "Refresh Room" button in `frontend/src/pages/LobbyPage.tsx` to work alongside auto-polling (remove manual refresh or keep as fallback) +- [X] T022 Run `quickstart.md` validation — manually test all scenarios documented in `specs/001-room-setup-lobby/quickstart.md` ---