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(), trainOwner, ); // 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> { 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> = {}; private cluster: Cluster | null = null; private railroads: Set = new Set(); // Quick lookup from neighboring station to connecting railroad private railroadByNeighbor: Map = 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 { 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 = new Set(); private tradeStations: Set = 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) { 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 { const tradingStations = new Set(); 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"; }