Use bigint for gold (#1000)

This commit is contained in:
Scott Anderson
2025-06-02 15:48:24 -04:00
committed by 1brucben
parent cbecfac981
commit e763ade705
20 changed files with 128 additions and 101 deletions
+3 -9
View File
@@ -4,6 +4,7 @@ import {
AllPlayers,
Cell,
GameType,
Gold,
PlayerID,
PlayerType,
Tick,
@@ -94,15 +95,13 @@ export class SendEmojiIntentEvent implements GameEvent {
export class SendDonateGoldIntentEvent implements GameEvent {
constructor(
public readonly sender: PlayerView,
public readonly recipient: PlayerView,
public readonly gold: number | null,
public readonly gold: Gold | null,
) {}
}
export class SendDonateTroopsIntentEvent implements GameEvent {
constructor(
public readonly sender: PlayerView,
public readonly recipient: PlayerView,
public readonly troops: number | null,
) {}
@@ -110,7 +109,6 @@ export class SendDonateTroopsIntentEvent implements GameEvent {
export class SendQuickChatEvent implements GameEvent {
constructor(
public readonly sender: PlayerView,
public readonly recipient: PlayerView,
public readonly quickChatKey: string,
public readonly variables: { [key: string]: string },
@@ -119,17 +117,13 @@ export class SendQuickChatEvent implements GameEvent {
export class SendEmbargoIntentEvent implements GameEvent {
constructor(
public readonly sender: PlayerView,
public readonly target: PlayerView,
public readonly action: "start" | "stop",
) {}
}
export class CancelAttackIntentEvent implements GameEvent {
constructor(
public readonly playerID: PlayerID,
public readonly attackID: string,
) {}
constructor(public readonly attackID: string) {}
}
export class CancelBoatIntentEvent implements GameEvent {
+2 -1
View File
@@ -4,7 +4,8 @@ export function renderTroops(troops: number): string {
return renderNumber(troops / 10);
}
export function renderNumber(num: number): string {
export function renderNumber(num: number | bigint): string {
num = Number(num);
num = Math.max(num, 0);
if (num >= 10_000_000) {
+3 -3
View File
@@ -12,7 +12,7 @@ import samlauncherIcon from "../../../../resources/images/SamLauncherIconWhite.s
import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { Cell, PlayerActions, UnitType } from "../../../core/game/Game";
import { Cell, Gold, PlayerActions, UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView } from "../../../core/game/GameView";
import { BuildUnitIntentEvent } from "../../Transport";
@@ -314,13 +314,13 @@ export class BuildMenu extends LitElement implements Layer {
return unit[0].canBuild !== false;
}
private cost(item: BuildItemDisplay): number {
private cost(item: BuildItemDisplay): Gold {
for (const bu of this.playerActions?.buildableUnits ?? []) {
if (bu.type === item.unitType) {
return bu.cost;
}
}
return 0;
return 0n;
}
private count(item: BuildItemDisplay): string {
-1
View File
@@ -236,7 +236,6 @@ export class ChatModal extends LitElement {
this.eventBus.emit(
new SendQuickChatEvent(
this.sender,
this.recipient,
this.selectedQuickChatKey,
variables,
+4 -3
View File
@@ -2,6 +2,7 @@ import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { Gold } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { AttackRatioEvent } from "../../InputHandler";
import { SendSetTargetTroopRatioEvent } from "../../Transport";
@@ -46,10 +47,10 @@ export class ControlPanel extends LitElement implements Layer {
private _manpower: number = 0;
@state()
private _gold: number;
private _gold: Gold;
@state()
private _goldPerSecond: number;
private _goldPerSecond: Gold;
private _lastPopulationIncreaseRate: number;
@@ -124,7 +125,7 @@ export class ControlPanel extends LitElement implements Layer {
this._troops = player.troops();
this._workers = player.workers();
this.popRate = this.game.config().populationIncreaseRate(player) * 10;
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10;
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10n;
this.currentTroopRatio = player.troops() / player.population();
this.requestUpdate();
+1 -1
View File
@@ -380,7 +380,7 @@ export class EventsDisplay extends LitElement implements Layer {
emitCancelAttackIntent(id: string) {
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
this.eventBus.emit(new CancelAttackIntentEvent(myPlayer.id(), id));
this.eventBus.emit(new CancelAttackIntentEvent(id));
}
emitBoatCancelIntent(id: number) {
+3 -1
View File
@@ -89,7 +89,9 @@ export class Leaderboard extends LitElement implements Layer {
switch (this._sortKey) {
case "gold":
sorted = sorted.sort((a, b) => compare(a.gold(), b.gold()));
sorted = sorted.sort((a, b) =>
compare(Number(a.gold()), Number(b.gold())),
);
break;
case "troops":
sorted = sorted.sort((a, b) => compare(a.troops(), b.troops()));
+3 -4
View File
@@ -90,7 +90,6 @@ export class PlayerPanel extends LitElement implements Layer {
e.stopPropagation();
this.eventBus.emit(
new SendDonateTroopsIntentEvent(
myPlayer,
other,
myPlayer.troops() * this.uiState.attackRatio,
),
@@ -104,7 +103,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendDonateGoldIntentEvent(myPlayer, other, null));
this.eventBus.emit(new SendDonateGoldIntentEvent(other, null));
this.hide();
}
@@ -114,7 +113,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendEmbargoIntentEvent(myPlayer, other, "start"));
this.eventBus.emit(new SendEmbargoIntentEvent(other, "start"));
this.hide();
}
@@ -124,7 +123,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendEmbargoIntentEvent(myPlayer, other, "stop"));
this.eventBus.emit(new SendEmbargoIntentEvent(other, "stop"));
this.hide();
}
+1 -1
View File
@@ -58,7 +58,7 @@ export class TeamStats extends LitElement implements Layer {
this.teams = Object.entries(grouped)
.map(([teamStr, teamPlayers]) => {
let totalGold = 0;
let totalGold = 0n;
let totalTroops = 0;
let totalScoreSort = 0;
+1 -1
View File
@@ -50,7 +50,7 @@ export class TopBar extends LitElement implements Layer {
const popRate = this.game.config().populationIncreaseRate(myPlayer) * 10;
const maxPop = this.game.config().maxPopulation(myPlayer);
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10;
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10n;
return html`
<div
+1 -1
View File
@@ -240,7 +240,7 @@ export const EmbargoIntentSchema = BaseIntentSchema.extend({
export const DonateGoldIntentSchema = BaseIntentSchema.extend({
type: z.literal("donate_gold"),
recipient: ID,
gold: z.number().nullable(),
gold: z.bigint().nullable(),
});
export const DonateTroopIntentSchema = BaseIntentSchema.extend({
+1 -1
View File
@@ -84,7 +84,7 @@ export interface Config {
startManpower(playerInfo: PlayerInfo): number;
populationIncreaseRate(player: Player | PlayerView): number;
goldAdditionRate(player: Player | PlayerView): number;
goldAdditionRate(player: Player | PlayerView): Gold;
troopAdjustmentRate(player: Player): number;
attackTilesPerTick(
attckTroops: number,
+60 -47
View File
@@ -281,7 +281,7 @@ export class DefaultConfig implements Config {
return this._gameConfig.infiniteTroops;
}
tradeShipGold(dist: number): Gold {
return 10000 + 150 * Math.pow(dist, 1.1);
return BigInt(Math.floor(10000 + 150 * Math.pow(dist, 1.1)));
}
tradeShipSpawnRate(numberOfPorts: number): number {
return Math.round(10 * Math.pow(numberOfPorts, 0.38));
@@ -291,44 +291,49 @@ export class DefaultConfig implements Config {
switch (type) {
case UnitType.TransportShip:
return {
cost: () => 0,
cost: () => 0n,
territoryBound: false,
};
case UnitType.Warship:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0
: Math.min(
1_000_000,
(p.unitsIncludingConstruction(UnitType.Warship).length + 1) *
250_000,
? 0n
: BigInt(
Math.min(
1_000_000,
(p.unitsIncludingConstruction(UnitType.Warship).length +
1) *
250_000,
),
),
territoryBound: false,
maxHealth: 1000,
};
case UnitType.Shell:
return {
cost: () => 0,
cost: () => 0n,
territoryBound: false,
damage: 250,
};
case UnitType.SAMMissile:
return {
cost: () => 0,
cost: () => 0n,
territoryBound: false,
};
case UnitType.Port:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0
: Math.min(
1_000_000,
Math.pow(
2,
p.unitsIncludingConstruction(UnitType.Port).length,
) * 125_000,
? 0n
: BigInt(
Math.min(
1_000_000,
Math.pow(
2,
p.unitsIncludingConstruction(UnitType.Port).length,
) * 125_000,
),
),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
@@ -336,41 +341,43 @@ export class DefaultConfig implements Config {
case UnitType.AtomBomb:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold() ? 0 : 750_000,
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: 750_000n,
territoryBound: false,
};
case UnitType.HydrogenBomb:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0
: 5_000_000,
? 0n
: 5_000_000n,
territoryBound: false,
};
case UnitType.MIRV:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0
: 25_000_000,
? 0n
: 25_000_000n,
territoryBound: false,
};
case UnitType.MIRVWarhead:
return {
cost: () => 0,
cost: () => 0n,
territoryBound: false,
};
case UnitType.TradeShip:
return {
cost: () => 0,
cost: () => 0n,
territoryBound: false,
};
case UnitType.MissileSilo:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0
: 1_000_000,
? 0n
: 1_000_000n,
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 10 * 10,
};
@@ -378,12 +385,14 @@ export class DefaultConfig implements Config {
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0
: Math.min(
250_000,
(p.unitsIncludingConstruction(UnitType.DefensePost).length +
1) *
50_000,
? 0n
: BigInt(
Math.min(
250_000,
(p.unitsIncludingConstruction(UnitType.DefensePost).length +
1) *
50_000,
),
),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 5 * 10,
@@ -392,12 +401,14 @@ export class DefaultConfig implements Config {
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0
: Math.min(
3_000_000,
(p.unitsIncludingConstruction(UnitType.SAMLauncher).length +
1) *
1_500_000,
? 0n
: BigInt(
Math.min(
3_000_000,
(p.unitsIncludingConstruction(UnitType.SAMLauncher).length +
1) *
1_500_000,
),
),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 30 * 10,
@@ -406,20 +417,22 @@ export class DefaultConfig implements Config {
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0
: Math.min(
1_000_000,
Math.pow(
2,
p.unitsIncludingConstruction(UnitType.City).length,
) * 125_000,
? 0n
: BigInt(
Math.min(
1_000_000,
Math.pow(
2,
p.unitsIncludingConstruction(UnitType.City).length,
) * 125_000,
),
),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
};
case UnitType.Construction:
return {
cost: () => 0,
cost: () => 0n,
territoryBound: true,
};
default:
@@ -684,8 +697,8 @@ export class DefaultConfig implements Config {
return Math.min(totalPop + toAdd, max) - totalPop;
}
goldAdditionRate(player: Player): number {
return 0.08 * player.workers() ** 0.65;
goldAdditionRate(player: Player): bigint {
return BigInt(Math.floor(0.08 * player.workers() ** 0.65));
}
troopAdjustmentRate(player: Player): number {
+2 -1
View File
@@ -2,6 +2,7 @@ import { consolex } from "../Consolex";
import {
Execution,
Game,
Gold,
Player,
PlayerID,
Tick,
@@ -26,7 +27,7 @@ export class ConstructionExecution implements Execution {
private ticksUntilComplete: Tick;
private cost: number;
private cost: Gold;
constructor(
private ownerId: PlayerID,
+7 -5
View File
@@ -1,5 +1,5 @@
import { consolex } from "../Consolex";
import { Execution, Game, Player, PlayerID } from "../game/Game";
import { Execution, Game, Gold, Player, PlayerID } from "../game/Game";
export class DonateGoldExecution implements Execution {
private sender: Player;
@@ -10,7 +10,7 @@ export class DonateGoldExecution implements Execution {
constructor(
private senderID: PlayerID,
private recipientID: PlayerID,
private gold: number | null,
private gold: Gold | null,
) {}
init(mg: Game, ticks: number): void {
@@ -28,14 +28,16 @@ export class DonateGoldExecution implements Execution {
this.sender = mg.player(this.senderID);
this.recipient = mg.player(this.recipientID);
if (this.gold === null) {
this.gold = Math.round(this.sender.gold() / 3);
this.gold = this.sender.gold() / 3n;
}
}
tick(ticks: number): void {
if (this.gold === null) throw new Error("not initialized");
if (this.sender.canDonate(this.recipient)) {
this.sender.donateGold(this.recipient, this.gold);
if (
this.sender.canDonate(this.recipient) &&
this.sender.donateGold(this.recipient, this.gold)
) {
this.recipient.updateRelation(this.sender, 50);
} else {
consolex.warn(
+4 -2
View File
@@ -34,8 +34,10 @@ export class DonateTroopsExecution implements Execution {
tick(ticks: number): void {
if (this.troops === null) throw new Error("not initialized");
if (this.sender.canDonate(this.recipient)) {
this.sender.donateTroops(this.recipient, this.troops);
if (
this.sender.canDonate(this.recipient) &&
this.sender.donateTroops(this.recipient, this.troops)
) {
this.recipient.updateRelation(this.sender, 50);
} else {
consolex.warn(
+2 -1
View File
@@ -4,6 +4,7 @@ import {
Difficulty,
Execution,
Game,
Gold,
Nation,
Player,
PlayerID,
@@ -543,7 +544,7 @@ export class FakeHumanExecution implements Execution {
return null;
}
private cost(type: UnitType): number {
private cost(type: UnitType): Gold {
if (this.player === null) throw new Error("not initialized");
return this.mg.unitInfo(type).cost(this.player);
}
+5 -5
View File
@@ -12,7 +12,7 @@ import { Stats } from "./Stats";
export type PlayerID = string;
export type Tick = number;
export type Gold = number;
export type Gold = bigint;
export const AllPlayers = "AllPlayers" as const;
@@ -451,7 +451,7 @@ export interface Player {
troops(): number;
targetTroopRatio(): number;
addGold(toAdd: Gold): void;
removeGold(toRemove: Gold): void;
removeGold(toRemove: Gold): Gold;
addWorkers(toAdd: number): void;
removeWorkers(toRemove: number): void;
setTargetTroopRatio(target: number): void;
@@ -507,8 +507,8 @@ export interface Player {
// Donation
canDonate(recipient: Player): boolean;
donateTroops(recipient: Player, troops: number): void;
donateGold(recipient: Player, gold: number): void;
donateTroops(recipient: Player, troops: number): boolean;
donateGold(recipient: Player, gold: Gold): boolean;
// Embargo
hasEmbargoAgainst(other: Player): boolean;
@@ -620,7 +620,7 @@ export interface PlayerActions {
export interface BuildableUnit {
canBuild: TileRef | false;
type: UnitType;
cost: number;
cost: Gold;
}
export interface PlayerProfile {
+2 -1
View File
@@ -2,6 +2,7 @@ import { AllPlayersStats, ClientID } from "../Schemas";
import {
EmojiMessage,
GameUpdates,
Gold,
MessageType,
NameViewData,
PlayerID,
@@ -103,7 +104,7 @@ export interface PlayerUpdate {
playerType: PlayerType;
isAlive: boolean;
tilesOwned: number;
gold: number;
gold: Gold;
population: number;
totalPopulation: number;
workers: number;
+23 -12
View File
@@ -137,7 +137,7 @@ export class PlayerImpl implements Player {
playerType: this.type(),
isAlive: this.isAlive(),
tilesOwned: this.numTilesOwned(),
gold: Number(this._gold),
gold: this._gold,
population: this.population(),
totalPopulation: this.totalPopulation(),
workers: this.workers(),
@@ -540,9 +540,13 @@ export class PlayerImpl implements Player {
return true;
}
donateTroops(recipient: Player, troops: number): void {
donateTroops(recipient: Player, troops: number): boolean {
if (troops <= 0) return false;
const removed = this.removeTroops(troops);
if (removed === 0) return false;
recipient.addTroops(removed);
this.sentDonations.push(new Donation(recipient, this.mg.ticks()));
recipient.addTroops(this.removeTroops(troops));
this.mg.displayMessage(
`Sent ${renderTroops(troops)} troops to ${recipient.name()}`,
MessageType.INFO,
@@ -553,10 +557,16 @@ export class PlayerImpl implements Player {
MessageType.SUCCESS,
recipient.id(),
);
return true;
}
donateGold(recipient: Player, gold: number): void {
donateGold(recipient: Player, gold: Gold): boolean {
if (gold <= 0n) return false;
const removed = this.removeGold(gold);
if (removed === 0n) return false;
recipient.addGold(removed);
this.sentDonations.push(new Donation(recipient, this.mg.ticks()));
recipient.addGold(this.removeGold(gold));
this.mg.displayMessage(
`Sent ${renderNumber(gold)} gold to ${recipient.name()}`,
MessageType.INFO,
@@ -567,6 +577,7 @@ export class PlayerImpl implements Player {
MessageType.SUCCESS,
recipient.id(),
);
return true;
}
hasEmbargoAgainst(other: Player): boolean {
@@ -633,20 +644,20 @@ export class PlayerImpl implements Player {
}
gold(): Gold {
return Number(this._gold);
return this._gold;
}
addGold(toAdd: Gold): void {
this._gold += toInt(toAdd);
this._gold += toAdd;
}
removeGold(toRemove: Gold): number {
if (toRemove <= 1) {
return 0;
removeGold(toRemove: Gold): Gold {
if (toRemove <= 0n) {
return 0n;
}
const actualRemoved = minInt(this._gold, toInt(toRemove));
const actualRemoved = minInt(this._gold, toRemove);
this._gold -= actualRemoved;
return Number(actualRemoved);
return actualRemoved;
}
population(): number {