Skip to content

Impassible terrain acts like map edge - #4922

Open
TKTK123456 wants to merge 7 commits into
openfrontio:mainfrom
TKTK123456:impassibleTerrain-acts-like-map-edge
Open

Impassible terrain acts like map edge#4922
TKTK123456 wants to merge 7 commits into
openfrontio:mainfrom
TKTK123456:impassibleTerrain-acts-like-map-edge

Conversation

@TKTK123456

Copy link
Copy Markdown
Contributor

Before opening a PR: discuss new features on Discord first, and file bugs or small improvements as issues. You must be assigned to an approved issue — unsolicited PRs will be auto-closed.

Add approved & assigned issue number here:

Resolves #4907

Description:

Impassible terrain acts like map edge

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

Please put your Discord username so you can be contacted if a bug or regression is found:

tktk1234567

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a7d7700-e4f9-4cd0-ae09-4d33d206e6ac

📥 Commits

Reviewing files that changed from the base of the PR and between c7fc0d1 and f5a3560.

📒 Files selected for processing (1)
  • tests/ImpassableTerrain.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


Walkthrough

The change updates terrain classification so impassable tiles remain land. Map-edge detection now treats adjacent impassable tiles as edges. Tests cover classification, annexation, rail traversal, nuke blasts, and water conversion.

Changes

Impassable terrain behavior

Layer / File(s) Summary
Terrain classification and edge handling
src/core/game/GameMap.ts
isLand, isWater, isShoreline, isImpassable, isOnEdgeOfMap, and terrainType now use the updated impassable-terrain rules.
Impassable terrain validation
tests/ImpassableTerrain.test.ts
Tests create bounded wall segments and validate impassable tiles as land, edge detection, annexation, rail traversal, nuke blasts, and water conversion.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to f5a35

This change makes impassable terrain behave like the map edge and updates the related tests; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: flopinguin, evanpelle

Poem

Impassable ground stays land
Edge checks include nearby walls
Rails stop before the barrier
Tests guard each terrain rule
The map keeps its boundaries

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The pull request includes terrain classification changes and terrain-generation or game-setup changes beyond the linked issue. These changes affect isLand, isShoreline, isImpassable, isWater, terrainT… Remove or separate the unrelated terrain classification and terrain-generation changes. Keep only the isOnEdgeOfMap fix and tests required for issue #4907, unless the additional changes have separate approved issue scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: impassible terrain is treated like the map edge. The spelling error does not make the title misleading.
Description check ✅ Passed The description identifies issue #4907 and explains that impassible terrain should act like the map edge. It also states that relevant tests were added.
Linked Issues check ✅ Passed The changes update isOnEdgeOfMap to treat impassable cardinal neighbors as map edges. The added tests cover adjacency, annexation, and rail pathfinding related to issue #4907.
Full details: Out of Scope Changes check

Explanation

The pull request includes terrain classification changes and terrain-generation or game-setup changes beyond the linked issue. These changes affect isLand, isShoreline, isImpassable, isWater, terrainType, and map wall bounds, although the issue only requires the isOnEdgeOfMap behavior.

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/core/pathfinding/algorithms/AStar.Rail.ts (1)

55-80: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject impassable destination tiles.

GameMap.isLand() now returns false for impassable tiles. Therefore, isWater(to) returns true for those tiles. When fromShoreline is true, isTraversable() accepts the impassable destination.

The rail graph can then enter impassable terrain. Check isImpassable(to) before applying the water rule.

Suggested fix
 private isTraversable(to: number, fromShoreline: boolean): boolean {
+  if (this.gameMap.isImpassable(to)) return false;
   const toWater = this.gameMap.isWater(to);
   if (!toWater) return true;
   return fromShoreline || this.gameMap.isShoreline(to);
 }

Add a regression test with a shoreline source and an adjacent impassable destination. As per coding guidelines, every change in src/core/ must include tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/pathfinding/algorithms/AStar.Rail.ts` around lines 55 - 80, Update
AStar.Rail’s isTraversable method to reject gameMap.isImpassable(to) before
applying the water and shoreline rules, including when fromShoreline is true.
Add a regression test covering a shoreline source with an adjacent impassable
destination, and ensure the rail graph does not include that destination.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/game/GameMap.ts`:
- Around line 210-217: Update terrainType() to evaluate isImpassable() before
isLand(), ensuring magnitude-31 tiles classify as impassable rather than Ocean.
In updateTile(), use the same passable-land predicate as isLand() when adjusting
numLandTiles_, including passable-to-impassable transitions. Add regression
tests covering both terrain classification and land-count updates.

---

Outside diff comments:
In `@src/core/pathfinding/algorithms/AStar.Rail.ts`:
- Around line 55-80: Update AStar.Rail’s isTraversable method to reject
gameMap.isImpassable(to) before applying the water and shoreline rules,
including when fromShoreline is true. Add a regression test covering a shoreline
source with an adjacent impassable destination, and ensure the rail graph does
not include that destination.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d820b799-2068-4b04-b199-9f80cf8a9f17

📥 Commits

Reviewing files that changed from the base of the PR and between 508d076 and 5019c72.

📒 Files selected for processing (2)
  • src/core/game/GameMap.ts
  • src/core/pathfinding/algorithms/AStar.Rail.ts

Comment thread src/core/game/GameMap.ts Outdated
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Aug 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/GameMap.ts (1)

210-223: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep numLandTiles_ aligned with isLand().

isLand() now excludes magnitude-31 tiles, but updateTile() still calculates isNowLand from only IS_LAND_BIT at Lines [547]-[551]. A passable-land to impassable transition does not decrement numLandTiles_. The reverse transition increments it incorrectly.

Use this.isLand(tile) after assigning terrainByte, and add tests for both transitions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/game/GameMap.ts` around lines 210 - 223, Update updateTile() so
isNowLand is computed with this.isLand(tile) after terrainByte is assigned,
keeping numLandTiles_ consistent with isLand() for passable-to-impassable and
impassable-to-passable transitions. Add tests covering both transitions and
verifying the land-tile count.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/core/game/GameMap.ts`:
- Around line 210-223: Update updateTile() so isNowLand is computed with
this.isLand(tile) after terrainByte is assigned, keeping numLandTiles_
consistent with isLand() for passable-to-impassable and impassable-to-passable
transitions. Add tests covering both transitions and verifying the land-tile
count.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c635906e-4a41-470f-a66d-55a9d153e20a

📥 Commits

Reviewing files that changed from the base of the PR and between 5019c72 and ff148a5.

📒 Files selected for processing (3)
  • src/core/game/GameImpl.ts
  • src/core/game/GameMap.ts
  • tests/ImpassableTerrain.test.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 9, 2026

@FloPinguin FloPinguin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Little AI Check

🔴 Critical issue — rails can now cross impassable terrain (reintroduces #4902)

The AStar.Rail.ts hunk removes the source-node isImpassable check from isTraversable(). That check was added by PR #4903 (same author, merged 2 days ago) specifically to stop rails from crossing impassable walls.

I proved this empirically with a throwaway test (wall map from the existing test file, rail path from one side to the other):

Branch Path crosses impassable?
main (with #4903 fix) ✅ No — 0 impassable tiles in path
PR #4922 Yes — 2 impassable tiles in path

The new isLand/isWater semantics don't compensate: impassable is not water, so isTraversable() returns true for impassable destinations, and with the source check gone impassable nodes are fully expandable by A*. The result is railroad tracks get built over impassable terrain again — exactly the bug #4903 fixed. This needs to be fixed before merge (re-add the source check, or add a destination-side isImpassable(to) guard) and needs a regression test.

🟠 Secondary issues

  1. Misleading test name. The new test is named "isLand returns false for impassable (can't pathfind trains through it)" — but after this PR, trains can pathfind through it. The parenthetical is factually wrong and there's no actual rail test anywhere, which is how this regression slipped through 2810 tests.

  2. isShoreline change silently breaks WaterManager cleanup. WaterManager.updateWater() has a defensive branch that clears stale shoreline bits on impassable tiles: if (map.isImpassable(tile)) { if (map.isShoreline(tile)) { map.clearShorelineBit(tile); ... } }. Because isShoreline now always returns false for impassable, that clearing never runs. The raw bit is still serialized via terrainByte() and uploaded to the GPU, so a stale bit could render a sand/water outline around impassable tiles. Low likelihood, but the defensive code is now dead — better to read the raw bit there.

  3. isOnEdgeOfMap bounds safety is fragile. ref(x, y) throws on invalid coordinates, and the new ref(x±1, y) calls are only safe because of short-circuit OR ordering (they never run when the tile is on the physical edge). It works, but any future reordering of the condition would throw at runtime. Worth a comment or a bounds guard.

@TKTK123456
TKTK123456 requested a review from FloPinguin August 9, 2026 20:07
@JB940

JB940 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Heya, what's the progress on dis?

@iiamlewis

Copy link
Copy Markdown
Contributor

@TKTK123456 (via Discord):

<@137605107385761792> you may work on it if you want

Keep only the isOnEdgeOfMap change, which by itself resolves openfrontio#4907
(a cluster hugging an impassable wall can no longer be "surrounded"
from that side), and rewrite it with the same bounds-guarded
ref±1/ref±w idiom used by isBorder/isOceanShore.

Revert the isLand/isWater/isShoreline redefinition, the AStar.Rail
guard removal and the conquer() reorder: making impassable neither
land nor water let RailAdapter.isTraversable() treat impassable tiles
as plain land, so rail A* routed straight through walls (reverting
openfrontio#4903), and client `!isLand` water branches started firing on walls.

Tests: isOnEdgeOfMap adjacency, the openfrontio#4907 annexation scenario, and
rail pathfinding across the wall. Each fails without its fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@evanpelle

Copy link
Copy Markdown
Collaborator

Pushed c7fc0d1 onto this branch (maintainer edit) to narrow the fix before merge.

Why: redefining isLand/isWater so impassable is neither land nor water made RailAdapter.isTraversable() treat impassable tiles as plain land once its isImpassable guard was removed — rail A* then routed straight through walls (repro: 2-wide wall, AStarRail.findPath returned a 31-tile path crossing it; base returned null). That reverted #4903. The client's !isLand "water" branches (PlayerInfoOverlay, HoverHighlightController) also started firing on impassable tiles.

What the commit does:

🤖 Generated with Claude Code

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 25, 2026
@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Verdict: The core fix (isOnEdgeOfMap in src/core/game/GameMap.ts) is correct, safe, and well-covered by its dedicated unit test — but one of the two new integration tests doesn't actually exercise the fix. Findings: 0 critical, 0 high, 1 medium, 0 low.

tests/ImpassableTerrain.test.ts (medium)

The "cluster hugging the impassable wall is not annexed" test (~lines 172-207) is vacuous — it passes with or without this PR's fix, so it provides no regression protection for the change it's meant to guard.

Trace of why, using the test's own geometry (200×200 map, impassable wall at x=100,101 spanning the full height y=0..199, pocket at x=97..99, y=50..52):

  • Annexation is gated by PlayerExecution.isSurrounded first (src/core/execution/PlayerExecution.ts:222), which itself calls map.isOnEdgeOfMap(tr) on each pocket tile (line 231). Post-fix, pocket tile (99,50) is adjacent to the wall at (100,50), so isOnEdgeOfMap now correctly returns true, and isSurrounded bails out immediately — this is the intended fix path.
  • Pre-fix, isOnEdgeOfMap only checks the literal map boundary, so isSurrounded doesn't bail here and proceeds to return true (enemy bounding box is inscribed in the cluster's box) → removeCluster runs.
  • But removeCluster then calls isEnclosed (PlayerExecution.ts:314), whose flood fill treats unowned impassable land as traversable (it only stops on water — line 341: if (ownerId === 0 && !map.isLand(n)) return false;) and also calls map.isOnEdgeOfMap(tile) on every visited tile (line 327). The fill walks from the pocket into the unowned wall column, which spans the entire map height and touches the literal top/bottom edges (y=0/y=199) — so isEnclosed returns false even pre-fix, and removeCluster bails before conquering anything.

Net effect: pre-fix, the pocket survives via the isEnclosed safety net (for the wrong reason — reaching the real map edge through the wall, not because the wall is edge-equivalent); post-fix, it survives via isSurrounded (the right reason). Since the test only asserts final tile ownership and never inspects isSurrounded/isEnclosed directly, it cannot distinguish the two paths and would not catch a regression of the isOnEdgeOfMap change.

Suggested fix: either assert on isSurrounded/isEnclosed behavior directly (not currently exposed for tests, so may need a small seam), or change the test geometry so the wall does not itself span to the literal map edge (e.g. cap the wall's height and have the far side be enemy-owned rather than open/unclaimed), so pre-fix the pocket would actually get annexed and post-fix it would not. The existing "isOnEdgeOfMap is true next to impassable terrain, false elsewhere" unit test does correctly cover the fix directly, so this is not a coverage gap for the fix overall — but the integration test's premise doesn't hold.

On main, isEnclosed (openfrontio#5027) walks unowned impassable tiles like land, so
with a full-height wall the flood fill reached the literal map edge and
rescued the pocket even without the isOnEdgeOfMap fix. Use a short wall
segment ringed by enemy territory so the pocket is annexed pre-fix and
survives only because impassable now counts as a map edge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@evanpelle

Copy link
Copy Markdown
Collaborator

Addressed the review's medium finding in f5a3560.

Confirmed the diagnosis on a scratch merge with main: with the full-height wall, isEnclosed walked the unowned impassable column to the literal map edge and rescued the pocket even with the original isOnEdgeOfMap, so the test passed both ways. The test now builds a short wall segment (y=45..57) fully ringed by enemy territory. On the merged tree it fails without the isOnEdgeOfMap change (pocket annexed) and passes with it; same at this PR's base.

🤖 Generated with Claude Code

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

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

Players being annexed well beside impassible terrain

5 participants