Skip to content

Conversation

@VariableVince
Copy link
Contributor

Description:

Describe the PR.

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:

DISCORD_USERNAME

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 26, 2025

Walkthrough

Adds a TransportShipFilter enum and threads it through game, worker, and client layers; player action and buildable-unit APIs accept an optional filter. Client/UI call sites use Only/Exclude. Transport-ship destination resolution gains a per‑tick cached resolver targetTransportTile.

Changes

Cohort / File(s) Summary
Core Types & Exports
src/core/game/Game.ts
Add exported TransportShipFilter enum (Exclude / Only) and update Player.buildableUnits() signature to accept an optional filter.
Player Implementation
src/core/game/PlayerImpl.ts
Update signatures (buildableUnits, canBuild, canUpgradeUnit, findUnitToUpgrade), add helpers canBuildUnitType / canUpgradeUnitType, and thread transportShipFilter / skipUnitTypeCheck through build/upgrade and spawn filtering.
Transport Ship Utilities
src/core/game/TransportShipUtils.ts
Add per‑tick cache and helpers, export targetTransportTile(gm, tile), route canBuildTransportShip through cached resolution, and remove sourceDstOceanShore.
Game View / Forwarding
src/core/game/GameView.ts
PlayerView.actions(tile?, transportShipFilter?) accepts and forwards the filter to the worker/playerInteraction.
GameRunner & Worker wiring
src/core/GameRunner.ts, src/core/worker/Worker.worker.ts
GameRunner.playerActions(...) gains optional transportShipFilter and forwards it into buildableUnits(...); worker forwards the filter into GameRunner.
Worker Protocol & Client
src/core/worker/WorkerMessages.ts, src/core/worker/WorkerClient.ts
Add transportShipFilter?: TransportShipFilter to PlayerActionsMessage and include transportShipFilter in WorkerClient.playerInteraction payload/signature.
Client UI & Runner
src/client/ClientGameRunner.ts, src/client/graphics/layers/StructureIconsLayer.ts, src/client/graphics/layers/UnitDisplay.ts
Import TransportShipFilter; client input paths call actions(..., TransportShipFilter.Only) for transport intents and UI layers call actions(..., TransportShipFilter.Exclude) to omit transport actions; some call sites add non‑null assertions for myPlayer.
Tests
tests/PlayerImpl.test.ts
Enqueue two SpawnExecution instances in setup to explicitly spawn both players before spawn-phase checks.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant WorkerClient
    participant Worker
    participant GameRunner
    participant Player

    Client->>WorkerClient: playerInteraction(playerID, x?, y?, transportShipFilter?)
    WorkerClient->>Worker: postMessage { type: "player_actions", transportShipFilter }
    Worker->>GameRunner: playerActions(playerID, x?, y?, transportShipFilter)
    GameRunner->>Player: buildableUnits(tile, transportShipFilter)
    alt transportShipFilter == Only
        Player-->>GameRunner: BuildableUnit[] (transports only)
    else transportShipFilter == Exclude
        Player-->>GameRunner: BuildableUnit[] (no transports)
    else
        Player-->>GameRunner: BuildableUnit[] (all units)
    end
    GameRunner-->>Worker: PlayerActions
    Worker-->>WorkerClient: response
    WorkerClient-->>Client: Promise<PlayerActions>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

  • Focus areas:
    • src/core/game/PlayerImpl.ts — new helpers, skipUnitTypeCheck semantics, and transport filter effects.
    • src/core/game/TransportShipUtils.ts — cache key, per‑tick invalidation, and targetTransportTile correctness.
    • Worker ↔ client message and signature changes: src/core/worker/WorkerMessages.ts, src/core/worker/WorkerClient.ts, src/core/worker/Worker.worker.ts.
    • Client call-sites for correct filter usage and safety around myPlayer! assertions.

Possibly related PRs

Suggested labels

Performance Optimization, Feature - New

Suggested reviewers

  • evanpelle
  • scottanderson

Poem

🚢 A tiny filter sets the course,
Caches whisper, saving force.
Actions split — hide or show,
Spawn and upgrade learn to flow.

Pre-merge checks

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description is completely empty and contains only placeholder template text. The body reads "Describe the PR." which is a template instruction, not an actual description of the changes. All checklist items are unchecked, and the Discord username field contains only the placeholder text "DISCORD_USERNAME." The description provides zero information about what changes were made, why they were made, or what problems they solve.
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.
Title Check ❓ Inconclusive The title "DRAFT Transport ship perf" is vague and uses non-descriptive terminology that doesn't clearly convey the main changes in the PR. While the title references "transport ship," which is relevant to the changeset, the abbreviation "perf" and the lack of specificity make it difficult to understand what the PR actually accomplishes without reading the full change summary. The title could refer to various types of performance work (optimization, refactoring, feature addition), but doesn't explain what specific changes were made or their primary purpose.

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: 10

Caution

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

⚠️ Outside diff range comments (1)
src/core/worker/WorkerClient.ts (1)

162-194: Rename parameter to follow TypeScript conventions.

The parameter TransportShipFilter shadows the type name. Use lowercase transportShipFilter for the parameter and message property.

   playerInteraction(
     playerID: PlayerID,
     x?: number,
     y?: number,
-    TransportShipFilter?: TransportShipFilter,
+    transportShipFilter?: TransportShipFilter,
   ): Promise<PlayerActions> {
     return new Promise((resolve, reject) => {
       if (!this.isInitialized) {
         reject(new Error("Worker not initialized"));
         return;
       }
 
       const messageId = generateID();
 
       this.messageHandlers.set(messageId, (message) => {
         if (
           message.type === "player_actions_result" &&
           message.result !== undefined
         ) {
           resolve(message.result);
         }
       });
 
       this.worker.postMessage({
         type: "player_actions",
         id: messageId,
         playerID: playerID,
         x: x,
         y: y,
-        TransportShipFilter: TransportShipFilter,
+        transportShipFilter: transportShipFilter,
       });
     });
   }
🧹 Nitpick comments (2)
src/core/game/Game.ts (1)

863-869: Add documentation for the enum.

The new TransportShipFilter enum lacks documentation explaining when to use each variant. Consider adding a JSDoc comment describing the purpose and usage of each value.

+/**
+ * Filter for buildable units to control transport ship inclusion.
+ * - Default: Include transport ships in results (standard behavior)
+ * - Exclude: Remove transport ships from results
+ * - Only: Return only transport ships
+ */
 export enum TransportShipFilter {
-  Default = "default", // Include
-  Exclude = "exclude", // Exclude
-  Only = "only", // TransportShip only
+  Default = "default",
+  Exclude = "exclude",
+  Only = "only",
 }
src/client/ClientGameRunner.ts (1)

416-432: Consider capturing myPlayer in a local const for safer async callback access.

The non-null assertion on line 426 assumes myPlayer remains non-null after the async actions() call. While safe in practice, capturing myPlayer in a local variable after the null check improves clarity and guards against future changes.

Apply this pattern:

 if (this.myPlayer === null) {
   const myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
   if (myPlayer === null) return;
   this.myPlayer = myPlayer;
 }
+const player = this.myPlayer;
-this.myPlayer.actions(tile, TransportShipFilter.Only).then((actions) => {
+player.actions(tile, TransportShipFilter.Only).then((actions) => {
   if (actions.canAttack) {
     this.eventBus.emit(
       new SendAttackIntentEvent(
         this.gameView.owner(tile).id(),
-        this.myPlayer!.troops() * this.renderer.uiState.attackRatio,
+        player.troops() * this.renderer.uiState.attackRatio,
       ),
     );

The same pattern applies to doBoatAttackUnderCursor (line 517) and doGroundAttackUnderCursor (line 536).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3478b3a and 6e39f21.

📒 Files selected for processing (11)
  • src/client/ClientGameRunner.ts (3 hunks)
  • src/client/graphics/layers/StructureIconsLayer.ts (2 hunks)
  • src/client/graphics/layers/UnitDisplay.ts (2 hunks)
  • src/core/GameRunner.ts (2 hunks)
  • src/core/game/Game.ts (2 hunks)
  • src/core/game/GameView.ts (2 hunks)
  • src/core/game/PlayerImpl.ts (2 hunks)
  • src/core/game/TransportShipUtils.ts (3 hunks)
  • src/core/worker/Worker.worker.ts (1 hunks)
  • src/core/worker/WorkerClient.ts (3 hunks)
  • src/core/worker/WorkerMessages.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
src/core/GameRunner.ts (2)
src/core/game/Game.ts (1)
  • PlayerActions (742-747)
src/core/game/GameView.ts (5)
  • player (594-600)
  • tile (103-105)
  • x (669-671)
  • y (672-674)
  • actions (276-286)
src/core/game/Game.ts (1)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/client/graphics/layers/UnitDisplay.ts (1)
src/core/game/GameView.ts (2)
  • player (594-600)
  • actions (276-286)
src/core/game/GameView.ts (2)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (1)
  • PlayerActions (742-747)
src/core/game/PlayerImpl.ts (3)
src/core/game/GameView.ts (1)
  • inSpawnPhase (642-644)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (1)
  • BuildableUnit (749-755)
src/client/ClientGameRunner.ts (2)
src/core/game/GameView.ts (2)
  • tile (103-105)
  • actions (276-286)
src/client/Transport.ts (1)
  • SendAttackIntentEvent (72-77)
src/core/game/TransportShipUtils.ts (2)
src/core/game/GameMap.ts (3)
  • TileRef (3-3)
  • andFN (459-464)
  • manhattanDistFN (364-380)
src/core/game/Game.ts (2)
  • Game (656-740)
  • Player (517-654)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (7)
src/client/graphics/layers/UnitDisplay.ts (1)

14-19: LGTM! Appropriate use of transport ship filter.

The change correctly excludes transport ships from the actions query for the UI display, which makes sense for this context.

Also applies to: 105-105

src/client/graphics/layers/StructureIconsLayer.ts (1)

13-13: LGTM! Correct filter usage.

The change appropriately excludes transport ships when querying actions for the ghost structure rendering logic.

Also applies to: 253-253

src/client/ClientGameRunner.ts (3)

15-19: LGTM!

The import additions are appropriate for the transport ship filtering functionality.


517-521: LGTM!

Correctly uses TransportShipFilter.Only for boat attack actions.


536-546: LGTM!

Ground attack correctly uses default filter behavior (no transport ship filtering).

src/core/game/PlayerImpl.ts (1)

904-935: LGTM!

The Default/Exclude filter path correctly handles unit filtering and upgrade logic. The explicit loop structure is clearer than a filtered map for this filtering requirement.

src/core/game/TransportShipUtils.ts (1)

75-117: Logic changes extend beyond performance optimization.

The PR title mentions "perf" but lines 75-117 introduce significant behavioral changes:

  • Line 76: Explicitly rejects building transport ships to your own territory
  • Lines 100-117: New fallback logic for lake/shore deployment when ocean shore conditions aren't met

These are feature enhancements, not just performance optimizations. Consider updating the PR description to document these behavioral changes.

Verify the new lake/shore deployment logic with edge cases:

  • When dst is not an ocean shore but a lake shore
  • When player has no border tiles that are ocean shores
  • When the BFS finds no suitable spawn tiles

You could add unit tests covering these scenarios.

Comment on lines 5 to 45
interface CacheEntry<T> {
tick: number;
value: T;
}

const transportTileCache = new Map<string, CacheEntry<TileRef | null>>();
const buildTransportShipCache = new Map<string, CacheEntry<TileRef | false>>();

function getTransportTileCacheKey(tile: TileRef, tick: number): string {
return `transport_${tile}_${tick}`;
}

function getBuildCacheKey(
playerId: string,
tile: TileRef,
tick: number,
): string {
return `build_${playerId}_${tile}_${tick}`;
}

let lastCleanupTick = 0;

function cleanupCache<T>(
cache: Map<string, CacheEntry<T>>,
currentTick: number,
cacheName: string, //TODO: remove after testing
): void {
if (currentTick < lastCleanupTick + 20) {
return;
}
lastCleanupTick = currentTick;

for (const [key, entry] of cache.entries()) {
if (entry.tick < currentTick) {
console.log(
`Cleaning up ${cacheName} cache for key ${key} at tick ${entry.tick}`,
);
cache.delete(key);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove debug logging and consider game instance isolation for caches.

The TODO comments (lines 30, 39-41) indicate debug logging should be removed before merging. Additionally, module-level caches shared across all game instances could cause issues if multiple games run simultaneously (e.g., in tests or future multi-game scenarios).

  1. Remove the debug logging:
 function cleanupCache<T>(
   cache: Map<string, CacheEntry<T>>,
   currentTick: number,
-  cacheName: string, //TODO: remove after testing
 ): void {
   if (currentTick < lastCleanupTick + 20) {
     return;
   }
   lastCleanupTick = currentTick;

   for (const [key, entry] of cache.entries()) {
     if (entry.tick < currentTick) {
-      console.log(
-        `Cleaning up ${cacheName} cache for key ${key} at tick ${entry.tick}`,
-      );
       cache.delete(key);
     }
   }
 }
  1. Consider scoping caches per-game instance rather than module-level to avoid cross-contamination between games:
// Instead of module-level:
// const transportTileCache = new Map<string, CacheEntry<TileRef | null>>();

// Consider passing game-scoped cache as parameter or storing in Game instance
🤖 Prompt for AI Agents
In src/core/game/TransportShipUtils.ts around lines 5 to 45, remove the
temporary console.log debug statement and the leftover TODO, and refactor
module-level caches so they are scoped to a game instance: stop using the
top-level transportTileCache and buildTransportShipCache maps and instead accept
cache objects (or store them) on the Game instance and pass them into functions
(or export a factory that creates per-game caches); update cleanupCache to
operate on the provided per-game cache and remove the debug log call and the
TODO comment.

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33818f3 and 81c1cc3.

📒 Files selected for processing (1)
  • src/core/game/PlayerImpl.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/core/game/PlayerImpl.ts (2)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (2)
  • Unit (427-502)
  • BuildableUnit (749-755)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (6)
src/core/game/PlayerImpl.ts (6)

36-36: LGTM!

The import is correctly added to support the new filter parameter.


848-866: LGTM!

The skipBuildCheck parameter correctly optimizes repeated feasibility checks. The method properly delegates to canUpgradeUnit and returns the nearest upgradable unit.


868-879: LGTM!

The method correctly checks upgradability first, then conditionally validates build feasibility. The skipBuildCheck parameter enables efficient reuse.


881-892: LGTM!

Good refactoring. This private helper cleanly centralizes the unit build feasibility checks (disabled state, gold cost, alive status) in one place.


908-911: LGTM!

Good performance optimization: canBuildUnit is checked once (line 926), then skipBuildCheck=true is passed to avoid redundant validation in canBuild and findUnitToUpgrade. The validTiles computation is also optimized by skipping when filter is Only (ships don't need it).

Also applies to: 923-931


944-988: LGTM!

The skipBuildCheck parameter is cleanly integrated. When false (default), it correctly validates build feasibility before proceeding. This maintains backward compatibility while enabling performance optimization for callers that pre-validate.

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 (1)
src/core/game/TransportShipUtils.ts (1)

106-106: Use a named constant for BFS search distance.

The hard-coded 300 reduces readability. Consider extracting it:

const LAKE_TRANSPORT_SEARCH_RADIUS = 300;

Then use manhattanDistFN(dst, LAKE_TRANSPORT_SEARCH_RADIUS) to clarify intent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81c1cc3 and 23e1010.

📒 Files selected for processing (1)
  • src/core/game/TransportShipUtils.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/core/game/TransportShipUtils.ts (2)
src/core/game/GameMap.ts (3)
  • TileRef (3-3)
  • andFN (459-464)
  • manhattanDistFN (364-380)
src/core/game/Game.ts (2)
  • Game (656-740)
  • Player (517-654)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev

return `build_${playerId}_${tile}_${tick}`;
}

let lastCleanupTick = 0;
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

Module-level lastCleanupTick compounds multi-game isolation issue.

The lastCleanupTick variable is shared across all game instances. If multiple games run concurrently at different tick rates, cleanup timing will be incorrect for some games. This amplifies the module-level cache concern already flagged in earlier comments.

Consider moving lastCleanupTick into per-game cache state along with the cache Maps themselves when addressing the broader isolation issue.

Based on learnings

🤖 Prompt for AI Agents
In src/core/game/TransportShipUtils.ts around line 25, the module-level let
lastCleanupTick = 0; is shared across all game instances causing incorrect
cleanup timing for concurrent games; move lastCleanupTick into the per-game
cache state object (the same object that holds the cache Maps) so each game
tracks its own lastCleanupTick, update all references/reads/writes to use
gameCache.lastCleanupTick instead of the module variable, initialize it when
creating the per-game cache, and remove the module-level declaration.

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: 0

🧹 Nitpick comments (1)
src/core/game/Game.ts (1)

863-869: Consider if "Default" member is necessary.

Since transportShipFilter is optional, callers can omit it to get default behavior. Having an explicit Default enum member might be redundant unless you need to distinguish between "not specified" and "explicitly default."

Additionally, modern TypeScript style favors string literal unions over enums for simple value sets:

export type TransportShipFilter = "default" | "exclude" | "only";

However, this enum is consistent with the existing patterns in this codebase (Difficulty, GameType, etc.), so changing it would create inconsistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23e1010 and 0160e0c.

📒 Files selected for processing (1)
  • src/core/game/Game.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/core/game/Game.ts (1)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (1)
src/core/game/Game.ts (1)

563-566: LGTM - Parameter naming issue resolved.

The parameter name now follows TypeScript conventions (lowercase transportShipFilter), addressing the concern from the previous review.

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: 0

🧹 Nitpick comments (2)
src/core/game/PlayerImpl.ts (2)

901-904: Consider adding an explicit default value for the filter parameter.

The transportShipFilter parameter is optional, and the code relies on undefined behaving as "Default" (include all units). This works but could be more explicit.

Apply this diff to make the default behavior explicit:

 public buildableUnits(
   tile: TileRef | null,
-  transportShipFilter?: TransportShipFilter,
+  transportShipFilter: TransportShipFilter = TransportShipFilter.Default,
 ): BuildableUnit[] {

(Note: This assumes TransportShipFilter.Default exists. If not, adjust accordingly.)


905-905: Consider renaming to avoid double negative.

The variable notInSpawnPhase uses a double negative which can hurt readability.

Apply this diff:

-const notInSpawnPhase = !this.mg.inSpawnPhase();
+const inGamePhase = !this.mg.inSpawnPhase();

And update line 926:

-if (tile !== null && this.canBuildUnit(u) && notInSpawnPhase) {
+if (tile !== null && this.canBuildUnit(u) && inGamePhase) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0160e0c and 1ea2f2d.

📒 Files selected for processing (7)
  • src/core/GameRunner.ts (2 hunks)
  • src/core/game/Game.ts (2 hunks)
  • src/core/game/GameView.ts (2 hunks)
  • src/core/game/PlayerImpl.ts (3 hunks)
  • src/core/worker/Worker.worker.ts (1 hunks)
  • src/core/worker/WorkerClient.ts (3 hunks)
  • src/core/worker/WorkerMessages.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/game/Game.ts
  • src/core/worker/WorkerClient.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/core/GameRunner.ts (2)
src/core/game/Game.ts (1)
  • PlayerActions (742-747)
src/core/game/GameView.ts (5)
  • player (594-600)
  • tile (103-105)
  • x (669-671)
  • y (672-674)
  • actions (276-286)
src/core/game/PlayerImpl.ts (2)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (2)
  • Unit (427-502)
  • BuildableUnit (749-755)
src/core/game/GameView.ts (2)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (1)
  • PlayerActions (742-747)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (8)
src/core/game/GameView.ts (1)

276-285: LGTM! Parameter threading is clean.

The transportShipFilter parameter is correctly named (lowercase) and properly forwarded to the worker interaction. The optional parameter pattern matches the rest of the API.

src/core/game/PlayerImpl.ts (4)

848-866: LGTM! Clean addition of skipBuildCheck parameter.

The skipBuildCheck parameter with a default value of false is a clean way to allow callers to bypass build feasibility checks when needed. The early return pattern keeps the logic clear.


868-892: Good refactor to centralize build feasibility checks.

Extracting canBuildUnit eliminates duplication and makes the build logic easier to maintain. The three checks (disabled, cost, alive) properly gate unit construction.


914-921: LGTM! Filter logic is now correct.

The filter correctly checks u !== UnitType.TransportShip for the Only case, ensuring:

  • Exclude: skip TransportShip, include others
  • Only: skip non-TransportShip, include only TransportShip
  • Default (undefined): include all units

This fixes the critical bug from the previous review where it checked TradeShip instead.


923-931: LGTM! canUpgrade logic is correct.

The code properly computes canUpgrade by calling findUnitToUpgrade and using the unit's ID when an upgradable unit exists. This fixes the issue where it was previously hardcoded to 0.

src/core/GameRunner.ts (1)

181-215: LGTM! Clean parameter threading.

The transportShipFilter parameter is correctly named (lowercase), properly typed, and correctly passed to player.buildableUnits. The signature matches the worker interface expectations.

src/core/worker/WorkerMessages.ts (1)

61-67: LGTM! Message interface correctly extended.

The transportShipFilter property is correctly named (lowercase), properly typed as optional, and aligns with the other optional coordinate parameters in the message.

src/core/worker/Worker.worker.ts (1)

76-96: LGTM! Worker correctly forwards the filter parameter.

The worker properly extracts message.transportShipFilter (lowercase) and passes it to playerActions. The error handling remains appropriate.

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: 0

♻️ Duplicate comments (1)
src/core/game/TransportShipUtils.ts (1)

5-15: Module-level cache isolation issue remains unresolved.

Past review comments identified that transportTileCache (line 9) and lastCleanupTick (line 15) are shared across all game instances, which can cause incorrect behavior if multiple games run concurrently. This concern still applies to the current code.

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

17-28: Consider cleaning stale entries more proactively.

The cleanup runs every 20 ticks (line 18), meaning stale cache entries from previous ticks accumulate in memory between cleanup cycles. While the cache check on line 113 ensures stale entries are never returned, they still consume memory.

Consider triggering cleanup when the tick changes, or clearing the entire cache when currentTick advances:

 function cleanupCache(currentTick: number): void {
-  if (currentTick < lastCleanupTick + 20) {
+  if (currentTick === lastCleanupTick) {
     return;
   }
-  lastCleanupTick = currentTick;
-
-  for (const [key, entry] of transportTileCache.entries()) {
-    if (entry.tick < currentTick) {
-      transportTileCache.delete(key);
-    }
-  }
+  // Clear entire cache when tick advances since entries are only valid for current tick
+  transportTileCache.clear();
+  lastCleanupTick = currentTick;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ea2f2d and bafe4e3.

📒 Files selected for processing (1)
  • src/core/game/TransportShipUtils.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/core/game/TransportShipUtils.ts (2)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (2)
  • Game (656-740)
  • Player (517-654)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (2)
src/core/game/TransportShipUtils.ts (2)

49-52: Clean refactor to use cached destination resolution.

The refactored logic correctly delegates to targetTransportTile, which provides per-tick caching. This improves performance while keeping the code readable.


108-129: Well-structured caching layer.

The new targetTransportTile function correctly implements per-tick caching with appropriate cache validation (line 113). Calling cleanupCache on every cache miss (line 126) is acceptable since cleanup early-exits when not needed.

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 (1)
src/core/game/PlayerImpl.ts (1)

898-909: Clarify method purpose: this checks a specific Unit instance, not a UnitType.

The method signature correctly expects a Unit instance (line 898), but it's being called with a UnitType at line 883. The instance-specific check at line 899 (unit.isMarkedForDeletion()) confirms this method is for checking specific units, not types in general.

Consider renaming to canUpgradeUnitInstance or adding a separate type-level check method to make the distinction clearer.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bafe4e3 and d962eda.

📒 Files selected for processing (5)
  • src/client/graphics/layers/StructureIconsLayer.ts (2 hunks)
  • src/core/GameRunner.ts (2 hunks)
  • src/core/game/Game.ts (2 hunks)
  • src/core/game/GameView.ts (2 hunks)
  • src/core/game/PlayerImpl.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/GameRunner.ts
  • src/core/game/Game.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/core/game/GameView.ts (2)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (1)
  • PlayerActions (750-756)
src/core/game/PlayerImpl.ts (3)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (2)
  • Unit (430-508)
  • BuildableUnit (758-764)
src/client/graphics/layers/UnitDisplay.ts (1)
  • canBuild (82-101)
🪛 GitHub Actions: 🧪 CI
src/core/game/PlayerImpl.ts

[error] 899-899: unit.isMarkedForDeletion is not a function

🪛 GitHub Actions: 🚀 Deploy
src/core/game/PlayerImpl.ts

[error] 883-883: TS2345: Argument of type 'import("/usr/src/app/src/core/game/Game").UnitType' is not assignable to parameter of type 'Unit'.

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

280-289: LGTM! Parameter threading looks clean.

The optional transportShipFilter parameter is properly typed and forwarded to the worker. The previous naming concern has been addressed correctly with the lowercase parameter name.

src/client/graphics/layers/StructureIconsLayer.ts (1)

253-253: LGTM! Filter usage is appropriate here.

Passing TransportShipFilter.Exclude for ghost structure placement makes sense—structure building UI doesn't need transport ship actions.

src/core/game/PlayerImpl.ts (3)

911-922: Nice consolidation of build feasibility checks.

Extracting the common checks (disabled, gold, alive) into a private helper method reduces duplication and makes the code easier to maintain.


931-978: Filter logic looks correct; depends on fixing findUnitToUpgrade type mismatch.

The TransportShipFilter integration is well-structured:

  • Exclude filter skips TransportShip (lines 946-950)
  • Only filter processes only TransportShip then breaks (lines 952-957)
  • validTiles optimization skips expensive computation when not needed (lines 939-942)

However, line 965 will fail due to the type mismatch bug in findUnitToUpgrade identified above. Once that's fixed, this logic should work correctly.

The skipBuildCheck=true pattern at lines 963 and 965 is correct since line 962 already validated canBuildUnit(u).


980-1024: Clean addition of skipBuildCheck parameter.

The optional skipBuildCheck parameter (line 984) allows callers to avoid redundant validations when they've already checked canBuildUnit. The conditional at line 986 correctly applies the check only when not skipping.

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: 0

🧹 Nitpick comments (1)
src/core/game/PlayerImpl.ts (1)

878-900: Consider removing redundant type check in canUpgradeUnit call.

When skipUnitTypeCheck is false, the code checks canUpgradeUnitType(type, ...) at line 883, then filters nearby units by that same type at line 889, and finally calls canUpgradeUnit(unit, ...) at line 895 which checks canUpgradeUnitType(unit.type()) again at line 909. Since line 889 already filters by type, unit.type() must equal the type parameter, making the second check redundant.

This is defensive but adds unnecessary overhead when skip is false. Consider either:

  1. Always passing true for skip to canUpgradeUnit since the type was already validated
  2. Documenting that the double-check is intentional
-    if (!this.canUpgradeUnit(unit, skipUnitTypeCheck)) {
+    // Type already validated at line 883, only check unit-specific conditions
+    if (!this.canUpgradeUnit(unit, true)) {
       return false;
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d962eda and 01fa931.

📒 Files selected for processing (1)
  • src/core/game/PlayerImpl.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/core/game/PlayerImpl.ts (2)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (2)
  • Unit (430-508)
  • BuildableUnit (758-764)
🪛 GitHub Actions: 🧪 CI
src/core/game/PlayerImpl.ts

[error] 1-1: City upgrade test failure: expected city.canUpgrade to be true, but it was false in 'City can be upgraded' scenario.


[error] 1-1: City upgrade test failure: expected cityToUpgrade to be truthy in 'City can be upgraded from another city' scenario.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (4)
src/core/game/PlayerImpl.ts (4)

928-939: Good refactor consolidating build eligibility checks.

This helper cleanly extracts the unit-type-level validation (disabled check, gold check, alive check) that was previously scattered or duplicated. The separation from spawn-location validation makes the code easier to reason about.


956-960: Good optimization: compute valid tiles once and reuse.

Computing validTiles once before the loop (lines 956-959) and passing it to canBuild (line 980) avoids redundant BFS traversals for each unit type. When filter is Only, the empty array is safe since TransportShip uses its own spawn logic.


997-1005: LGTM: skipUnitTypeCheck parameter addition.

The skipUnitTypeCheck parameter follows the same pattern established in findUnitToUpgrade and canUpgradeUnit, allowing callers to bypass type validation when they've already performed it. This improves efficiency in buildableUnits which pre-checks with canBuildUnitType before calling this method.


948-995: The review comment is incorrect. No issues exist in the code.

The buildableUnits method correctly gates both canBuild and canUpgrade on notInSpawnPhase (line 979). This is the intended design: upgrades are not allowed during spawn phase.

Evidence:

  • The "City can be upgraded" test (tests/PlayerImpl.test.ts:38) explicitly exits spawn phase before testing: while (game.inSpawnPhase()) { game.executeNextTick(); }
  • The UI disables build and upgrade menus during spawn phase (RadialMenuElements.ts)
  • Game logic skips non-spawn actions during spawn (ClientGameRunner.ts:413-414)

Blocking upgrades during spawn phase is by design, not a bug. The test setup confirms this is working as intended.

Likely an incorrect or invalid review comment.

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: 0

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

883-905: Update interface signature to match implementation.

The interface at src/core/game/Game.ts:586 defines findUnitToUpgrade(type: UnitType, targetTile: TileRef): Unit | false without the skipUnitTypeCheck parameter, while the implementation at src/core/game/PlayerImpl.ts:883 includes skipUnitTypeCheck: boolean = false.

The logic at line 900 (hardcoding true to canUpgradeUnit) appears intentional—validate type eligibility once upfront, then skip type recheck on the specific unit instance. However, the interface and implementation contract mismatch means code using this method through the Game interface type cannot pass the skipUnitTypeCheck argument. The call at line 990 works because it directly accesses the PlayerImpl instance.

Add the optional parameter to the interface signature to expose this functionality consistently.

♻️ Duplicate comments (1)
src/core/game/TransportShipUtils.ts (1)

9-9: ** Module-level caches still cause multi-game isolation issues.**

The transportTileCache map and lastCleanupTick variable remain at module scope, shared across all game instances. This was flagged in previous reviews. If multiple games run concurrently (tests, future multi-game scenarios), cache entries and cleanup timing will be incorrect.

Based on learnings

Also applies to: 15-15

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

17-28: Consider renaming parameter or making cleanup more explicit.

The cleanupCache function now only operates on the module-level transportTileCache, but doesn't accept the cache as a parameter. This makes it less clear that it's tied to a specific cache and harder to refactor when addressing the multi-game isolation issue.

Minor: Once the cache is moved to per-game scope, this function should accept the cache as a parameter:

-function cleanupCache(currentTick: number): void {
+function cleanupCache(
+  cache: Map<string, CacheEntry<TileRef | null>>,
+  currentTick: number,
+): void {
   if (currentTick < lastCleanupTick + 20) {
     return;
   }
   lastCleanupTick = currentTick;

-  for (const [key, entry] of transportTileCache.entries()) {
+  for (const [key, entry] of cache.entries()) {
     if (entry.tick < currentTick) {
-      transportTileCache.delete(key);
+      cache.delete(key);
     }
   }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d01862 and 6a4d775.

📒 Files selected for processing (3)
  • src/core/game/Game.ts (2 hunks)
  • src/core/game/PlayerImpl.ts (4 hunks)
  • src/core/game/TransportShipUtils.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/game/Game.ts
🧰 Additional context used
🧠 Learnings (8)
📚 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/game/PlayerImpl.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/game/PlayerImpl.ts
📚 Learning: 2025-06-05T02:34:45.899Z
Learnt from: Egraveline
Repo: openfrontio/OpenFrontIO PR: 1012
File: src/core/execution/UpgradeStructureExecution.ts:49-54
Timestamp: 2025-06-05T02:34:45.899Z
Learning: In the upgrade system, gold deduction for structure upgrades is handled internally by the `upgradeUnit` method in PlayerImpl, not in the UpgradeStructureExecution class. The UpgradeStructureExecution only needs to check if the player has sufficient gold before calling `upgradeUnit`.

Applied to files:

  • src/core/game/PlayerImpl.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/game/PlayerImpl.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/game/PlayerImpl.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/game/PlayerImpl.ts
📚 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-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/game/TransportShipUtils.ts
🧬 Code graph analysis (2)
src/core/game/PlayerImpl.ts (3)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (2)
  • Unit (432-510)
  • BuildableUnit (761-767)
src/client/graphics/layers/UnitDisplay.ts (1)
  • canBuild (82-101)
src/core/game/TransportShipUtils.ts (2)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
src/core/game/Game.ts (2)
  • Game (667-751)
  • Player (525-665)
🔇 Additional comments (5)
src/core/game/TransportShipUtils.ts (2)

108-129: Cache key includes tick for proper per-tick isolation.

The caching implementation correctly includes the tick in the cache key, ensuring results are only reused within the same tick. The cleanup logic appropriately removes stale entries from previous ticks.


49-68: Simplified ocean logic validation is preserved—no issue found.

The opponent ocean border check has not been removed; it's validated implicitly earlier in the function flow. Here's why:

  1. targetTransportTile(game, tile) retrieves the destination player's closest shore tile by calling closestShoreFromPlayer()
  2. If the destination player is landlocked (no shore access), closestShoreFromPlayer returns null
  3. Lines 50-51 catch this: if (dst === null) { return false; }
  4. Only if the destination has valid shore access does the code reach line 54

The ocean path then checks: current player borders ocean + destination has shore access (already verified). This is correct logic. The refactoring removed redundant explicit checks in favor of factoring validation into targetTransportTile, making the code cleaner without losing safety.

Transport ship spawning correctly rejects attempts against landlocked players through the existing targetTransportTile validation.

src/core/game/PlayerImpl.ts (3)

923-947: Helper methods cleanly separate type-level and instance-level validation.

The new canUpgradeUnitType and canBuildUnitType helpers properly separate concerns: type eligibility (upgradable flag, disabled status, cost, alive status) vs. instance validity (deletion status, ownership). This makes the validation flow clearer and reusable.


956-1003: buildableUnits correctly handles TransportShipFilter modes.

The filter logic:

  • Exclude: Skips TransportShip (line 971-975)
  • Only: Processes only TransportShip, breaks early (line 977-982)
  • Default: Processes all unit types

The validTiles computation (lines 964-967) correctly excludes computation when TransportShipFilter.Only is active, since TransportShip doesn't require structure spawn tile validation. The canBuild call at line 988 passes skipUnitTypeCheck=true to avoid redundant checks after canBuildUnitType validation.


1005-1049: canBuild signature extended with skipUnitTypeCheck parameter.

The new parameter enables callers to skip type-level validation when already verified upstream (e.g., in buildableUnits). Line 1011 guards the canBuildUnitType call with the flag. For TransportShip (line 1034), the delegation to canBuildTransportShip is unchanged and doesn't use the validTiles parameter, which is correct.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants