See `requirements.md` for what is built; requirement ids (R1.1, …) are referenced below. Builds on `game-history`
(live game snapshots in Valkey, saved games in Postgres).
## Decisions
| Decision | Reason |
|---|---|
| The server is the only authority on time; clients only display it | Players cannot gain time by changing their device clock or client. A move arriving too late is refused on the server (R2.3). |
| The clock is stored as "time left when the turn started" plus `turnStartedAt`, not as a ticking value | The state only changes on moves, so nothing is written per second; the Valkey snapshot that is already written on every move carries it. The time left at any moment is computed. |
| A scheduled service checks all live games every second, in addition to the check when a move arrives | It ends games where nobody does anything (R2.2) and is simple to reason about and to restore after a restart. Because late moves are refused, checking once a second never changes a result; it only decides how soon the end is announced. Per-game timers were considered: more precise, but every move, end and restore would have to keep them in sync. A few hundred live games are a trivial loop. |
| The end reason "timeout" is reused | It keeps its meaning: the player whose turn it was ran out of time. The database check constraint, results, texts and the recent games list need no change. Old saved timeouts came from the offline rule; the difference is not shown. |
| The offline rules from `game-history` (`TURN_TIMEOUT_MS`, `INACTIVE_TIMEOUT_MS`, `offlineSince`) are removed | The running clock ends any game where the player to move is away within 5 minutes (R6.1), so the old rules can no longer trigger. One mechanism instead of two. |
| 5 minutes, no increment, set by `GAME_CLOCK_MS` (default `300000`) | R1.1. The variable allows short clocks in tests and later tuning. The initial time travels with each game, so clients do not hard-code it and a running game keeps its time control if the setting changes. |
| Time while the backend is down is not charged (R5.1) | A deploy must not make anyone lose. On restore, the running clock restarts from the time left at the start of the turn; the player gets back what they spent on that turn before the restart. |
| Messages carry the server's time | Clients compute the offset between their clock and the server's and count down from it, so a wrong device clock does not matter (R3.3). |
| Animation and network delay are not compensated | A long move animates for a few seconds (300 ms per step) on the next player's time. This is accepted for now, like network delay; time added per move is the fix if it turns out to matter (out of scope). |
## Shared types (`packages/core`)
```ts
// models/GameClock.ts
interface GameClock {
initialMs: number;
// Time left for each player. For the player whose turn it is: at turnStartedAt.
player1Ms: number;
player2Ms: number;
// Server time when the running clock started; undefined when no clock runs (the game has ended).
turnStartedAt?: number;
}
const CLOCK_LOW_TIME_MS = 30_000; // R3.2
```
-`Game` (live game messages) gains `clock?: GameClock` and `serverTime?: number` (server time when the message was
built). Optional, so older servers and clients keep working.
-`GameEndMessage` gains `clock?: GameClock`, the stopped clocks (R3.5).
-`MoveRecord` gains `remainingMs?: number`, the mover's time left after the move (R4.1).
-`FinishedGameSummary` gains `clockInitialMs?: number`; undefined for games saved before this feature (R4.3).
Helpers in `packages/core/src/clock/`, used by the backend, the web app and the mobile app: