hoangsonww / hoangsonww/Tic-Tac-Toe-Fullstack-Game

Feature: Friends, Direct Challenges & Private Rooms (Best-of-N, Presence, Chat)

Open
#3 0 comments 0 reactions 1 assignee Claimed by @hoangsonww View on GitHub
documentation enhancement good first issue help wanted question
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.