mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:30:43 +00:00
c1bf01d3ba
## Description: This change enables loop formation in the station network, allowing for enhanced connectivity. However, it can sometimes result in clustered areas as a trade-off. To mitigate excessive clustering, loops are only permitted when there are at least 5 intermediary stations between the source and destination stations.  Also a few fixes for `.dev` players. ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors
264 lines
7.6 KiB
TypeScript
264 lines
7.6 KiB
TypeScript
import { RailroadExecution } from "../execution/RailroadExecution";
|
|
import { PathFindResultType } from "../pathfinding/AStar";
|
|
import { MiniAStar } from "../pathfinding/MiniAStar";
|
|
import { SerialAStar } from "../pathfinding/SerialAStar";
|
|
import { Game, Unit, UnitType } from "./Game";
|
|
import { TileRef } from "./GameMap";
|
|
import { RailNetwork } from "./RailNetwork";
|
|
import { Railroad } from "./Railroad";
|
|
import { Cluster, TrainStation, TrainStationMapAdapter } from "./TrainStation";
|
|
|
|
/**
|
|
* The Stations handle their own neighbors so the graph is naturally traversable,
|
|
* but it would be expensive to look through the graph to find a station.
|
|
* This class stores the existing stations for quick access
|
|
*/
|
|
export interface StationManager {
|
|
addStation(station: TrainStation): void;
|
|
removeStation(station: TrainStation): void;
|
|
findStation(unit: Unit): TrainStation | null;
|
|
getAll(): Set<TrainStation>;
|
|
}
|
|
|
|
export class StationManagerImpl implements StationManager {
|
|
private stations: Set<TrainStation> = new Set();
|
|
|
|
addStation(station: TrainStation) {
|
|
this.stations.add(station);
|
|
}
|
|
|
|
removeStation(station: TrainStation) {
|
|
this.stations.delete(station);
|
|
}
|
|
|
|
findStation(unit: Unit): TrainStation | null {
|
|
for (const station of this.stations) {
|
|
if (station.unit === unit) return station;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getAll(): Set<TrainStation> {
|
|
return this.stations;
|
|
}
|
|
}
|
|
|
|
export interface RailPathFinderService {
|
|
findTilePath(from: TileRef, to: TileRef): TileRef[];
|
|
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
|
|
}
|
|
|
|
class RailPathFinderServiceImpl implements RailPathFinderService {
|
|
constructor(private game: Game) {}
|
|
|
|
findTilePath(from: TileRef, to: TileRef): TileRef[] {
|
|
const astar = new MiniAStar(
|
|
this.game.map(),
|
|
this.game.miniMap(),
|
|
from,
|
|
to,
|
|
5000,
|
|
20,
|
|
false,
|
|
3,
|
|
);
|
|
return astar.compute() === PathFindResultType.Completed
|
|
? astar.reconstructPath()
|
|
: [];
|
|
}
|
|
|
|
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[] {
|
|
const stationAStar = new SerialAStar(
|
|
from,
|
|
to,
|
|
5000,
|
|
20,
|
|
new TrainStationMapAdapter(this.game),
|
|
);
|
|
return stationAStar.compute() === PathFindResultType.Completed
|
|
? stationAStar.reconstructPath()
|
|
: [];
|
|
}
|
|
}
|
|
|
|
export function createRailNetwork(game: Game): RailNetwork {
|
|
const stationManager = new StationManagerImpl();
|
|
const pathService = new RailPathFinderServiceImpl(game);
|
|
return new RailNetworkImpl(game, stationManager, pathService);
|
|
}
|
|
|
|
export class RailNetworkImpl implements RailNetwork {
|
|
private maxConnectionDistance: number = 4;
|
|
|
|
constructor(
|
|
private game: Game,
|
|
private stationManager: StationManager,
|
|
private pathService: RailPathFinderService,
|
|
) {}
|
|
|
|
connectStation(station: TrainStation) {
|
|
this.stationManager.addStation(station);
|
|
this.connectToNearbyStations(station);
|
|
}
|
|
|
|
removeStation(unit: Unit): void {
|
|
const station = this.stationManager.findStation(unit);
|
|
if (!station) return;
|
|
|
|
const neighbors = station.neighbors();
|
|
this.disconnectFromNetwork(station);
|
|
this.stationManager.removeStation(station);
|
|
|
|
const cluster = station.getCluster();
|
|
if (!cluster) return;
|
|
if (neighbors.length === 1) {
|
|
cluster.removeStation(station);
|
|
} else if (neighbors.length > 1) {
|
|
for (const neighbor of neighbors) {
|
|
const stations = this.computeCluster(neighbor);
|
|
const newCluster = new Cluster();
|
|
newCluster.addStations(stations);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the intermediary stations connecting two stations
|
|
*/
|
|
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[] {
|
|
return this.pathService.findStationsPath(from, to);
|
|
}
|
|
|
|
private connectToNearbyStations(station: TrainStation) {
|
|
const neighbors = this.game.nearbyUnits(
|
|
station.tile(),
|
|
this.game.config().trainStationMaxRange(),
|
|
[UnitType.City, UnitType.Factory, UnitType.Port],
|
|
);
|
|
|
|
const editedClusters = new Set<Cluster>();
|
|
neighbors.sort((a, b) => a.distSquared - b.distSquared);
|
|
|
|
for (const neighbor of neighbors) {
|
|
if (neighbor.unit === station.unit) continue;
|
|
const neighborStation = this.stationManager.findStation(neighbor.unit);
|
|
if (!neighborStation) continue;
|
|
|
|
const distanceToStation = this.distanceFrom(
|
|
neighborStation,
|
|
station,
|
|
this.maxConnectionDistance,
|
|
);
|
|
|
|
const neighborCluster = neighborStation.getCluster();
|
|
if (neighborCluster === null) continue;
|
|
const connectionAvailable =
|
|
distanceToStation > this.maxConnectionDistance ||
|
|
distanceToStation === -1;
|
|
if (
|
|
connectionAvailable &&
|
|
neighbor.distSquared > this.game.config().trainStationMinRange() ** 2
|
|
) {
|
|
if (this.connect(station, neighborStation)) {
|
|
neighborCluster.addStation(station);
|
|
editedClusters.add(neighborCluster);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If multiple clusters own the new station, merge them into a single cluster
|
|
if (editedClusters.size > 1) {
|
|
this.mergeClusters(editedClusters);
|
|
} else if (editedClusters.size === 0) {
|
|
// If no cluster owns the station, creates a new one for it
|
|
const newCluster = new Cluster();
|
|
newCluster.addStation(station);
|
|
}
|
|
}
|
|
|
|
private disconnectFromNetwork(station: TrainStation) {
|
|
for (const rail of station.getRailroads()) {
|
|
rail.delete(this.game);
|
|
}
|
|
station.clearRailroads();
|
|
const cluster = station.getCluster();
|
|
if (cluster !== null && cluster.size() === 1) {
|
|
this.deleteCluster(cluster);
|
|
}
|
|
}
|
|
|
|
private deleteCluster(cluster: Cluster) {
|
|
for (const station of cluster.stations) {
|
|
station.setCluster(null);
|
|
}
|
|
cluster.clear();
|
|
}
|
|
|
|
private connect(from: TrainStation, to: TrainStation) {
|
|
const path = this.pathService.findTilePath(from.tile(), to.tile());
|
|
if (path.length > 0 && path.length < this.game.config().railroadMaxSize()) {
|
|
const railRoad = new Railroad(from, to, path);
|
|
this.game.addExecution(new RailroadExecution(railRoad));
|
|
from.addRailroad(railRoad);
|
|
to.addRailroad(railRoad);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private distanceFrom(
|
|
start: TrainStation,
|
|
dest: TrainStation,
|
|
maxDistance: number,
|
|
): number {
|
|
if (start === dest) return 0;
|
|
|
|
const visited = new Set<TrainStation>();
|
|
const queue: Array<{ station: TrainStation; distance: number }> = [
|
|
{ station: start, distance: 0 },
|
|
];
|
|
|
|
while (queue.length > 0) {
|
|
const { station, distance } = queue.shift()!;
|
|
if (visited.has(station)) continue;
|
|
visited.add(station);
|
|
|
|
if (distance >= maxDistance) continue;
|
|
|
|
for (const neighbor of station.neighbors()) {
|
|
if (neighbor === dest) return distance + 1;
|
|
if (!visited.has(neighbor)) {
|
|
queue.push({ station: neighbor, distance: distance + 1 });
|
|
}
|
|
}
|
|
}
|
|
|
|
// If destination not found within maxDistance
|
|
return -1;
|
|
}
|
|
|
|
private computeCluster(start: TrainStation): Set<TrainStation> {
|
|
const visited = new Set<TrainStation>();
|
|
const queue = [start];
|
|
|
|
while (queue.length > 0) {
|
|
const current = queue.shift()!;
|
|
if (visited.has(current)) continue;
|
|
visited.add(current);
|
|
|
|
for (const neighbor of current.neighbors()) {
|
|
if (!visited.has(neighbor)) queue.push(neighbor);
|
|
}
|
|
}
|
|
|
|
return visited;
|
|
}
|
|
|
|
private mergeClusters(clustersToMerge: Set<Cluster>) {
|
|
const merged = new Cluster();
|
|
for (const cluster of clustersToMerge) {
|
|
merged.merge(cluster);
|
|
}
|
|
}
|
|
}
|