mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:20:50 +00:00
Ofm tournament - Log Final standings and Per-Kill eliminations (#4350)
**Add approved & assigned issue number here:** Resolves #4349 ## Description: The infra related PR is linked to this one and would need to be pushed first (376) Two changes for organized/tournament matches: 1. **Final standings.** `setWinner` snapshots each player's tiles owned at game end into `PlayerStats` (`finalTiles`). It's a deterministic integer captured in the sim, so it's replay-safe and rides into the existing game record. This lets standings be derived directly (winner, then surviving players by territory, then eliminated players by when they died) without re-simulating, which matters because a domination win ends with many players still alive. 2. **Per-kill log**. Records, per player, which humans they eliminated and at what tick (kills on PlayerStats). This lets standings attribute each kill to the victim's final placement, and gives a deterministic kill graph for integrity review. Hooked once in conquerPlayer (the single elimination funnel), humans only. Additive optional field that rides the existing game record, no archive or wire changes. These are off by default with no effect on normal play. ## Please complete the following: - [x] I have added screenshots for all UI updates (no UI changes in this PR) - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file (no new user-facing text) - [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: zixer._
This commit is contained in:
@@ -108,6 +108,12 @@ export const PlayerStatsSchema = z
|
||||
attacks: AtLeastOneNumberSchema.optional(),
|
||||
betrayals: BigIntStringSchema.optional(),
|
||||
killedAt: BigIntStringSchema.optional(),
|
||||
// Tiles owned at game end, for OFM standings (set on setWinner).
|
||||
finalTiles: BigIntStringSchema.optional(),
|
||||
// Humans this player eliminated (victim clientID + tick), for OFM kill scoring.
|
||||
kills: z
|
||||
.array(z.object({ victim: z.string(), tick: BigIntStringSchema }))
|
||||
.optional(),
|
||||
conquests: AtLeastOneNumberSchema.optional(),
|
||||
boats: z.partialRecord(BoatUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
bombs: z.partialRecord(BombUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
|
||||
@@ -851,6 +851,10 @@ export class GameImpl implements Game {
|
||||
|
||||
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void {
|
||||
this._winner = winner;
|
||||
// OFM: snapshot final tiles for standings (bots skipped in recordFinalTiles).
|
||||
for (const player of this.players()) {
|
||||
this.stats().recordFinalTiles(player, player.numTilesOwned());
|
||||
}
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.Win,
|
||||
winner: this.makeWinner(winner),
|
||||
@@ -1251,6 +1255,9 @@ export class GameImpl implements Game {
|
||||
this.stats().goldWar(conqueror, conquered, goldCaptured);
|
||||
}
|
||||
|
||||
// OFM: per-kill log for standings (humans-only filtered in recordKill).
|
||||
this.stats().recordKill(conqueror, conquered, this.ticks());
|
||||
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.ConquestEvent,
|
||||
conquerorId: conqueror.id(),
|
||||
|
||||
@@ -102,6 +102,12 @@ export interface Stats {
|
||||
// player was killed (0 tiles)
|
||||
playerKilled(player: Player, tick: number): void;
|
||||
|
||||
// Record tiles owned at game end (final standings).
|
||||
recordFinalTiles(player: Player, tiles: number | bigint): void;
|
||||
|
||||
// Record that player eliminated human victim at tick (OFM kill scoring).
|
||||
recordKill(player: Player, victim: Player, tick: number | bigint): void;
|
||||
|
||||
// Player's train arrives at any station, generating gold
|
||||
trainSelfTrade(player: Player, gold: number | bigint): void;
|
||||
|
||||
|
||||
@@ -286,6 +286,22 @@ export class StatsImpl implements Stats {
|
||||
this._addPlayerKilled(player, tick);
|
||||
}
|
||||
|
||||
recordFinalTiles(player: Player, tiles: BigIntLike): void {
|
||||
const p = this._makePlayerStats(player);
|
||||
if (p === undefined) return;
|
||||
p.finalTiles = _bigint(tiles);
|
||||
}
|
||||
|
||||
recordKill(player: Player, victim: Player, tick: BigIntLike): void {
|
||||
if (victim.type() !== PlayerType.Human) return;
|
||||
const victimId = victim.clientID();
|
||||
if (victimId === null) return;
|
||||
const p = this._makePlayerStats(player);
|
||||
if (p === undefined) return;
|
||||
p.kills ??= [];
|
||||
p.kills.push({ victim: victimId, tick: _bigint(tick) });
|
||||
}
|
||||
|
||||
trainSelfTrade(player: Player, gold: BigIntLike): void {
|
||||
this._addGold(player, GOLD_INDEX_TRAIN_SELF, gold);
|
||||
}
|
||||
|
||||
@@ -239,10 +239,41 @@ describe("Stats", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("recordKill", () => {
|
||||
stats.recordKill(player1, player2, 30);
|
||||
stats.recordKill(player1, player2, 35);
|
||||
expect(stats.getPlayerStats(player1)?.kills).toStrictEqual([
|
||||
{ victim: "client2", tick: 30n },
|
||||
{ victim: "client2", tick: 35n },
|
||||
]);
|
||||
expect(stats.getPlayerStats(player2)?.kills).toBeUndefined();
|
||||
});
|
||||
|
||||
test("stringify", () => {
|
||||
stats.unitLose(player1, UnitType.Port);
|
||||
expect(JSON.stringify(stats.stats(), replacer)).toBe(
|
||||
'{"client1":{"units":{"port":["0","0","0","1"]}}}',
|
||||
);
|
||||
});
|
||||
|
||||
test("recordFinalTiles", () => {
|
||||
stats.recordFinalTiles(player1, 42);
|
||||
expect(stats.getPlayerStats(player1)?.finalTiles).toBe(42n);
|
||||
});
|
||||
|
||||
test("setWinner snapshots finalTiles for each player", () => {
|
||||
let count = 0;
|
||||
game.map().forEachTile((tile) => {
|
||||
if (count >= 5) return;
|
||||
if (!game.map().isLand(tile)) return;
|
||||
player1.conquer(tile);
|
||||
count++;
|
||||
});
|
||||
game.setWinner(player1, game.stats().stats());
|
||||
const tiles = player1.numTilesOwned();
|
||||
expect(tiles).toBeGreaterThan(0);
|
||||
expect(game.stats().getPlayerStats(player1)?.finalTiles).toBe(
|
||||
BigInt(tiles),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user