Skip to content

Conversation

@FloPinguin
Copy link
Contributor

@FloPinguin FloPinguin commented Nov 26, 2025

Description:

Saw this in an Enzo video about beating impossible nations:

You can just donate 1% gold / 1% troops to a nation to get a friendly relation with them.

This PR adds randomized minimum donation requirements based on the difficulty.
Randomized in a way that there is a minimum someone has to donate to surely get the relation improvement, but you can also gamble and send less.

For troop donations, the minimum is calculated based on what percentage of their troops the sender donated.

For gold donations, it's fixed values. We cannot use percentages here because “having nearly no gold” is a usual case. Donating 100% of your gold wouldn’t hurt if you just spent all your gold on buildings.

I tried to add tests for this but it's really horrible. Because the test would have to wait until the relation update from the alliance accepting is gone (we need to have an alliance to send stuff), has returned to Neutral, and then changes back to Friendly after the donation.

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:

FloPinguin

@FloPinguin FloPinguin requested a review from a team as a code owner November 26, 2025 22:45
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Walkthrough

DonateGoldExecution and DonateTroopExecution now store the Game reference, seed a PseudoRandom with game ticks, and use Difficulty + randomness to compute donation thresholds; relation changes are applied only when donated gold/troops meet the computed threshold.

Changes

Cohort / File(s) Summary
Donation execution — gold & troop thresholding
src/core/execution/DonateGoldExecution.ts, src/core/execution/DonateTroopExecution.ts
Added Difficulty and PseudoRandom; introduced private random and mg: Game, seeded in init() with mg.ticks(); adjusted recipient-not-found logs; replaced fixed relation updates with computed checks: getGoldChunkSize() + calculateRelationUpdate() for gold and getMinTroopsForRelationUpdate() for troops; apply relation change only when donated amount ≥ computed threshold; exposed the new public methods.

Sequence Diagram(s)

sequenceDiagram
    participant Donor
    participant Exec as DonateExecution
    participant Game as Game(mg)
    participant Rand as PseudoRandom
    participant Recipient

    Donor->>Exec: init(mg, request)
    Exec->>Game: read ticks, difficulty
    Game-->>Exec: ticks, difficulty
    Exec->>Rand: seed(ticks)
    Exec->>Rand: compute threshold (difficulty, recipient)
    Rand-->>Exec: threshold
    Exec->>Recipient: attemptDonation(amount)
    Recipient-->>Exec: donationResult(success/failure)
    alt success and amount >= threshold
        Exec->>Recipient: updateRelation(+computed)
        Recipient-->>Exec: relationUpdated
    else success but amount < threshold
        Exec-->>Recipient: no relation change
    end
    Exec-->>Donor: finish(status)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay attention to:
    • Difficulty-to-threshold calculations and caps (gold chunk math, max relation).
    • Proper seeding and deterministic use of PseudoRandom with mg.ticks().
    • Correct gating of relation updates when donation succeeds but is below threshold.
    • Updated log messages referencing the correct execution classes.

Possibly related PRs

Suggested labels

Bug Fix

Suggested reviewers

  • evanpelle
  • tnhnblgl

Poem

A tick, a seed, a random roll,
Gifts counted toward a friendly goal.
Thresholds set by difficulty's art,
Relations shift when chunks play their part. 🎲

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix nation relation exploit' directly describes the main change: fixing an exploit where minimal donations granted undeserved friendly relations.
Description check ✅ Passed The description clearly explains the exploit being fixed, the solution approach (randomized minimum donation requirements by difficulty), and distinguishes between troop and gold donation strategies.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90dff31 and 6a83b35.

📒 Files selected for processing (1)
  • src/core/execution/DonateGoldExecution.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/execution/DonateGoldExecution.ts

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/core/execution/DonateTroopExecution.ts (1)

54-86: Consider making this method private.

getMinTroopsForRelationUpdate() is an internal calculation used only within tick(). Marking it private keeps the public API surface clean.

-  getMinTroopsForRelationUpdate(): number {
+  private getMinTroopsForRelationUpdate(): number {
src/core/execution/DonateGoldExecution.ts (1)

62-72: Consider making this method private.

getMinGoldForRelationUpdate() is only used internally within tick(). Marking it private keeps the API surface clean and consistent.

-  getMinGoldForRelationUpdate(): number {
+  private getMinGoldForRelationUpdate(): number {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f53785 and 543d0bb.

📒 Files selected for processing (2)
  • src/core/execution/DonateGoldExecution.ts (4 hunks)
  • src/core/execution/DonateTroopExecution.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 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/DonateGoldExecution.ts
  • src/core/execution/DonateTroopExecution.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/DonateGoldExecution.ts
  • src/core/execution/DonateTroopExecution.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/DonateGoldExecution.ts
  • src/core/execution/DonateTroopExecution.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/DonateTroopExecution.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/DonateTroopExecution.ts
🧬 Code graph analysis (2)
src/core/execution/DonateGoldExecution.ts (2)
src/core/game/Game.ts (3)
  • Execution (350-355)
  • Player (525-661)
  • Game (663-747)
src/core/PseudoRandom.ts (1)
  • PseudoRandom (3-76)
src/core/execution/DonateTroopExecution.ts (2)
src/core/game/Game.ts (3)
  • Execution (350-355)
  • Player (525-661)
  • Game (663-747)
src/core/PseudoRandom.ts (1)
  • PseudoRandom (3-76)
🔇 Additional comments (5)
src/core/execution/DonateTroopExecution.ts (2)

1-7: LGTM on imports and new fields.

Clean additions for Difficulty, PseudoRandom, and the instance fields needed for the randomized threshold logic.


17-27: LGTM on initialization.

Seeding PseudoRandom with mg.ticks() ensures deterministic behavior across all clients for the same game state. Warning message correctly references DonateTroopExecution.

src/core/execution/DonateGoldExecution.ts (3)

1-15: LGTM on imports and new fields.

Clean imports and field additions. Pattern matches DonateTroopExecution nicely.


28-42: LGTM on initialization.

Deterministic seeding with mg.ticks() and correct warning message referencing DonateGoldExecution.


50-53: Relation update logic looks good.

Unlike the troop version, gold thresholds are fixed values, so no timing issue here. The comparison correctly converts the threshold to BigInt for comparison with Gold.

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

57-89: Fallback return 0 re-enables the exploit for unknown difficulty values.

If the difficulty somehow doesn't match any known value, returning 0 means any donation would trigger a relation update, which defeats the exploit fix. Consider throwing an error or defaulting to the strictest threshold (Impossible).

Apply this diff to use a safer fallback:

     // ~14.3k - ~20k troops (for 100k troops)
     if (difficulty === Difficulty.Impossible)
       return this.random.nextInt(
         this.sender.troops() / 7,
         this.sender.troops() / 5,
       );
 
-    return 0;
+    // Fallback to strictest threshold for any unexpected difficulty
+    return this.random.nextInt(
+      this.sender.troops() / 7,
+      this.sender.troops() / 5,
+    );
   }

Alternatively, use a switch statement with exhaustive handling to catch unhandled enum values at compile time.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 543d0bb and ed71be8.

📒 Files selected for processing (1)
  • src/core/execution/DonateTroopExecution.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 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/DonateTroopExecution.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/DonateTroopExecution.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/DonateTroopExecution.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/DonateTroopExecution.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/DonateTroopExecution.ts
🧬 Code graph analysis (1)
src/core/execution/DonateTroopExecution.ts (2)
src/core/game/Game.ts (3)
  • Execution (350-355)
  • Player (525-661)
  • Game (663-747)
src/core/PseudoRandom.ts (1)
  • PseudoRandom (3-76)
🔇 Additional comments (4)
src/core/execution/DonateTroopExecution.ts (4)

1-7: LGTM!

Clean imports with no unnecessary dependencies. The new private members for random and mg are appropriate for the threshold calculation logic.


17-34: LGTM!

Using mg.ticks() as the random seed keeps behavior deterministic for replay and multiplayer sync.


36-55: LGTM!

The threshold is now computed before donateTroops() runs, so the minimum is based on the sender's pre-donation troop count. This correctly addresses the previous review feedback.


91-98: LGTM!

Standard execution lifecycle methods, no issues.

coderabbitai[bot]
coderabbitai bot previously approved these changes Nov 26, 2025
tick(ticks: number): void {
if (this.troops === null) throw new Error("not initialized");

const minTroops = this.getMinTroopsForRelationUpdate();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this variable can be inlined

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had it inlined, but then coderabbit rightfully said that we need to calculate the troops BEFORE donating them.

this.active = false;
}

getMinTroopsForRelationUpdate(): number {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you use a switch case for this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

// ~7.7k - ~9.1k troops (for 100k troops)
if (difficulty === Difficulty.Easy)
return this.random.nextInt(
this.sender.troops() / 13,
Copy link
Collaborator

Choose a reason for hiding this comment

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

What do you think about making it a function of the recipient max troops? This way you need to give more troops to be in favor of larger nations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, I added that


getMinGoldForRelationUpdate(): number {
const { difficulty } = this.mg.config().gameConfig();
if (difficulty === Difficulty.Easy) return this.random.nextInt(0, 25_000);
Copy link
Collaborator

Choose a reason for hiding this comment

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

this can be a switch statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

this.active = false;
}

getMinGoldForRelationUpdate(): number {
Copy link
Collaborator

Choose a reason for hiding this comment

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

instead of a binary update or not, what if we updated relations proportionally to the gold sent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, I added that

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 (3)
src/core/execution/DonateGoldExecution.ts (2)

50-86: Guard updateRelation on positive update and check BigInt→Number cast

Here calculateRelationUpdate can return 0 when goldSent < chunkSize, but you still call updateRelation with 0. It is harmless but noisy and hides the intent.

You can make the intent clearer and avoid the no-op call:

-      const relationUpdate = this.calculateRelationUpdate(Number(this.gold));
-      this.recipient.updateRelation(this.sender, relationUpdate);
+      const relationUpdate = this.calculateRelationUpdate(Number(this.gold));
+      if (relationUpdate > 0) {
+        this.recipient.updateRelation(this.sender, relationUpdate);
+      }

Also, gold is effectively a bigint (see this.sender.gold() / 3n). Casting to Number will lose precision if gold can grow beyond Number.MAX_SAFE_INTEGER. If that can happen in long games, consider keeping the math in bigint and only converting the final capped result (≤100) to number.


28-32: Random seed and chunk ranges – confirm they match the exploit-fix intent

Seeding PseudoRandom with mg.ticks() means every DonateGoldExecution created in the same tick will get the same first random value, so all of them will use the same chunk size per difficulty. That is deterministic, but donations in the same tick are perfectly correlated. If you want more per-donation variety, you may want to mix in something like sender/recipient identity into the seed (while still keeping it deterministic).

On Easy, getGoldChunkSize() can be as low as 1, so in rare cases a 1‑gold donation will still give 5 relation points. Given the exploit you are fixing (very small donations buying a Friendly relation), please double‑check that this “tiny but low‑odds” reward is intended and that you do not want a higher lower bound here (for example a few thousand gold).

Also applies to: 61-75

src/core/execution/DonateTroopExecution.ts (1)

36-55: Threshold logic for troop donations looks good; consider handling unknown Difficulty explicitly

Computing minTroops once before donateTroops and basing it on recipientMaxTroops fixes the earlier “post‑donation threshold” issue and matches the idea that larger nations need larger donations. The exploit is much harder now, especially on higher difficulties.

One small thing: getMinTroopsForRelationUpdate() returns 0 in the default branch. If a new Difficulty value is ever added and not covered in this switch, any positive donation would again give +50 relation. You might want to:

  • Treat unknown difficulties as one of the existing ones (for example Medium or Hard), or
  • Log a warning and pick a conservative range, or
  • Use a helper like assertNever(difficulty) so the compiler forces you to handle new enum values.

This keeps the exploit fix safe against future Difficulty additions.

Also applies to: 57-89

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed71be8 and 1ce1eae.

📒 Files selected for processing (2)
  • src/core/execution/DonateGoldExecution.ts (4 hunks)
  • src/core/execution/DonateTroopExecution.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
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.
📚 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/DonateGoldExecution.ts
  • src/core/execution/DonateTroopExecution.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/DonateGoldExecution.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/DonateGoldExecution.ts
  • src/core/execution/DonateTroopExecution.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/DonateTroopExecution.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/DonateTroopExecution.ts
🧬 Code graph analysis (2)
src/core/execution/DonateGoldExecution.ts (2)
src/core/game/Game.ts (3)
  • Execution (350-355)
  • Player (525-661)
  • Game (663-747)
src/core/PseudoRandom.ts (1)
  • PseudoRandom (3-76)
src/core/execution/DonateTroopExecution.ts (2)
src/core/game/Game.ts (3)
  • Execution (350-355)
  • Player (525-661)
  • Game (663-747)
src/core/PseudoRandom.ts (1)
  • PseudoRandom (3-76)
🔇 Additional comments (1)
src/core/execution/DonateTroopExecution.ts (1)

21-24: Fix typo in warning prefix (DonateTroopExecutionDonateTroopsExecution)

Small nit: the log string uses DonateTroopExecution while the class name is DonateTroopsExecution. This can make log filtering and grepping harder.

-      console.warn(
-        `DonateTroopExecution recipient ${this.recipientID} not found`,
-      );
+      console.warn(
+        `DonateTroopsExecution recipient ${this.recipientID} not found`,
+      );

Likely an incorrect or invalid review comment.

coderabbitai[bot]
coderabbitai bot previously approved these changes Nov 27, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/core/execution/DonateGoldExecution.ts (3)

29-35: Consider using the ticks argument to seed PseudoRandom

new PseudoRandom(mg.ticks()) works, but seeding from the explicit ticks parameter would better follow the Execution.init(mg, ticks) contract and make the time dependency clearer (and a bit easier to test). The updated console.warn message is correct and more precise.


63-77: Difficulty-based chunk sizing is clear; consider caching per execution

The difficulty ranges and use of PseudoRandom.nextInt avoid a zero chunk size and look good. If this method ever gets called more than once per donation, consider drawing once and storing a goldChunkSize: Gold field so the threshold for a single donation cannot change mid-flow.


79-89: Keep chunk arithmetic in bigint and cast only at the end

Gold is a bigint, so converting goldSent / chunkSize to number is safe here (because you cap at 100), but you can keep all math in bigint and only cast the small final value, which is more idiomatic and future‑proof:

-  calculateRelationUpdate(goldSent: Gold): number {
-    const chunkSize = this.getGoldChunkSize();
-    // Calculate how many complete chunks were donated
-    // BigInt division automatically truncates (integer division)
-    const chunks = Number(goldSent / chunkSize);
-    // Each chunk gives 5 relation points
-    const relationUpdate = chunks * 5;
-    // Cap at 100 relation points
-    if (relationUpdate > 100) return 100;
-    return relationUpdate;
-  }
+  calculateRelationUpdate(goldSent: Gold): number {
+    const chunkSize = this.getGoldChunkSize();
+    const chunks = goldSent / chunkSize; // bigint
+
+    if (chunks <= 0n) return 0;
+    if (chunks >= 20n) return 100; // 20 * 5 = 100 (cap)
+
+    return Number(chunks * 5n);
+  }

This keeps the core logic in one numeric domain and only uses number for the small, bounded relation result.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ce1eae and 90dff31.

📒 Files selected for processing (2)
  • src/core/execution/DonateGoldExecution.ts (4 hunks)
  • src/core/execution/DonateTroopExecution.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 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/DonateTroopExecution.ts
  • src/core/execution/DonateGoldExecution.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/DonateTroopExecution.ts
  • src/core/execution/DonateGoldExecution.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/DonateTroopExecution.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/DonateTroopExecution.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/DonateTroopExecution.ts
  • src/core/execution/DonateGoldExecution.ts
🧬 Code graph analysis (2)
src/core/execution/DonateTroopExecution.ts (2)
src/core/game/Game.ts (2)
  • Execution (350-355)
  • Game (663-747)
src/core/PseudoRandom.ts (1)
  • PseudoRandom (3-76)
src/core/execution/DonateGoldExecution.ts (1)
src/core/PseudoRandom.ts (1)
  • PseudoRandom (3-76)
🔇 Additional comments (7)
src/core/execution/DonateGoldExecution.ts (2)

1-15: Imports and new fields are wired correctly

Bringing in Difficulty, Game, Gold, and PseudoRandom, and adding random/mg fields, matches the pattern in DonateTroopExecution and keeps the execution self-contained; no issues here.


50-54: Relation scaling with donated gold closes the exploit

Using calculateRelationUpdate(this.gold) and skipping updates when the result is 0 cleanly removes the “1 gold ⇒ big relation jump” exploit while still keeping the logic simple and easy to reason about.

src/core/execution/DonateTroopExecution.ts (5)

1-2: LGTM! Clean imports for the new threshold logic.

The additions of Difficulty and PseudoRandom support the exploit fix by enabling difficulty-based, randomized donation thresholds.


6-7: Verify that identical seeds for same-tick donations are intended.

The PseudoRandom instance is seeded with mg.ticks(), which means all troop donations executed at the same tick will compute identical minimum thresholds. This creates deterministic behavior for replays but reduces per-player randomness.

If unique thresholds per execution are desired, consider including sender/recipient IDs in the seed:

-    this.random = new PseudoRandom(mg.ticks());
+    this.random = new PseudoRandom(
+      mg.ticks() + this.sender.id().hashCode() + this.recipientID.hashCode()
+    );

Otherwise, document that same-tick donations share thresholds by design.

Also applies to: 18-19


22-24: LGTM! Improved warning message specificity.

Adding "DonateTroopExecution" to the warning helps debugging by clearly identifying the execution type.


39-39: Excellent fix for the relation exploit!

Computing minTroops before the donation (line 39) and conditionally updating the relation (lines 46-48) correctly prevents the exploit where tiny percentage-based donations could immediately grant Friendly status. The pre-donation calculation ensures the threshold reflects the donor's actual capacity.

Also applies to: 45-48


57-89: LGTM! Clean difficulty-based threshold calculation.

The switch case cleanly implements escalating minimum thresholds tied to recipient capacity. The ranges are well-calibrated and comments accurately document the thresholds for a 100k max troop recipient. Using recipientMaxTroops as the base (per previous feedback) ensures larger nations require proportionally larger donations.

The default fallback handles unexpected difficulty values gracefully.

coderabbitai[bot]
coderabbitai bot previously approved these changes Nov 27, 2025
@FloPinguin FloPinguin requested a review from evanpelle December 1, 2025 12:00

getGoldChunkSize(): Gold {
const { difficulty } = this.mg.config().gameConfig();
switch (difficulty) {
Copy link
Contributor

@Lavodan Lavodan Dec 4, 2025

Choose a reason for hiding this comment

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

Would it be better to put this into defaultConfig?

You could also use the unused difficultyModifier function if you want, but it's not needed.

Some goes for the troop switch

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Uh I don't know... Putting something in defaultConfig is probably most helpful if its used in multiple places?

If we want all constants in defaultConfig then a lot is missing there

const { difficulty } = this.mg.config().gameConfig();
switch (difficulty) {
case Difficulty.Easy:
return BigInt(this.random.nextInt(1, 2_500));
Copy link
Contributor

@Lavodan Lavodan Dec 4, 2025

Choose a reason for hiding this comment

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

Could we scale these values with tick number, or with number of factories/ports on the map?

The more economy buildings there are, the less valuable donations become, and in the endgame,

Copy link
Contributor Author

@FloPinguin FloPinguin Dec 4, 2025

Choose a reason for hiding this comment

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

Thats an interesting idea, but then players would have to do calculations if they want to "buy" a good relation with a nation.

I'm not sure if thats good or bad

Copy link
Contributor

Choose a reason for hiding this comment

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

My thought was that the more it hurts you/the more it helps the nation, the more it raises the relation, so players wouldn't necessarily have to do calculations, they'd just give as much as they are comfortable with.

But this was just an idea to be clear, nothing necessary.

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