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

Feature: Competitive Integrity & Reliability: Server-Authoritative Engine, Turn Timers, Reconnect & Anti-Stall

Open
#7 0 comments 0 reactions 1 assignee Claimed by @hoangsonww View on GitHub
bug 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

Make online matches rock-solid and fair by moving to a **server-authoritative match engine** with:

* **Hard turn timers** and grace periods
* **Auto-reconnect & resume** (tab refresh, brief network loss)
* **Anti-stall & rage-quit handling** (forfeit after timer; ELO-safe)
* **Deterministic state log** (for replays/debug)

This complements your ELO + matchmaking and paves the way for future features like replays & tournaments.

---

## Goals (v1)

1. Server is source of truth for board, turn, timers, result.
2. Client may *request* moves; server validates & broadcasts.
3. Turn timer (e.g., **15s** default) with **one 10s grace** per player per match.
4. Auto-pause if both clients disconnect; auto-resume on first rejoin within **90s**.
5. Anti-stall: on timer expiry → warning → forfeit; record **rageQuit=true**.
6. Persist **match log** (initial seed, ordered events) for replay/debug.

---

## UX

* In-game header shows **Your turn • 00:15** countdown; on low time (<5s) animate pulse.
* If disconnected: inline banner **“Reconnecting…”**; on success **“Resumed”**.
* If opponent times out: toast **“Opponent forfeited (time)”**; result screen explains ELO handling.
* If you disconnect and fail to return in time, result screen states **forfeit (disconnect)**.

---

## API & Socket (proposed)

**Events (client → server)**

* `move:request` → `{ matchId, cellIndex }`
* `client:ping` → `{ matchId }` (keepalive / latency)
* `client:resume` → `{ matchId }` (after reconnect)

**Events (server → clients)**

* `state:update` → `{ board, nextPlayer, clocks: {X,Y}, lastMove }`
* `timer:tock` → `{ remainingMs }` (throttled, e.g., 1/s)
* `match:result` → `{ outcome: 'X|O|draw|forfeit', reason }`
* `match:warning` → `{ type: 'lowTime' | 'desync' | 'invalidMove' }`

**HTTP**

* `POST /leaderboard/match/heartbeat` (fallback for NAT/websocket issues)
* `GET /leaderboard/match/state?matchId=…` (cold resume hydrate)
* `GET /leaderboard/match/log?matchId=…` (admin / future replays)

---

## Validation Rules (server)

* Reject moves when:

* Not player’s turn
* Cell occupied
* Timer expired
* Match already finished
* On valid move:

* Update board, check win/draw (pure function)
* Advance turn, reset/adjust clocks
* Append to `events[]`

---

## Data Model (minimal changes)

`matches`

* `state.board: string[9]`
* `state.turn: 'X'|'O'`
* `clocks: { X:number, Y:number }` (ms remaining)
* `grace: { X:number, Y:number }` (grace used count)
* `events: Array<{ t:number, by:'X'|'O'|'sys', type:'move'|'timeout'|'resume'|'forfeit', payload:any }>`
* `flags: { rageQuit:boolean }`

---

## ELO / Result Handling

* **Forfeit (timeout/disconnect)** → counted as a loss for forfeiter.
* If opponent disconnects first but *you* also disconnect and neither returns within window → **draw** (ELO-safe fail-closed).
* Record `rageQuit` for analytics and future penalties (e.g., soft matchmaking cooldowns).

---

## Acceptance Criteria

* [ ] Server blocks all illegal/late moves; clients reflect authoritative state within 200ms on average local tests.
* [ ] Turn timer visible; when it hits 0 without move → server emits `match:result` with `reason='timeout'`.
* [ ] Refreshing the page mid-match resumes state (board + clocks) within 2s.
* [ ] Disconnecting one client for >90s yields forfeit; ELO updates accordingly.
* [ ] Event log contains at least: start, each move `{cellIndex}`, any `timeout/forfeit`, end.
* [ ] Unit tests for win/draw logic and timer transitions; integration test simulating disconnect/reconnect.

---

## Edge Cases

* **Simultaneous moves:** honor **first server-received**; reject the other with `invalidMove`.
* **Clock drift:** timers run server-side; clients display server-pushed remaining time.
* **Network flaps:** throttle `timer:tock` and rely on `state:update` as truth after reconnect.
* **Mobile backgrounding:** treat as disconnect; grace applies.

---

## Tech Notes

* Keep current Socket.io; add **server-side room clock** (Node `setInterval` or a single game loop tick).
* Use a **pure reducer** for match transitions (input: state + event → new state) to guarantee determinism.
* Persist every **N** events or on important transitions to reduce I/O.
* Feature flag behind `ENGINE_V2=true` and run A/B on a subset.

---

## Metrics & Observability

* `match_reconnect_rate`, `avg_reconnect_time_ms`
* `timeout_forfeits_rate`, `rage_quit_rate`
* `authoritative_rejections` (invalid moves)
* Trace per match id for timeline (move → broadcast → client ack)

---

## Subtasks

* [ ] Backend: state reducer + timers + persistence
* [ ] Backend: new socket events & resume flow
* [ ] Frontend: countdown UI + reconnect banners
* [ ] Tests: reducer/logic + websocket integration
* [ ] ELO: result reasons & safeguards
* [ ] Docs: README + Swagger notes for resume endpoints

---

**Why now?**
It meaningfully improves fairness, reduces bad-feeling losses, and stabilizes online play—unlocking future **replays**, **spectator mode quality**, and **tournaments** with confidence.

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.