mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 00:05:21 +00:00
experiment: distribute part of track profits
This commit is contained in:
@@ -117,7 +117,30 @@ export class TrainExecution implements Execution {
|
||||
tiles.length > 0
|
||||
? tiles[Math.floor(tiles.length / 2)]
|
||||
: railroad.getStart().tile();
|
||||
this.player.addGold(-fare, midTile);
|
||||
let netFare = fare;
|
||||
|
||||
// Optimization: if the train owner is also the sole territory owner along this railroad,
|
||||
// they would immediately get back the full 20% share. In that case, just charge the net
|
||||
// 80% fare and skip the distribution step.
|
||||
let shouldDistributeShare = true;
|
||||
if (
|
||||
this.mg &&
|
||||
fare > 0n &&
|
||||
rail.isSoleTerritoryOwner(this.mg, this.player)
|
||||
) {
|
||||
const profitShare = fare / 5n; // 20%
|
||||
netFare = fare - profitShare;
|
||||
shouldDistributeShare = false;
|
||||
}
|
||||
|
||||
// Charge fare (possibly reduced by owner share optimization) to the train owner
|
||||
this.player.addGold(-netFare, midTile);
|
||||
|
||||
// Share 20% of the fare with territory owners along the railroad,
|
||||
// proportional to the number of tiles they own under this track.
|
||||
if (shouldDistributeShare && this.mg && fare > 0n) {
|
||||
rail.distributeFareShare(this.mg, fare);
|
||||
}
|
||||
// Update client-side coloring when fare changes significantly
|
||||
if (this.mg !== null) {
|
||||
rail.updateFare(this.mg);
|
||||
|
||||
@@ -606,6 +606,8 @@ export class GameImpl implements Game {
|
||||
type: GameUpdateType.Tile,
|
||||
update: this.toTileUpdate(tile),
|
||||
});
|
||||
// Notify rail network so it can invalidate cached territory ownership
|
||||
this._railNetwork.onTileOwnerChanged(tile);
|
||||
}
|
||||
|
||||
relinquish(tile: TileRef) {
|
||||
@@ -627,6 +629,8 @@ export class GameImpl implements Game {
|
||||
type: GameUpdateType.Tile,
|
||||
update: this.toTileUpdate(tile),
|
||||
});
|
||||
// Notify rail network so it can invalidate cached territory ownership
|
||||
this._railNetwork.onTileOwnerChanged(tile);
|
||||
}
|
||||
|
||||
private updateBorders(tile: TileRef) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Unit } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
import { TrainStation } from "./TrainStation";
|
||||
|
||||
export interface RailNetwork {
|
||||
connectStation(station: TrainStation): void;
|
||||
removeStation(unit: Unit): void;
|
||||
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
|
||||
// Notify the rail network that the owner of a tile has changed,
|
||||
// so any railroads crossing that tile can update cached territory ownership.
|
||||
onTileOwnerChanged(tile: TileRef): void;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ export function createRailNetwork(game: Game): RailNetwork {
|
||||
|
||||
export class RailNetworkImpl implements RailNetwork {
|
||||
private maxConnectionDistance: number = 4;
|
||||
// Index which railroads cross which tiles so we can quickly invalidate
|
||||
// cached territory ownership on conquests.
|
||||
private railsByTile: Map<TileRef, Set<Railroad>> = new Map();
|
||||
|
||||
constructor(
|
||||
private game: Game,
|
||||
@@ -96,6 +99,28 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
private pathService: RailPathFinderService,
|
||||
) {}
|
||||
|
||||
private registerRailroad(railRoad: Railroad) {
|
||||
for (const tile of railRoad.tiles) {
|
||||
let set = this.railsByTile.get(tile);
|
||||
if (!set) {
|
||||
set = new Set<Railroad>();
|
||||
this.railsByTile.set(tile, set);
|
||||
}
|
||||
set.add(railRoad);
|
||||
}
|
||||
}
|
||||
|
||||
private unregisterRailroad(railRoad: Railroad) {
|
||||
for (const tile of railRoad.tiles) {
|
||||
const set = this.railsByTile.get(tile);
|
||||
if (!set) continue;
|
||||
set.delete(railRoad);
|
||||
if (set.size === 0) {
|
||||
this.railsByTile.delete(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectStation(station: TrainStation) {
|
||||
this.stationManager.addStation(station);
|
||||
this.connectToNearbyStations(station);
|
||||
@@ -124,6 +149,15 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
station.unit.setTrainStation(false);
|
||||
}
|
||||
|
||||
onTileOwnerChanged(tile: TileRef): void {
|
||||
const rails = this.railsByTile.get(tile);
|
||||
if (!rails) return;
|
||||
|
||||
for (const rail of rails) {
|
||||
rail.markTerritoryDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the intermediary stations connecting two stations
|
||||
*/
|
||||
@@ -180,6 +214,7 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
|
||||
private disconnectFromNetwork(station: TrainStation) {
|
||||
for (const rail of station.getRailroads()) {
|
||||
this.unregisterRailroad(rail);
|
||||
rail.delete(this.game);
|
||||
}
|
||||
station.clearRailroads();
|
||||
@@ -200,6 +235,7 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
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.registerRailroad(railRoad);
|
||||
this.game.addExecution(new RailroadExecution(railRoad));
|
||||
from.addRailroad(railRoad);
|
||||
to.addRailroad(railRoad);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Game, Tick } from "./Game";
|
||||
import { Game, Player, Tick } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
import { GameUpdateType, RailTile, RailType } from "./GameUpdates";
|
||||
import { TrainStation } from "./TrainStation";
|
||||
@@ -13,6 +13,12 @@ export class Railroad {
|
||||
private railTiles: RailTile[] | null = null;
|
||||
// Last fare used for client-side coloring
|
||||
private lastFare: bigint | null = null;
|
||||
// Cached territory ownership along this railroad: which players own how many tiles.
|
||||
private territoryOwners: Map<
|
||||
Player,
|
||||
{ count: number; sampleTile: TileRef }
|
||||
> | null = null;
|
||||
private territoryDirty: boolean = true;
|
||||
|
||||
constructor(
|
||||
public from: TrainStation,
|
||||
@@ -44,6 +50,93 @@ export class Railroad {
|
||||
this.updateCongestionEma(currentTick);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark cached territory ownership as dirty; should be called when any tile owner
|
||||
* along this railroad changes.
|
||||
*/
|
||||
markTerritoryDirty(): void {
|
||||
this.territoryDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily (re)compute which players own tiles under this railroad.
|
||||
*/
|
||||
private ensureTerritoryOwners(
|
||||
game: Game,
|
||||
): Map<Player, { count: number; sampleTile: TileRef }> {
|
||||
if (!this.territoryDirty && this.territoryOwners) {
|
||||
return this.territoryOwners;
|
||||
}
|
||||
|
||||
const owners = new Map<Player, { count: number; sampleTile: TileRef }>();
|
||||
|
||||
for (const tile of this.tiles) {
|
||||
const ownerOrNull = game.owner(tile);
|
||||
if (ownerOrNull && ownerOrNull.isPlayer()) {
|
||||
const owner = ownerOrNull as Player;
|
||||
const existing = owners.get(owner);
|
||||
if (existing) {
|
||||
existing.count += 1;
|
||||
} else {
|
||||
owners.set(owner, { count: 1, sampleTile: tile });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.territoryOwners = owners;
|
||||
this.territoryDirty = false;
|
||||
return owners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute a 20% share of the given fare to territory owners along this railroad,
|
||||
* proportional to the number of tiles they own under the track.
|
||||
*/
|
||||
distributeFareShare(game: Game, fare: bigint): void {
|
||||
if (fare <= 0n) return;
|
||||
|
||||
const profitShare = fare / 5n; // 20%
|
||||
if (profitShare <= 0n) return;
|
||||
|
||||
const owners = this.ensureTerritoryOwners(game);
|
||||
if (owners.size === 0) return;
|
||||
|
||||
let totalTiles = 0;
|
||||
owners.forEach((entry) => {
|
||||
totalTiles += entry.count;
|
||||
});
|
||||
if (totalTiles <= 0) return;
|
||||
|
||||
const totalTilesBig = BigInt(totalTiles);
|
||||
let distributed = 0n;
|
||||
|
||||
const entries = Array.from(owners.entries());
|
||||
entries.forEach(([owner, { count, sampleTile }], index) => {
|
||||
let share: bigint;
|
||||
if (index === entries.length - 1) {
|
||||
// Last owner gets the remaining share to avoid rounding loss.
|
||||
share = profitShare - distributed;
|
||||
} else {
|
||||
share = (profitShare * BigInt(count)) / totalTilesBig;
|
||||
distributed += share;
|
||||
}
|
||||
if (share > 0n) {
|
||||
owner.addGold(share, sampleTile);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if there is exactly one territory owner along this railroad
|
||||
* and that owner is the given player.
|
||||
*/
|
||||
isSoleTerritoryOwner(game: Game, player: Player): boolean {
|
||||
const owners = this.ensureTerritoryOwners(game);
|
||||
if (owners.size !== 1) return false;
|
||||
const [onlyOwner] = owners.keys();
|
||||
return onlyOwner === player;
|
||||
}
|
||||
|
||||
private updateCongestionEma(currentTick: Tick): void {
|
||||
if (this.lastCongestionTick === null) {
|
||||
this.lastCongestionTick = currentTick;
|
||||
|
||||
Reference in New Issue
Block a user