Skip to content

Fix: Retreat naval invasions when alliance is accepted (#3502)#3516

Open
AlexBesios wants to merge 3 commits intoopenfrontio:mainfrom
AlexBesios:fix/3502-autoretreat-alliance-landing
Open

Fix: Retreat naval invasions when alliance is accepted (#3502)#3516
AlexBesios wants to merge 3 commits intoopenfrontio:mainfrom
AlexBesios:fix/3502-autoretreat-alliance-landing

Conversation

@AlexBesios
Copy link
Copy Markdown

@AlexBesios AlexBesios commented Mar 26, 2026

If this PR fixes an issue, link it below. If not, delete these two lines.
Resolves #3502

Description:

When an alliance is accepted, any naval invasions between the two newly allied players now automatically retreat. This prevents the exploit where a player could launch a naval invasion, request an alliance, and gain a risk-free foothold if the target accepts. The retreating troops are then added back

  • Retreat uses existing [orderBoatRetreat()] mechanics: boats reroute home with 25% troop casualty on arrival.
  • Retreating boats are not targeted by warships
  • Follows the same pattern as [cancelNukesBetweenAlliedPlayers()] which already destroys in-flight nukes on alliance acceptance.
screenshot-2026-03-26_15-48-45

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:

deathllotus

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 26, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Walkthrough

When an alliance is accepted, in-flight transport ships targeting the new ally are ordered to retreat; the change adds retreat logic to alliance acceptance, two i18n keys, and tests verifying retreat state and display events.

Changes

Cohort / File(s) Summary
Localization
resources/lang/en.json
Added two i18n keys: events_display.alliance_boats_retreated_outgoing and events_display.alliance_boats_retreated_incoming using ICU pluralization and {name} interpolation.
Alliance Execution Logic
src/core/execution/alliance/AllianceRequestExecution.ts
Added retreatBoatsBetweenAlliedPlayers(recipient: Player) and invoke after alliance acceptance; scans active TransportShip units for both players, orders qualifying boats to retreat, counts retreats per side, and emits outgoing/incoming display events.
Tests
tests/AllianceAcceptBoatRetreat.test.ts
New test suite that spawns players and transport ships, accepts alliances, asserts boats targeting the new ally switch to retreating (and own-targeting boats do not), checks mutual retreats, and verifies display event emission.

Sequence Diagram(s)

sequenceDiagram
    participant PlayerA as Player A
    participant PlayerB as Player B
    participant AllianceExec as AllianceRequestExecution
    participant Ships as TransportShip Units
    participant Updates as GameUpdate

    PlayerA->>AllianceExec: accept alliance with Player B
    AllianceExec->>Ships: scan active transport ships for both players
    Ships-->>AllianceExec: return ships with targets
    AllianceExec->>Ships: filter ships targeting the allied player
    Ships-->>AllianceExec: confirm matching ships
    AllianceExec->>Ships: order retreat on each matching ship
    Ships-->>AllianceExec: set retreating = true
    AllianceExec->>Updates: emit outgoing retreat event (count, name)
    AllianceExec->>Updates: emit incoming retreat event (count, name)
    Updates-->>PlayerA: display retreat notifications
    Updates-->>PlayerB: display retreat notifications
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

⛵ Allies clasp hands across the sea,
Boats turn their bows and head back free,
Counts sent as whispers, names in light,
Calm waters restored by a tidy right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix: Retreat naval invasions when alliance is accepted (#3502)' accurately summarizes the main change: adding auto-retreat logic for naval invasions when alliances are accepted.
Description check ✅ Passed The description clearly explains the feature: preventing an exploit by auto-retreating naval invasions when alliance is accepted, using existing orderBoatRetreat() mechanics with 25% casualties, following the cancelNukesBetweenAlliedPlayers() pattern.
Linked Issues check ✅ Passed The PR fully addresses issue #3502 by implementing auto-retreat of naval invasions when alliance is accepted, preventing the risk-free foothold exploit described in the issue.
Out of Scope Changes check ✅ Passed All changes are in-scope: localization strings for retreat messages, the retreatBoatsBetweenAlliedPlayers() method in alliance execution, and comprehensive tests for the new functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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
Copy Markdown
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.

🧹 Nitpick comments (1)
tests/AllianceAcceptBoatRetreat.test.ts (1)

101-125: Make the display-event assertion stricter.

This case should emit both outgoing and incoming retreat messages; checking only “some” can miss partial regressions.

Proposed test assertion update
-    expect(
-      messages.some(
-        (m) =>
-          m === "events_display.alliance_boats_retreated_outgoing" ||
-          m === "events_display.alliance_boats_retreated_incoming",
-      ),
-    ).toBe(true);
+    expect(messages).toContain(
+      "events_display.alliance_boats_retreated_outgoing",
+    );
+    expect(messages).toContain(
+      "events_display.alliance_boats_retreated_incoming",
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/AllianceAcceptBoatRetreat.test.ts` around lines 101 - 125, The current
test in AllianceAcceptBoatRetreat.test.ts uses a loose assertion that only
checks for the presence of either
"events_display.alliance_boats_retreated_outgoing" or
"events_display.alliance_boats_retreated_incoming"; tighten it to assert that
both messages are emitted. Locate the messages array (derived from
updates[GameUpdateType.DisplayEvent]?.map((e) => e.message) ?? []) and replace
the .some(...) check with explicit checks that
messages.includes("events_display.alliance_boats_retreated_outgoing") and
messages.includes("events_display.alliance_boats_retreated_incoming"), asserting
both are true so the test fails if one of the two retreat display events is
missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/AllianceAcceptBoatRetreat.test.ts`:
- Around line 101-125: The current test in AllianceAcceptBoatRetreat.test.ts
uses a loose assertion that only checks for the presence of either
"events_display.alliance_boats_retreated_outgoing" or
"events_display.alliance_boats_retreated_incoming"; tighten it to assert that
both messages are emitted. Locate the messages array (derived from
updates[GameUpdateType.DisplayEvent]?.map((e) => e.message) ?? []) and replace
the .some(...) check with explicit checks that
messages.includes("events_display.alliance_boats_retreated_outgoing") and
messages.includes("events_display.alliance_boats_retreated_incoming"), asserting
both are true so the test fails if one of the two retreat display events is
missing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba157898-88a3-48fc-8173-0dbb73acc66c

📥 Commits

Reviewing files that changed from the base of the PR and between 23150f0 and 995639c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • resources/lang/en.json
  • src/core/execution/alliance/AllianceRequestExecution.ts
  • tests/AllianceAcceptBoatRetreat.test.ts

Copy link
Copy Markdown
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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/AllianceAcceptBoatRetreat.test.ts`:
- Around line 39-41: The test contains an unbounded loop using
game.inSpawnPhase() and game.executeNextTick() that can hang; add a guarded loop
with a max tick counter (e.g., const MAX_SPAWN_TICKS = 1000 or similar) and
iterate at most that many times calling game.executeNextTick(), and if the
counter is exceeded assert/fail the test (or throw) with a descriptive message
so the test fails fast instead of hanging. Ensure you replace the current while
loop with the bounded loop using MAX_SPAWN_TICKS and check the counter after
each tick to fail when exceeded.
- Around line 87-90: The two alliance requests are queued in the same tick
making tests order-dependent; change to the two-tick pattern used elsewhere:
call game.addExecution(new AllianceRequestExecution(player1, player2.id()));
then call game.executeNextTick(); then call game.addExecution(new
AllianceRequestExecution(player2, player1.id())); then call
game.executeNextTick() so acceptance is deterministic; update the identical
block that appears later (the other AllianceRequestExecution pair) the same way.
🪄 Autofix (Beta)

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

Run ID: 557b122f-f579-45c9-9fdc-e2368c80e60d

📥 Commits

Reviewing files that changed from the base of the PR and between 995639c and 2d1fb1f.

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

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Mar 26, 2026
Copy link
Copy Markdown
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.

🧹 Nitpick comments (2)
tests/AllianceAcceptBoatRetreat.test.ts (2)

121-127: Strengthen the display-message assertion to verify both keys.

The current check passes if only one key is emitted. This can miss regressions where one side’s message is dropped.

Suggested assertion update
-    expect(
-      messages.some(
-        (m) =>
-          m === "events_display.alliance_boats_retreated_outgoing" ||
-          m === "events_display.alliance_boats_retreated_incoming",
-      ),
-    ).toBe(true);
+    expect(messages).toContain(
+      "events_display.alliance_boats_retreated_outgoing",
+    );
+    expect(messages).toContain(
+      "events_display.alliance_boats_retreated_incoming",
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/AllianceAcceptBoatRetreat.test.ts` around lines 121 - 127, The test
currently only asserts that at least one of the two display-message keys is
present, which can miss regressions; update the assertion in
AllianceAcceptBoatRetreat.test (the block that checks the messages
array/variable) to assert that both
"events_display.alliance_boats_retreated_outgoing" and
"events_display.alliance_boats_retreated_incoming" are present (e.g., check
messages includes/contains both keys or assert messages.some(...) for each key
separately) so the test fails if either key is missing.

59-63: Consider extracting alliance-accept flow into a tiny helper.

This sequence is repeated four times; a helper would reduce noise and make test intent clearer.

Refactor sketch
+function acceptAlliance(a: Player, b: Player): void {
+  game.addExecution(new AllianceRequestExecution(a, b.id()));
+  game.executeNextTick(); // creates request
+  game.addExecution(new AllianceRequestExecution(b, a.id()));
+  game.executeNextTick(); // counter-request auto-accepts
+}
...
-    game.addExecution(new AllianceRequestExecution(player1, player2.id()));
-    game.executeNextTick(); // creates request
-    game.addExecution(new AllianceRequestExecution(player2, player1.id()));
-    game.executeNextTick(); // counter-request auto-accepts
+    acceptAlliance(player1, player2);

Also applies to: 90-94, 111-114, 141-144

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/AllianceAcceptBoatRetreat.test.ts` around lines 59 - 63, Extract the
repeated two-step alliance request/auto-accept sequence into a helper like
sendAndAutoAcceptAlliance(initiator, counterparty): it should call
game.addExecution(new AllianceRequestExecution(initiator, counterparty.id())),
then game.executeNextTick(), then game.addExecution(new
AllianceRequestExecution(counterparty, initiator.id())), then
game.executeNextTick(); replace each repeated block (the occurrences around
AllianceRequestExecution + game.executeNextTick) with calls to this helper to
make the tests clearer and remove duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/AllianceAcceptBoatRetreat.test.ts`:
- Around line 121-127: The test currently only asserts that at least one of the
two display-message keys is present, which can miss regressions; update the
assertion in AllianceAcceptBoatRetreat.test (the block that checks the messages
array/variable) to assert that both
"events_display.alliance_boats_retreated_outgoing" and
"events_display.alliance_boats_retreated_incoming" are present (e.g., check
messages includes/contains both keys or assert messages.some(...) for each key
separately) so the test fails if either key is missing.
- Around line 59-63: Extract the repeated two-step alliance request/auto-accept
sequence into a helper like sendAndAutoAcceptAlliance(initiator, counterparty):
it should call game.addExecution(new AllianceRequestExecution(initiator,
counterparty.id())), then game.executeNextTick(), then game.addExecution(new
AllianceRequestExecution(counterparty, initiator.id())), then
game.executeNextTick(); replace each repeated block (the occurrences around
AllianceRequestExecution + game.executeNextTick) with calls to this helper to
make the tests clearer and remove duplication.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 45a3fe4d-ab2c-46aa-8ead-d46a6630ba86

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1fb1f and 818169a.

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

@github-project-automation github-project-automation bot moved this from Development to Final Review in OpenFront Release Management Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Final Review

Development

Successfully merging this pull request may close these issues.

Autoretreat landing when alliance accepted on target

2 participants