-
Notifications
You must be signed in to change notification settings - Fork 718
Scan cluster boundary #2607
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Scan cluster boundary #2607
Conversation
Refactor PlayerExecution.surroundedBySamePlayer to avoid per-tile callback allocations and replace Set/Array.from enemy tracking with scalars while keeping the same neighbor-walk semantics.
- Extract common boundary scanning logic into reusable scanClusterBoundary method - Refactor surroundedBySamePlayer and isSurrounded to use the new helper
WalkthroughAdded two private helpers to PlayerExecution: Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks✅ Passed checks (3 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧠 Learnings (19)📓 Common learnings📚 Learning: 2025-12-13T14:58:22.026ZApplied to files:
📚 Learning: 2025-11-29T22:22:37.178ZApplied to files:
📚 Learning: 2025-08-23T07:48:19.060ZApplied to files:
📚 Learning: 2025-10-20T20:15:28.858ZApplied to files:
📚 Learning: 2025-10-08T17:14:49.369ZApplied to files:
📚 Learning: 2025-10-18T17:54:01.311ZApplied to files:
📚 Learning: 2025-10-27T09:47:26.395ZApplied to files:
📚 Learning: 2025-10-20T11:02:16.969ZApplied to files:
📚 Learning: 2025-10-08T17:14:49.369ZApplied to files:
📚 Learning: 2025-08-29T16:16:11.309ZApplied to files:
📚 Learning: 2025-11-01T00:24:33.860ZApplied to files:
📚 Learning: 2025-05-31T18:15:03.445ZApplied to files:
📚 Learning: 2025-05-21T04:10:33.435ZApplied to files:
📚 Learning: 2025-05-21T04:10:33.435ZApplied to files:
📚 Learning: 2025-05-21T04:10:59.706ZApplied to files:
📚 Learning: 2025-08-12T00:31:50.144ZApplied to files:
📚 Learning: 2025-06-22T05:48:19.241ZApplied to files:
📚 Learning: 2025-11-26T22:27:31.844ZApplied to files:
🧬 Code graph analysis (1)src/core/execution/PlayerExecution.ts (2)
🔇 Additional comments (4)
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. Comment |
There was a problem hiding this 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 (2)
src/core/execution/PlayerExecution.ts (2)
24-33:isEdgeTileFastassumes TileRef is row-major; consider switching to(x,y)-based edge check.
Line 31 usestile < width/tile >= (height - 1) * width. That’s only correct ifTileRefis the linear index in row-major order. IfTileRefis ever remapped, this becomes wrong while still “looking” correct.Suggested safe + still-fast tweak:
- private isEdgeTileFast( - tile: TileRef, - x: number, - width: number, - height: number, - ): boolean { - return ( - x === 0 || x === width - 1 || tile < width || tile >= (height - 1) * width - ); - } + private isEdgeTileFast(x: number, y: number, width: number, height: number): boolean { + return x === 0 || x === width - 1 || y === 0 || y === height - 1; + }(Then in the caller, compute
const y = mg.y(tile)once.)
35-154:scanClusterBoundarystructure looks solid; consider making rejection outcome explicit (optional).
Returningundefinedfor any rejection is fast, but it makes debugging/telemetry hard (everything collapses to “falsey”). If you ever need to tune these heuristics, returning{ rejected: true, reason: "edge" | "shore" | ... }can help without changing the hot path much (still no allocations per neighbor).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/core/execution/PlayerExecution.ts(3 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2539
File: src/core/game/GameImpl.ts:520-542
Timestamp: 2025-11-29T22:22:37.178Z
Learning: In GameImpl.ts, neighborsWithDiag() and forEachNeighborWithDiag() intentionally duplicate coordinate iteration logic. They serve different purposes: forEachNeighborWithDiag() is a zero-allocation hot-path optimization while neighborsWithDiag() is a convenience method that returns an array. Refactoring one to call the other would add callback/closure allocations and indirection overhead, defeating the performance goals.
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.
📚 Learning: 2025-11-29T22:22:37.178Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2539
File: src/core/game/GameImpl.ts:520-542
Timestamp: 2025-11-29T22:22:37.178Z
Learning: In GameImpl.ts, neighborsWithDiag() and forEachNeighborWithDiag() intentionally duplicate coordinate iteration logic. They serve different purposes: forEachNeighborWithDiag() is a zero-allocation hot-path optimization while neighborsWithDiag() is a convenience method that returns an array. Refactoring one to call the other would add callback/closure allocations and indirection overhead, defeating the performance goals.
Applied to files:
src/core/execution/PlayerExecution.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/execution/PlayerExecution.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.
Applied to files:
src/core/execution/PlayerExecution.ts
🧬 Code graph analysis (1)
src/core/execution/PlayerExecution.ts (4)
src/core/game/GameMap.ts (4)
TileRef(3-3)x(122-124)width(134-136)height(137-139)src/core/game/GameImpl.ts (3)
x(831-833)width(840-842)height(843-845)src/core/game/Game.ts (2)
Player(529-665)Cell(316-338)src/core/Util.ts (2)
calculateBoundingBox(74-92)inscribed(161-171)
🔇 Additional comments (1)
src/core/execution/PlayerExecution.ts (1)
297-315:isSurroundedrefactor is clear and keeps the previous “shore bit” semantics.
The early!scan || !scan.hasEnemyNeighborguard (Line 307) correctly prevents using Infinity bbox values. The comment about ocean-vs-shore nuance is helpful for future callers.
…es scan) Compute enemy bounding box from boundary-neighbor tiles gathered during scanClusterBoundary (trackEnemyBBox) instead of calculateBoundingBox(enemy.borderTiles()).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/core/execution/PlayerExecution.ts (2)
24-33:isEdgeTileFastassumes row-majorTileReflayout — please document that invariant.
This uses bothxand rawtileindex ranges (tile < width,tile >= (height-1)*width). IfTileRefencoding ever changes, this silently breaks. A short comment like “TileRef is row-major index” (or an assertion in debug builds) would make this safer.
35-154:scanClusterBoundaryoption default is surprising (rejectIfEdgeTile ?? true) — make it explicit.
BecauserejectIfEdgeTileis optional, defaulting it totruecan bite future callers (they may expect “unset means off”). Consider either:
- require
rejectIfEdgeTile: booleaninopts, or- default
false, and settrueat call sites, or- add a
const rejectIfEdgeTile = opts.rejectIfEdgeTile ?? true;at the top and comment that “edge reject is the default behavior for this helper”.Also, returning
enemyMinX/Y/... = Infinity/-Infinitywhen bbox isn’t tracked (or no enemy exists) is easy to misuse. Prefer a typed union like{ hasEnemyNeighbor: false } | { hasEnemyNeighbor: true; enemyBBox?: {min: Cell; max: Cell}; ... }so callers can’t accidentally constructCell(Infinity, Infinity).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/core/execution/PlayerExecution.ts(3 hunks)
🧰 Additional context used
🧠 Learnings (18)
📓 Common learnings
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:22.026Z
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.
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2539
File: src/core/game/GameImpl.ts:520-542
Timestamp: 2025-11-29T22:22:37.178Z
Learning: In GameImpl.ts, neighborsWithDiag() and forEachNeighborWithDiag() intentionally duplicate coordinate iteration logic. They serve different purposes: forEachNeighborWithDiag() is a zero-allocation hot-path optimization while neighborsWithDiag() is a convenience method that returns an array. Refactoring one to call the other would add callback/closure allocations and indirection overhead, defeating the performance goals.
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.
📚 Learning: 2025-12-13T14:58:22.026Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:22.026Z
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.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-11-29T22:22:37.178Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2539
File: src/core/game/GameImpl.ts:520-542
Timestamp: 2025-11-29T22:22:37.178Z
Learning: In GameImpl.ts, neighborsWithDiag() and forEachNeighborWithDiag() intentionally duplicate coordinate iteration logic. They serve different purposes: forEachNeighborWithDiag() is a zero-allocation hot-path optimization while neighborsWithDiag() is a convenience method that returns an array. Refactoring one to call the other would add callback/closure allocations and indirection overhead, defeating the performance goals.
Applied to files:
src/core/execution/PlayerExecution.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/execution/PlayerExecution.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-10-18T17:54:01.311Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:172-173
Timestamp: 2025-10-18T17:54:01.311Z
Learning: In src/core/execution/FakeHumanExecution.ts, MIRVs and ground attacks should not be mutually exclusive. The considerMIRV() method should not short-circuit maybeAttack() - both actions can occur in the same tick.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-10-20T11:02:16.969Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:57-57
Timestamp: 2025-10-20T11:02:16.969Z
Learning: In src/core/execution/FakeHumanExecution.ts, the correct MIRV victory denial thresholds are VICTORY_DENIAL_TEAM_THRESHOLD = 0.8 (80% for team games) and VICTORY_DENIAL_INDIVIDUAL_THRESHOLD = 0.65 (65% for individual players), not 0.85 and 0.7 as might be mentioned in some documentation.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-08-29T16:16:11.309Z
Learnt from: BrewedCoffee
Repo: openfrontio/OpenFrontIO PR: 1957
File: src/core/execution/PlayerExecution.ts:40-52
Timestamp: 2025-08-29T16:16:11.309Z
Learning: In OpenFrontIO PlayerExecution.ts, when Defense Posts are captured due to tile ownership changes, the intended behavior is to first call u.decreaseLevel() to downgrade them, then still transfer them to the capturing player via captureUnit(). This is not a bug - Defense Posts should be both downgraded and captured, not destroyed outright.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-11-01T00:24:33.860Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2161
File: src/core/execution/FakeHumanExecution.ts:670-678
Timestamp: 2025-11-01T00:24:33.860Z
Learning: In OpenFrontIO, PlayerType.Bot entities cannot be in teams and do not have friendliness relationships. Only PlayerType.Human and PlayerType.FakeHuman can participate in teams and alliances. Therefore, when targeting bot-owned tiles, friendliness checks like `owner.isFriendly(this.player)` are unnecessary and meaningless for bots.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-05-31T18:15:03.445Z
Learnt from: 1brucben
Repo: openfrontio/OpenFrontIO PR: 977
File: src/core/execution/AttackExecution.ts:123-125
Timestamp: 2025-05-31T18:15:03.445Z
Learning: The removeTroops function in PlayerImpl.ts already prevents negative troop counts by using minInt(this._troops, toInt(troops)) to ensure it never removes more troops than available.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as a type inferred from a Zod schema that is marked as optional, which means PlayerStats already includes undefined as a possible type (PlayerStats | undefined).
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as `z.infer<typeof PlayerStatsSchema>` where PlayerStatsSchema has `.optional()` applied at the object level, making PlayerStats a union type that already includes undefined (PlayerStats | undefined).
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-05-21T04:10:59.706Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:44-53
Timestamp: 2025-05-21T04:10:59.706Z
Learning: The PlayerStats type from ArchiveSchemas already includes undefined in its definition, making explicit union types with undefined (PlayerStats | undefined) redundant.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.
Applied to files:
src/core/execution/PlayerExecution.ts
📚 Learning: 2025-06-22T05:48:19.241Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 786
File: src/client/TerritoryPatternsModal.ts:337-338
Timestamp: 2025-06-22T05:48:19.241Z
Learning: In src/client/TerritoryPatternsModal.ts, the bit shifting operators (<<) used in coordinate calculations with decoder.getScale() are intentional and should not be changed to multiplication. The user scottanderson confirmed this is functioning as intended.
Applied to files:
src/core/execution/PlayerExecution.ts
🧬 Code graph analysis (1)
src/core/execution/PlayerExecution.ts (4)
src/core/game/GameMap.ts (4)
TileRef(3-3)x(122-124)width(134-136)height(137-139)src/core/game/GameImpl.ts (3)
x(831-833)width(840-842)height(843-845)src/core/game/Game.ts (2)
Player(529-665)Cell(316-338)src/core/Util.ts (2)
calculateBoundingBox(74-92)inscribed(161-171)
🔇 Additional comments (1)
src/core/execution/PlayerExecution.ts (1)
302-320: TheisSurroundedmethod intentionally omitsrejectIfUnownedNeighborto maintain prior behavior, as stated in the code comment: "This keeps the priormg.isShore(tile)behavior." The difference betweenisSurrounded()andsurroundedBySamePlayer()is intentional—the two methods serve different purposes and have appropriate strictness levels. No change needed.Likely an incorrect or invalid review comment.
…ency - Updated comments in PlayerExecution to clarify the behavior of `rejectIfShoreTile` and `rejectIfOceanNeighbor`. - Removed redundant comments and streamlined explanations to enhance understanding of shoreline and ocean adjacency rejection logic.
Description:
This PR refactors cluster “surrounded” detection to use a new fast boundary scan helper (
scanClusterBoundary) that avoids per-tile allocations and short-circuits early on common rejection cases (edge-of-map, shore/ocean adjacency, unowned adjacency, multiple-enemy adjacency).Why
surroundedBySamePlayer(...)andisSurrounded(...)are hot-path style checks that previously:enemies,enemyTiles) and (in one case) used logic that can imply extra work on large clusters.This change centralizes boundary scanning into a single pass and enables bounding-box tracking without allocating a tile set.
What changed
New helpers
isEdgeTileFast(...): quick edge check without calling into heavier map helpers.scanClusterBoundary(...): single-pass boundary scan with configurable rejection/collection options:mg.isShore(tile)), ocean adjacency via neighbor check (mg.isOcean(n)), unowned neighbor.surroundedBySamePlayer(cluster)scanClusterBoundarywith:isOceanShore(tile)semantics), unowned neighborisSurrounded(cluster)scanClusterBoundarywith:mg.isShore(tile)(matches prior behavior; note this can include lake shorelines)Cell.Behavior notes / edge cases
surroundedBySamePlayerpreviously rejectedmg.isOceanShore(tile); it now rejects via neighbor ocean detection (mg.isOcean(n)) which is intended to match “true ocean adjacency” without allocating neighbor arrays.isSurroundedkeeps the priormg.isShore(tile)behavior (terrain shoreline bit), which may include lake shorelines as well as ocean.Please complete the following:
Please put your Discord username so you can be contacted if a bug or regression is found:
DISCORD_USERNAME