mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:10:42 +00:00
fix (pathfinding): prioritize best connected water neighbor in ShoreCoercingTransformer (#2937)
## Description: **Describe the PR.** This PR improves how pathfinding finds a starting water tile when launching a transport ship from a shore. Previously, the code simply picked the first water neighbor it found. This caused issues where, if a boat were traveling east, it might launch out of a northern tile from a shore. <img width="896" height="353" alt="image" src="https://github.com/user-attachments/assets/69d83012-3397-43b3-8ab0-9ebde6ffea97" /> <img width="342" height="219" alt="image" src="https://github.com/user-attachments/assets/a191f5cf-97da-4e34-a191-55ce14c794f0" /> The new logic checks all water neighbors and picks the "best" one by counting how many water tiles surround it. This ensures transport ships launch into the main body of water instead of suboptimal positions. If two tiles have water neighbors with the same score, they are tie-broken through a euclidean distance check. ## 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: Scisyph --------- Co-authored-by: WilliamT-byte <williamt2023@tamu.edu> Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
This commit is contained in:
@@ -47,8 +47,8 @@ export class PathFinding {
|
||||
return PathFinderBuilder.create(pf)
|
||||
.wrap((pf) => new ComponentCheckTransformer(pf, componentCheckFn))
|
||||
.wrap((pf) => new SmoothingWaterTransformer(pf, miniMap))
|
||||
.wrap((pf) => new ShoreCoercingTransformer(pf, miniMap))
|
||||
.wrap((pf) => new MiniMapTransformer(pf, game.map(), miniMap))
|
||||
.wrap((pf) => new ShoreCoercingTransformer(pf, game.map()))
|
||||
.buildWithStepper(tileStepperConfig(game));
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ export class PathFinding {
|
||||
const pf = new AStarWater(miniMap);
|
||||
|
||||
return PathFinderBuilder.create(pf)
|
||||
.wrap((pf) => new ShoreCoercingTransformer(pf, miniMap))
|
||||
.wrap((pf) => new MiniMapTransformer(pf, game.map(), miniMap))
|
||||
.wrap((pf) => new ShoreCoercingTransformer(pf, game.map()))
|
||||
.buildWithStepper(tileStepperConfig(game));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// Shore-coercing transformer that converts shore tiles to water tiles for pathfinding
|
||||
|
||||
import { GameMap, TileRef } from "../../game/GameMap";
|
||||
import { PathFinder } from "../types";
|
||||
|
||||
@@ -7,9 +5,6 @@ import { PathFinder } from "../types";
|
||||
* Wraps a PathFinder to handle shore tiles.
|
||||
* Coerces shore tiles to nearby water tiles before pathfinding,
|
||||
* then fixes the path extremes to include the original shore tiles.
|
||||
*
|
||||
* Works at whatever resolution the map provides - can be used with
|
||||
* full map or minimap-based pathfinders.
|
||||
*/
|
||||
export class ShoreCoercingTransformer implements PathFinder<number> {
|
||||
constructor(
|
||||
@@ -34,20 +29,18 @@ export class ShoreCoercingTransformer implements PathFinder<number> {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Coerce to tile
|
||||
const coercedTo = this.coerceToWater(to);
|
||||
if (coercedTo.water === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search on water tiles
|
||||
const fromTiles = waterFrom.length === 1 ? waterFrom[0] : waterFrom;
|
||||
const path = this.inner.findPath(fromTiles, coercedTo.water);
|
||||
if (!path || path.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Look up the actual path start in the map
|
||||
// Restore original start shore tile
|
||||
const originalShore = waterToOriginal.get(path[0]);
|
||||
if (originalShore !== undefined && originalShore !== null) {
|
||||
path.unshift(originalShore);
|
||||
@@ -67,25 +60,43 @@ export class ShoreCoercingTransformer implements PathFinder<number> {
|
||||
/**
|
||||
* Coerce a tile to water for pathfinding.
|
||||
* If tile is already water, returns it unchanged.
|
||||
* If tile is shore (land with water neighbor), finds the nearest water neighbor.
|
||||
* If tile is shore, finds the best adjacent water neighbor.
|
||||
*/
|
||||
private coerceToWater(tile: TileRef): {
|
||||
water: TileRef | null;
|
||||
original: TileRef | null;
|
||||
} {
|
||||
// If already water, no coercion needed
|
||||
if (this.map.isWater(tile)) {
|
||||
return { water: tile, original: null };
|
||||
}
|
||||
|
||||
// Find adjacent water neighbor
|
||||
let best: TileRef | null = null;
|
||||
let maxScore = -1;
|
||||
|
||||
for (const n of this.map.neighbors(tile)) {
|
||||
if (this.map.isWater(n)) {
|
||||
return { water: n, original: tile };
|
||||
if (!this.map.isWater(n)) continue;
|
||||
|
||||
// Score by water neighbor count (connectivity)
|
||||
const score = this.countWaterNeighbors(n);
|
||||
|
||||
// Pick highest connectivity
|
||||
if (score > maxScore) {
|
||||
maxScore = score;
|
||||
best = n;
|
||||
}
|
||||
}
|
||||
|
||||
// No water neighbor found - let HPA* handle at minimap level
|
||||
if (best !== null) {
|
||||
return { water: best, original: tile };
|
||||
}
|
||||
return { water: null, original: tile };
|
||||
}
|
||||
|
||||
private countWaterNeighbors(tile: TileRef): number {
|
||||
let count = 0;
|
||||
for (const n of this.map.neighbors(tile)) {
|
||||
if (this.map.isWater(n)) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user