store values as bigints to prevent floating point drift

This commit is contained in:
Evan
2025-03-02 09:39:51 -08:00
parent 9b85651ad8
commit d726fd66b5
4 changed files with 55 additions and 31 deletions
+16
View File
@@ -306,3 +306,19 @@ export function generateID(): GameID {
); );
return nanoid(); return nanoid();
} }
export function toInt(num: number): bigint {
return BigInt(Math.floor(num));
}
export function maxInt(a: bigint, b: bigint): bigint {
return a > b ? a : b;
}
export function minInt(a: bigint, b: bigint): bigint {
return a < b ? a : b;
}
export function withinInt(num: bigint, min: bigint, max: bigint): bigint {
const atLeastMin = maxInt(num, min);
return minInt(atLeastMin, max);
}
+1 -1
View File
@@ -76,7 +76,7 @@ export class PlayerExecution implements Execution {
} }
const popInc = this.config.populationIncreaseRate(this.player); const popInc = this.config.populationIncreaseRate(this.player);
this.player.addWorkers(popInc * (1 - this.player.targetTroopRatio())); // (1 - this.player.targetTroopRatio())) this.player.addWorkers(popInc * (1 - this.player.targetTroopRatio()));
this.player.addTroops(popInc * this.player.targetTroopRatio()); this.player.addTroops(popInc * this.player.targetTroopRatio());
this.player.addGold(this.config.goldAdditionRate(this.player)); this.player.addGold(this.config.goldAdditionRate(this.player));
const adjustRate = this.config.troopAdjustmentRate(this.player); const adjustRate = this.config.troopAdjustmentRate(this.player);
+29 -25
View File
@@ -26,9 +26,12 @@ import {
assertNever, assertNever,
closestOceanShoreFromPlayer, closestOceanShoreFromPlayer,
distSortUnit, distSortUnit,
maxInt,
minInt,
simpleHash, simpleHash,
sourceDstOceanShore, sourceDstOceanShore,
targetTransportTile, targetTransportTile,
toInt,
within, within,
} from "../Util"; } from "../Util";
import { CellString, GameImpl } from "./GameImpl"; import { CellString, GameImpl } from "./GameImpl";
@@ -37,7 +40,6 @@ import { MessageType } from "./Game";
import { renderTroops } from "../../client/Utils"; import { renderTroops } from "../../client/Utils";
import { TerraNulliusImpl } from "./TerraNulliusImpl"; import { TerraNulliusImpl } from "./TerraNulliusImpl";
import { andFN, manhattanDistFN, TileRef } from "./GameMap"; import { andFN, manhattanDistFN, TileRef } from "./GameMap";
import { Emoji } from "discord.js";
import { AttackImpl } from "./AttackImpl"; import { AttackImpl } from "./AttackImpl";
interface Target { interface Target {
@@ -55,10 +57,12 @@ class Donation {
export class PlayerImpl implements Player { export class PlayerImpl implements Player {
public _lastTileChange: number = 0; public _lastTileChange: number = 0;
private _gold: Gold; private _gold: bigint;
private _troops: number; private _troops: bigint;
private _workers: number; private _workers: bigint;
private _targetTroopRatio: number = 1;
// 0 to 100
private _targetTroopRatio: bigint = 100n;
isTraitor_ = false; isTraitor_ = false;
@@ -88,14 +92,14 @@ export class PlayerImpl implements Player {
private mg: GameImpl, private mg: GameImpl,
private _smallID: number, private _smallID: number,
private readonly playerInfo: PlayerInfo, private readonly playerInfo: PlayerInfo,
startPopulation: number, startTroops: number,
) { ) {
this._flag = playerInfo.flag; this._flag = playerInfo.flag;
this._name = playerInfo.name; this._name = playerInfo.name;
this._targetTroopRatio = 1; this._targetTroopRatio = 100n;
this._troops = startPopulation * this._targetTroopRatio; this._troops = toInt(startTroops);
this._workers = startPopulation * (1 - this._targetTroopRatio); this._workers = 0n;
this._gold = 0; this._gold = 0n;
this._displayName = this._name; // processName(this._name) this._displayName = this._name; // processName(this._name)
} }
@@ -117,7 +121,7 @@ export class PlayerImpl implements Player {
playerType: this.type(), playerType: this.type(),
isAlive: this.isAlive(), isAlive: this.isAlive(),
tilesOwned: this.numTilesOwned(), tilesOwned: this.numTilesOwned(),
gold: this._gold, gold: Number(this._gold),
population: this.population(), population: this.population(),
workers: this.workers(), workers: this.workers(),
troops: this.troops(), troops: this.troops(),
@@ -234,7 +238,7 @@ export class PlayerImpl implements Player {
return true as const; return true as const;
} }
setTroops(troops: number) { setTroops(troops: number) {
this._troops = Math.floor(troops); this._troops = toInt(troops);
} }
conquer(tile: TileRef) { conquer(tile: TileRef) {
this.mg.conquer(this, tile); this.mg.conquer(this, tile);
@@ -503,11 +507,11 @@ export class PlayerImpl implements Player {
} }
gold(): Gold { gold(): Gold {
return this._gold; return Number(this._gold);
} }
addGold(toAdd: Gold): void { addGold(toAdd: Gold): void {
this._gold += toAdd; this._gold += toInt(toAdd);
} }
removeGold(toRemove: Gold): void { removeGold(toRemove: Gold): void {
@@ -516,24 +520,24 @@ export class PlayerImpl implements Player {
`Player ${this} does not enough gold (${toRemove} vs ${this._gold}))`, `Player ${this} does not enough gold (${toRemove} vs ${this._gold}))`,
); );
} }
this._gold -= toRemove; this._gold -= toInt(toRemove);
} }
population(): number { population(): number {
return this._troops + this._workers; return Number(this._troops + this._workers);
} }
workers(): number { workers(): number {
return Math.max(1, this._workers); return Math.max(1, Number(this._workers));
} }
addWorkers(toAdd: number): void { addWorkers(toAdd: number): void {
this._workers += toAdd; this._workers += toInt(toAdd);
} }
removeWorkers(toRemove: number): void { removeWorkers(toRemove: number): void {
this._workers = Math.max(1, this._workers - toRemove); this._workers = maxInt(1n, this._workers - toInt(toRemove));
} }
targetTroopRatio(): number { targetTroopRatio(): number {
return this._targetTroopRatio; return Number(this._targetTroopRatio) / 100;
} }
setTargetTroopRatio(target: number): void { setTargetTroopRatio(target: number): void {
@@ -542,11 +546,11 @@ export class PlayerImpl implements Player {
`invalid targetTroopRatio ${target} set on player ${PlayerImpl}`, `invalid targetTroopRatio ${target} set on player ${PlayerImpl}`,
); );
} }
this._targetTroopRatio = target; this._targetTroopRatio = toInt(target * 100);
} }
troops(): number { troops(): number {
return this._troops; return Number(this._troops);
} }
addTroops(troops: number): void { addTroops(troops: number): void {
@@ -554,15 +558,15 @@ export class PlayerImpl implements Player {
this.removeTroops(-1 * troops); this.removeTroops(-1 * troops);
return; return;
} }
this._troops += Math.floor(troops); this._troops += toInt(troops);
} }
removeTroops(troops: number): number { removeTroops(troops: number): number {
if (troops <= 1) { if (troops <= 1) {
return 0; return 0;
} }
const toRemove = Math.floor(Math.min(this._troops - 1, troops)); const toRemove = minInt(this._troops, toInt(troops));
this._troops -= toRemove; this._troops -= toRemove;
return toRemove; return Number(toRemove);
} }
captureUnit(unit: Unit): void { captureUnit(unit: Unit): void {
+9 -5
View File
@@ -1,7 +1,7 @@
import { MessageType } from "./Game"; import { MessageType } from "./Game";
import { UnitUpdate } from "./GameUpdates"; import { UnitUpdate } from "./GameUpdates";
import { GameUpdateType } from "./GameUpdates"; import { GameUpdateType } from "./GameUpdates";
import { simpleHash, within } from "../Util"; import { simpleHash, toInt, within, withinInt } from "../Util";
import { Unit, TerraNullius, UnitType, Player, UnitInfo } from "./Game"; import { Unit, TerraNullius, UnitType, Player, UnitInfo } from "./Game";
import { GameImpl } from "./GameImpl"; import { GameImpl } from "./GameImpl";
import { PlayerImpl } from "./PlayerImpl"; import { PlayerImpl } from "./PlayerImpl";
@@ -9,7 +9,7 @@ import { TileRef } from "./GameMap";
export class UnitImpl implements Unit { export class UnitImpl implements Unit {
private _active = true; private _active = true;
private _health: number; private _health: bigint;
private _lastTile: TileRef = null; private _lastTile: TileRef = null;
private _constructionType: UnitType = undefined; private _constructionType: UnitType = undefined;
@@ -37,7 +37,7 @@ export class UnitImpl implements Unit {
isActive: this._active, isActive: this._active,
pos: this._tile, pos: this._tile,
lastPos: this._lastTile, lastPos: this._lastTile,
health: this.hasHealth() ? this._health : undefined, health: this.hasHealth() ? Number(this._health) : undefined,
constructionType: this._constructionType, constructionType: this._constructionType,
}; };
} }
@@ -65,7 +65,7 @@ export class UnitImpl implements Unit {
return this._troops; return this._troops;
} }
health(): number { health(): number {
return this._health; return Number(this._health);
} }
hasHealth(): boolean { hasHealth(): boolean {
return this.info().maxHealth != undefined; return this.info().maxHealth != undefined;
@@ -94,7 +94,11 @@ export class UnitImpl implements Unit {
} }
modifyHealth(delta: number): void { modifyHealth(delta: number): void {
this._health = within(this._health + delta, 0, this.info().maxHealth ?? 1); this._health = withinInt(
this._health + toInt(delta),
0n,
toInt(this.info().maxHealth ?? 1),
);
} }
delete(displayMessage: boolean = true): void { delete(displayMessage: boolean = true): void {