mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-16 13:33:49 +00:00
## Description: A player reported that income received from opponents' trains was missing from match result logs. Fix external train trade attribution to the station owner and add regression tests for train income stats. ## 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: aotumuri
262 lines
6.4 KiB
TypeScript
262 lines
6.4 KiB
TypeScript
import { TrainExecution } from "../execution/TrainExecution";
|
|
import { PseudoRandom } from "../PseudoRandom";
|
|
import { Game, Player, Unit, UnitType } from "./Game";
|
|
import { TileRef } from "./GameMap";
|
|
import { GameUpdateType } from "./GameUpdates";
|
|
import { Railroad } from "./Railroad";
|
|
|
|
/**
|
|
* Handle train stops at various station types
|
|
*/
|
|
interface TrainStopHandler {
|
|
onStop(mg: Game, station: TrainStation, trainExecution: TrainExecution): void;
|
|
}
|
|
|
|
class TradeStationStopHandler implements TrainStopHandler {
|
|
onStop(
|
|
mg: Game,
|
|
station: TrainStation,
|
|
trainExecution: TrainExecution,
|
|
): void {
|
|
const stationOwner = station.unit.owner();
|
|
const trainOwner = trainExecution.owner();
|
|
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());
|
|
mg.stats().trainExternalTrade(stationOwner, gold);
|
|
}
|
|
trainOwner.addGold(gold, station.tile());
|
|
mg.stats().trainSelfTrade(trainOwner, gold);
|
|
}
|
|
}
|
|
|
|
class FactoryStopHandler implements TrainStopHandler {
|
|
onStop(
|
|
mg: Game,
|
|
station: TrainStation,
|
|
trainExecution: TrainExecution,
|
|
): void {}
|
|
}
|
|
|
|
export function createTrainStopHandlers(
|
|
random: PseudoRandom,
|
|
): Partial<Record<UnitType, TrainStopHandler>> {
|
|
return {
|
|
[UnitType.City]: new TradeStationStopHandler(),
|
|
[UnitType.Port]: new TradeStationStopHandler(),
|
|
[UnitType.Factory]: new FactoryStopHandler(),
|
|
};
|
|
}
|
|
|
|
export class TrainStation {
|
|
id: number = -1; // assigned by StationManager
|
|
private readonly stopHandlers: Partial<Record<UnitType, TrainStopHandler>> =
|
|
{};
|
|
private cluster: Cluster | null = null;
|
|
private railroads: Set<Railroad> = new Set();
|
|
// Quick lookup from neighboring station to connecting railroad
|
|
private railroadByNeighbor: Map<TrainStation, Railroad> = new Map();
|
|
|
|
constructor(
|
|
private mg: Game,
|
|
public unit: Unit,
|
|
) {
|
|
this.stopHandlers = createTrainStopHandlers(new PseudoRandom(mg.ticks()));
|
|
}
|
|
|
|
tradeAvailable(otherPlayer: Player): boolean {
|
|
const player = this.unit.owner();
|
|
return otherPlayer === player || player.canTrade(otherPlayer);
|
|
}
|
|
|
|
clearRailroads() {
|
|
this.railroads.clear();
|
|
this.railroadByNeighbor.clear();
|
|
}
|
|
|
|
addRailroad(railRoad: Railroad) {
|
|
this.railroads.add(railRoad);
|
|
const neighbor = railRoad.from === this ? railRoad.to : railRoad.from;
|
|
this.railroadByNeighbor.set(neighbor, railRoad);
|
|
}
|
|
|
|
removeRailroad(railRoad: Railroad) {
|
|
this.railroads.delete(railRoad);
|
|
const neighbor = railRoad.from === this ? railRoad.to : railRoad.from;
|
|
this.railroadByNeighbor.delete(neighbor);
|
|
}
|
|
|
|
removeNeighboringRails(station: TrainStation) {
|
|
const toRemove = [...this.railroads].find(
|
|
(r) => r.from === station || r.to === station,
|
|
);
|
|
if (toRemove) {
|
|
this.mg.addUpdate({
|
|
type: GameUpdateType.RailroadDestructionEvent,
|
|
id: toRemove.id,
|
|
});
|
|
this.removeRailroad(toRemove);
|
|
}
|
|
}
|
|
|
|
neighbors(): TrainStation[] {
|
|
const neighbors: TrainStation[] = [];
|
|
for (const r of this.railroads) {
|
|
if (r.from !== this) {
|
|
neighbors.push(r.from);
|
|
} else {
|
|
neighbors.push(r.to);
|
|
}
|
|
}
|
|
return neighbors;
|
|
}
|
|
|
|
tile(): TileRef {
|
|
return this.unit.tile();
|
|
}
|
|
|
|
isActive(): boolean {
|
|
return this.unit.isActive();
|
|
}
|
|
|
|
getRailroads(): Set<Railroad> {
|
|
return this.railroads;
|
|
}
|
|
|
|
getRailroadTo(station: TrainStation): Railroad | null {
|
|
return this.railroadByNeighbor.get(station) ?? null;
|
|
}
|
|
|
|
setCluster(cluster: Cluster | null) {
|
|
// Properly disconnect cluster if it's already set
|
|
if (this.cluster !== null) {
|
|
this.cluster.removeStation(this);
|
|
}
|
|
this.cluster = cluster;
|
|
}
|
|
|
|
getCluster(): Cluster | null {
|
|
return this.cluster;
|
|
}
|
|
|
|
onTrainStop(trainExecution: TrainExecution) {
|
|
const type = this.unit.type();
|
|
const handler = this.stopHandlers[type];
|
|
if (handler) {
|
|
handler.onStop(this.mg, this, trainExecution);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cluster of connected stations
|
|
*/
|
|
export class Cluster {
|
|
public stations: Set<TrainStation> = new Set();
|
|
private tradeStations: Set<TrainStation> = new Set();
|
|
|
|
private isTradeStation(station: TrainStation): boolean {
|
|
const type = station.unit.type();
|
|
return type === UnitType.City || type === UnitType.Port;
|
|
}
|
|
|
|
has(station: TrainStation) {
|
|
return this.stations.has(station);
|
|
}
|
|
|
|
addStation(station: TrainStation) {
|
|
this.stations.add(station);
|
|
if (this.isTradeStation(station)) {
|
|
this.tradeStations.add(station);
|
|
}
|
|
station.setCluster(this);
|
|
}
|
|
|
|
removeStation(station: TrainStation) {
|
|
this.stations.delete(station);
|
|
this.tradeStations.delete(station);
|
|
}
|
|
|
|
addStations(stations: Set<TrainStation>) {
|
|
for (const station of stations) {
|
|
this.addStation(station);
|
|
}
|
|
}
|
|
|
|
merge(other: Cluster) {
|
|
for (const s of other.stations) {
|
|
this.addStation(s);
|
|
}
|
|
}
|
|
|
|
hasAnyTradeDestination(player: Player): boolean {
|
|
for (const station of this.tradeStations) {
|
|
if (station.tradeAvailable(player)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
randomTradeDestination(
|
|
player: Player,
|
|
random: PseudoRandom,
|
|
): TrainStation | null {
|
|
let selected: TrainStation | null = null;
|
|
let eligibleSeen = 0;
|
|
|
|
for (const station of this.tradeStations) {
|
|
if (!station.tradeAvailable(player)) continue;
|
|
eligibleSeen++;
|
|
|
|
// Reservoir sampling: keep each eligible station with probability 1/eligibleSeen.
|
|
if (random.nextInt(0, eligibleSeen) === 0) {
|
|
selected = station;
|
|
}
|
|
}
|
|
|
|
return selected;
|
|
}
|
|
|
|
availableForTrade(player: Player): Set<TrainStation> {
|
|
const tradingStations = new Set<TrainStation>();
|
|
for (const station of this.tradeStations) {
|
|
if (station.tradeAvailable(player)) {
|
|
tradingStations.add(station);
|
|
}
|
|
}
|
|
return tradingStations;
|
|
}
|
|
|
|
size() {
|
|
return this.stations.size;
|
|
}
|
|
|
|
clear() {
|
|
this.stations.clear();
|
|
this.tradeStations.clear();
|
|
}
|
|
}
|
|
|
|
function rel(
|
|
player: Player,
|
|
other: Player,
|
|
): "self" | "team" | "ally" | "other" {
|
|
if (player === other) {
|
|
return "self";
|
|
}
|
|
if (player.isOnSameTeam(other)) {
|
|
return "team";
|
|
}
|
|
if (player.isAlliedWith(other)) {
|
|
return "ally";
|
|
}
|
|
return "other";
|
|
}
|