mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
reduce train gold after each city (#3400)
## Description: Now that cities snap to existing rails, it's possible to line up dozens of cities in a row, producing way too much gold. This PR reduces the gold after each stop to prevent that. Gold only starts decreasing after the 3rd city. ## 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: evan
This commit is contained in:
@@ -132,7 +132,10 @@ export interface Config {
|
||||
tradeShipSpawnRejections: number,
|
||||
numTradeShips: number,
|
||||
): number;
|
||||
trainGold(rel: "self" | "team" | "ally" | "other"): Gold;
|
||||
trainGold(
|
||||
rel: "self" | "team" | "ally" | "other",
|
||||
citiesVisited: number,
|
||||
): Gold;
|
||||
trainSpawnRate(numPlayerFactories: number): number;
|
||||
trainStationMinRange(): number;
|
||||
trainStationMaxRange(): number;
|
||||
|
||||
@@ -20,7 +20,7 @@ import { PlayerView } from "../game/GameView";
|
||||
import { UserSettings } from "../game/UserSettings";
|
||||
import { GameConfig, GameID, TeamCountConfig } from "../Schemas";
|
||||
import { NukeType } from "../StatsSchemas";
|
||||
import { assertNever, sigmoid, simpleHash, within } from "../Util";
|
||||
import { assertNever, sigmoid, simpleHash, toInt, within } from "../Util";
|
||||
import { Config, GameEnv, NukeMagnitude, ServerConfig, Theme } from "./Config";
|
||||
import { Env } from "./Env";
|
||||
import { PastelTheme } from "./PastelTheme";
|
||||
@@ -273,22 +273,28 @@ export class DefaultConfig implements Config {
|
||||
// expected number of trains = numPlayerFactories / trainSpawnRate(numPlayerFactories)
|
||||
return (numPlayerFactories + 10) * 18;
|
||||
}
|
||||
trainGold(rel: "self" | "team" | "ally" | "other"): Gold {
|
||||
const multiplier = this.goldMultiplier();
|
||||
let baseGold: bigint;
|
||||
trainGold(
|
||||
rel: "self" | "team" | "ally" | "other",
|
||||
citiesVisited: number,
|
||||
): Gold {
|
||||
// No penalty for the first 3 cities.
|
||||
citiesVisited = Math.max(0, citiesVisited - 2);
|
||||
let baseGold: number;
|
||||
switch (rel) {
|
||||
case "ally":
|
||||
baseGold = 35_000n;
|
||||
baseGold = 35_000;
|
||||
break;
|
||||
case "team":
|
||||
case "other":
|
||||
baseGold = 25_000n;
|
||||
baseGold = 25_000;
|
||||
break;
|
||||
case "self":
|
||||
baseGold = 10_000n;
|
||||
baseGold = 10_000;
|
||||
break;
|
||||
}
|
||||
return BigInt(Math.floor(Number(baseGold) * multiplier));
|
||||
const distPenalty = citiesVisited * 5_000;
|
||||
const gold = Math.max(5000, baseGold - distPenalty);
|
||||
return toInt(gold * this.goldMultiplier());
|
||||
}
|
||||
|
||||
trainStationMinRange(): number {
|
||||
|
||||
@@ -24,6 +24,7 @@ export class TrainExecution implements Execution {
|
||||
private stations: TrainStation[] = [];
|
||||
private currentRailroad: OrientedRailroad | null = null;
|
||||
private speed: number = 2;
|
||||
private _tradeStopsVisited: number = 0;
|
||||
|
||||
constructor(
|
||||
private railNetwork: RailNetwork,
|
||||
@@ -37,6 +38,10 @@ export class TrainExecution implements Execution {
|
||||
return this.player;
|
||||
}
|
||||
|
||||
public tradeStopsVisited(): number {
|
||||
return this._tradeStopsVisited;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
const stations = this.railNetwork.findStationsPath(
|
||||
@@ -261,6 +266,10 @@ export class TrainExecution implements Execution {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
this.stations[1].onTrainStop(this);
|
||||
const stationType = this.stations[1].unit.type();
|
||||
if (stationType === UnitType.City || stationType === UnitType.Port) {
|
||||
this._tradeStopsVisited++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -697,7 +697,10 @@ export class NationStructureBehavior {
|
||||
unitToCluster.set(station.unit, station.getCluster());
|
||||
}
|
||||
|
||||
const maxTradeGold = Math.max(Number(game.config().trainGold("ally")), 1);
|
||||
const maxTradeGold = Math.max(
|
||||
Number(game.config().trainGold("ally", 0)),
|
||||
1,
|
||||
);
|
||||
const result: Array<{
|
||||
tile: TileRef;
|
||||
cluster: Cluster | null;
|
||||
@@ -705,7 +708,8 @@ export class NationStructureBehavior {
|
||||
}> = [];
|
||||
|
||||
// Own structures — weighted by "self" trade gold.
|
||||
const selfWeight = Number(game.config().trainGold("self")) / maxTradeGold;
|
||||
const selfWeight =
|
||||
Number(game.config().trainGold("self", 0)) / maxTradeGold;
|
||||
for (const unit of player.units(
|
||||
UnitType.City,
|
||||
UnitType.Port,
|
||||
@@ -730,7 +734,7 @@ export class NationStructureBehavior {
|
||||
: player.isAlliedWith(neighbor)
|
||||
? "ally"
|
||||
: "other";
|
||||
const weight = Number(game.config().trainGold(relType)) / maxTradeGold;
|
||||
const weight = Number(game.config().trainGold(relType, 0)) / maxTradeGold;
|
||||
for (const unit of neighbor.units(
|
||||
UnitType.City,
|
||||
UnitType.Port,
|
||||
|
||||
@@ -20,7 +20,12 @@ class TradeStationStopHandler implements TrainStopHandler {
|
||||
): void {
|
||||
const stationOwner = station.unit.owner();
|
||||
const trainOwner = trainExecution.owner();
|
||||
const gold = mg.config().trainGold(rel(trainOwner, stationOwner));
|
||||
const gold = mg
|
||||
.config()
|
||||
.trainGold(
|
||||
rel(trainOwner, stationOwner),
|
||||
trainExecution.tradeStopsVisited(),
|
||||
);
|
||||
// Share revenue with the station owner if it's not the current player
|
||||
if (trainOwner !== stationOwner) {
|
||||
stationOwner.addGold(gold, station.tile());
|
||||
|
||||
@@ -28,7 +28,7 @@ function makeStation(unit: any, cluster: Cluster | null = null): any {
|
||||
function makeGame(stations: any[] = []): any {
|
||||
return {
|
||||
config: () => ({
|
||||
trainGold: (rel: string) => TRAIN_GOLD[rel] ?? 0n,
|
||||
trainGold: (rel: string, _citiesVisited: number) => TRAIN_GOLD[rel] ?? 0n,
|
||||
}),
|
||||
railNetwork: () => ({
|
||||
stationManager: () => ({ getAll: () => new Set(stations) }),
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import { GameUpdateType } from "src/core/game/GameUpdates";
|
||||
import { vi, type Mocked } from "vitest";
|
||||
import { DefaultConfig } from "../../../src/core/configuration/DefaultConfig";
|
||||
import { TrainExecution } from "../../../src/core/execution/TrainExecution";
|
||||
import { Game, Player, Unit, UnitType } from "../../../src/core/game/Game";
|
||||
import {
|
||||
Difficulty,
|
||||
Game,
|
||||
GameMapSize,
|
||||
GameMapType,
|
||||
GameMode,
|
||||
GameType,
|
||||
Player,
|
||||
Unit,
|
||||
UnitType,
|
||||
} from "../../../src/core/game/Game";
|
||||
import { Cluster, TrainStation } from "../../../src/core/game/TrainStation";
|
||||
import { UserSettings } from "../../../src/core/game/UserSettings";
|
||||
import { GameConfig } from "../../../src/core/Schemas";
|
||||
import { TestServerConfig } from "../../util/TestServerConfig";
|
||||
|
||||
vi.mock("../../../src/core/game/Game");
|
||||
vi.mock("../../../src/core/execution/TrainExecution");
|
||||
@@ -18,8 +32,8 @@ describe("TrainStation", () => {
|
||||
game = {
|
||||
ticks: vi.fn().mockReturnValue(123),
|
||||
config: vi.fn().mockReturnValue({
|
||||
trainGold: (isFriendly: boolean) =>
|
||||
isFriendly ? BigInt(1000) : BigInt(500),
|
||||
trainGold: (rel: string, _tradeStopsVisited: number) =>
|
||||
rel !== "other" ? BigInt(1000) : BigInt(500),
|
||||
}),
|
||||
addUpdate: vi.fn(),
|
||||
addExecution: vi.fn(),
|
||||
@@ -48,6 +62,7 @@ describe("TrainStation", () => {
|
||||
loadCargo: vi.fn(),
|
||||
owner: vi.fn().mockReturnValue(player),
|
||||
level: vi.fn(),
|
||||
tradeStopsVisited: vi.fn().mockReturnValue(0),
|
||||
} as any;
|
||||
});
|
||||
|
||||
@@ -74,6 +89,20 @@ describe("TrainStation", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes tradeStopsVisited to trainGold", () => {
|
||||
unit.type.mockReturnValue(UnitType.City);
|
||||
const trainGoldSpy = vi.fn().mockReturnValue(500n);
|
||||
(game.config as any).mockReturnValue({
|
||||
trainGold: trainGoldSpy,
|
||||
});
|
||||
(trainExecution as any).tradeStopsVisited = vi.fn().mockReturnValue(3);
|
||||
const station = new TrainStation(game, unit);
|
||||
|
||||
station.onTrainStop(trainExecution);
|
||||
|
||||
expect(trainGoldSpy).toHaveBeenCalledWith(expect.any(String), 3);
|
||||
});
|
||||
|
||||
it("checks trade availability (same owner)", () => {
|
||||
const otherUnit = {
|
||||
owner: vi.fn().mockReturnValue(unit.owner()),
|
||||
@@ -133,3 +162,66 @@ describe("TrainStation", () => {
|
||||
expect(station.isActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefaultConfig.trainGold trade stop penalty", () => {
|
||||
let config: DefaultConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
const serverConfig = new TestServerConfig();
|
||||
const gameConfig: GameConfig = {
|
||||
gameMap: GameMapType.Asia,
|
||||
gameMapSize: GameMapSize.Normal,
|
||||
gameMode: GameMode.FFA,
|
||||
gameType: GameType.Singleplayer,
|
||||
difficulty: Difficulty.Medium,
|
||||
nations: "default",
|
||||
donateGold: false,
|
||||
donateTroops: false,
|
||||
bots: 0,
|
||||
infiniteGold: false,
|
||||
infiniteTroops: false,
|
||||
instantBuild: false,
|
||||
disableNavMesh: false,
|
||||
randomSpawn: false,
|
||||
};
|
||||
config = new DefaultConfig(
|
||||
serverConfig,
|
||||
gameConfig,
|
||||
new UserSettings(),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns full base gold within free window (stops 0-2)", () => {
|
||||
// first 3 stops (0, 1, 2) are free — no penalty
|
||||
expect(config.trainGold("self", 0)).toBe(10_000n);
|
||||
expect(config.trainGold("self", 1)).toBe(10_000n);
|
||||
expect(config.trainGold("self", 2)).toBe(10_000n);
|
||||
});
|
||||
|
||||
it("reduces gold by 5k per stop after the free window", () => {
|
||||
// stop 3: effective = 3-2 = 1 -> 10k - 5k = 5k
|
||||
expect(config.trainGold("self", 3)).toBe(5_000n);
|
||||
});
|
||||
|
||||
it("floors at 5k when penalty exceeds base gold", () => {
|
||||
// stop 5: effective = 3 -> 10k - 15k -> floor at 5k
|
||||
expect(config.trainGold("self", 5)).toBe(5_000n);
|
||||
});
|
||||
|
||||
it("floors at 5k for ally base even with heavy penalty", () => {
|
||||
// ally base 35k, stop 20: effective = 18 -> penalty 90k -> floor at 5k
|
||||
expect(config.trainGold("ally", 20)).toBe(5_000n);
|
||||
});
|
||||
|
||||
it("ally base gold reduces correctly after free window", () => {
|
||||
// ally base 35k, stop 4: effective = 2 -> 35k - 10k = 25k
|
||||
expect(config.trainGold("ally", 4)).toBe(25_000n);
|
||||
});
|
||||
|
||||
it("other/team base gold reduces correctly after free window", () => {
|
||||
// other base 25k, stop 3: effective = 1 -> 25k - 5k = 20k
|
||||
expect(config.trainGold("other", 3)).toBe(20_000n);
|
||||
expect(config.trainGold("team", 3)).toBe(20_000n);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user