Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
89afc4c
fix: correct fallback API base URL in frontend
GoguAnjali718 May 29, 2026
7b70a5d
speckit.constitution: define engineering principles, AI usage guideli…
GoguAnjali718 May 30, 2026
cc13d60
speckit.discovery: identify existing gaps, verify starter codebase st…
GoguAnjali718 May 30, 2026
d8b251b
speckit.specify: document room lifecycle setup, lobby polling frequen…
GoguAnjali718 May 30, 2026
a59db41
speckit.plan: map required backend and frontend state model modificat…
GoguAnjali718 May 30, 2026
d6c8493
speckit.tasks: define checkpoints, task orders, and dependencies for …
GoguAnjali718 May 30, 2026
0a754fd
feat: implement Scenario 1 - Room Setup & Lobby lifecycle with polling
GoguAnjali718 May 30, 2026
0c412ab
feat: implement Scenario 2 - Game Start & Drawer Flow layout and stat…
GoguAnjali718 May 30, 2026
92f6abd
feat: implement canvas drawing synchronization and guess submissions …
GoguAnjali718 May 30, 2026
b4e636e
feat: implement game restart, results view, and player exit with host…
GoguAnjali718 May 30, 2026
e531334
fix: keep room active when non-host player disconnects
GoguAnjali718 May 30, 2026
660c831
fix: prevent drawing canvas from expanding during drawing
GoguAnjali718 May 30, 2026
391f3a4
feat: add undo functionality for drawing actions
GoguAnjali718 May 30, 2026
5c4d879
docs: structure Spec Kit artifacts and reflection report to satisfy C…
GoguAnjali718 May 30, 2026
2407854
resolve gaps in secret-word selection, canvas clear, and restart UI
GoguAnjali718 May 31, 2026
4cea883
docs: use explicit top-level headings for gaps and assumptions in spe…
GoguAnjali718 May 31, 2026
5dfcce2
docs/refactor: address grading feedback for discovery notes, canvas b…
GoguAnjali718 May 31, 2026
dc667d7
docs/refactor: ensure restart button is visible in active game view a…
GoguAnjali718 May 31, 2026
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
90 changes: 90 additions & 0 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Spec Kit Constitution: Scribble Starter Lab

This constitution defines the standards, rules, and constraints governing the engineering, AI-assisted development, review, and scope of the Scribble project.

---

## 1. Engineering and Coding Principles

### General Guidelines
* **TypeScript First**: All new code and refactorings must be fully typed. The use of `any` is strictly prohibited. Use `unknown` for truly dynamic types.
* **Imports**: Use standard relative and absolute ES module imports. For backend code, omit file extensions or handle them via `.js` standard if required.
* **Immutability**: Prefer immutable data structures. Write pure functions wherever possible to prevent side effects.
* **Error Handling**: Fail fast and gracefully. Use centralized error handlers on the backend, and ensure the frontend UI does not crash when API exceptions occur.

### Backend Guidelines (`/backend`)
* **Validation**: All request payloads and response shapes must be strictly validated using `Zod`.
* **Structure**: Maintain the established directory structure:
* `src/api` for routes and request handling.
* `src/services` for core business logic (e.g., room lifecycle).
* `src/models` for data types and entity representations.
* **State Management**: Keep the memory footprint for active game rooms minimal. Explicitly delete inactive rooms to prevent stateful bloat.

### Frontend Guidelines (`/frontend`)
* **React Patterns**: Use functional components and strict React hooks (`useState`, `useEffect`, etc.).
* **Routing**: Use `react-router-dom` v6 paradigms exclusively.
* **State Management**: Complex state must be held in `src/state` (e.g., via Zustand or Context API) following the pattern established in `roomStore.ts`.
* **Styling**: Classes must reside in `app.css` or CSS modules. Keep components structurally clean and avoid ad-hoc styling utilities.

---

## 2. AI-Assisted Development Rules

* **Scope Enforcement**: The AI must not generate files, routes, or features outside of the explicitly defined project scope.
* **No Unnecessary Rewrites**: The AI must not rewrite entire files or components if a localized, incremental change is sufficient.
* **Code Verification**: All code generated by AI must be inspected for type safety, syntax correctness, and compliance with the project's styling and architectural patterns.
* **Prompt Discipline**: Do not ask the AI to implement features that violate the "Strictly Forbidden" constraints.

---

## 3. Self-Review and Code Review Requirements

* **Local Compilation**: Before any commit or pull request, both the frontend and backend must build cleanly without warnings or errors:
* Run `npm run build` in `/backend`
* Run `npm run build` in `/frontend`
* **Schema Integrity**: Ensure all Zod schemas correspond accurately to frontend state models and API contracts.
* **Resource Cleanup**: Verify that rooms are cleaned up when empty or inactive to avoid memory leaks.
* **Dead Code**: Ensure no debugging logs (`console.log`), unused variables, or dead code remain in the committed changes.

---

## 4. Testing and Validation Expectations

* **Multiplayer Validation**: All changes must be manually validated using at least two concurrent browser tabs to verify multiplayer synchronization, state isolation, and host permissions.
* **Lobby Polling Cadence**: Verify that room updates are polled at a regular interval (~2 seconds) and do not cause performance degradation.
* **Boundary and Edge Case Validation**:
* Verify that empty or whitespace-only usernames and invalid room codes are rejected with clear error feedback.
* Validate case-insensitivity of guesses.
* Ensure room state remains completely isolated across different room codes.

---

## 5. Commit and Pull Request Discipline

* **Granular Commits**: Commit changes in small, logical, atomic units (e.g., "Add Zod schema for joining room", "Implement lobby polling hooks").
* **Traceability**: Each commit should be explainable and directly traceable to a requirement in the specification or task checklist.
* **Pull Request Requirements**: Raise the PR from your branch to `main` on your fork. Include your email, role, and fill out the provided PR template in detail.

---

## 6. Working in an Existing Codebase

* **Respect Architecture**: Do not introduce new state-management, routing, or database libraries. Enhance the existing Express/React/Zod scaffolding.
* **Minimize Footprint**: Make minimal changes required to implement the features. Leave unrelated files untouched.
* **Read Before Coding**: Always read the existing files and understand their design before adding or altering code.

---

## 7. Scope-Control & Out-of-Scope Rules

The following items are **strictly out of scope**. They must not be implemented, specified, planned, or included in any task checklist:

* **No WebSockets**: Do not use WebSockets, Socket.io, or any real-time push protocol. All synchronization must use HTTP polling.
* **No Databases**: Do not use any database (SQL, NoSQL, SQLite, etc.). All data is stored in-memory.
* **No Authentication**: Do not add user authentication, sessions, JWT, or OAuth.
* **No Deployment/CI**: Do not configure hosting, CI/CD pipelines, Docker, or infrastructure.
* **No Library Proliferation**: Do not install new state-management or routing libraries.
* **No Game Over-Engineering**: Do not implement multiple rounds, drawer rotation, timers, countdowns, speed bonuses, or drawer scoring bonuses.
* **No Content Customization**: Do not support custom or random word packs (use only the starter list).
* **No Spectators/Moderation**: Do not implement spectator mode, or moderation features like mute and kick.
* **No Invite Utilities**: Do not implement room passwords or invite link sharing.
3 changes: 0 additions & 3 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

117 changes: 115 additions & 2 deletions backend/src/api/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
HttpError,
joinRoomSchema,
roomCodeParamsSchema,
roomViewerQuerySchema
roomViewerQuerySchema,
updateDrawingSchema,
submitGuessSchema
} from "./schemas.js";
import { createRoom, getRoom, joinRoom, toRoomSnapshot } from "../services/roomStore.js";
import { createRoom, getRoom, joinRoom, toRoomSnapshot, startGame, updateDrawing, clearDrawing, submitGuess, leaveRoom, restartGame } from "../services/roomStore.js";

export function createRoomsRouter() {
const router = Router();
Expand Down Expand Up @@ -44,6 +46,117 @@ export function createRoomsRouter() {
}
});

router.post("/:code/start", (request, response, next) => {
try {
const { code } = roomCodeParamsSchema.parse(request.params);
const { participantId } = roomViewerQuerySchema.parse(request.query);

if (!participantId) {
throw new HttpError(400, "participantId is required");
}

const room = startGame(code.toUpperCase(), participantId);
response.json({
room: toRoomSnapshot(room, participantId)
});
} catch (error) {
next(error);
}
});

router.post("/:code/drawing", (request, response, next) => {
try {
const { code } = roomCodeParamsSchema.parse(request.params);
const { participantId } = roomViewerQuerySchema.parse(request.query);
const { drawingData } = updateDrawingSchema.parse(request.body);

if (!participantId) {
throw new HttpError(400, "participantId is required");
}

const room = updateDrawing(code.toUpperCase(), participantId, drawingData);
response.json({
room: toRoomSnapshot(room, participantId)
});
} catch (error) {
next(error);
}
});

router.post("/:code/clear", (request, response, next) => {
try {
const { code } = roomCodeParamsSchema.parse(request.params);
const { participantId } = roomViewerQuerySchema.parse(request.query);

if (!participantId) {
throw new HttpError(400, "participantId is required");
}

const room = clearDrawing(code.toUpperCase(), participantId);
response.json({
room: toRoomSnapshot(room, participantId)
});
} catch (error) {
next(error);
}
});

router.post("/:code/guess", (request, response, next) => {
try {
const { code } = roomCodeParamsSchema.parse(request.params);
const { participantId } = roomViewerQuerySchema.parse(request.query);
const { guessText } = submitGuessSchema.parse(request.body);

if (!participantId) {
throw new HttpError(400, "participantId is required");
}

const room = submitGuess(code.toUpperCase(), participantId, guessText);
response.json({
room: toRoomSnapshot(room, participantId)
});
} catch (error) {
next(error);
}
});

router.post("/:code/leave", (request, response, next) => {
try {
const { code } = roomCodeParamsSchema.parse(request.params);
const { participantId } = roomViewerQuerySchema.parse(request.query);

if (!participantId) {
throw new HttpError(400, "participantId is required");
}

const room = leaveRoom(code.toUpperCase(), participantId);
response.json({
success: true,
room: room ? toRoomSnapshot(room, participantId) : null
});
} catch (error) {
next(error);
}
});

router.post("/:code/restart", (request, response, next) => {
try {
const { code } = roomCodeParamsSchema.parse(request.params);
const { participantId } = roomViewerQuerySchema.parse(request.query);

if (!participantId) {
throw new HttpError(400, "participantId is required");
}

const room = restartGame(code.toUpperCase(), participantId);
response.json({
room: toRoomSnapshot(room, participantId)
});
} catch (error) {
next(error);
}
});

router.get("/:code", (request, response, next) => {
try {
const { code } = roomCodeParamsSchema.parse(request.params);
Expand Down
22 changes: 18 additions & 4 deletions backend/src/api/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import { describe, expect, it } from "vitest";
import { createRoomSchema, roomCodeParamsSchema } from "./schemas.js";
import { createRoomSchema, roomCodeParamsSchema, joinRoomSchema } from "./schemas.js";

describe("schemas", () => {
it("createRoomSchema accepts a valid body with playerName", () => {
const result = createRoomSchema.parse({ playerName: "Alice" });
it("createRoomSchema accepts a valid body with playerName and trims it", () => {
const result = createRoomSchema.parse({ playerName: " Alice " });

expect(result.playerName).toBe("Alice");
});

it("roomCodeParamsSchema rejects missing code", () => {
it("createRoomSchema rejects empty or whitespace-only name", () => {
expect(() => createRoomSchema.parse({ playerName: "" })).toThrow();
expect(() => createRoomSchema.parse({ playerName: " " })).toThrow();
expect(() => createRoomSchema.parse({})).toThrow();
});

it("joinRoomSchema rejects empty or whitespace-only name", () => {
expect(() => joinRoomSchema.parse({ playerName: "" })).toThrow();
expect(() => joinRoomSchema.parse({ playerName: " " })).toThrow();
expect(() => joinRoomSchema.parse({})).toThrow();
});

it("roomCodeParamsSchema rejects missing or empty code", () => {
expect(() => roomCodeParamsSchema.parse({})).toThrow();
expect(() => roomCodeParamsSchema.parse({ code: "" })).toThrow();
expect(() => roomCodeParamsSchema.parse({ code: " " })).toThrow();
});
});
14 changes: 11 additions & 3 deletions backend/src/api/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { z } from "zod";

export const createRoomSchema = z.object({
playerName: z.string().optional()
playerName: z.string().trim().min(1, "Player name cannot be empty")
});

export const joinRoomSchema = z.object({
playerName: z.string().optional()
playerName: z.string().trim().min(1, "Player name cannot be empty")
});

export const roomCodeParamsSchema = z.object({
code: z.string()
code: z.string().trim().min(1, "Room code cannot be empty")
});

export const roomViewerQuerySchema = z.object({
participantId: z.string().optional()
});

export const updateDrawingSchema = z.object({
drawingData: z.string()
});

export const submitGuessSchema = z.object({
guessText: z.string().trim().min(1, "Please enter a guess.")
});

export class HttpError extends Error {
statusCode: number;

Expand Down
22 changes: 21 additions & 1 deletion backend/src/models/game.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
export type ParticipantRole = "drawer" | "guesser";
export type RoomStatus = "lobby";
export type RoomStatus = "lobby" | "game" | "result";

export interface Participant {
id: string;
name: string;
joinedAt: string;
score: number;
}

export interface Guess {
senderId: string;
senderName: string;
text: string;
correct: boolean;
timestamp: string;
}

export interface Room {
code: string;
status: RoomStatus;
participants: Participant[];
hostId: string;
drawerId: string | null;
secretWord: string | null;
drawingData: string;
guesses: Guess[];
previousWords: string[];
createdAt: string;
updatedAt: string;
}
Expand All @@ -21,6 +36,11 @@ export interface RoomSnapshot {
participants: Participant[];
availableWords: string[];
roles: ParticipantRole[];
hostId: string;
drawerId: string | null;
secretWord: string | null;
drawingData: string;
guesses: Guess[];
}

export interface RoomSessionResponse {
Expand Down
Loading
Loading