diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index a67396205..971e8b1ad 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -135,6 +135,7 @@ export interface Config { deleteUnitCooldown(): Tick; defaultDonationAmount(sender: Player): number; unitInfo(type: UnitType): UnitInfo; + tradeShipShortRangeDebuff(): number; tradeShipGold(dist: number, numPorts: number): Gold; tradeShipSpawnRate( numTradeShips: number, diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index a9e9c01a7..3aca8e369 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -371,9 +371,10 @@ export class DefaultConfig implements Config { } tradeShipGold(dist: number, numPorts: number): Gold { - // Sigmoid: concave start, sharp S-curve middle, linear end - heavily punishes trades under 200 + // Sigmoid: concave start, sharp S-curve middle, linear end - heavily punishes trades under range debuff. + const debuff = this.tradeShipShortRangeDebuff(); const baseGold = - 100_000 / (1 + Math.exp(-0.03 * (dist - 200))) + 100 * dist; + 100_000 / (1 + Math.exp(-0.03 * (dist - debuff))) + 100 * dist; const numPortBonus = numPorts - 1; // Hyperbolic decay, midpoint at 5 ports, 3x bonus max. const bonus = 1 + 2 * (numPortBonus / (numPortBonus + 5)); @@ -777,6 +778,10 @@ export class DefaultConfig implements Config { return 20; } + tradeShipShortRangeDebuff(): number { + return 200; + } + proximityBonusPortsNb(totalPorts: number) { return within(totalPorts / 3, 4, totalPorts); } diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index cdc16a496..de7b70752 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -58,7 +58,7 @@ export class PortExecution implements Execution { return; } - const ports = this.player.tradingPorts(this.port); + const ports = this.tradingPorts(); if (ports.length === 0) { return; @@ -103,4 +103,40 @@ export class PortExecution implements Execution { } } } + + // It's a probability list, so if an element appears twice it's because it's + // twice more likely to be picked later. + tradingPorts(): Unit[] { + const ports = this.mg + .players() + .filter((p) => p !== this.port!.owner() && p.canTrade(this.port!.owner())) + .flatMap((p) => p.units(UnitType.Port)) + .sort((p1, p2) => { + return ( + this.mg.manhattanDist(this.port!.tile(), p1.tile()) - + this.mg.manhattanDist(this.port!.tile(), p2.tile()) + ); + }); + + const weightedPorts: Unit[] = []; + + for (const [i, otherPort] of ports.entries()) { + const expanded = new Array(otherPort.level()).fill(otherPort); + weightedPorts.push(...expanded); + const tooClose = + this.mg.manhattanDist(this.port!.tile(), otherPort.tile()) < + this.mg.config().tradeShipShortRangeDebuff(); + const closeBonus = + i < this.mg.config().proximityBonusPortsNb(ports.length); + if (!tooClose && closeBonus) { + // If the port is close, but not too close, add it again + // to increase the chances of trading with it. + weightedPorts.push(...expanded); + } + if (!tooClose && this.port!.owner().isFriendly(otherPort.owner())) { + weightedPorts.push(...expanded); + } + } + return weightedPorts; + } } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index bd7a184f6..061143d55 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -648,7 +648,6 @@ export interface Player { // Misc toUpdate(): PlayerUpdate; playerProfile(): PlayerProfile; - tradingPorts(port: Unit): Unit[]; // WARNING: this operation is expensive. bestTransportShipSpawn(tile: TileRef): TileRef | false; } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 955eb1474..2ebd85aba 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -1214,34 +1214,4 @@ export class PlayerImpl implements Player { bestTransportShipSpawn(targetTile: TileRef): TileRef | false { return bestShoreDeploymentSource(this.mg, this, targetTile); } - - // It's a probability list, so if an element appears twice it's because it's - // twice more likely to be picked later. - tradingPorts(port: Unit): Unit[] { - const ports = this.mg - .players() - .filter((p) => p !== port.owner() && p.canTrade(port.owner())) - .flatMap((p) => p.units(UnitType.Port)) - .sort((p1, p2) => { - return ( - this.mg.manhattanDist(port.tile(), p1.tile()) - - this.mg.manhattanDist(port.tile(), p2.tile()) - ); - }); - - const weightedPorts: Unit[] = []; - - for (const [i, otherPort] of ports.entries()) { - const expanded = new Array(otherPort.level()).fill(otherPort); - weightedPorts.push(...expanded); - if (i < this.mg.config().proximityBonusPortsNb(ports.length)) { - weightedPorts.push(...expanded); - } - if (port.owner().isFriendly(otherPort.owner())) { - weightedPorts.push(...expanded); - } - } - - return weightedPorts; - } } diff --git a/tests/PlayerImpl.test.ts b/tests/PlayerImpl.test.ts index 7f47a1cd2..71d022f22 100644 --- a/tests/PlayerImpl.test.ts +++ b/tests/PlayerImpl.test.ts @@ -83,22 +83,6 @@ describe("PlayerImpl", () => { expect(cityToUpgrade).toBe(false); }); - test("Destination ports chances scale with level", () => { - game.config().proximityBonusPortsNb = () => 0; - - player.conquer(game.ref(10, 10)); - const playerPort = player.buildUnit(UnitType.Port, game.ref(10, 10), {}); - - other.conquer(game.ref(0, 0)); - const otherPort = other.buildUnit(UnitType.Port, game.ref(0, 0), {}); - otherPort.increaseLevel(); - otherPort.increaseLevel(); - - const ports = player.tradingPorts(playerPort); - - expect(ports.length).toBe(3); - }); - test("Can't send alliance requests when dead", () => { // conquer other const otherTiles = other.tiles(); diff --git a/tests/PortExecution.test.ts b/tests/PortExecution.test.ts new file mode 100644 index 000000000..d38500315 --- /dev/null +++ b/tests/PortExecution.test.ts @@ -0,0 +1,92 @@ +import { PortExecution } from "../src/core/execution/PortExecution"; +import { + Game, + Player, + PlayerInfo, + PlayerType, + UnitType, +} from "../src/core/game/Game"; +import { setup } from "./util/Setup"; + +let game: Game; +let player: Player; +let other: Player; + +describe("PortExecution", () => { + beforeEach(async () => { + game = await setup( + "half_land_half_ocean", + { + instantBuild: true, + }, + [ + new PlayerInfo("player", PlayerType.Human, null, "player_id"), + new PlayerInfo("other", PlayerType.Human, null, "other_id"), + ], + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + player = game.player("player_id"); + player.addGold(BigInt(1000000)); + other = game.player("other_id"); + + game.config().structureMinDist = () => 10; + }); + + test("Destination ports chances scale with level", () => { + game.config().proximityBonusPortsNb = () => 0; + game.config().tradeShipShortRangeDebuff = () => 0; + + player.conquer(game.ref(7, 10)); + const execution = new PortExecution(player, game.ref(7, 10)); + execution.init(game, 0); + execution.tick(0); + + other.conquer(game.ref(0, 0)); + const otherPort = other.buildUnit(UnitType.Port, game.ref(0, 0), {}); + otherPort.increaseLevel(); + otherPort.increaseLevel(); + + const ports = execution.tradingPorts(); + + expect(ports.length).toBe(3); + }); + + test("Trade ship proximity bonus", () => { + game.config().proximityBonusPortsNb = () => 10; + game.config().tradeShipShortRangeDebuff = () => 0; + + player.conquer(game.ref(7, 10)); + const execution = new PortExecution(player, game.ref(7, 10)); + execution.init(game, 0); + execution.tick(0); + + other.conquer(game.ref(0, 0)); + other.buildUnit(UnitType.Port, game.ref(0, 0), {}); + + const ports = execution.tradingPorts(); + + expect(ports.length).toBe(2); + }); + + test("Trade ship short range debuff", () => { + game.config().proximityBonusPortsNb = () => 10; + // Short range debuff cancels out the proximity bonus. + game.config().tradeShipShortRangeDebuff = () => 100; + + player.conquer(game.ref(7, 10)); + const execution = new PortExecution(player, game.ref(7, 10)); + execution.init(game, 0); + execution.tick(0); + + other.conquer(game.ref(0, 0)); + other.buildUnit(UnitType.Port, game.ref(0, 0), {}); + + const ports = execution.tradingPorts(); + + expect(ports.length).toBe(1); + }); +});