mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:50:19 +00:00
increase mirv price with total number of merged launched (#2621)
## Description: To prevent MAD stalemates, have the price of MIRVs increase after each launch. This will encourage players to launch a MIRV once they have enough money for it. Also reduce the price of the first MIRV to 25 million to reduce snowballing, each subsequent MIRV cost an extra 15 million: 1. 25 million 2. 40 million 3. 60 million 4. etc ## 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:
@@ -487,7 +487,12 @@ export class DefaultConfig implements Config {
|
||||
};
|
||||
case UnitType.MIRV:
|
||||
return {
|
||||
cost: this.costWrapper(() => 35_000_000, UnitType.MIRV),
|
||||
cost: (game: Game, player: Player) => {
|
||||
if (player.type() === PlayerType.Human && this.infiniteGold()) {
|
||||
return 0n;
|
||||
}
|
||||
return 25_000_000n + game.stats().numMirvsLaunched() * 15_000_000n;
|
||||
},
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.MIRVWarhead:
|
||||
@@ -567,14 +572,15 @@ export class DefaultConfig implements Config {
|
||||
private costWrapper(
|
||||
costFn: (units: number) => number,
|
||||
...types: UnitType[]
|
||||
): (p: Player) => bigint {
|
||||
return (p: Player) => {
|
||||
if (p.type() === PlayerType.Human && this.infiniteGold()) {
|
||||
): (g: Game, p: Player) => bigint {
|
||||
return (game: Game, player: Player) => {
|
||||
if (player.type() === PlayerType.Human && this.infiniteGold()) {
|
||||
return 0n;
|
||||
}
|
||||
const numUnits = types.reduce(
|
||||
(acc, type) =>
|
||||
acc + Math.min(p.unitsOwned(type), p.unitsConstructed(type)),
|
||||
acc +
|
||||
Math.min(player.unitsOwned(type), player.unitsConstructed(type)),
|
||||
0,
|
||||
);
|
||||
return BigInt(costFn(numUnits));
|
||||
|
||||
@@ -562,7 +562,7 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
private cost(type: UnitType): Gold {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
return this.mg.unitInfo(type).cost(this.player);
|
||||
return this.mg.unitInfo(type).cost(this.mg, this.player);
|
||||
}
|
||||
|
||||
sendBoatRandomly(borderingEnemies: Player[] = []) {
|
||||
|
||||
@@ -175,7 +175,7 @@ export enum GameMapSize {
|
||||
}
|
||||
|
||||
export interface UnitInfo {
|
||||
cost: (player: Player) => Gold;
|
||||
cost: (game: Game, player: Player) => Gold;
|
||||
// Determines if its owner changes when its tile is conquered.
|
||||
territoryBound: boolean;
|
||||
maxHealth?: number;
|
||||
@@ -753,7 +753,6 @@ export interface Game extends GameMap {
|
||||
nations(): Nation[];
|
||||
|
||||
numTilesWithFallout(): number;
|
||||
// Optional as it's not initialized before the end of spawn phase
|
||||
stats(): Stats;
|
||||
|
||||
addUpdate(update: GameUpdate): void;
|
||||
|
||||
@@ -864,7 +864,7 @@ export class PlayerImpl implements Player {
|
||||
);
|
||||
}
|
||||
|
||||
const cost = this.mg.unitInfo(type).cost(this);
|
||||
const cost = this.mg.unitInfo(type).cost(this.mg, this);
|
||||
const b = new UnitImpl(
|
||||
type,
|
||||
this.mg,
|
||||
@@ -911,7 +911,9 @@ export class PlayerImpl implements Player {
|
||||
if (this.mg.config().isUnitDisabled(unit.type())) {
|
||||
return false;
|
||||
}
|
||||
if (this._gold < this.mg.config().unitInfo(unit.type()).cost(this)) {
|
||||
if (
|
||||
this._gold < this.mg.config().unitInfo(unit.type()).cost(this.mg, this)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (unit.owner() !== this) {
|
||||
@@ -921,7 +923,7 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
upgradeUnit(unit: Unit) {
|
||||
const cost = this.mg.unitInfo(unit.type()).cost(this);
|
||||
const cost = this.mg.unitInfo(unit.type()).cost(this.mg, this);
|
||||
this.removeGold(cost);
|
||||
unit.increaseLevel();
|
||||
this.recordUnitConstructed(unit.type());
|
||||
@@ -944,7 +946,7 @@ export class PlayerImpl implements Player {
|
||||
? false
|
||||
: this.canBuild(u, tile, validTiles),
|
||||
canUpgrade: canUpgrade,
|
||||
cost: this.mg.config().unitInfo(u).cost(this),
|
||||
cost: this.mg.config().unitInfo(u).cost(this.mg, this),
|
||||
} as BuildableUnit;
|
||||
});
|
||||
}
|
||||
@@ -958,7 +960,7 @@ export class PlayerImpl implements Player {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cost = this.mg.unitInfo(unitType).cost(this);
|
||||
const cost = this.mg.unitInfo(unitType).cost(this.mg, this);
|
||||
if (!this.isAlive() || this.gold() < cost) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface Stats {
|
||||
getPlayerStats(player: Player): PlayerStats | null;
|
||||
stats(): AllPlayersStats;
|
||||
|
||||
numMirvsLaunched(): bigint;
|
||||
|
||||
// Player attacks target
|
||||
attack(
|
||||
player: Player,
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
unitTypeToBombUnit,
|
||||
unitTypeToOtherUnit,
|
||||
} from "../StatsSchemas";
|
||||
import { Player, TerraNullius } from "./Game";
|
||||
import { Player, TerraNullius, UnitType } from "./Game";
|
||||
import { Stats } from "./Stats";
|
||||
|
||||
type BigIntLike = bigint | number;
|
||||
@@ -42,6 +42,12 @@ function _bigint(value: BigIntLike): bigint {
|
||||
export class StatsImpl implements Stats {
|
||||
private readonly data: AllPlayersStats = {};
|
||||
|
||||
private _numMirvLaunched: bigint = 0n;
|
||||
|
||||
numMirvsLaunched(): bigint {
|
||||
return this._numMirvLaunched;
|
||||
}
|
||||
|
||||
getPlayerStats(player: Player): PlayerStats {
|
||||
const clientID = player.clientID();
|
||||
if (clientID === null) return undefined;
|
||||
@@ -217,6 +223,9 @@ export class StatsImpl implements Stats {
|
||||
target: Player | TerraNullius,
|
||||
type: NukeType,
|
||||
): void {
|
||||
if (type === UnitType.MIRV) {
|
||||
this._numMirvLaunched++;
|
||||
}
|
||||
this._addBomb(player, type, BOMB_INDEX_LAUNCH, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,9 +66,8 @@ describe("FakeHuman MIRV Retaliation", () => {
|
||||
fakehuman.buildUnit(UnitType.MissileSilo, game.ref(50, 50), {});
|
||||
|
||||
// Give both players enough gold for MIRVs
|
||||
attacker.addGold(100_000_000n);
|
||||
fakehuman.addGold(100_000_000n);
|
||||
|
||||
attacker.addGold(1_000_000_000n);
|
||||
fakehuman.addGold(1_000_000_000n);
|
||||
// Verify preconditions
|
||||
expect(attacker.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
expect(fakehuman.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ConstructionExecution } from "../../src/core/execution/ConstructionExecution";
|
||||
import { NukeExecution } from "../../src/core/execution/NukeExecution";
|
||||
import { SpawnExecution } from "../../src/core/execution/SpawnExecution";
|
||||
import {
|
||||
Game,
|
||||
@@ -12,31 +13,38 @@ import { setup } from "../util/Setup";
|
||||
describe("Construction economy", () => {
|
||||
let game: Game;
|
||||
let player: Player;
|
||||
let other: Player;
|
||||
const builderInfo = new PlayerInfo(
|
||||
"builder",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"builder_id",
|
||||
);
|
||||
const otherInfo = new PlayerInfo("other", PlayerType.Human, null, "other_id");
|
||||
|
||||
beforeEach(async () => {
|
||||
game = await setup("ocean_and_land", {
|
||||
infiniteGold: false,
|
||||
instantBuild: false,
|
||||
infiniteTroops: true,
|
||||
});
|
||||
const info = new PlayerInfo(
|
||||
"builder",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"builder_id",
|
||||
game = await setup(
|
||||
"plains",
|
||||
{
|
||||
infiniteGold: false,
|
||||
instantBuild: false,
|
||||
infiniteTroops: true,
|
||||
},
|
||||
[builderInfo, otherInfo],
|
||||
);
|
||||
game.addPlayer(info);
|
||||
const spawn = game.ref(0, 10);
|
||||
game.addExecution(new SpawnExecution(info, spawn));
|
||||
game.addExecution(new SpawnExecution(builderInfo, spawn));
|
||||
game.addExecution(new SpawnExecution(otherInfo, spawn));
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
player = game.player(info.id);
|
||||
player = game.player(builderInfo.id);
|
||||
other = game.player(otherInfo.id);
|
||||
});
|
||||
|
||||
test("City charges gold once and no refund thereafter (allow passive income)", () => {
|
||||
const target = game.ref(0, 10);
|
||||
const cost = game.unitInfo(UnitType.City).cost(player);
|
||||
const cost = game.unitInfo(UnitType.City).cost(game, player);
|
||||
player.addGold(cost);
|
||||
expect(player.gold()).toBe(cost);
|
||||
|
||||
@@ -68,4 +76,30 @@ describe("Construction economy", () => {
|
||||
(player.units(UnitType.City)[0] as any).isUnderConstruction?.() ?? false,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("MIRV gets more expensive with each launch", () => {
|
||||
expect(game.config().unitInfo(UnitType.MIRV).cost(game, other)).toBe(
|
||||
25_000_000n,
|
||||
);
|
||||
|
||||
player.addGold(100_000_000n);
|
||||
|
||||
player.conquer(game.ref(1, 1));
|
||||
player.buildUnit(UnitType.MissileSilo, game.ref(1, 1), {});
|
||||
|
||||
other.conquer(game.ref(10, 10));
|
||||
game.addExecution(
|
||||
new NukeExecution(UnitType.MIRV, player, game.ref(10, 10)),
|
||||
);
|
||||
game.executeNextTick(); // init
|
||||
game.executeNextTick(); // create MIRV unit
|
||||
game.executeNextTick();
|
||||
|
||||
expect(player.units(UnitType.MIRV)).toHaveLength(1);
|
||||
|
||||
// Price of the MIRV increases for everyone with each launch.
|
||||
expect(game.config().unitInfo(UnitType.MIRV).cost(game, other)).toBe(
|
||||
40_000_000n,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("Hydrogen Bomb and MIRV flows", () => {
|
||||
const goldBeforeSilo = playerWithConstruction.gold();
|
||||
const siloCost = gameWithConstruction
|
||||
.unitInfo(UnitType.MissileSilo)
|
||||
.cost(playerWithConstruction);
|
||||
.cost(gameWithConstruction, playerWithConstruction);
|
||||
playerWithConstruction.addGold(siloCost);
|
||||
|
||||
// Start construction of silo
|
||||
@@ -144,7 +144,7 @@ describe("Hydrogen Bomb and MIRV flows", () => {
|
||||
playerWithConstruction.conquer(targetTile);
|
||||
const hydrogenBombCost = gameWithConstruction
|
||||
.unitInfo(UnitType.HydrogenBomb)
|
||||
.cost(playerWithConstruction);
|
||||
.cost(gameWithConstruction, playerWithConstruction);
|
||||
playerWithConstruction.addGold(hydrogenBombCost);
|
||||
|
||||
const canBuildAfterCompletion = playerWithConstruction.canBuild(
|
||||
|
||||
Reference in New Issue
Block a user