mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:20:47 +00:00
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
This commit is contained in:
@@ -233,7 +233,8 @@
|
||||
"naval_trade": "Tradeship",
|
||||
"conquest_gold": "Conquered player gold",
|
||||
"stolen_gold": "Stolen with warships",
|
||||
"num_of_conquests": "Number of conquered players",
|
||||
"num_of_conquests_humans": "Player kills",
|
||||
"num_of_conquests_bots": "Bot kills",
|
||||
"duration": "Duration",
|
||||
"survival_time": "Survival time",
|
||||
"war": "War",
|
||||
|
||||
@@ -5,10 +5,14 @@ import {
|
||||
GOLD_INDEX_TRAIN_OTHER,
|
||||
GOLD_INDEX_TRAIN_SELF,
|
||||
GOLD_INDEX_WAR,
|
||||
PLAYER_INDEX_BOT,
|
||||
PLAYER_INDEX_HUMAN,
|
||||
PLAYER_INDEX_NATION,
|
||||
} from "../../../../core/StatsSchemas";
|
||||
|
||||
export enum RankType {
|
||||
Conquests = "Conquests",
|
||||
ConquestHumans = "ConquestHumans",
|
||||
ConquestBots = "ConquestBots",
|
||||
Atoms = "Atoms",
|
||||
Hydros = "Hydros",
|
||||
MIRV = "MIRV",
|
||||
@@ -27,7 +31,7 @@ export interface PlayerInfo {
|
||||
tag?: string;
|
||||
killedAt?: number;
|
||||
gold: bigint[];
|
||||
conquests: number;
|
||||
conquests: bigint[];
|
||||
flag?: string;
|
||||
winner: boolean;
|
||||
atoms: number;
|
||||
@@ -79,12 +83,13 @@ export class Ranking {
|
||||
username = match[2];
|
||||
}
|
||||
const gold = (stats.gold ?? []).map((v) => BigInt(v ?? 0));
|
||||
const conquests = (stats.conquests ?? []).map((v) => BigInt(v ?? 0));
|
||||
players[player.clientID] = {
|
||||
id: player.clientID,
|
||||
rawUsername: player.username,
|
||||
username,
|
||||
tag: player.clanTag,
|
||||
conquests: Number(stats.conquests) || 0,
|
||||
conquests,
|
||||
flag: player.cosmetics?.flag ?? undefined,
|
||||
killedAt: stats.killedAt !== null ? Number(stats.killedAt) : undefined,
|
||||
gold,
|
||||
@@ -125,8 +130,13 @@ export class Ranking {
|
||||
return (player.killedAt / Math.max(this.duration, 1)) * 10;
|
||||
}
|
||||
return 100;
|
||||
case RankType.Conquests:
|
||||
return player.conquests;
|
||||
case RankType.ConquestHumans:
|
||||
return Number(player.conquests[PLAYER_INDEX_HUMAN] ?? 0n);
|
||||
case RankType.ConquestBots:
|
||||
return (
|
||||
Number(player.conquests[PLAYER_INDEX_BOT] ?? 0n) +
|
||||
Number(player.conquests[PLAYER_INDEX_NATION] ?? 0n)
|
||||
);
|
||||
case RankType.Atoms:
|
||||
return player.atoms;
|
||||
case RankType.Hydros:
|
||||
|
||||
@@ -63,7 +63,8 @@ export class PlayerRow extends LitElement {
|
||||
private renderPlayerInfo() {
|
||||
switch (this.rankType) {
|
||||
case RankType.Lifetime:
|
||||
case RankType.Conquests:
|
||||
case RankType.ConquestHumans:
|
||||
case RankType.ConquestBots:
|
||||
return this.renderScoreAsBar();
|
||||
case RankType.Atoms:
|
||||
case RankType.Hydros:
|
||||
|
||||
@@ -10,19 +10,25 @@ const economyRankings = new Set([
|
||||
RankType.NavalTrade,
|
||||
RankType.TrainTrade,
|
||||
]);
|
||||
const tradeRankings = new Set([RankType.NavalTrade, RankType.TrainTrade]);
|
||||
const bombRankings = new Set([RankType.Atoms, RankType.Hydros, RankType.MIRV]);
|
||||
const warRankings = new Set([
|
||||
RankType.Conquests,
|
||||
RankType.ConquestHumans,
|
||||
RankType.ConquestBots,
|
||||
RankType.Atoms,
|
||||
RankType.Hydros,
|
||||
RankType.MIRV,
|
||||
]);
|
||||
const tradeRankings = new Set([RankType.NavalTrade, RankType.TrainTrade]);
|
||||
const bombRankings = new Set([RankType.Atoms, RankType.Hydros, RankType.MIRV]);
|
||||
const conquestRankings = new Set([
|
||||
RankType.ConquestHumans,
|
||||
RankType.ConquestBots,
|
||||
]);
|
||||
|
||||
const isEconomyRanking = (t: RankType) => economyRankings.has(t);
|
||||
const isTradeRanking = (t: RankType) => tradeRankings.has(t);
|
||||
const isBombRanking = (t: RankType) => bombRankings.has(t);
|
||||
const isWarRanking = (t: RankType) => warRankings.has(t);
|
||||
const isConquestRanking = (t: RankType) => conquestRankings.has(t);
|
||||
|
||||
@customElement("ranking-controls")
|
||||
export class RankingControls extends LitElement {
|
||||
@@ -41,7 +47,7 @@ export class RankingControls extends LitElement {
|
||||
"game_info_modal.duration",
|
||||
)}
|
||||
${this.renderButton(
|
||||
RankType.Conquests,
|
||||
RankType.ConquestHumans,
|
||||
isWarRanking(this.rankType),
|
||||
"game_info_modal.war",
|
||||
)}
|
||||
@@ -78,8 +84,8 @@ export class RankingControls extends LitElement {
|
||||
"game_info_modal.bombs",
|
||||
)}
|
||||
${this.renderSubButton(
|
||||
RankType.Conquests,
|
||||
this.rankType === RankType.Conquests,
|
||||
RankType.ConquestHumans,
|
||||
isConquestRanking(this.rankType),
|
||||
"game_info_modal.conquests",
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export class RankingHeader extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<li
|
||||
class="text-lg border-white/5 bg-white/[0.02] text-white/60 text-xs uppercase tracking-wider relative pt-2 pb-2 pr-5 pl-5 flex justify-between items-center"
|
||||
class="h-[30px] text-lg border-white/5 bg-white/[0.02] text-white/60 text-xs uppercase tracking-wider relative pt-2 pb-2 pr-5 pl-5 flex justify-between items-center"
|
||||
>
|
||||
${this.renderHeaderContent()}
|
||||
</li>
|
||||
@@ -27,10 +27,21 @@ export class RankingHeader extends LitElement {
|
||||
return html`<div class="w-full">
|
||||
${translateText("game_info_modal.survival_time")}
|
||||
</div>`;
|
||||
case RankType.Conquests:
|
||||
return html`<div class="w-full">
|
||||
${translateText("game_info_modal.num_of_conquests")}
|
||||
</div>`;
|
||||
case RankType.ConquestHumans:
|
||||
case RankType.ConquestBots:
|
||||
return html`
|
||||
<div class="flex justify-between sm:px-17.5 w-full">
|
||||
${this.renderMultipleChoiceHeaderButton(
|
||||
translateText("game_info_modal.num_of_conquests_humans"),
|
||||
RankType.ConquestHumans,
|
||||
)}
|
||||
/
|
||||
${this.renderMultipleChoiceHeaderButton(
|
||||
translateText("game_info_modal.num_of_conquests_bots"),
|
||||
RankType.ConquestBots,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
case RankType.Atoms:
|
||||
case RankType.Hydros:
|
||||
case RankType.MIRV:
|
||||
|
||||
@@ -149,7 +149,7 @@ export class PlayerStatsTreeView extends LitElement {
|
||||
attacks: this.mergeStatArrays(base.attacks, next.attacks),
|
||||
betrayals: this.mergeStatValue(base.betrayals, next.betrayals),
|
||||
killedAt: this.mergeStatValue(base.killedAt, next.killedAt),
|
||||
conquests: this.mergeStatValue(base.conquests, next.conquests),
|
||||
conquests: this.mergeStatArrays(base.conquests, next.conquests),
|
||||
boats: this.mergeStatRecord(base.boats, next.boats),
|
||||
bombs: this.mergeStatRecord(base.bombs, next.bombs),
|
||||
gold: this.mergeStatArrays(base.gold, next.gold),
|
||||
@@ -203,7 +203,7 @@ export class PlayerStatsTreeView extends LitElement {
|
||||
attacks: stats.attacks ? [...stats.attacks] : undefined,
|
||||
betrayals: stats.betrayals,
|
||||
killedAt: stats.killedAt,
|
||||
conquests: stats.conquests,
|
||||
conquests: stats.conquests ? [...stats.conquests] : undefined,
|
||||
boats: stats.boats ? { ...stats.boats } : undefined,
|
||||
bombs: stats.bombs ? { ...stats.bombs } : undefined,
|
||||
gold: stats.gold ? [...stats.gold] : undefined,
|
||||
|
||||
@@ -62,6 +62,11 @@ export const ATTACK_INDEX_SENT = 0; // Outgoing attack troops
|
||||
export const ATTACK_INDEX_RECV = 1; // Incmoing attack troops
|
||||
export const ATTACK_INDEX_CANCEL = 2; // Cancelled attack troops
|
||||
|
||||
// Player types
|
||||
export const PLAYER_INDEX_HUMAN = 0;
|
||||
export const PLAYER_INDEX_NATION = 1;
|
||||
export const PLAYER_INDEX_BOT = 2;
|
||||
|
||||
// Boats
|
||||
export const BOAT_INDEX_SENT = 0; // Boats launched
|
||||
export const BOAT_INDEX_ARRIVE = 1; // Boats arrived
|
||||
@@ -102,7 +107,7 @@ export const PlayerStatsSchema = z
|
||||
attacks: AtLeastOneNumberSchema.optional(),
|
||||
betrayals: BigIntStringSchema.optional(),
|
||||
killedAt: BigIntStringSchema.optional(),
|
||||
conquests: BigIntStringSchema.optional(),
|
||||
conquests: AtLeastOneNumberSchema.optional(),
|
||||
boats: z.partialRecord(BoatUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
bombs: z.partialRecord(BombUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
gold: AtLeastOneNumberSchema.optional(),
|
||||
|
||||
@@ -24,11 +24,14 @@ import {
|
||||
OTHER_INDEX_LOST,
|
||||
OTHER_INDEX_UPGRADE,
|
||||
OtherUnitType,
|
||||
PLAYER_INDEX_BOT,
|
||||
PLAYER_INDEX_HUMAN,
|
||||
PLAYER_INDEX_NATION,
|
||||
PlayerStats,
|
||||
unitTypeToBombUnit,
|
||||
unitTypeToOtherUnit,
|
||||
} from "../StatsSchemas";
|
||||
import { Player, TerraNullius, UnitType } from "./Game";
|
||||
import { Player, PlayerType, TerraNullius, UnitType } from "./Game";
|
||||
import { Stats } from "./Stats";
|
||||
|
||||
type BigIntLike = bigint | number;
|
||||
@@ -41,6 +44,12 @@ function _bigint(value: BigIntLike): bigint {
|
||||
}
|
||||
}
|
||||
|
||||
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 = {};
|
||||
|
||||
@@ -138,14 +147,12 @@ export class StatsImpl implements Stats {
|
||||
p.units[type][index] += _bigint(value);
|
||||
}
|
||||
|
||||
private _addConquest(player: Player) {
|
||||
private _addConquest(player: Player, index: number) {
|
||||
const p = this._makePlayerStats(player);
|
||||
if (p === undefined) return;
|
||||
if (p.conquests === undefined) {
|
||||
p.conquests = _bigint(1);
|
||||
} else {
|
||||
p.conquests += _bigint(1);
|
||||
}
|
||||
p.conquests ??= [0n];
|
||||
while (p.conquests.length <= index) p.conquests.push(0n);
|
||||
p.conquests[index] += _bigint(1);
|
||||
}
|
||||
|
||||
private _addPlayerKilled(player: Player, tick: number) {
|
||||
@@ -249,7 +256,10 @@ export class StatsImpl implements Stats {
|
||||
|
||||
goldWar(player: Player, captured: Player, gold: BigIntLike): void {
|
||||
this._addGold(player, GOLD_INDEX_WAR, gold);
|
||||
this._addConquest(player);
|
||||
const conquestType = conquest_by_type[captured.type()];
|
||||
if (conquestType !== undefined) {
|
||||
this._addConquest(player, conquestType);
|
||||
}
|
||||
}
|
||||
|
||||
unitBuild(player: Player, type: OtherUnitType): void {
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("Ranking class", () => {
|
||||
cosmetics: { flag: "USA" },
|
||||
stats: {
|
||||
units: { port: [2n, 0n, 0n, 2n] },
|
||||
conquests: 5n,
|
||||
conquests: [5n],
|
||||
gold: [0n, 100n, 20n, 0n, 15n, 5n], // total 140
|
||||
bombs: {
|
||||
abomb: [1n],
|
||||
@@ -71,7 +71,7 @@ describe("Ranking class", () => {
|
||||
username: "Bob",
|
||||
stats: {
|
||||
units: { city: [2n, 0n, 0n, 2n] },
|
||||
conquests: 8n,
|
||||
conquests: [8n],
|
||||
gold: [0n, 50n, 10n, 5n], // total 65, no train trade
|
||||
bombs: {
|
||||
abomb: [0n],
|
||||
@@ -86,7 +86,7 @@ describe("Ranking class", () => {
|
||||
username: "Charlie",
|
||||
stats: {
|
||||
// no units, but has conquests/killedAt to count as played
|
||||
conquests: 8n,
|
||||
conquests: [8n],
|
||||
killedAt: BigInt(600),
|
||||
gold: [0n, 10n, 2n, 10n, 0n, 5n], // total 27
|
||||
bombs: {},
|
||||
@@ -110,21 +110,21 @@ describe("Ranking class", () => {
|
||||
|
||||
test("summarizes players correctly", () => {
|
||||
const r = new Ranking(makeSession());
|
||||
const players = r.sortedBy(RankType.Conquests);
|
||||
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).toBe(5);
|
||||
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.Conquests).find((p) => p.id === "p2")!;
|
||||
const p2 = r.sortedBy(RankType.ConquestHumans).find((p) => p.id === "p2")!;
|
||||
expect(p2.winner).toBe(true);
|
||||
});
|
||||
|
||||
@@ -157,7 +157,7 @@ describe("Ranking class", () => {
|
||||
|
||||
test("lifetime score is percentage of duration", () => {
|
||||
const r = new Ranking(makeSession());
|
||||
const p3 = r.sortedBy(RankType.Conquests).find((p) => p.id === "p3")!;
|
||||
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);
|
||||
});
|
||||
@@ -170,7 +170,7 @@ describe("Ranking class", () => {
|
||||
|
||||
test("winners should be ahead of players with same score", () => {
|
||||
const r = new Ranking(makeSession());
|
||||
const sortedPlayers = r.sortedBy(RankType.Conquests);
|
||||
const sortedPlayers = r.sortedBy(RankType.ConquestHumans);
|
||||
expect(sortedPlayers[0].id).toBe("p2"); // p2 & p3 same score but winner first
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -162,14 +162,14 @@ describe("Stats", () => {
|
||||
expect(stats.stats()).toStrictEqual({
|
||||
client1: {
|
||||
gold: [0n, 1n],
|
||||
conquests: 1n,
|
||||
conquests: [1n],
|
||||
},
|
||||
});
|
||||
stats.goldWar(player1, player2, 1);
|
||||
expect(stats.stats()).toStrictEqual({
|
||||
client1: {
|
||||
gold: [0n, 2n],
|
||||
conquests: 2n,
|
||||
conquests: [2n],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user