Remove workers & troop ratio bar, only have troops (#1676)

## Description:

The troop/worker ratio bar is almost never changed. so remove it and the
entire concept of workers. Now there is just troops.

Now players get a consistent 1k/s gold.

## 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
- [x] I have read and accepted the CLA agreement (only required once).

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

evan
This commit is contained in:
evanpelle
2025-08-01 16:06:59 -07:00
committed by GitHub
parent cba4baccbd
commit 3b8a36166a
22 changed files with 57 additions and 325 deletions
-10
View File
@@ -35,7 +35,6 @@ export type Intent =
| EmojiIntent
| DonateGoldIntent
| DonateTroopsIntent
| TargetTroopRatioIntent
| BuildUnitIntent
| EmbargoIntent
| QuickChatIntent
@@ -59,9 +58,6 @@ export type EmojiIntent = z.infer<typeof EmojiIntentSchema>;
export type DonateGoldIntent = z.infer<typeof DonateGoldIntentSchema>;
export type DonateTroopsIntent = z.infer<typeof DonateTroopIntentSchema>;
export type EmbargoIntent = z.infer<typeof EmbargoIntentSchema>;
export type TargetTroopRatioIntent = z.infer<
typeof TargetTroopRatioIntentSchema
>;
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>;
export type UpgradeStructureIntent = z.infer<
typeof UpgradeStructureIntentSchema
@@ -314,11 +310,6 @@ export const DonateTroopIntentSchema = BaseIntentSchema.extend({
troops: z.number().nullable(),
});
export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({
type: z.literal("troop_ratio"),
ratio: z.number().min(0).max(1),
});
export const BuildUnitIntentSchema = BaseIntentSchema.extend({
type: z.literal("build_unit"),
unit: z.enum(UnitType),
@@ -378,7 +369,6 @@ const IntentSchema = z.discriminatedUnion("type", [
EmojiIntentSchema,
DonateGoldIntentSchema,
DonateTroopIntentSchema,
TargetTroopRatioIntentSchema,
BuildUnitIntentSchema,
UpgradeStructureIntentSchema,
EmbargoIntentSchema,
+4 -5
View File
@@ -89,9 +89,8 @@ export interface Config {
playerTeams(): TeamCountConfig;
startManpower(playerInfo: PlayerInfo): number;
populationIncreaseRate(player: Player | PlayerView): number;
troopIncreaseRate(player: Player | PlayerView): number;
goldAdditionRate(player: Player | PlayerView): Gold;
troopAdjustmentRate(player: Player): number;
attackTilesPerTick(
attckTroops: number,
attacker: Player,
@@ -114,8 +113,8 @@ export interface Config {
// When computing likelihood of trading for any given port, the X closest port
// are twice more likely to be selected. X is determined below.
proximityBonusPortsNb(totalPorts: number): number;
maxPopulation(player: Player | PlayerView): number;
cityPopulationIncrease(): number;
maxTroops(player: Player | PlayerView): number;
cityTroopIncrease(): number;
boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number;
shellLifetime(): number;
boatMaxNumber(): number;
@@ -162,7 +161,7 @@ export interface Config {
nukeType: NukeType,
humans: number,
tilesOwned: number,
maxPop: number,
maxTroops: number,
): number;
structureMinDist(): number;
isReplay(): boolean;
+21 -36
View File
@@ -273,7 +273,7 @@ export class DefaultConfig implements Config {
}
}
cityPopulationIncrease(): number {
cityTroopIncrease(): number {
return 250_000;
}
@@ -714,8 +714,8 @@ export class DefaultConfig implements Config {
return this.infiniteTroops() ? 1_000_000 : 25_000;
}
maxPopulation(player: Player | PlayerView): number {
const maxPop =
maxTroops(player: Player | PlayerView): number {
const maxTroops =
player.type() === PlayerType.Human && this.infiniteTroops()
? 1_000_000_000
: 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) +
@@ -723,34 +723,34 @@ export class DefaultConfig implements Config {
.units(UnitType.City)
.map((city) => city.level())
.reduce((a, b) => a + b, 0) *
this.cityPopulationIncrease();
this.cityTroopIncrease();
if (player.type() === PlayerType.Bot) {
return maxPop / 2;
return maxTroops / 2;
}
if (player.type() === PlayerType.Human) {
return maxPop;
return maxTroops;
}
switch (this._gameConfig.difficulty) {
case Difficulty.Easy:
return maxPop * 0.5;
return maxTroops * 0.5;
case Difficulty.Medium:
return maxPop * 1;
return maxTroops * 1;
case Difficulty.Hard:
return maxPop * 1.5;
return maxTroops * 1.5;
case Difficulty.Impossible:
return maxPop * 2;
return maxTroops * 2;
}
}
populationIncreaseRate(player: Player): number {
const max = this.maxPopulation(player);
troopIncreaseRate(player: Player): number {
const max = this.maxTroops(player);
let toAdd = 10 + Math.pow(player.population(), 0.73) / 4;
let toAdd = 10 + Math.pow(player.troops(), 0.73) / 4;
const ratio = 1 - player.population() / max;
const ratio = 1 - player.troops() / max;
toAdd *= ratio;
if (player.type() === PlayerType.Bot) {
@@ -774,26 +774,11 @@ export class DefaultConfig implements Config {
}
}
return Math.min(player.population() + toAdd, max) - player.population();
return Math.min(player.troops() + toAdd, max) - player.troops();
}
goldAdditionRate(player: Player): Gold {
return BigInt(Math.floor(0.045 * player.workers() ** 0.7));
}
troopAdjustmentRate(player: Player): number {
const maxDiff = this.maxPopulation(player) / 1000;
const target = player.population() * player.targetTroopRatio();
const diff = target - player.troops();
if (Math.abs(diff) < maxDiff) {
return diff;
}
const adjustment = maxDiff * Math.sign(diff);
// Can ramp down troops much faster
if (adjustment < 0) {
return adjustment * 5;
}
return adjustment;
return 100n;
}
nukeMagnitudes(unitType: UnitType): NukeMagnitude {
@@ -824,22 +809,22 @@ export class DefaultConfig implements Config {
return 80;
}
// Humans can be population, soldiers attacking, soldiers in boat etc.
// Humans can be soldiers, soldiers attacking, soldiers in boat etc.
nukeDeathFactor(
nukeType: NukeType,
humans: number,
tilesOwned: number,
maxPop: number,
maxTroops: number,
): number {
if (nukeType !== UnitType.MIRVWarhead) {
return (5 * humans) / Math.max(1, tilesOwned);
}
const targetPop = 0.03 * maxPop;
const excessPop = Math.max(0, humans - targetPop);
const targetTroops = 0.03 * maxTroops;
const excessTroops = Math.max(0, humans - targetTroops);
const scalingFactor = 500;
const steepness = 2;
const normalizedExcess = excessPop / maxPop;
const normalizedExcess = excessTroops / maxTroops;
return scalingFactor * (1 - Math.exp(-steepness * normalizedExcess));
}
-4
View File
@@ -74,10 +74,6 @@ export class DevConfig extends DefaultConfig {
// return 1
// }
// populationIncreaseRate(player: Player): number {
// return this.maxPopulation(player)
// }
// boatMaxDistance(): number {
// return 5000
// }
-1
View File
@@ -31,7 +31,6 @@ export class BotExecution implements Execution {
init(mg: Game) {
this.mg = mg;
this.bot.setTargetTroopRatio(0.7);
}
tick(ticks: number) {
+1 -1
View File
@@ -21,7 +21,7 @@ export class DonateTroopsExecution implements Execution {
this.recipient = mg.player(this.recipientID);
this.troops ??= mg.config().defaultDonationAmount(this.sender);
const maxDonation =
mg.config().maxPopulation(this.recipient) - this.recipient.population();
mg.config().maxTroops(this.recipient) - this.recipient.troops();
this.troops = Math.min(this.troops, maxDonation);
}
-3
View File
@@ -20,7 +20,6 @@ import { MoveWarshipExecution } from "./MoveWarshipExecution";
import { NoOpExecution } from "./NoOpExecution";
import { QuickChatExecution } from "./QuickChatExecution";
import { RetreatExecution } from "./RetreatExecution";
import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution";
import { SpawnExecution } from "./SpawnExecution";
import { TargetPlayerExecution } from "./TargetPlayerExecution";
import { TransportShipExecution } from "./TransportShipExecution";
@@ -98,8 +97,6 @@ export class Executor {
);
case "donate_gold":
return new DonateGoldExecution(player, intent.recipient, intent.gold);
case "troop_ratio":
return new SetTargetTroopRatioExecution(player, intent.ratio);
case "embargo":
return new EmbargoExecution(player, intent.targetID, intent.action);
case "build_unit":
-7
View File
@@ -153,13 +153,6 @@ export class FakeHumanExecution implements Execution {
return;
}
if (
this.player.troops() > 100_000 &&
this.player.targetTroopRatio() > 0.7
) {
this.player.setTargetTroopRatio(0.7);
}
this.updateRelationsFromEmbargos();
this.behavior.handleAllianceRequests();
this.handleUnits();
+5 -15
View File
@@ -203,8 +203,8 @@ export class NukeExecution implements Execution {
const toDestroy = this.tilesToDestroy();
this.maybeBreakAlliances(toDestroy);
const maxPop = this.target().isPlayer()
? this.mg.config().maxPopulation(this.target() as Player)
const maxTroops = this.target().isPlayer()
? this.mg.config().maxTroops(this.target() as Player)
: 1;
for (const tile of toDestroy) {
@@ -218,17 +218,7 @@ export class NukeExecution implements Execution {
this.nukeType,
owner.troops(),
owner.numTilesOwned(),
maxPop,
),
);
owner.removeWorkers(
this.mg
.config()
.nukeDeathFactor(
this.nukeType,
owner.workers(),
owner.numTilesOwned(),
maxPop,
maxTroops,
),
);
owner.outgoingAttacks().forEach((attack) => {
@@ -239,7 +229,7 @@ export class NukeExecution implements Execution {
this.nukeType,
attack.troops(),
owner.numTilesOwned(),
maxPop,
maxTroops,
) ?? 0;
attack.setTroops(attack.troops() - deaths);
});
@@ -251,7 +241,7 @@ export class NukeExecution implements Execution {
this.nukeType,
attack.troops(),
owner.numTilesOwned(),
maxPop,
maxTroops,
) ?? 0;
attack.setTroops(attack.troops() - deaths);
});
+2 -7
View File
@@ -56,19 +56,14 @@ export class PlayerExecution implements Execution {
return;
}
const popInc = this.config.populationIncreaseRate(this.player);
this.player.addWorkers(popInc * (1 - this.player.targetTroopRatio()));
this.player.addTroops(popInc * this.player.targetTroopRatio());
const troopInc = this.config.troopIncreaseRate(this.player);
this.player.addTroops(troopInc);
const goldFromWorkers = this.config.goldAdditionRate(this.player);
this.player.addGold(goldFromWorkers);
// Record stats
this.mg.stats().goldWork(this.player, goldFromWorkers);
const adjustRate = this.config.troopAdjustmentRate(this.player);
this.player.addTroops(adjustRate);
this.player.removeWorkers(adjustRate);
const alliances = Array.from(this.player.alliances());
for (const alliance of alliances) {
if (alliance.expiresAt() <= this.mg.ticks()) {
@@ -1,31 +0,0 @@
import { Execution, Game, Player } from "../game/Game";
export class SetTargetTroopRatioExecution implements Execution {
private active = true;
constructor(
private player: Player,
private targetTroopsRatio: number,
) {}
init(mg: Game, ticks: number): void {}
tick(ticks: number): void {
if (this.targetTroopsRatio < 0 || this.targetTroopsRatio > 1) {
console.warn(
`target troop ratio of ${this.targetTroopsRatio} for player ${this.player} invalid`,
);
} else {
this.player.setTargetTroopRatio(this.targetTroopsRatio);
}
this.active = false;
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+3 -4
View File
@@ -59,8 +59,8 @@ export class BotBehavior {
}
private hasSufficientTroops(): boolean {
const maxPop = this.game.config().maxPopulation(this.player);
const ratio = this.player.population() / maxPop;
const maxTroops = this.game.config().maxTroops(this.player);
const ratio = this.player.troops() / maxTroops;
return ratio >= this.triggerRatio;
}
@@ -208,8 +208,7 @@ export class BotBehavior {
sendAttack(target: Player | TerraNullius) {
if (target.isPlayer() && this.player.isOnSameTeam(target)) return;
const maxPop = this.game.config().maxPopulation(this.player);
const maxTroops = maxPop * this.player.targetTroopRatio();
const maxTroops = this.game.config().maxTroops(this.player);
const reserveRatio = target.isPlayer()
? this.reserveRatio
: this.expandRatio;
+2 -8
View File
@@ -510,17 +510,11 @@ export interface Player {
conquer(tile: TileRef): void;
relinquish(tile: TileRef): void;
// Resources & Population
// Resources & Troops
gold(): Gold;
population(): number;
workers(): number;
troops(): number;
targetTroopRatio(): number;
addGold(toAdd: Gold, tile?: TileRef): void;
removeGold(toRemove: Gold): Gold;
addWorkers(toAdd: number): void;
removeWorkers(toRemove: number): void;
setTargetTroopRatio(target: number): void;
troops(): number;
setTroops(troops: number): void;
addTroops(troops: number): void;
removeTroops(troops: number): number;
-3
View File
@@ -154,10 +154,7 @@ export interface PlayerUpdate {
isDisconnected: boolean;
tilesOwned: number;
gold: Gold;
population: number;
workers: number;
troops: number;
targetTroopRatio: number;
allies: number[];
embargoes: Set<PlayerID>;
isTraitor: boolean;
-9
View File
@@ -291,15 +291,6 @@ export class PlayerView {
gold(): Gold {
return this.data.gold;
}
population(): number {
return this.data.population;
}
workers(): number {
return this.data.workers;
}
targetTroopRatio(): number {
return this.data.targetTroopRatio;
}
troops(): number {
return this.data.troops;
+1 -37
View File
@@ -4,7 +4,6 @@ import { ClientID } from "../Schemas";
import {
assertNever,
distSortUnit,
maxInt,
minInt,
simpleHash,
toInt,
@@ -72,10 +71,6 @@ export class PlayerImpl implements Player {
private _gold: bigint;
private _troops: bigint;
private _workers: bigint;
// 0 to 100
private _targetTroopRatio: bigint;
markedTraitorTick = -1;
@@ -115,9 +110,7 @@ export class PlayerImpl implements Player {
private readonly _team: Team | null,
) {
this._name = sanitizeUsername(playerInfo.name);
this._targetTroopRatio = 95n;
this._troops = toInt(startTroops);
this._workers = 0n;
this._gold = 0n;
this._displayName = this._name; // processName(this._name)
this._pseudo_random = new PseudoRandom(simpleHash(this.playerInfo.id));
@@ -144,10 +137,7 @@ export class PlayerImpl implements Player {
isDisconnected: this.isDisconnected(),
tilesOwned: this.numTilesOwned(),
gold: this._gold,
population: this.population(),
workers: this.workers(),
troops: this.troops(),
targetTroopRatio: this.targetTroopRatio(),
allies: this.alliances().map((a) => a.other(this).smallID()),
embargoes: new Set([...this.embargoes.keys()].map((p) => p.toString())),
isTraitor: this.isTraitor(),
@@ -731,32 +721,6 @@ export class PlayerImpl implements Player {
return actualRemoved;
}
population(): number {
return Number(this._troops + this._workers);
}
workers(): number {
return Math.max(1, Number(this._workers));
}
addWorkers(toAdd: number): void {
this._workers += toInt(toAdd);
}
removeWorkers(toRemove: number): void {
this._workers = maxInt(1n, this._workers - toInt(toRemove));
}
targetTroopRatio(): number {
return Number(this._targetTroopRatio) / 100;
}
setTargetTroopRatio(target: number): void {
if (target < 0 || target > 1) {
throw new Error(
`invalid targetTroopRatio ${target} set on player ${PlayerImpl}`,
);
}
this._targetTroopRatio = toInt(target * 100);
}
troops(): number {
return Number(this._troops);
}
@@ -1051,7 +1015,7 @@ export class PlayerImpl implements Player {
hash(): number {
return (
simpleHash(this.id()) * (this.population() + this.numTilesOwned()) +
simpleHash(this.id()) * (this.troops() + this.numTilesOwned()) +
this._units.reduce((acc, unit) => acc + unit.hash(), 0)
);
}