Commit Graph
2 Commits
Author SHA1 Message Date
Josh HarrisandGitHub e57939aba1 sec: require strict majority in 1v1 winner-vote consensus (backport) (#4581)
**Add approved & assigned issue number here:**

Resolves #4136

## Description:

Backport of #4580 to `main`. `VoteRound.result()` (used by
`handleWinner` in `GameServer.ts` to reach consensus on a reported game
winner) accepted an exact tie as a majority: `votes * 2 >=
totalUniqueIPs`. In a 1v1 game the electorate is 2 unique IPs, so a
single client's vote alone satisfied `1 * 2 >= 2` and was accepted
immediately — letting one player unilaterally declare themselves the
winner without any agreement from the other player, and without the
server ever checking the report against the actual simulation outcome.

This fixes the comparison to require a strict majority (`votes * 2 >
totalUniqueIPs`), so a 1v1 now requires both clients to agree.
Legitimate forfeit-on-disconnect is unaffected: once a player actually
disconnects, the electorate shrinks to 1 unique IP, and the sole
remaining vote still resolves immediately (`1 * 2 > 1`).

The `v32` hotfix (#4580) is being deployed first; this PR keeps `main`
in sync with the same fix.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory

## Please put your Discord username so you can be contacted if a bug or
regression is found:

jish
2026-07-12 12:47:01 +01:00
181368f962 Add live game stats endpoint to the admin bot API (#4399)
## Summary

The game simulation runs **client-side**, so the server can't directly
see what's happening in a running game. This adds a way for the admin
bot to observe a live game: clients report a live stats snapshot every
~10s, the server reaches consensus on it (reusing the winner's vote
mechanism), and a new admin-bot endpoint serves it.

## How it works

1. **`LiveStatsController`** (client) emits a snapshot every **100
turns** (~10s at 100ms/turn) — only deterministic sim values, with
players sorted by clientID, so in-sync clients produce an identical
payload.
2. The snapshot is sent as a new **`live_stats`** wire message wrapping
a `LiveStats` object (`turn` + per-human-player
`tilesOwned`/`troops`/`gold`/`isAlive`/`team`).
3. **`GameServer.handleLiveStats`** tallies a per-turn **IP-weighted
majority vote** — the same consensus the winner uses — and keeps the
latest agreed snapshot.
4. **`GET /api/adminbot/game/:id/stats`** returns it, enriched with
usernames the server already holds. `liveStats` is `null` until the
first consensus.

The winner's vote tally was extracted into a small reusable
**`VoteRound`** (`src/server/VoteTally.ts`) and is now used for both
winner and live-stats consensus.

Names are deliberately **excluded** from the voted payload (they vary
per client under name anonymization, which would break exact-match
consensus); the server joins `clientID → username` instead.

## Changes

- `src/server/VoteTally.ts` *(new)* — reusable IP-weighted `VoteRound`
- `src/core/Schemas.ts` — `PlayerLiveStatsSchema`, `LiveStatsSchema`,
`ClientSendLiveStatsSchema` + unions
- `src/client/controllers/LiveStatsController.ts` *(new)* — per-100-turn
snapshot reporter
- `src/client/Transport.ts` — `SendLiveStatsEvent` + sender
- `src/client/hud/GameRenderer.ts` — register the controller
- `src/server/GameServer.ts` — refactor winner onto `VoteRound`; add
live-stats consensus + `liveStats()` accessor
- `src/server/AdminBotRoutes.ts` — `GET …/stats` endpoint

## Testing

- **Unit:** `tests/server/VoteTally.test.ts` (majority/dedup/ties),
`tests/server/LiveStats.test.ts` (consensus, disagreement, per-client
dedup, stale-turn rejection, turn advance, out-of-sync exclusion, +
endpoint 200/404/400). Full suite green (`npm test`), typecheck + lint
clean.
- **Manual e2e** against the dev server: created an admin-bot game,
joined it in a browser, force-started via `toggle_game_start_timer`, and
confirmed `GET …/stats` returned the consensus snapshot with username
enrichment and an advancing `turn`. Also verified wrong-worker → 400 and
missing-key → 401.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:21:52 -07:00