mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:40:43 +00:00
Block captured trade routes from port destination selection for 100 ticks
This commit is contained in:
@@ -111,6 +111,16 @@ export class PortExecution implements Execution {
|
||||
const weightedPorts: Unit[] = [];
|
||||
|
||||
for (const [i, otherPort] of ports.entries()) {
|
||||
if (
|
||||
this.mg.isTradeRouteBlocked(
|
||||
this.port.id(),
|
||||
otherPort.id(),
|
||||
this.mg.ticks(),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const expanded = new Array(otherPort.level()).fill(otherPort);
|
||||
weightedPorts.push(...expanded);
|
||||
const tooClose =
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Unit,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TRADE_ROUTE_BLOCK_DURATION_TICKS } from "../game/GameImpl";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
@@ -21,12 +22,17 @@ export class TradeShipExecution implements Execution {
|
||||
private tilesTraveled = 0;
|
||||
private motionPlanId = 1;
|
||||
private motionPlanDst: TileRef | null = null;
|
||||
private readonly srcPortId: number;
|
||||
private readonly dstPortId: number;
|
||||
|
||||
constructor(
|
||||
private origOwner: Player,
|
||||
private srcPort: Unit,
|
||||
private _dstPort: Unit,
|
||||
) {}
|
||||
) {
|
||||
this.srcPortId = srcPort.id();
|
||||
this.dstPortId = _dstPort.id();
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
@@ -61,6 +67,11 @@ export class TradeShipExecution implements Execution {
|
||||
if (this.wasCaptured !== true && this.origOwner !== tradeShipOwner) {
|
||||
// Store as variable in case ship is recaptured by previous owner
|
||||
this.wasCaptured = true;
|
||||
this.mg.blockTradeRouteUntil(
|
||||
this.srcPortId,
|
||||
this.dstPortId,
|
||||
this.mg.ticks() + TRADE_ROUTE_BLOCK_DURATION_TICKS,
|
||||
);
|
||||
}
|
||||
|
||||
// If a player captures another player's port while trading we should delete
|
||||
|
||||
@@ -913,6 +913,12 @@ export interface Game extends GameMap {
|
||||
miniWaterGraph(): AbstractGraph | null;
|
||||
getWaterComponent(tile: TileRef): number | null;
|
||||
hasWaterComponent(tile: TileRef, component: number): boolean;
|
||||
blockTradeRouteUntil(srcPortId: number, dstPortId: number, tick: Tick): void;
|
||||
isTradeRouteBlocked(
|
||||
srcPortId: number,
|
||||
dstPortId: number,
|
||||
nowTick: Tick,
|
||||
): boolean;
|
||||
}
|
||||
|
||||
export interface PlayerActions {
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
TeamGameSpawnAreas,
|
||||
TerrainType,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
Trios,
|
||||
Unit,
|
||||
UnitInfo,
|
||||
@@ -75,6 +76,8 @@ export function createGame(
|
||||
|
||||
export type CellString = string;
|
||||
|
||||
const TRADE_ROUTE_BLOCK_DURATION_TICKS = 100;
|
||||
|
||||
export class GameImpl implements Game {
|
||||
private _ticks = 0;
|
||||
|
||||
@@ -112,6 +115,7 @@ export class GameImpl implements Game {
|
||||
private _miniWaterGraph: AbstractGraph | null = null;
|
||||
private _miniWaterHPA: AStarWaterHierarchical | null = null;
|
||||
private _teamGameSpawnAreas: TeamGameSpawnAreas | undefined;
|
||||
private readonly tradeRouteBlockedUntil = new Map<string, number>();
|
||||
|
||||
constructor(
|
||||
private _humans: PlayerInfo[],
|
||||
@@ -487,11 +491,55 @@ export class GameImpl implements Game {
|
||||
return packed;
|
||||
}
|
||||
|
||||
private makeTradeRouteKey(srcPortId: number, dstPortId: number): string {
|
||||
return `${srcPortId}:${dstPortId}`;
|
||||
}
|
||||
|
||||
private pruneExpiredTradeRouteBlocks(nowTick: Tick): void {
|
||||
for (const [routeKey, blockedUntil] of this.tradeRouteBlockedUntil) {
|
||||
if (blockedUntil <= nowTick) {
|
||||
this.tradeRouteBlockedUntil.delete(routeKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockTradeRouteUntil(srcPortId: number, dstPortId: number, tick: Tick): void {
|
||||
this.pruneExpiredTradeRouteBlocks(this._ticks);
|
||||
const routeKey = this.makeTradeRouteKey(srcPortId, dstPortId);
|
||||
const existing = this.tradeRouteBlockedUntil.get(routeKey) ?? 0;
|
||||
if (tick > existing) {
|
||||
this.tradeRouteBlockedUntil.set(routeKey, tick);
|
||||
}
|
||||
}
|
||||
|
||||
isTradeRouteBlocked(
|
||||
srcPortId: number,
|
||||
dstPortId: number,
|
||||
nowTick: Tick,
|
||||
): boolean {
|
||||
const routeKey = this.makeTradeRouteKey(srcPortId, dstPortId);
|
||||
const blockedUntil = this.tradeRouteBlockedUntil.get(routeKey);
|
||||
if (blockedUntil === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (blockedUntil <= nowTick) {
|
||||
this.tradeRouteBlockedUntil.delete(routeKey);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private hash(): number {
|
||||
this.pruneExpiredTradeRouteBlocks(this._ticks);
|
||||
let hash = 1;
|
||||
this._players.forEach((p) => {
|
||||
hash += p.hash();
|
||||
});
|
||||
for (const [routeKey, blockedUntil] of Array.from(
|
||||
this.tradeRouteBlockedUntil.entries(),
|
||||
).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
hash += simpleHash(routeKey) + blockedUntil;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -1247,6 +1295,8 @@ export class GameImpl implements Game {
|
||||
}
|
||||
}
|
||||
|
||||
export { TRADE_ROUTE_BLOCK_DURATION_TICKS };
|
||||
|
||||
// Or a more dynamic approach that will catch new enum values:
|
||||
const createGameUpdatesMap = (): GameUpdates => {
|
||||
const map = {} as GameUpdates;
|
||||
|
||||
@@ -104,4 +104,79 @@ describe("PortExecution", () => {
|
||||
|
||||
expect(ports.length).toBe(1);
|
||||
});
|
||||
|
||||
test("Blocked trade route is omitted from trading ports", () => {
|
||||
game.config().proximityBonusPortsNb = () => 0;
|
||||
game.config().tradeShipShortRangeDebuff = () => 0;
|
||||
|
||||
player.conquer(game.ref(7, 10));
|
||||
const spawn = player.canBuild(UnitType.Port, game.ref(7, 10));
|
||||
if (spawn === false) {
|
||||
throw new Error("Unable to build port for test");
|
||||
}
|
||||
const port = player.buildUnit(UnitType.Port, spawn, {});
|
||||
const execution = new PortExecution(port);
|
||||
execution.init(game, 0);
|
||||
|
||||
other.conquer(game.ref(0, 0));
|
||||
const blockedPort = other.buildUnit(UnitType.Port, game.ref(0, 0), {});
|
||||
other.conquer(game.ref(0, 1));
|
||||
const openPort = other.buildUnit(UnitType.Port, game.ref(0, 1), {});
|
||||
|
||||
game.blockTradeRouteUntil(port.id(), blockedPort.id(), game.ticks() + 100);
|
||||
|
||||
const ports = execution.tradingPorts();
|
||||
|
||||
expect(ports).toContain(openPort);
|
||||
expect(ports).not.toContain(blockedPort);
|
||||
});
|
||||
|
||||
test("Blocked trade route becomes eligible again after expiry", () => {
|
||||
game.config().proximityBonusPortsNb = () => 0;
|
||||
game.config().tradeShipShortRangeDebuff = () => 0;
|
||||
|
||||
player.conquer(game.ref(7, 10));
|
||||
const spawn = player.canBuild(UnitType.Port, game.ref(7, 10));
|
||||
if (spawn === false) {
|
||||
throw new Error("Unable to build port for test");
|
||||
}
|
||||
const port = player.buildUnit(UnitType.Port, spawn, {});
|
||||
const execution = new PortExecution(port);
|
||||
execution.init(game, 0);
|
||||
|
||||
other.conquer(game.ref(0, 0));
|
||||
const blockedPort = other.buildUnit(UnitType.Port, game.ref(0, 0), {});
|
||||
|
||||
game.blockTradeRouteUntil(port.id(), blockedPort.id(), game.ticks() + 1);
|
||||
|
||||
expect(execution.tradingPorts()).not.toContain(blockedPort);
|
||||
expect(
|
||||
game.isTradeRouteBlocked(port.id(), blockedPort.id(), game.ticks()),
|
||||
).toBe(true);
|
||||
expect(
|
||||
game.isTradeRouteBlocked(port.id(), blockedPort.id(), game.ticks() + 1),
|
||||
).toBe(false);
|
||||
expect(execution.tradingPorts()).toContain(blockedPort);
|
||||
});
|
||||
|
||||
test("Trade route blacklist affects hash and expires cleanly", () => {
|
||||
player.conquer(game.ref(7, 10));
|
||||
const spawn = player.canBuild(UnitType.Port, game.ref(7, 10));
|
||||
if (spawn === false) {
|
||||
throw new Error("Unable to build port for test");
|
||||
}
|
||||
const port = player.buildUnit(UnitType.Port, spawn, {});
|
||||
|
||||
other.conquer(game.ref(0, 0));
|
||||
const otherPort = other.buildUnit(UnitType.Port, game.ref(0, 0), {});
|
||||
|
||||
const baseHash = (game as any).hash();
|
||||
game.blockTradeRouteUntil(port.id(), otherPort.id(), game.ticks() + 100);
|
||||
const blockedHash = (game as any).hash();
|
||||
(game as any)._ticks += 100;
|
||||
const expiredHash = (game as any).hash();
|
||||
|
||||
expect(blockedHash).not.toBe(baseHash);
|
||||
expect(expiredHash).toBe(baseHash);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,6 +55,7 @@ describe("TradeShipExecution", () => {
|
||||
} as any;
|
||||
|
||||
piratePort = {
|
||||
id: vi.fn(() => 201),
|
||||
tile: vi.fn(() => 56),
|
||||
owner: vi.fn(() => pirate),
|
||||
isActive: vi.fn(() => true),
|
||||
@@ -63,6 +64,7 @@ describe("TradeShipExecution", () => {
|
||||
} as any;
|
||||
|
||||
piratePort2 = {
|
||||
id: vi.fn(() => 202),
|
||||
tile: vi.fn(() => 75),
|
||||
owner: vi.fn(() => pirate),
|
||||
isActive: vi.fn(() => true),
|
||||
@@ -71,6 +73,7 @@ describe("TradeShipExecution", () => {
|
||||
} as any;
|
||||
|
||||
srcPort = {
|
||||
id: vi.fn(() => 101),
|
||||
tile: vi.fn(() => 10),
|
||||
owner: vi.fn(() => origOwner),
|
||||
isActive: vi.fn(() => true),
|
||||
@@ -79,6 +82,7 @@ describe("TradeShipExecution", () => {
|
||||
} as any;
|
||||
|
||||
dstPort = {
|
||||
id: vi.fn(() => 102),
|
||||
tile: vi.fn(() => 100),
|
||||
owner: vi.fn(() => dstOwner),
|
||||
isActive: vi.fn(() => true),
|
||||
@@ -131,6 +135,43 @@ describe("TradeShipExecution", () => {
|
||||
expect(tradeShip.setTargetUnit).toHaveBeenCalledWith(piratePort);
|
||||
});
|
||||
|
||||
it("blacklists the original route immediately on first capture", () => {
|
||||
tradeShip.owner = vi.fn(() => pirate);
|
||||
|
||||
tradeShipExecution.tick(1);
|
||||
|
||||
expect(
|
||||
game.isTradeRouteBlocked(srcPort.id(), dstPort.id(), game.ticks()),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the blacklist on the original route after retargeting", () => {
|
||||
tradeShip.owner = vi.fn(() => pirate);
|
||||
|
||||
tradeShipExecution.tick(1);
|
||||
|
||||
expect(
|
||||
game.isTradeRouteBlocked(srcPort.id(), dstPort.id(), game.ticks()),
|
||||
).toBe(true);
|
||||
expect(
|
||||
game.isTradeRouteBlocked(srcPort.id(), piratePort.id(), game.ticks()),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not add a second blacklist event when the ship is recaptured", () => {
|
||||
const routeKey = `${srcPort.id()}:${dstPort.id()}`;
|
||||
tradeShip.owner = vi.fn(() => pirate);
|
||||
tradeShipExecution.tick(1);
|
||||
const blockedUntil = (game as any).tradeRouteBlockedUntil.get(routeKey);
|
||||
|
||||
tradeShip.owner = vi.fn(() => origOwner);
|
||||
tradeShipExecution.tick(2);
|
||||
|
||||
expect((game as any).tradeRouteBlockedUntil.get(routeKey)).toBe(
|
||||
blockedUntil,
|
||||
);
|
||||
});
|
||||
|
||||
it("should complete trade and award gold", () => {
|
||||
tradeShipExecution["pathFinder"] = {
|
||||
next: vi.fn(() => ({ status: PathStatus.COMPLETE, node: 32 })),
|
||||
@@ -141,4 +182,27 @@ describe("TradeShipExecution", () => {
|
||||
expect(tradeShipExecution.isActive()).toBe(false);
|
||||
expect(game.displayMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not blacklist on NOT_FOUND", () => {
|
||||
tradeShipExecution["pathFinder"] = {
|
||||
next: vi.fn(() => ({ status: PathStatus.NOT_FOUND, node: 32 })),
|
||||
findPath: vi.fn((from: number) => [from]),
|
||||
} as any;
|
||||
|
||||
tradeShipExecution.tick(1);
|
||||
|
||||
expect(
|
||||
game.isTradeRouteBlocked(srcPort.id(), dstPort.id(), game.ticks()),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not blacklist when destination becomes invalid", () => {
|
||||
dstPort.isActive = vi.fn(() => false);
|
||||
|
||||
tradeShipExecution.tick(1);
|
||||
|
||||
expect(
|
||||
game.isTradeRouteBlocked(srcPort.id(), dstPort.id(), game.ticks()),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user