Files
OpenFrontIO/src/core/game/StatsImpl.ts
T
DevelopingTom f367ea1940 Record human/nation/bot conquests (#2949)
## Description:

Conquests are currently mixing all player types.

This is not ideal as people wonders why a 50 player game can lead to
hundred of kills.
Having separate records can also help with achievements and better
balancing.

This PR splits the conquests record into 3 categories: human, nations
and bots.

It is linked to this infra PR:
https://github.com/openfrontio/infra/pull/246

<img width="895" height="497" alt="image"
src="https://github.com/user-attachments/assets/66e49100-8114-4406-84ab-d9627355956d"
/>

While the recorded data make a distinction between bots/nations, it's
only displayed here as a single "bot" category.

## 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
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

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

IngloriousTom
2026-01-18 20:51:12 -08:00

299 lines
7.9 KiB
TypeScript

import { AllPlayersStats } from "../Schemas";
import {
ATTACK_INDEX_CANCEL,
ATTACK_INDEX_RECV,
ATTACK_INDEX_SENT,
BOAT_INDEX_ARRIVE,
BOAT_INDEX_CAPTURE,
BOAT_INDEX_DESTROY,
BOAT_INDEX_SENT,
BoatUnit,
BOMB_INDEX_INTERCEPT,
BOMB_INDEX_LAND,
BOMB_INDEX_LAUNCH,
GOLD_INDEX_STEAL,
GOLD_INDEX_TRADE,
GOLD_INDEX_TRAIN_OTHER,
GOLD_INDEX_TRAIN_SELF,
GOLD_INDEX_WAR,
GOLD_INDEX_WORK,
NukeType,
OTHER_INDEX_BUILT,
OTHER_INDEX_CAPTURE,
OTHER_INDEX_DESTROY,
OTHER_INDEX_LOST,
OTHER_INDEX_UPGRADE,
OtherUnitType,
PLAYER_INDEX_BOT,
PLAYER_INDEX_HUMAN,
PLAYER_INDEX_NATION,
PlayerStats,
unitTypeToBombUnit,
unitTypeToOtherUnit,
} from "../StatsSchemas";
import { Player, PlayerType, TerraNullius, UnitType } from "./Game";
import { Stats } from "./Stats";
type BigIntLike = bigint | number;
function _bigint(value: BigIntLike): bigint {
switch (typeof value) {
case "bigint":
return value;
case "number":
return BigInt(Math.floor(value));
}
}
const conquest_by_type: Record<PlayerType, number> = {
[PlayerType.Human]: PLAYER_INDEX_HUMAN,
[PlayerType.Nation]: PLAYER_INDEX_NATION,
[PlayerType.Bot]: PLAYER_INDEX_BOT,
};
export class StatsImpl implements Stats {
private readonly data: AllPlayersStats = {};
private _numMirvLaunched: bigint = 0n;
numMirvsLaunched(): bigint {
return this._numMirvLaunched;
}
getPlayerStats(player: Player): PlayerStats {
const clientID = player.clientID();
if (clientID === null) return undefined;
return this.data[clientID];
}
stats() {
return this.data;
}
private _makePlayerStats(player: Player): PlayerStats {
const clientID = player.clientID();
if (clientID === null) return undefined;
if (clientID in this.data) {
return this.data[clientID];
}
const data = {} satisfies PlayerStats;
this.data[clientID] = data;
return data;
}
private _addAttack(player: Player, index: number, value: BigIntLike) {
const p = this._makePlayerStats(player);
if (p === undefined) return;
p.attacks ??= [0n];
while (p.attacks.length <= index) p.attacks.push(0n);
p.attacks[index] += _bigint(value);
}
private _addBetrayal(player: Player, value: BigIntLike) {
const data = this._makePlayerStats(player);
if (data === undefined) return;
data.betrayals ??= 0n;
data.betrayals += _bigint(value);
}
private _addBoat(
player: Player,
type: BoatUnit,
index: number,
value: BigIntLike,
) {
const p = this._makePlayerStats(player);
if (p === undefined) return;
p.boats ??= { [type]: [0n] };
p.boats[type] ??= [0n];
while (p.boats[type].length <= index) p.boats[type].push(0n);
p.boats[type][index] += _bigint(value);
}
private _addBomb(
player: Player,
nukeType: NukeType,
index: number,
value: BigIntLike,
): void {
const type = unitTypeToBombUnit[nukeType];
const p = this._makePlayerStats(player);
if (p === undefined) return;
p.bombs ??= { [type]: [0n] };
p.bombs[type] ??= [0n];
while (p.bombs[type].length <= index) p.bombs[type].push(0n);
p.bombs[type][index] += _bigint(value);
}
private _addGold(player: Player, index: number, value: BigIntLike) {
const p = this._makePlayerStats(player);
if (p === undefined) return;
p.gold ??= [0n];
while (p.gold.length <= index) p.gold.push(0n);
p.gold[index] += _bigint(value);
}
private _addOtherUnit(
player: Player,
otherUnitType: OtherUnitType,
index: number,
value: BigIntLike,
) {
const type = unitTypeToOtherUnit[otherUnitType];
const p = this._makePlayerStats(player);
if (p === undefined) return;
p.units ??= { [type]: [0n] };
p.units[type] ??= [0n];
while (p.units[type].length <= index) p.units[type].push(0n);
p.units[type][index] += _bigint(value);
}
private _addConquest(player: Player, index: number) {
const p = this._makePlayerStats(player);
if (p === undefined) return;
p.conquests ??= [0n];
while (p.conquests.length <= index) p.conquests.push(0n);
p.conquests[index] += _bigint(1);
}
private _addPlayerKilled(player: Player, tick: number) {
const p = this._makePlayerStats(player);
if (p === undefined) return;
p.killedAt = _bigint(tick);
}
attack(
player: Player,
target: Player | TerraNullius,
troops: BigIntLike,
): void {
this._addAttack(player, ATTACK_INDEX_SENT, troops);
if (target.isPlayer()) {
this._addAttack(target, ATTACK_INDEX_RECV, troops);
}
}
attackCancel(
player: Player,
target: Player | TerraNullius,
troops: BigIntLike,
): void {
this._addAttack(player, ATTACK_INDEX_CANCEL, troops);
this._addAttack(player, ATTACK_INDEX_SENT, -troops);
if (target.isPlayer()) {
this._addAttack(target, ATTACK_INDEX_RECV, -troops);
}
}
betray(player: Player): void {
this._addBetrayal(player, 1);
}
boatSendTrade(player: Player, target: Player): void {
this._addBoat(player, "trade", BOAT_INDEX_SENT, 1);
}
boatArriveTrade(player: Player, target: Player, gold: BigIntLike): void {
this._addBoat(player, "trade", BOAT_INDEX_ARRIVE, 1);
this._addGold(player, GOLD_INDEX_TRADE, gold);
this._addGold(target, GOLD_INDEX_TRADE, gold);
}
boatCapturedTrade(player: Player, target: Player, gold: BigIntLike): void {
this._addBoat(player, "trade", BOAT_INDEX_CAPTURE, 1);
this._addGold(player, GOLD_INDEX_STEAL, gold);
}
boatDestroyTrade(player: Player, target: Player): void {
this._addBoat(player, "trade", BOAT_INDEX_DESTROY, 1);
}
boatSendTroops(
player: Player,
target: Player | TerraNullius,
troops: BigIntLike,
): void {
this._addBoat(player, "trans", BOAT_INDEX_SENT, 1);
}
boatArriveTroops(
player: Player,
target: Player | TerraNullius,
troops: BigIntLike,
): void {
this._addBoat(player, "trans", BOAT_INDEX_ARRIVE, 1);
}
boatDestroyTroops(player: Player, target: Player, troops: BigIntLike): void {
this._addBoat(player, "trans", BOAT_INDEX_DESTROY, 1);
}
bombLaunch(
player: Player,
target: Player | TerraNullius,
type: NukeType,
): void {
if (type === UnitType.MIRV) {
this._numMirvLaunched++;
}
this._addBomb(player, type, BOMB_INDEX_LAUNCH, 1);
}
bombLand(
player: Player,
target: Player | TerraNullius,
type: NukeType,
): void {
this._addBomb(player, type, BOMB_INDEX_LAND, 1);
}
bombIntercept(player: Player, type: NukeType, count: BigIntLike): void {
this._addBomb(player, type, BOMB_INDEX_INTERCEPT, count);
}
goldWork(player: Player, gold: BigIntLike): void {
this._addGold(player, GOLD_INDEX_WORK, gold);
}
goldWar(player: Player, captured: Player, gold: BigIntLike): void {
this._addGold(player, GOLD_INDEX_WAR, gold);
const conquestType = conquest_by_type[captured.type()];
if (conquestType !== undefined) {
this._addConquest(player, conquestType);
}
}
unitBuild(player: Player, type: OtherUnitType): void {
this._addOtherUnit(player, type, OTHER_INDEX_BUILT, 1);
}
unitCapture(player: Player, type: OtherUnitType): void {
this._addOtherUnit(player, type, OTHER_INDEX_CAPTURE, 1);
}
unitUpgrade(player: Player, type: OtherUnitType): void {
this._addOtherUnit(player, type, OTHER_INDEX_UPGRADE, 1);
}
unitDestroy(player: Player, type: OtherUnitType): void {
this._addOtherUnit(player, type, OTHER_INDEX_DESTROY, 1);
}
unitLose(player: Player, type: OtherUnitType): void {
this._addOtherUnit(player, type, OTHER_INDEX_LOST, 1);
}
playerKilled(player: Player, tick: number): void {
this._addPlayerKilled(player, tick);
}
trainSelfTrade(player: Player, gold: BigIntLike): void {
this._addGold(player, GOLD_INDEX_TRAIN_SELF, gold);
}
trainExternalTrade(player: Player, gold: BigIntLike): void {
this._addGold(player, GOLD_INDEX_TRAIN_OTHER, gold);
}
lobbyFillTime(fillTimeMs: number): void {}
}