mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 04:08:00 +00:00
365 lines
10 KiB
TypeScript
365 lines
10 KiB
TypeScript
import {
|
|
Execution,
|
|
Game,
|
|
Player,
|
|
TrainType,
|
|
Unit,
|
|
UnitType,
|
|
} from "../game/Game";
|
|
import { TileRef } from "../game/GameMap";
|
|
import { RailNetwork } from "../game/RailNetwork";
|
|
import { getOrientedRailroad, OrientedRailroad } from "../game/Railroad";
|
|
import { TrainStation } from "../game/TrainStation";
|
|
|
|
export class TrainExecution implements Execution {
|
|
private active = true;
|
|
private mg: Game | null = null;
|
|
private train: Unit | null = null;
|
|
private cars: Unit[] = [];
|
|
private hasCargo: boolean = false;
|
|
private currentTile: number = 0;
|
|
private spacing = 2;
|
|
private usedTiles: TileRef[] = []; // used for cars behind
|
|
private currentRailroad: OrientedRailroad | null = null;
|
|
private currentStation: TrainStation | null = null;
|
|
private speed: number = 2;
|
|
// Journey tracking for organic route discovery - simplified to immediate neighbors only
|
|
private hasProcessedArrival: boolean = false;
|
|
private journeyPreviousStation: TrainStation | null = null; // Immediate previous station
|
|
private journeyHopCount: number = 0;
|
|
|
|
// Local greedy routing properties
|
|
private recentStations: TrainStation[] = []; // Recently visited stations (for loop prevention)
|
|
private maxHops: number = 50; // Maximum hops before giving up
|
|
private recentMemorySize: number = 30; // How many recent stations to remember
|
|
|
|
constructor(
|
|
private railNetwork: RailNetwork,
|
|
private player: Player,
|
|
private source: TrainStation,
|
|
private destination: TrainStation,
|
|
private numCars: number,
|
|
) {
|
|
this.journeyPreviousStation = null; // Starting station has no previous
|
|
}
|
|
|
|
public owner(): Player {
|
|
return this.player;
|
|
}
|
|
|
|
/**
|
|
* Share journey information with a station for organic route discovery
|
|
*/
|
|
public shareJourneyInfo(): {
|
|
routeInformation: Array<{
|
|
destination: TrainStation;
|
|
nextHop: TrainStation | null;
|
|
distance: number;
|
|
}>;
|
|
} {
|
|
const routeInformation: Array<{
|
|
destination: TrainStation;
|
|
nextHop: TrainStation | null;
|
|
distance: number;
|
|
}> = [];
|
|
|
|
// Derive routing info from recentStations array
|
|
// recentStations = [oldest, ..., previous, current]
|
|
const immediatePrevious =
|
|
this.recentStations.length > 1
|
|
? this.recentStations[this.recentStations.length - 2]
|
|
: null;
|
|
|
|
// Find the start index for sharing journey information
|
|
// Only share information about stations visited since the last time we passed through the current station
|
|
let startIndex = 0;
|
|
const currentStation = this.recentStations[this.recentStations.length - 1];
|
|
|
|
// Look for the last occurrence of current station before the current visit
|
|
for (let i = this.recentStations.length - 2; i >= 0; i--) {
|
|
if (this.recentStations[i] === currentStation) {
|
|
// Found the last previous visit to this station, start sharing from after that visit
|
|
startIndex = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Only share routes to stations we visited since our last visit to this station (not including current)
|
|
for (let i = startIndex; i < this.recentStations.length - 1; i++) {
|
|
const destination = this.recentStations[i];
|
|
// For reverse routing: to reach any destination, go through the station we came from
|
|
const nextHop = immediatePrevious;
|
|
// Distance from current station to this destination
|
|
const distance = this.recentStations.length - 1 - i;
|
|
|
|
routeInformation.push({
|
|
destination,
|
|
nextHop,
|
|
distance,
|
|
});
|
|
}
|
|
|
|
return {
|
|
routeInformation,
|
|
};
|
|
}
|
|
|
|
init(mg: Game, ticks: number): void {
|
|
this.mg = mg;
|
|
|
|
// Validate that source and destination are active
|
|
if (!this.source.isActive() || !this.destination.isActive()) {
|
|
this.active = false;
|
|
return;
|
|
}
|
|
|
|
// If source and destination are the same, we're already there
|
|
if (this.source === this.destination) {
|
|
this.active = false;
|
|
return;
|
|
}
|
|
|
|
this.currentStation = this.source;
|
|
|
|
const spawn = this.player.canBuild(UnitType.Train, this.source.tile());
|
|
if (spawn === false) {
|
|
console.warn(`cannot build train`);
|
|
this.active = false;
|
|
return;
|
|
}
|
|
this.train = this.createTrainUnits(spawn);
|
|
}
|
|
|
|
tick(ticks: number): void {
|
|
if (this.train === null) {
|
|
throw new Error("Not initialized");
|
|
}
|
|
if (!this.train.isActive() || !this.activeSourceOrDestination()) {
|
|
this.deleteTrain();
|
|
return;
|
|
}
|
|
|
|
const tile = this.getNextTile();
|
|
if (tile) {
|
|
this.updateCarsPositions(tile);
|
|
} else {
|
|
this.targetReached();
|
|
this.deleteTrain();
|
|
}
|
|
}
|
|
|
|
loadCargo() {
|
|
if (this.hasCargo || this.train === null) {
|
|
return;
|
|
}
|
|
this.hasCargo = true;
|
|
// Starts at 1: don't load tail engine
|
|
for (let i = 1; i < this.cars.length; i++) {
|
|
this.cars[i].setLoaded(true);
|
|
}
|
|
}
|
|
|
|
private targetReached() {
|
|
if (this.train === null) {
|
|
return;
|
|
}
|
|
|
|
// Record train arrival statistics
|
|
if (this.mg) {
|
|
this.mg.recordTrainArrival(this.journeyHopCount);
|
|
}
|
|
|
|
this.train.setReachedTarget();
|
|
this.cars.forEach((car: Unit) => {
|
|
car.setReachedTarget();
|
|
});
|
|
}
|
|
|
|
private createTrainUnits(tile: TileRef): Unit {
|
|
const train = this.player.buildUnit(UnitType.Train, tile, {
|
|
targetUnit: this.destination.unit,
|
|
trainType: TrainType.Engine,
|
|
});
|
|
// Tail is also an engine, just for cosmetics
|
|
this.cars.push(
|
|
this.player.buildUnit(UnitType.Train, tile, {
|
|
targetUnit: this.destination.unit,
|
|
trainType: TrainType.Engine,
|
|
}),
|
|
);
|
|
for (let i = 0; i < this.numCars; i++) {
|
|
this.cars.push(
|
|
this.player.buildUnit(UnitType.Train, tile, {
|
|
trainType: TrainType.Carriage,
|
|
loaded: this.hasCargo,
|
|
}),
|
|
);
|
|
}
|
|
return train;
|
|
}
|
|
|
|
private deleteTrain() {
|
|
this.active = false;
|
|
if (this.train?.isActive()) {
|
|
this.train.delete(false);
|
|
}
|
|
for (const car of this.cars) {
|
|
if (car.isActive()) {
|
|
car.delete(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private activeSourceOrDestination(): boolean {
|
|
return this.source.isActive() && this.destination.isActive();
|
|
}
|
|
|
|
/**
|
|
* Save the tiles the train go through so the cars can reuse them
|
|
* Don't simply save the tiles the engine uses, otherwise the spacing will be dictated by the train speed
|
|
*/
|
|
private saveTraversedTiles(from: number, speed: number) {
|
|
if (!this.currentRailroad) {
|
|
return;
|
|
}
|
|
let tileToSave: number = from;
|
|
for (
|
|
let i = 0;
|
|
i < speed && tileToSave < this.currentRailroad.getTiles().length;
|
|
i++
|
|
) {
|
|
this.saveTile(this.currentRailroad.getTiles()[tileToSave]);
|
|
tileToSave = tileToSave + 1;
|
|
}
|
|
}
|
|
|
|
private saveTile(tile: TileRef) {
|
|
this.usedTiles.push(tile);
|
|
if (this.usedTiles.length > this.cars.length * this.spacing + 3) {
|
|
this.usedTiles.shift();
|
|
}
|
|
}
|
|
|
|
private updateCarsPositions(newTile: TileRef) {
|
|
if (this.cars.length > 0) {
|
|
for (let i = this.cars.length - 1; i >= 0; --i) {
|
|
const carTileIndex = (i + 1) * this.spacing + 2;
|
|
if (this.usedTiles.length > carTileIndex) {
|
|
this.cars[i].move(this.usedTiles[carTileIndex]);
|
|
}
|
|
}
|
|
}
|
|
if (this.train !== null) {
|
|
this.train.move(newTile);
|
|
}
|
|
}
|
|
|
|
private isAtStation(): boolean {
|
|
if (!this.train || !this.currentStation || !this.mg) return false;
|
|
|
|
// Check if train is at the current station's tile
|
|
const trainTile = this.train.tile();
|
|
return (
|
|
this.mg.x(trainTile) === this.mg.x(this.currentStation.tile()) &&
|
|
this.mg.y(trainTile) === this.mg.y(this.currentStation.tile())
|
|
);
|
|
}
|
|
|
|
private getNextTile(): TileRef | null {
|
|
// If we're at a station, decide where to go next
|
|
if (this.isAtStation()) {
|
|
// Process arrival if we haven't already for this station visit
|
|
if (!this.hasProcessedArrival) {
|
|
this.stationReached(); // Handle arrival at current station
|
|
this.hasProcessedArrival = true;
|
|
}
|
|
|
|
// Check if we've reached the destination
|
|
if (this.currentStation === this.destination) {
|
|
this.targetReached();
|
|
return null;
|
|
}
|
|
|
|
// Check if we've exceeded max hops
|
|
if (this.journeyHopCount >= this.maxHops) {
|
|
// Give up - we've wandered too long
|
|
this.active = false;
|
|
return null;
|
|
}
|
|
|
|
// Use local greedy routing to choose next station
|
|
const nextHop = this.currentStation!.chooseNextStation(
|
|
this.destination,
|
|
this.recentStations,
|
|
this.player,
|
|
);
|
|
|
|
if (!nextHop) {
|
|
// No good options available - stay and wait
|
|
return null;
|
|
}
|
|
|
|
// Get railroad to next hop
|
|
const railroad = getOrientedRailroad(this.currentStation!, nextHop);
|
|
if (!railroad) {
|
|
return null; // No direct connection
|
|
}
|
|
|
|
// Reset arrival flag since we're departing
|
|
this.hasProcessedArrival = false;
|
|
|
|
// Notify current station that train is departing
|
|
this.currentStation!.onTrainDepartureFromStation(this);
|
|
|
|
// Update recent stations memory for loop prevention
|
|
this.recentStations.push(nextHop);
|
|
if (this.recentStations.length > this.recentMemorySize) {
|
|
this.recentStations.shift(); // Remove oldest
|
|
}
|
|
|
|
// Update journey tracking - remember where we came from BEFORE changing currentStation
|
|
// This should happen after arrival processing but before departure
|
|
this.journeyHopCount++;
|
|
this.journeyPreviousStation = this.currentStation;
|
|
|
|
this.currentStation = nextHop;
|
|
this.currentRailroad = railroad;
|
|
this.currentTile = 0;
|
|
}
|
|
|
|
// Follow current railroad
|
|
if (
|
|
this.currentRailroad &&
|
|
this.currentTile < this.currentRailroad.getTiles().length
|
|
) {
|
|
this.saveTraversedTiles(this.currentTile, this.speed);
|
|
this.currentTile += this.speed;
|
|
|
|
if (this.currentTile >= this.currentRailroad.getTiles().length) {
|
|
// We've reached the next station
|
|
this.currentTile = this.currentRailroad.getTiles().length - 1;
|
|
}
|
|
|
|
return this.currentRailroad.getTiles()[this.currentTile];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private stationReached() {
|
|
if (this.mg === null || this.player === null || !this.currentStation) {
|
|
throw new Error("Not initialized");
|
|
}
|
|
|
|
this.currentStation.onTrainStop(this);
|
|
}
|
|
|
|
isActive(): boolean {
|
|
return this.active;
|
|
}
|
|
|
|
activeDuringSpawnPhase(): boolean {
|
|
return false;
|
|
}
|
|
}
|