Files
OpenFrontIO/tests/GameInfoRanking.test.ts
T
Evan Pelle 43d07ca85f refactor: rename core-public -> engine-public; fold helpers in; drop shared
Tighten the package model around the determinism invariant: everything the
engine imports must itself be deterministic.

- Rename core-public -> engine-public (package, dir, `engine-public/*` alias,
  all 118 import sites). It is the deterministic public surface the engine
  depends on and that client/server also use.
- Move the pure formatting helpers (renderNumber/renderTroops) from shared
  into engine-public/format — the engine uses them and they're deterministic.
- Drop the now-empty shared package (and its alias). `shared` was intended for
  client/server-shared, engine-forbidden code; there is none yet, so it is
  removed and can be re-added when such code appears.

Final graph: engine-public (leaf) <- engine <- {client, server};
client/server also -> engine-public.

Verified: tsc clean; 1364 + 65 tests pass; prod build succeeds; engine-public
is a leaf; engine has no client/server imports. Lockfile regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:32:32 +00:00

198 lines
5.7 KiB
TypeScript

import {
Ranking,
RankType,
} from "client/components/baseComponents/ranking/GameInfoRanking";
import {
Difficulty,
GameMapSize,
GameMapType,
GameMode,
GameType,
} from "engine/game/Game";
import { AnalyticsRecord, GameConfig } from "engine-public/Schemas";
import {
GOLD_INDEX_STEAL,
GOLD_INDEX_TRADE,
GOLD_INDEX_TRAIN_OTHER,
GOLD_INDEX_TRAIN_SELF,
GOLD_INDEX_WAR,
} from "engine-public/StatsSchemas";
describe("Ranking class", () => {
const mockConfig: GameConfig = {
gameMap: GameMapType.Montreal,
difficulty: Difficulty.Medium,
donateGold: false,
donateTroops: false,
gameType: GameType.Public,
gameMode: GameMode.FFA,
gameMapSize: GameMapSize.Normal,
nations: "disabled",
bots: 0,
infiniteGold: false,
infiniteTroops: false,
instantBuild: false,
maxPlayers: 40,
disabledUnits: [],
randomSpawn: false,
};
const gameTickDuration = 1000;
const gameDuration = gameTickDuration / 10;
function makeSession(
overrides: Partial<AnalyticsRecord> = {},
): AnalyticsRecord {
return {
version: "v0.0.2",
info: {
duration: gameTickDuration,
winner: ["player", "p2"],
players: [
{
clientID: "p1",
username: "Alice",
clanTag: "X",
cosmetics: { flag: "USA" },
stats: {
units: { port: [2n, 0n, 0n, 2n] },
conquests: [5n],
gold: [0n, 100n, 20n, 0n, 15n, 5n], // total 140
bombs: {
abomb: [1n],
hbomb: [1n],
mirv: [2n],
},
},
persistentID: null,
},
{
clientID: "p2",
username: "Bob",
clanTag: null,
stats: {
units: { city: [2n, 0n, 0n, 2n] },
conquests: [8n],
gold: [0n, 50n, 10n, 5n], // total 65, no train trade
bombs: {
abomb: [0n],
hbomb: [2n],
mirv: [0n],
},
},
persistentID: null,
},
{
clientID: "p3",
username: "Charlie",
clanTag: null,
stats: {
// no units, but has conquests/killedAt to count as played
conquests: [8n],
killedAt: BigInt(600),
gold: [0n, 10n, 2n, 10n, 0n, 5n], // total 27
bombs: {},
},
persistentID: null,
},
],
gameID: "",
lobbyCreatedAt: 0,
config: { ...mockConfig },
start: 0,
end: 0,
num_turns: 0,
lobbyFillTime: 0,
},
gitCommit: "DEV",
subdomain: "",
domain: "",
};
}
test("summarizes players correctly", () => {
const r = new Ranking(makeSession());
const players = r.sortedBy(RankType.ConquestHumans);
expect(players.length).toBe(3);
const p1 = players.find((p) => p.id === "p1")!;
expect(p1.username).toBe("Alice");
expect(p1.flag).toBe("USA");
expect(p1.conquests).toStrictEqual([5n]);
expect(p1.atoms).toBe(1);
expect(p1.mirv).toBe(2);
});
test("correctly identifies winner", () => {
const r = new Ranking(makeSession());
const p2 = r.sortedBy(RankType.ConquestHumans).find((p) => p.id === "p2")!;
expect(p2.winner).toBe(true);
});
test("rank by total gold", () => {
const r = new Ranking(makeSession());
const rankedPlayers = r.sortedBy(RankType.TotalGold);
expect(rankedPlayers.length).toBe(3);
expect(rankedPlayers[0].id).toBe("p1");
expect(rankedPlayers[1].id).toBe("p2");
expect(rankedPlayers[2].id).toBe("p3");
});
test("rank by stolen gold", () => {
const r = new Ranking(makeSession());
const rankedPlayers = r.sortedBy(RankType.StolenGold);
expect(rankedPlayers.length).toBe(3);
expect(rankedPlayers[0].id).toBe("p3");
expect(rankedPlayers[1].id).toBe("p2");
expect(rankedPlayers[2].id).toBe("p1");
});
test("rank by hydros", () => {
const r = new Ranking(makeSession());
const rankedPlayers = r.sortedBy(RankType.Hydros);
expect(rankedPlayers.length).toBe(3);
expect(rankedPlayers[0].id).toBe("p2");
expect(rankedPlayers[1].id).toBe("p1");
expect(rankedPlayers[2].id).toBe("p3");
});
test("lifetime score is percentage of duration", () => {
const r = new Ranking(makeSession());
const p3 = r.sortedBy(RankType.ConquestHumans).find((p) => p.id === "p3")!;
const expected = Number(BigInt(600)) / gameDuration;
expect(r.score(p3, RankType.Lifetime)).toBe(expected);
});
test("lifetime score gives 100 when alive", () => {
const r = new Ranking(makeSession());
const p1 = r.allPlayers.find((p) => p.id === "p1")!;
expect(r.score(p1, RankType.Lifetime)).toBe(100);
});
test("winners should be ahead of players with same score", () => {
const r = new Ranking(makeSession());
const sortedPlayers = r.sortedBy(RankType.ConquestHumans);
expect(sortedPlayers[0].id).toBe("p2"); // p2 & p3 same score but winner first
});
test("gold scores work correctly", () => {
const r = new Ranking(makeSession());
const p1 = r.sortedBy(RankType.TotalGold).find((p) => p.id === "p1")!;
expect(r.score(p1, RankType.StolenGold)).toBe(
Number(p1.gold[GOLD_INDEX_STEAL] ?? 0n),
);
expect(r.score(p1, RankType.NavalTrade)).toBe(
Number(p1.gold[GOLD_INDEX_TRADE] ?? 0n),
);
const ownTrain = p1.gold[GOLD_INDEX_TRAIN_SELF] ?? 0n;
const otherTrain = p1.gold[GOLD_INDEX_TRAIN_OTHER] ?? 0n;
expect(r.score(p1, RankType.TrainTrade)).toBe(
Number(ownTrain + otherTrain),
);
expect(r.score(p1, RankType.ConqueredGold)).toBe(
Number(p1.gold[GOLD_INDEX_WAR] ?? 0n),
);
});
});