hoangsonww / hoangsonww/Tic-Tac-Toe-Fullstack-Game
Feature: Friends, Direct Challenges & Private Rooms (Best-of-N, Presence, Chat)
- Dominant language
- TypeScript
- Stars
- 16
- Forks
- 11
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
Add a social layer so players can **add friends**, see **presence** (online/ingame), and issue **direct challenges** into **private rooms** (invite link or friend list). Rooms support **Best-of-N series**, quick **rematch**, and lightweight **in-room chat/emotes**. By default, private matches are **unrated**; owners can toggle **Rated** if both players meet minimum requirements (e.g., verified + min games).
This builds on the existing MERN + Socket.io foundation and complements matchmaking/leaderboards without overlapping Issue #2 (Spectator/Replays/Rage-Quit handling).
---
## Why
* Players want to play with friends on demand, not only via random matchmaking.
* Presence + invites reduces friction and keeps players in your ecosystem.
* Best-of-N and rematch improve engagement and session length.
* Private rooms are also a safe scaffold for future features (spectators, replays, tournaments).
---
## Scope (v1)
* Friends: requests/accept/decline/remove; presence (online/ingame).
* Direct Challenge: invite a friend or share a room link (code).
* Private Room: owner controls board size, series length (Bo1/3/5), rated toggle.
* In-Room Chat: text + 6 safe emotes; profanity filter; mute per user.
* Rematch flow: same participants/settings with one click.
* Rated rules: off by default; on only if both players eligible (see Permissions).
### Out of scope (follow-ups)
* Spectators & replays (covered in Issue #2).
* Public lobbies & tournaments.
* Voice chat.
---
## Data Model (MongoDB/Mongoose)
```ts
// models/Friendship.ts
export interface Friendship {
_id: ObjectId;
userA: ObjectId;
userB: ObjectId;
status: 'PENDING' | 'ACCEPTED' | 'BLOCKED';
requestedBy: ObjectId;
createdAt: Date;
updatedAt: Date;
}
// models/Room.ts
export interface Room {
_id: ObjectId;
code: string; // short, random, e.g., 8-10 base62 chars
ownerId: ObjectId;
players: ObjectId[]; // max 2
settings: {
boardSize: number; // 3..8 (existing variants)
series: 1 | 3 | 5; // BoN
rated: boolean; // default false
turnTimerSec?: number; // optional time control later
};
status: 'OPEN' | 'IN_PROGRESS' | 'FINISHED';
createdAt: Date;
updatedAt: Date;
}
// models/Message.ts (in-room chat)
export interface Message {
_id: ObjectId;
roomId: ObjectId;
fromUserId: ObjectId;
type: 'TEXT' | 'EMOTE';
text?: string; // sanitized
emoteKey?: string; // limited set: 'gg','wp','oops','hello','nice','wow'
createdAt: Date;
}
// Presence: ephemeral (in-memory/redis), not persisted
type PresenceState = {
userId: string;
status: 'OFFLINE' | 'ONLINE' | 'INGAME';
lastSeen: number;
}
```
**Indexes**
* `Friendship`: unique compound `{ userA: 1, userB: 1 }` (store ordered pair).
* `Room`: `code` unique; `ownerId`; `status`.
* `Message`: `roomId`, `createdAt`.
---
## API (Next.js `/api`)
```
# Friends
POST /api/friends/requests { toUserId } // send request
GET /api/friends/requests → incoming/outgoing lists
POST /api/friends/requests/:id/accept
POST /api/friends/requests/:id/decline
DELETE /api/friends/:friendUserId // remove friendship
GET /api/friends → accepted friends + presence summary
# Rooms
POST /api/rooms { settings } → { room, inviteUrl }
GET /api/rooms/:code → room (if member or invited)
POST /api/rooms/:code/join // friend or invite-link join
POST /api/rooms/:code/leave
PATCH /api/rooms/:code/settings { ... } // owner only, before IN_PROGRESS
POST /api/rooms/:code/rematch // re-open room with same settings & players
```
**OpenAPI**: Add schemas for `Friendship`, `Room`, `Message`; document 401/403 and 409 cases (duplicate request, room full, etc.).
---
## Socket.io Channels & Events
**Namespaces/rooms**
* `presence` (global presence)
* `room:{code}` (per private room)
**Events**
* Presence
* client→server: `presence:online`, `presence:ingame`, `presence:offline`
* server→client: `presence:update` `{ userId, status }`
* Room lifecycle
* `room:join` `{ code }` → ack or error
* `room:state` `{ status, players, settings }`
* `room:start` (when first game of series begins)
* `room:move` `{ cell }` → broadcast validated move
* `room:gameOver` `{ result, seriesScore }` // track BoN
* `room:rematch:offer|accept|decline`
* `room:leave` → `room:state` update
* Chat
* `chat:send` `{ type, text|emoteKey }` → server sanitizes → `chat:new` to room
* `chat:mute` (owner-only per user, server enforces)
Reuse existing online PvP engine for validation; layer room/series logic on top.
---
## Rated Match Rules (v1)
* **Default:** `rated = false`.
* **Enable rated** only if:
* both players are **verified** (e.g., email verified flag),
* both have **≥ N** completed ranked matches (e.g., 10),
* board size is **3×3** and time control (if used) ≥ 10s/move,
* no duplicate IP lockouts in last 24h (simple anti-boosting heuristic).
* ELO updates apply **per game** in the series (not only at series end).
* Room owner sees eligibility checklist in UI; cannot toggle if unmet.
---
## UI/UX
* **Friends Drawer** (left sidebar on desktop, modal on mobile):
* Tabs: `Friends` | `Requests` | `Find`
* Each friend: avatar, status badge, `Challenge` button.
* **Direct Challenge Modal**:
* Pick friend → choose **board size**, **Best-of-N**, Rated toggle (with eligibility hints).
* On create → show invite link + “Copy” and “Open Room”.
* **Room View** (`/room/[code]`):
* Top: room code, copy invite link, rated badge, settings.
* Middle: existing **Board** component + series score (e.g., 1–0).
* Right: **Chat** (text + emotes) with mute dropdown (owner-only).
* Bottom: **Rematch** CTA after a game or **Leave Room**.
* **Presence**: small green/yellow red dot on avatars; hover shows exact status.
---
## Permissions & Security
* Auth required for all endpoints/events.
* Only **owner** can change room settings or kick/mute.
* Profanity filter on messages (`bad-words`/similar lib) + length limit (200 chars).
* Rate-limit chat sends (e.g., 5 msgs/5s window).
* Invite links use unguessable codes; optional owner **Rotate link** (invalidate old).
* On friend removal/block, sever active room invites and mute chat from that user.
* Return 404 for non-members on `GET /rooms/:code` (avoid info leaks).
---
## Acceptance Criteria
* Send/accept/decline friend requests; see real-time presence changes.
* Create a room via friend challenge; second player joins via friend list or invite link.
* Play a **Bo3**; series score increments per game; rematch works.
* Chat messages render in real time; emotes show as safe icons; mute hides messages.
* Rated toggle enforces eligibility; ELO updates trigger only when allowed.
* All flows covered in Swagger; Socket errors handled with toasts.
---
## Task Breakdown
### Backend
* [ ] Mongoose models: `Friendship`, `Room`, `Message`.
* [ ] REST: friends (requests CRUD), rooms (create/join/leave/settings/rematch).
* [ ] Socket: presence service + `room:{code}` events; integrate with existing game engine.
* [ ] ELO gate & update hooks when `rated=true` and eligibility passes.
* [ ] Chat service with sanitization, rate-limit, and mute.
* [ ] Tests: unit (models, guards) + integration (room lifecycle, rated gates).
### Frontend
* [ ] Friends Drawer UI + requests flow.
* [ ] Challenge modal + room creation.
* [ ] `/room/[code]` page with series HUD, rematch, and chat/emotes.
* [ ] Presence indicators across navbar/friends list.
* [ ] Rated eligibility checklist component.
* [ ] Toasts for errors (room full, not eligible, invite expired).
### DevOps
* [ ] ENV: `INVITE_CODE_LENGTH`, `CHAT_RATE_WINDOW`, `CHAT_RATE_LIMIT`, `RATED_MIN_GAMES`.
* [ ] OpenAPI updates + screenshots for README.
* [ ] Add WS e2e smoke in CI (room create → join → move → gameOver).
---
## Open Questions
* Minimum default for **RATED_MIN_GAMES**? Proposal: **10**.
* Should we allow **block** in addition to remove friend in v1? (Safer yes.)
* Persist **series history** in `Match` for profile stats? (Nice-to-have; can append later.)
---
## Follow-ups (separate issues)
* **Spectators for private rooms** (ties into #2).
* **Tournament Brackets** using private rooms as matches.
* **Reactions** on final board (post-game) + quick share image.
Contributor guide
Assessment
This issue has not been assessed yet.