This commit is contained in:
Ryan Barlow
2026-03-01 15:04:56 +00:00
parent 401501aa2d
commit 13a75e8519
7 changed files with 129 additions and 135 deletions
+3 -19
View File
@@ -1,5 +1,6 @@
import { AllPlayers, ColoredTeams, nukeTypes } from "../../core/game/Game";
import { AllPlayers, nukeTypes } from "../../core/game/Game";
import { GameView, PlayerView } from "../../core/game/GameView";
import { computeTeamTiles, findCrownTeam } from "../../core/game/TeamUtils";
import allianceIcon from "/images/AllianceIcon.svg?url";
import allianceIconFaded from "/images/AllianceIconFaded.svg?url";
import allianceRequestBlackIcon from "/images/AllianceRequestBlackIcon.svg?url";
@@ -59,24 +60,7 @@ export function getFirstPlacePlayer(game: GameView): PlayerView | null {
/** Returns the team with the most total tiles, or null if no team leads. */
export function getCrownTeam(game: GameView): string | null {
const teamToTiles = new Map<string, number>();
for (const player of game.playerViews()) {
const team = player.team();
if (team === null || team === ColoredTeams.Bot) continue;
teamToTiles.set(
team,
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
);
}
let maxTiles = 0;
let crownTeam: string | null = null;
for (const [team, tiles] of teamToTiles) {
if (tiles > maxTiles) {
maxTiles = tiles;
crownTeam = team;
}
}
return crownTeam;
return findCrownTeam(computeTeamTiles(game.playerViews()));
}
export function getPlayerIcons(
+24 -43
View File
@@ -8,6 +8,11 @@ import {
UnitType,
} from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import {
computeTeamTiles,
findCrownTeam,
normalizeCrownSeconds,
} from "../../../core/game/TeamUtils";
import { SendWinnerEvent } from "../../Transport";
import {
formatPercentage,
@@ -98,15 +103,9 @@ export class TeamStats extends LitElement implements Layer {
}
private trackMetrics() {
const teamToTiles = new Map<Team, number>();
for (const player of this.game.playerViews()) {
const team = player.team();
if (team === null || team === ColoredTeams.Bot) continue;
const teamToTiles = computeTeamTiles(this.game.playerViews());
for (const team of teamToTiles.keys()) {
this._knownTeams.add(team);
teamToTiles.set(
team,
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
);
}
for (const [team, tiles] of teamToTiles) {
const prev = this._peakTiles.get(team) ?? 0;
@@ -119,14 +118,9 @@ export class TeamStats extends LitElement implements Layer {
private freezeScores() {
const numTilesWithoutFallout =
this.game.numLandTiles() - this.game.numTilesWithFallout();
for (const player of this.game.playerViews()) {
const team = player.team();
if (team === null || team === ColoredTeams.Bot) continue;
this._frozenCurrentPercent.set(
team,
(this._frozenCurrentPercent.get(team) ?? 0) +
player.numTilesOwned() / numTilesWithoutFallout,
);
const teamToTiles = computeTeamTiles(this.game.playerViews());
for (const [team, tiles] of teamToTiles) {
this._frozenCurrentPercent.set(team, tiles / numTilesWithoutFallout);
}
for (const [team, peakTiles] of this._peakTiles) {
this._frozenPeakPercent.set(team, peakTiles / numTilesWithoutFallout);
@@ -174,32 +168,19 @@ export class TeamStats extends LitElement implements Layer {
? Math.min(Math.floor(elapsedGameTicks / 10), maxTimerValue * 60)
: Math.floor(elapsedGameTicks / 10);
const serverCrownTicks = this.game.teamCrownTicks() ?? {};
// Crown holder = team with most tiles (same logic as WinCheckExecution)
let crownHolder: string | null = null;
let maxTilesNow = 0;
for (const [team, players] of Object.entries(grouped)) {
const teamTotal = players.reduce((sum, p) => sum + p.numTilesOwned(), 0);
if (teamTotal > maxTilesNow) {
maxTilesNow = teamTotal;
crownHolder = team;
}
}
// Non-holder teams get floor(ticks/10), holder gets remainder
const normalizedCrownTicks = new Map<string, number>();
let othersSeconds = 0;
for (const [team, ticks] of Object.entries(serverCrownTicks)) {
if (team !== crownHolder) {
const secs = Math.floor(ticks / 10);
normalizedCrownTicks.set(team, secs * 10);
othersSeconds += secs;
}
}
if (crownHolder !== null) {
normalizedCrownTicks.set(
crownHolder,
Math.max(0, elapsedSeconds - othersSeconds) * 10,
);
}
const allTeamKeys = Object.keys(grouped);
const crownHolder = findCrownTeam(
computeTeamTiles(this.game.playerViews()),
);
const serverTicksMap = new Map<Team, number>(
Object.entries(serverCrownTicks),
);
const crownSecondsMap = normalizeCrownSeconds(
allTeamKeys,
serverTicksMap,
crownHolder,
elapsedSeconds,
);
this.teams = Object.entries(grouped)
.map(([teamStr, teamPlayers]) => {
@@ -245,7 +226,7 @@ export class TeamStats extends LitElement implements Layer {
this._gameOver && this.game.competitiveScores()
? (this.game.competitiveScores()!.find((s) => s.team === teamStr)
?.crownTimeSeconds ?? 0)
: Math.floor((normalizedCrownTicks.get(teamStr) ?? 0) / 10),
: (crownSecondsMap.get(teamStr) ?? 0),
totalLaunchers: renderNumber(totalLaunchers),
totalSAMs: renderNumber(totalSAMs),
+1
View File
@@ -121,6 +121,7 @@ export class AttackExecution implements Execution {
// CrownBreakPoint: 25% troop bonus when attacking the crown-holding team
if (
this.mg.config().gameConfig().competitiveScoring &&
this.mg.config().gameConfig().gameMode === GameMode.Team &&
this.target.isPlayer()
) {
+4 -20
View File
@@ -1,4 +1,5 @@
import { ColoredTeams, Execution, Game, GameMode, Team } from "../game/Game";
import { Execution, Game, GameMode } from "../game/Game";
import { computeTeamTiles, findCrownTeam } from "../game/TeamUtils";
/**
* Tracks team-level competitive metrics every 10 ticks:
@@ -30,30 +31,13 @@ export class TeamMetricsExecution implements Execution {
if (elapsedSeconds >= maxTimerValue * 60) return;
}
const teamToTiles = new Map<Team, number>();
for (const player of this.mg.players()) {
const team = player.team();
if (team === null || team === ColoredTeams.Bot) continue;
teamToTiles.set(
team,
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
);
}
const teamToTiles = computeTeamTiles(this.mg.players());
// Track peak tiles for each team
for (const [team, tiles] of teamToTiles) {
this.mg.updateTeamPeakTiles(team, tiles);
}
// Track crown (team with most tiles)
let maxTiles = 0;
let crownTeam: Team | null = null;
for (const [team, tiles] of teamToTiles) {
if (tiles > maxTiles) {
maxTiles = tiles;
crownTeam = team;
}
}
const crownTeam = findCrownTeam(teamToTiles);
if (crownTeam !== null) {
this.mg.addCrownTick(crownTeam, 10);
}
+19 -34
View File
@@ -13,6 +13,11 @@ import {
RankedType,
Team,
} from "../game/Game";
import {
computeTeamTiles,
findCrownTeam,
normalizeCrownSeconds,
} from "../game/TeamUtils";
export class WinEvent implements GameEvent {
constructor(public readonly winner: Player) {}
@@ -148,7 +153,7 @@ export class WinCheckExecution implements Execution {
if (winner === undefined) return;
const scores = this.mg.config().gameConfig().competitiveScoring
? this.computeScores(teamToTiles, numTilesWithoutFallout)
? this.computeScores(this.mg, teamToTiles, numTilesWithoutFallout)
: undefined;
this.mg.setWinner(winner[0], this.mg.stats().stats(), scores);
console.log(`${winner[0]} has won the game`);
@@ -157,17 +162,15 @@ export class WinCheckExecution implements Execution {
}
private computeScores(
mg: Game,
teamToTiles: Map<Team, number>,
numTilesWithoutFallout: number,
) {
if (this.mg === null) return undefined;
const eliminationOrder = this.mg.teamEliminationOrder();
const eliminationOrder = mg.teamEliminationOrder();
const allTeams = Array.from(teamToTiles.keys()).filter(
(t) => t !== ColoredTeams.Bot,
);
const totalGameTicks =
this.mg.ticks() - this.mg.config().numSpawnPhaseTurns();
const totalGameTicks = mg.ticks() - mg.config().numSpawnPhaseTurns();
// Rank surviving teams by current tiles (more tiles = better placement)
const survivingTeams = allTeams.filter(
@@ -177,37 +180,19 @@ export class WinCheckExecution implements Execution {
(a, b) => (teamToTiles.get(b) ?? 0) - (teamToTiles.get(a) ?? 0),
);
// Normalize crown seconds: non-crown teams get floor(ticks/10),
// crown holder gets the remainder so the sum matches the game timer.
const nonBotTiles = computeTeamTiles(mg.allPlayers());
const crownHolder = findCrownTeam(nonBotTiles);
const elapsedSeconds = Math.floor(Math.max(0, totalGameTicks) / 10);
let crownHolder: Team | null = null;
let maxTiles = 0;
for (const [team, tiles] of teamToTiles) {
if (team !== ColoredTeams.Bot && tiles > maxTiles) {
maxTiles = tiles;
crownHolder = team;
}
}
const allCrownTicks = this.mg!.allTeamCrownTicks();
const teamCrownSeconds = new Map<Team, number>();
let othersSum = 0;
for (const team of allTeams) {
const ticks = allCrownTicks.get(team) ?? 0;
if (team !== crownHolder) {
const secs = Math.floor(ticks / 10);
teamCrownSeconds.set(team, secs);
othersSum += secs;
}
}
if (crownHolder !== null) {
teamCrownSeconds.set(
crownHolder,
Math.max(0, elapsedSeconds - othersSum),
);
}
const allCrownTicks = mg.allTeamCrownTicks();
const teamCrownSeconds = normalizeCrownSeconds(
allTeams,
allCrownTicks,
crownHolder,
elapsedSeconds,
);
const metrics: TeamRawMetrics[] = allTeams.map((team) => {
const peakTiles = this.mg!.teamPeakTiles(team);
const peakTiles = mg.teamPeakTiles(team);
const peakTilePercentage = (peakTiles / numTilesWithoutFallout) * 100;
const crownTicks = allCrownTicks.get(team) ?? 0;
const crownRatio = totalGameTicks > 0 ? crownTicks / totalGameTicks : 0;
+3 -19
View File
@@ -51,6 +51,7 @@ import { createRailNetwork } from "./RailNetworkImpl";
import { Stats } from "./Stats";
import { StatsImpl } from "./StatsImpl";
import { assignTeams } from "./TeamAssignment";
import { computeTeamTiles, findCrownTeam } from "./TeamUtils";
import { TerraNulliusImpl } from "./TerraNulliusImpl";
import { UnitGrid, UnitPredicate } from "./UnitGrid";
@@ -1256,28 +1257,11 @@ export class GameImpl implements Game {
}
crownTeam(): Team | null {
const teamToTiles = new Map<Team, number>();
for (const player of this.players()) {
const team = player.team();
if (team === null || team === ColoredTeams.Bot) continue;
teamToTiles.set(
team,
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
);
}
let maxTiles = 0;
let crown: Team | null = null;
for (const [team, tiles] of teamToTiles) {
if (tiles > maxTiles) {
maxTiles = tiles;
crown = team;
}
}
return crown;
return findCrownTeam(computeTeamTiles(this.players()));
}
allTeamCrownTicks(): ReadonlyMap<Team, number> {
return new Map(this._teamCrownTicks);
return this._teamCrownTicks;
}
addCrownTick(team: Team, amount: number): void {
+75
View File
@@ -0,0 +1,75 @@
import { ColoredTeams, Team } from "./Game";
/**
* Any object that exposes a team and a tile count —
* satisfied by both server-side `Player` and client-side `PlayerView`.
*/
interface HasTeamAndTiles {
team(): Team | null;
numTilesOwned(): number;
}
/**
* Sums tile counts per team from the given players.
* By default bots and null-team players are excluded.
*/
export function computeTeamTiles(
players: Iterable<HasTeamAndTiles>,
excludeBots = true,
): Map<Team, number> {
const teamToTiles = new Map<Team, number>();
for (const player of players) {
const team = player.team();
if (team === null) continue;
if (excludeBots && team === ColoredTeams.Bot) continue;
teamToTiles.set(
team,
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
);
}
return teamToTiles;
}
/**
* Returns the team with the highest tile count, or `null` if the map is empty.
*/
export function findCrownTeam(teamToTiles: Map<Team, number>): Team | null {
let maxTiles = 0;
let crown: Team | null = null;
for (const [team, tiles] of teamToTiles) {
if (tiles > maxTiles) {
maxTiles = tiles;
crown = team;
}
}
return crown;
}
/**
* Converts raw crown-tick counts into display-friendly seconds.
*
* Non-crown-holder teams get `floor(ticks / 10)`.
* The current crown holder receives the remainder so the total sums to
* `totalElapsedSeconds`, keeping the sidebar timer and crown time in sync.
*/
export function normalizeCrownSeconds(
allTeams: Team[],
crownTicks: ReadonlyMap<Team, number>,
crownHolder: Team | null,
totalElapsedSeconds: number,
): Map<Team, number> {
const result = new Map<Team, number>();
let othersSum = 0;
for (const team of allTeams) {
const ticks = crownTicks.get(team) ?? 0;
if (team !== crownHolder) {
const secs = Math.floor(ticks / 10);
result.set(team, secs);
othersSum += secs;
}
}
if (crownHolder !== null) {
result.set(crownHolder, Math.max(0, totalElapsedSeconds - othersSum));
}
return result;
}