Skip to content

Conversation

@Amrit-Mohapatra-2008
Copy link

@Amrit-Mohapatra-2008 Amrit-Mohapatra-2008 commented Dec 24, 2025

Description:

Replace Manhattan-based shoreline sampling and per-candidate path checks with a single water-expansion search (multi-source, multi-target) that finds the nearest reachable shoreline by true boat distance.

Describe the PR.
The current boat pathfinding struggles with large disparities between straightline distance and real travel distance. This is because the heuristics based approach didn't punish land tiles enough.

The way I solved for this was by having a fill approach to path finding. Now, the target pixel will select an adjacent water node and check to see if it's a valid expansion. It will continue doing this until it eventually reaches the first shoreline pixel for the player. It then calls the AStar to determine what the ideal route would be.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • 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:

gravityswitch

Replace Manhattan-based shoreline sampling and per-candidate path checks with a single water-expansion search (multi-source, multi-target) that finds the nearest reachable shoreline by true boat distance.
@Amrit-Mohapatra-2008 Amrit-Mohapatra-2008 requested a review from a team as a code owner December 24, 2025 05:15
@CLAassistant
Copy link

CLAassistant commented Dec 24, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 24, 2025

Walkthrough

Added a water-aware Dijkstra-like search and MinHeap to TransportShipUtils, plus helpers to identify and traverse water tiles. Replaced multiple MiniAStar/BFS water path calculations with the new exported closestShoreFromPlayerByWater and adjusted canBuildTransportShip to return a TileRef | false.

Changes

Cohort / File(s) Summary
Transport ship water pathfinding
src/core/game/TransportShipUtils.ts
Added internal MinHeap<T> and water utilities (isWaterTile, adjacentWaterTiles), implemented closestShoreFromPlayerByWater(...) (exported) performing a Dijkstra-like search across water tiles with optional maxSteps and waterFilter. Replaced prior MiniAStar/BFS usage in canBuildTransportShip, transportShipSpawn, sourceDstOceanShore, bestShoreDeploymentSource, and related internal calls with the new water-based search. Changed canBuildTransportShip signature/return to `TileRef

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

Performance

Suggested reviewers

  • evanpelle

Poem

⛵ MinHeaps hum, the water lane,
Tiles connect across the main,
Dijkstra guides the ship to shore,
Waves compute, and ports explore,
New paths found, the sea sings more.

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Refactor transport ship shore finding logic' directly and specifically summarizes the main change—replacing shore finding approaches with a new water-expansion search method.
Description check ✅ Passed The description is related to the changeset, explaining the motivation (Manhattan-distance issues), the new approach (water-expansion fill), and how it improves boat pathfinding by using true boat distance instead of heuristics.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/core/game/TransportShipUtils.ts (4)

4-52: MinHeap implementation looks solid.

The binary min-heap is correctly implemented with standard bubble operations. The bitwise shift for parent index calculation is efficient and idiomatic.

Optional: Consider extracting to a shared utility if other pathfinding code could benefit

If Dijkstra-style searches are used elsewhere in the codebase, extracting MinHeap to a shared data structures file could reduce duplication. For now, keeping it local is fine.


65-144: Dijkstra-based water pathfinding is correctly implemented.

The algorithm correctly performs multi-source (water tiles adjacent to target shore) to multi-target (water tiles adjacent to player shores) expansion. The early termination optimization (lines 98-101) and stale entry filtering (lines 118-119) are standard Dijkstra optimizations.

Minor items:

  1. Line 98 comment typo: "If the like destination water" should be "If the destination water tiles are" or just "If destination water"
  2. Lines 85-86, 87-88: Odd formatting with continue on a separate line - this will be fixed by Prettier
  3. Performance consideration: The default maxSteps of 250,000 provides safety but could take time on complex maps. The caller-specified values of 3,000,000 in some call sites are quite high. Consider documenting the expected performance characteristics or validating with large maps.
Quick typo fix
-  // If the like destination water is already next to the player's shore, return immediately
+  // If destination water is already next to the player's shore, return immediately

196-202: Water-based spawn detection correctly replaces previous logic.

The integration of closestShoreFromPlayerByWater with lake filtering is appropriate for determining valid lake transport spawn points.

The maxSteps: 3_000_000 is quite high. If lake pathfinding on large maps becomes slow, consider tuning this value or adding early-exit optimizations.


283-288: Shore deployment source correctly uses water pathfinding.

The integration is correct and the validation checks (line 287) are appropriate.

Line 283 comment clarity: "IMPORTANT CHANGE:One exact water search replaces MiniAStar + sampling" has a formatting issue (missing space after colon) and could be clearer. Suggest:

-  // IMPORTANT CHANGE:One exact water search replaces MiniAStar + sampling.
+  // Uses single water-expansion search instead of MiniAStar + sampling
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0793153 and 67e6335.

📒 Files selected for processing (1)
  • src/core/game/TransportShipUtils.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-19T06:00:38.007Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:125-134
Timestamp: 2025-05-19T06:00:38.007Z
Learning: In StatsImpl.ts, unused parameters in boat/stats-related methods are intentionally kept for future use and shouldn't be removed.

Applied to files:

  • src/core/game/TransportShipUtils.ts
📚 Learning: 2025-08-23T07:48:19.060Z
Learnt from: ElMelchizedek
Repo: openfrontio/OpenFrontIO PR: 1876
File: src/core/execution/FakeHumanExecution.ts:470-473
Timestamp: 2025-08-23T07:48:19.060Z
Learning: In FakeHumanExecution.ts DefensePost placement logic, returning -Infinity from structureSpawnTileValue when no sampled border tiles neighbor enemies is intentional. The logic samples up to 50 border tiles as a heuristic - if none are adjacent to enemies, it assumes DefensePost placement is unnecessary and aborts the entire placement attempt rather than continuing to evaluate individual tiles.

Applied to files:

  • src/core/game/TransportShipUtils.ts
🪛 GitHub Actions: 🧪 CI
src/core/game/TransportShipUtils.ts

[warning] 1-1: Code style issues found in this file. Run 'prettier --write' to fix.

🪛 GitHub Check: 🔍 ESLint
src/core/game/TransportShipUtils.ts

[failure] 57-57:
Prefer using nullish coalescing operator (??) instead of a logical or (||), as it is a safer operator.

🔇 Additional comments (5)
src/core/game/TransportShipUtils.ts (5)

61-63: LGTM!

Clean helper function with clear intent.


214-216: LGTM on spawn point determination.

Correctly uses the new water-based search to find reachable spawn shores.


231-238: Correct bidirectional water pathfinding integration.

Both source and destination shore determination now correctly use water-based reachability rather than Manhattan distance.


310-336: LGTM on refactoring to use filtered border tiles.

The variable naming is clearer with borderShoreTiles. Note that this function still uses Manhattan distance rather than water-based pathfinding, which appears intentional since it's generating candidates for testing rather than performing final pathfinding.


54-59: Remove unnecessary as any cast—all methods are already on the GameMap interface.

The fallback pattern checks for methods (isWater, isOcean, isLake) that are all defined on the GameMap interface. The as any cast can be removed. Simplify to:

function isWaterTile(gm: GameMap, t: TileRef): boolean {
  return gm.isWater(t);
}

This uses clean, idiomatic TypeScript without bypassing the type system.

Note: The || operator on line 57 is correct—it returns true if either condition is true. The ?? operator would not work here.

⛔ Skipped due to learnings
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

@iiamlewis iiamlewis added the needs-grooming This feature has not been approved. label Dec 24, 2025
@iiamlewis iiamlewis added this to the v29 milestone Dec 24, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/game/TransportShipUtils.ts (1)

334-345: Fix broken extremum tracking logic.

The else-if chain is incorrect. If a tile has both the minimum X coordinate AND minimum Y coordinate, only minX gets set, and minY remains null. Each extremum check should be independent.

🔎 Fix with independent checks
     if (cell.x < minX) {
       minX = cell.x;
       extremumTiles.minX = tile;
-    } else if (cell.y < minY) {
+    }
+    if (cell.y < minY) {
       minY = cell.y;
       extremumTiles.minY = tile;
-    } else if (cell.x > maxX) {
+    }
+    if (cell.x > maxX) {
       maxX = cell.x;
       extremumTiles.maxX = tile;
-    } else if (cell.y > maxY) {
+    }
+    if (cell.y > maxY) {
+      maxY = cell.y;
       extremumTiles.maxY = tile;
     }
🧹 Nitpick comments (2)
src/core/game/TransportShipUtils.ts (2)

4-52: MinHeap implementation looks correct.

The min-heap operations (push, pop, bubbleUp, bubbleDown) follow standard algorithms and handle edge cases properly.

Consider using a well-known priority queue library instead of a custom implementation to reduce maintenance burden and potential bugs in the future.


54-62: Avoid any cast; use proper type guards.

The cast to any bypasses TypeScript's type safety. Consider using type guards or extending the GameMap interface to include isWater if it's expected to exist.

🔎 Improved type-safe approach
 function isWaterTile(gm: GameMap, t: TileRef): boolean {
-  const anyGM = gm as any;
-  if (typeof anyGM.isWater === "function") return anyGM.isWater(t);
-  if (typeof anyGM.isOcean === "function") {
-    if (anyGM.isOcean(t)) return true;
+  const gmWithWater = gm as GameMap & { isWater?: (t: TileRef) => boolean };
+  if (gmWithWater.isWater) return gmWithWater.isWater(t);
+  if (gm.isOcean(t)) return true;
-    return gm.isLake(t);
-  }
   return gm.isLake(t);
 }

Note: From the relevant code snippets, GameMap already has isWater, isOcean, and isLake methods, so the defensive checks may be unnecessary.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67e6335 and e3600a1.

📒 Files selected for processing (1)
  • src/core/game/TransportShipUtils.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-19T06:00:38.007Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:125-134
Timestamp: 2025-05-19T06:00:38.007Z
Learning: In StatsImpl.ts, unused parameters in boat/stats-related methods are intentionally kept for future use and shouldn't be removed.

Applied to files:

  • src/core/game/TransportShipUtils.ts
📚 Learning: 2025-08-23T07:48:19.060Z
Learnt from: ElMelchizedek
Repo: openfrontio/OpenFrontIO PR: 1876
File: src/core/execution/FakeHumanExecution.ts:470-473
Timestamp: 2025-08-23T07:48:19.060Z
Learning: In FakeHumanExecution.ts DefensePost placement logic, returning -Infinity from structureSpawnTileValue when no sampled border tiles neighbor enemies is intentional. The logic samples up to 50 border tiles as a heuristic - if none are adjacent to enemies, it assumes DefensePost placement is unnecessary and aborts the entire placement attempt rather than continuing to evaluate individual tiles.

Applied to files:

  • src/core/game/TransportShipUtils.ts
🧬 Code graph analysis (1)
src/core/game/TransportShipUtils.ts (3)
src/core/game/GameMap.ts (2)
  • GameMap (6-51)
  • TileRef (3-3)
src/core/game/Game.ts (1)
  • Player (530-667)
src/core/worker/WorkerClient.ts (1)
  • transportShipSpawn (228-256)
🔇 Additional comments (3)
src/core/game/TransportShipUtils.ts (3)

68-147: Verify maxSteps default doesn't cause performance issues.

The default maxSteps of 250,000 is quite large. On maps with extensive water networks, this could cause noticeable delays or even timeouts in the pathfinding loop.

Consider testing this function on the largest expected maps to ensure the default limit provides adequate safety without degrading user experience. If timeouts occur, consider lowering the default or making it configurable based on map size.


236-246: LGTM: Water-based pathfinding correctly integrated.

The refactor properly uses the new water-aware search for both source and destination shores, with appropriate fallback to closestShoreTN for terra nullis.


291-298: Water-based search correctly replaces sampling approach.

The unified water search is cleaner than the previous MiniAStar + sampling logic. The validation at line 297 correctly ensures the result is a valid shore owned by the player.

Note: maxSteps: 500_000 is still quite high; consider reducing if performance testing reveals issues.

Comment on lines +201 to +207
const spawn = closestShoreFromPlayerByWater(game, player, dst, {
waterFilter: (t) => game.isLake(t),
maxSteps: 3_000_000,
});

for (const t of sorted) {
if (game.owner(t) === player) {
return transportShipSpawn(game, player, t);
}
}
return false;
if (spawn === null) return false;
return transportShipSpawn(game, player, spawn);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Excessive maxSteps value may cause performance degradation.

The maxSteps: 3_000_000 is extremely large and could cause significant delays or lock up the game on maps with large lake networks. This is 12× the default value of 250,000.

Consider reducing this to a more reasonable value (e.g., 500,000 or less) or making it proportional to map size. If long searches are truly necessary, consider adding progress indicators or moving this to a web worker to avoid blocking the main thread.

🤖 Prompt for AI Agents
In src/core/game/TransportShipUtils.ts around lines 201 to 207 the explicit
maxSteps value is set to an excessively large 3_000_000 which can freeze or
severely slow the game on large lake networks; change this to a safer default
(e.g., 500_000 or lower) or compute it proportionally from map size (for example
derive maxSteps from total tile count or map dimensions) and use that computed
value here; if long searches are required, move the search off the main thread
(web worker) or make it incremental with progress indicators to avoid blocking;
update the call to closestShoreFromPlayerByWater to use the new
bounded/proportional maxSteps.

Comment on lines +219 to +221
const spawn = closestShoreFromPlayerByWater(game, player, targetTile, {
maxSteps: 3_000_000,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Excessive maxSteps value here as well.

Same concern as in canBuildTransportShip: the maxSteps: 3_000_000 could cause performance issues on large maps.

Reduce to a more reasonable value or make it configurable based on map characteristics.

maxY: null,
};

maxY = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove dead code: maxY = 1; assignment.

This assignment appears to be leftover debug code or a copy-paste error. The value is never read before being potentially overwritten at line 343-344.

🔎 Remove dead code
-  maxY = 1;
   const borderShoreTiles = Array.from(player.borderTiles()).filter((t) =>
     gm.isShore(t),
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
maxY = 1;
const borderShoreTiles = Array.from(player.borderTiles()).filter((t) =>
gm.isShore(t),
);
🤖 Prompt for AI Agents
In src/core/game/TransportShipUtils.ts around line 320, there is a dead
assignment "maxY = 1;" that is never read before being potentially overwritten
later (lines ~343-344); remove that line entirely and run tests/lint to ensure
no other code relies on that temporary value and that variable
initialization/usage remains correct where maxY is actually set.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-grooming This feature has not been approved.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants