Skip to content

v22 trade#3318

Open
ryanbarlow97 wants to merge 6 commits intomainfrom
trade
Open

v22 trade#3318
ryanbarlow97 wants to merge 6 commits intomainfrom
trade

Conversation

@ryanbarlow97
Copy link
Contributor

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

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

Copilot AI review requested due to automatic review settings March 1, 2026 23:11
@ryanbarlow97 ryanbarlow97 requested a review from a team as a code owner March 1, 2026 23:11
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

Walkthrough

The PR renames the second parameter of tradeShipSpawnRate from numTradeShips to numPlayerPorts, updates DefaultConfig to use port-count thresholds and a fixed MIRV cost, updates PortExecution to pass port counts to the spawn-rate call, and removes an 18-hour supervisord timeout in startup.sh. (49 words)

Changes

Cohort / File(s) Summary
Config API
src/core/configuration/Config.ts
Renamed tradeShipSpawnRate second parameter from numTradeShips to numPlayerPorts (signature change).
Default configuration
src/core/configuration/DefaultConfig.ts
Rewrote tradeShipSpawnRate to use discrete numPlayerPorts thresholds; added toInt import; changed tradeShipGold formula to a power-law; simplified MIRV cost to costWrapper(() => 35_000_000).
Port execution
src/core/execution/PortExecution.ts
Changed caller to pass owner's port count (numPlayerPorts) into tradeShipSpawnRate instead of trade-ship count.
Deployment script
startup.sh
Removed the 18-hour timeout wrapper when starting supervisord for the openfront.dev non-main subdomain case.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • use v22 trade meta #2935: Modifies the same trade-ship spawn-rate API and DefaultConfig implementations (parameter rename to numPlayerPorts, logic changes, and MIRV/tradeShipGold updates).

Poem

⚓ Ports now count where trade winds start,
Sigmoids yield to thresholds smart.
MIRV fixed, gold shaped by law,
Supervisord runs without the pause.
Small changes steer the harbor's chart.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description contains only the default template text with unchecked placeholders and no actual description of the changes being made. Replace the template text with a clear description of the changes: explain what was modified (trade ship spawn rate logic, MIRV cost handling, port execution), why these changes were made, and confirm the completion status of relevant checklist items.
Title check ❓ Inconclusive The title 'v22 trade' is vague and does not clearly describe the specific changes made in the pull request, using non-descriptive terminology. Provide a more descriptive title that summarizes the main changes, such as 'Refactor trade ship spawn rate calculation to use player ports' or 'Update trade configuration for v22 trade metadata'.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates trade-related economy knobs (gold payout + spawn rates) and aligns trade-ship spawning logic with player port count, plus a small runtime startup script change.

Changes:

  • Adjust trade ship gold calculation and trade ship spawn rate configuration.
  • Update PortExecution to pass player port count into tradeShipSpawnRate(...).
  • Remove the timeout 18h wrapper from the startup.sh supervisord invocation (and leave a now-redundant conditional).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
startup.sh Removes the 18h timeout around supervisord startup (conditional now executes the same command either way).
src/core/execution/PortExecution.ts Changes spawn-rate input from total trade ships to the owning player’s port count.
src/core/configuration/DefaultConfig.ts Reworks trade ship gold formula and spawn-rate logic; adjusts MIRV cost definition.
src/core/configuration/Config.ts Renames tradeShipSpawnRate parameter to numPlayerPorts to match new call site.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 88 to 92
if [ "$DOMAIN" = openfront.dev ] && [ "$SUBDOMAIN" != main ]; then
exec timeout 18h /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
else
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
fi
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

The if branch is now redundant because both branches exec /usr/bin/supervisord ... with the same arguments. Either remove this conditional entirely, or restore the intended branch-specific behavior (e.g., the previous timeout 18h for non-main openfront.dev).

Copilot uses AI. Check for mistakes.
50_000 / (1 + Math.exp(-0.03 * (dist - debuff))) + 50 * dist;
const multiplier = this.goldMultiplier();
return BigInt(Math.floor(baseGold * multiplier));
return toInt(10000 + 150 * Math.pow(dist, 1.1));
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

tradeShipGold no longer applies goldMultiplier(), while other gold sources (e.g., trainGold, goldAdditionRate) still do. If goldMultiplier is intended to scale the whole economy for a match, consider applying it here as well to keep trade income consistent with the rest of the config.

Suggested change
return toInt(10000 + 150 * Math.pow(dist, 1.1));
const multiplier = this.goldMultiplier();
const baseGold = 10000 + 150 * Math.pow(dist, 1.1);
return toInt(Math.floor(baseGold * multiplier));

Copilot uses AI. Check for mistakes.
const rejectionModifier = 1 / (tradeShipSpawnRejections + 1);

return Math.floor((100 * rejectionModifier) / baseSpawnRate);
return Math.floor((100 * rejectionModifier) / 50);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

For numPlayerPorts > 12, Math.floor((100 * rejectionModifier) / 50) can return 1 or 0 very quickly (e.g., after 1 rejection it becomes 1, after 2 it becomes 0). Passing 0/1 into PseudoRandom.chance() makes spawns effectively guaranteed (and 0 odds is a special-case that always returns true). This likely causes runaway trade-ship spawning; ensure this function never returns < 2 (or at least < 1) and revisit the formula so it scales as intended.

Suggested change
return Math.floor((100 * rejectionModifier) / 50);
// Clamp to a minimum spawn rate to avoid 0/1 odds causing runaway spawns.
return Math.max(2, Math.floor((100 * rejectionModifier) / 50));

Copilot uses AI. Check for mistakes.
}
return 25_000_000n + game.stats().numMirvsLaunched() * 15_000_000n;
},
return {
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

case UnitType.MIRV returns directly from unitInfo(), which bypasses the unitInfoCache.set(type, info) logic used for every other unit type, and also leaves a dead break statement. Consider assigning to info and break like the other cases so MIRV info is cached consistently.

Suggested change
return {
info = {

Copilot uses AI. Check for mistakes.
Comment on lines 71 to 76
shouldSpawnTradeShip(): boolean {
const numTradeShips = this.mg.unitCount(UnitType.TradeShip);
const numPlayerPorts = this.port!.owner().unitCount(UnitType.Port);
const spawnRate = this.mg
.config()
.tradeShipSpawnRate(this.tradeShipSpawnRejections, numTradeShips);
.tradeShipSpawnRate(this.tradeShipSpawnRejections, numPlayerPorts);
for (let i = 0; i < this.port!.level(); i++) {
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

shouldSpawnTradeShip() behavior changed but existing tests/PortExecution.test.ts only covers tradingPorts() weighting, not spawn decisions. Adding/adjusting tests around spawn rate inputs (ports count / rejections) would help prevent regressions in trade ship spawning frequency.

Copilot uses AI. Check for mistakes.
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/configuration/DefaultConfig.ts (1)

378-381: Unreachable break after return.

Line 381 is dead code since the return on line 378-380 exits the function.

🧹 Remove unreachable break
       case UnitType.MIRV:
         return {
           cost: this.costWrapper(() => 35_000_000),
         };
-        break;

Or for consistency with other cases, assign to info instead:

       case UnitType.MIRV:
-        return {
+        info = {
           cost: this.costWrapper(() => 35_000_000),
         };
         break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/configuration/DefaultConfig.ts` around lines 378 - 381, The switch
case in DefaultConfig.ts returns an object directly and then has an unreachable
break; remove the dead `break` statement after the `return` in the cost case (or
alternatively refactor to assign the object to `info` like other cases and
`break` afterwards) so the function/method containing `cost: this.costWrapper(()
=> 35_000_000)` no longer contains unreachable code.
startup.sh (1)

88-92: Redundant conditional block - both branches are identical.

After removing the timeout wrapper, both the if and else branches now run the same command. You can simplify this to just:

exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
🧹 Proposed simplification
-if [ "$DOMAIN" = openfront.dev ] && [ "$SUBDOMAIN" != main ]; then
-    exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
-else
-    exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
-fi
+exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@startup.sh` around lines 88 - 92, The if/else in startup.sh is redundant
because both branches call exec /usr/bin/supervisord -c
/etc/supervisor/conf.d/supervisord.conf; remove the conditional using DOMAIN and
SUBDOMAIN and replace the whole if/else block with a single exec
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf line so
startup.sh always executes supervisord directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/configuration/DefaultConfig.ts`:
- Around line 306-320: The tradeShipSpawnRate function returns 0 for high
rejection counts which breaks the documented contract "Probability of trade ship
spawn = 1 / tradeShipSpawnRate"; clamp the computed pity-timer result to a
minimum of 1 before returning so the rate never becomes zero (locate function
tradeShipSpawnRate and apply a max-with-1 to the final Math.floor result),
preserving existing behavior for the fixed port ranges.

---

Nitpick comments:
In `@src/core/configuration/DefaultConfig.ts`:
- Around line 378-381: The switch case in DefaultConfig.ts returns an object
directly and then has an unreachable break; remove the dead `break` statement
after the `return` in the cost case (or alternatively refactor to assign the
object to `info` like other cases and `break` afterwards) so the function/method
containing `cost: this.costWrapper(() => 35_000_000)` no longer contains
unreachable code.

In `@startup.sh`:
- Around line 88-92: The if/else in startup.sh is redundant because both
branches call exec /usr/bin/supervisord -c
/etc/supervisor/conf.d/supervisord.conf; remove the conditional using DOMAIN and
SUBDOMAIN and replace the whole if/else block with a single exec
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf line so
startup.sh always executes supervisord directly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1125e0 and 97fc8b2.

📒 Files selected for processing (4)
  • src/core/configuration/Config.ts
  • src/core/configuration/DefaultConfig.ts
  • src/core/execution/PortExecution.ts
  • startup.sh

Comment on lines 306 to 320
tradeShipSpawnRate(
tradeShipSpawnRejections: number,
numTradeShips: number,
numPlayerPorts: number,
): number {
const decayRate = Math.LN2 / 50;

// Approaches 0 as numTradeShips increase
const baseSpawnRate = 1 - sigmoid(numTradeShips, decayRate, 200);
if (numPlayerPorts <= 3) return 18;
if (numPlayerPorts <= 5) return 25;
if (numPlayerPorts <= 8) return 35;
if (numPlayerPorts <= 10) return 40;
if (numPlayerPorts <= 12) return 45;

// Pity timer: increases spawn chance after consecutive rejections
const rejectionModifier = 1 / (tradeShipSpawnRejections + 1);

return Math.floor((100 * rejectionModifier) / baseSpawnRate);
return Math.floor((100 * rejectionModifier) / 50);
}
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

🧩 Analysis chain

🏁 Script executed:

# Find Random.chance() implementation
fd -t f "Random\|random" | head -20

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Search for chance method definition
rg -A 5 "chance\s*\(" --type ts | head -40

Repository: openfrontio/OpenFrontIO

Length of output: 2660


🏁 Script executed:

# Check usage of tradeShipSpawnRate in PortExecution.ts
rg "tradeShipSpawnRate" --type ts -B 2 -A 2

Repository: openfrontio/OpenFrontIO

Length of output: 1190


🏁 Script executed:

# Verify the imports and class structure in DefaultConfig.ts
rg "class DefaultConfig|interface DefaultConfig" --type ts -A 5

Repository: openfrontio/OpenFrontIO

Length of output: 590


🏁 Script executed:

# Find Random class implementation
fd -t f "Random\." --type ts | grep -i random

Repository: openfrontio/OpenFrontIO

Length of output: 236


🏁 Script executed:

# Search for Random class definition and chance method
rg "class Random|interface Random" --type ts -A 10

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Find chance() method specifically
ast-grep --pattern 'chance($ARG) {
  $$$
}'

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Direct search for the method
rg "chance\s*\(\s*rate" --type ts -B 2 -A 10

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Find files containing Random class
fd Random --type f | head -20

Repository: openfrontio/OpenFrontIO

Length of output: 149


🏁 Script executed:

# Search for Random class definition broadly
rg "class Random" -A 15

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Search for chance method definition
rg "chance.*\(" -A 8 | head -60

Repository: openfrontio/OpenFrontIO

Length of output: 4713


🏁 Script executed:

# Look in common locations
find src -name "*.ts" | xargs grep -l "chance" | head -10

Repository: openfrontio/OpenFrontIO

Length of output: 459


🏁 Script executed:

# Examine PseudoRandom.ts to find chance() implementation
cat -n src/core/PseudoRandom.ts

Repository: openfrontio/OpenFrontIO

Length of output: 2587


🏁 Script executed:

# Also check if there's a Random interface
rg "interface.*Random|type.*Random" -A 5

Repository: openfrontio/OpenFrontIO

Length of output: 2043


🏁 Script executed:

# Check the exact formula calculation and any comments about the intended behavior
sed -n '300,325p' src/core/configuration/DefaultConfig.ts

Repository: openfrontio/OpenFrontIO

Length of output: 869


🏁 Script executed:

# Check if there are any tests for tradeShipSpawnRate behavior
rg "tradeShipSpawnRate" -B 5 -A 5

Repository: openfrontio/OpenFrontIO

Length of output: 2455


The pity timer logic is inverted in your analysis, but there is still a real issue with the return value.

The formula actually increases spawn chance with rejections (as the comment promises):

  • rejections = 0spawnRate = 2chance(2) = 1/2 = 50%
  • rejections = 1spawnRate = 1chance(1) = 1/1 = 100%
  • rejections >= 2spawnRate = 0 → violates documented contract

The documented comment states "Probability of trade ship spawn = 1 / tradeShipSpawnRate". When spawnRate = 0, this formula becomes undefined mathematically. While chance(0) works in practice (always returns true), it breaks the semantic contract.

For consistency with the documented behavior and the fixed values (18-45 for fewer ports), ensure the formula returns at least 1: Math.max(1, Math.floor((100 * rejectionModifier) / 50)).

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

In `@src/core/configuration/DefaultConfig.ts` around lines 306 - 320, The
tradeShipSpawnRate function returns 0 for high rejection counts which breaks the
documented contract "Probability of trade ship spawn = 1 / tradeShipSpawnRate";
clamp the computed pity-timer result to a minimum of 1 before returning so the
rate never becomes zero (locate function tradeShipSpawnRate and apply a
max-with-1 to the final Math.floor result), preserving existing behavior for the
fixed port ranges.

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

♻️ Duplicate comments (1)
src/core/configuration/DefaultConfig.ts (1)

316-319: ⚠️ Potential issue | 🟠 Major

Clamp the pity-timer result to at least 1.

For numPlayerPorts > 12, Line 319 becomes Math.floor(2 / (tradeShipSpawnRejections + 1)), so it returns 0 once rejections reach 2. That breaks the 1 / tradeShipSpawnRate contract documented on Line 305 and makes chance(0) an accidental special case.

Suggested fix
-    return Math.floor((100 * rejectionModifier) / 50);
+    return Math.max(1, Math.floor((100 * rejectionModifier) / 50));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/configuration/DefaultConfig.ts` around lines 316 - 319, The
pity-timer result can drop to 0 for higher numPlayerPorts which breaks the
1/tradeShipSpawnRate contract; in the code that computes trade ship spawn chance
(uses rejectionModifier and returns Math.floor((100 * rejectionModifier) / 50)),
clamp the result to a minimum of 1 (e.g. wrap the computed value with
Math.max(1, ...)) so the function that uses rejectionModifier never returns 0.
🧹 Nitpick comments (1)
src/core/configuration/DefaultConfig.ts (1)

377-381: Keep MIRV on the shared unitInfo cache path.

This early return skips this.unitInfoCache.set(type, info) below, so MIRV is the only unit type that rebuilds a fresh UnitInfo object every call. Assign to info here like the other cases and let the common return path cache it.

Suggested refactor
       case UnitType.MIRV:
-        return {
+        info = {
           cost: this.costWrapper(() => 35_000_000),
         };
         break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/configuration/DefaultConfig.ts` around lines 377 - 381, The MIRV
case in DefaultConfig currently returns early (case UnitType.MIRV) which
bypasses the shared caching path and unitInfoCache.set; change the case to
assign the constructed object to the existing local variable (e.g., set info = {
cost: this.costWrapper(() => 35_000_000) }) instead of returning, then allow the
common flow to call this.unitInfoCache.set(type, info) and return the cached
info like the other cases; keep the use of this.costWrapper and the
UnitType.MIRV selector unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/core/configuration/DefaultConfig.ts`:
- Around line 316-319: The pity-timer result can drop to 0 for higher
numPlayerPorts which breaks the 1/tradeShipSpawnRate contract; in the code that
computes trade ship spawn chance (uses rejectionModifier and returns
Math.floor((100 * rejectionModifier) / 50)), clamp the result to a minimum of 1
(e.g. wrap the computed value with Math.max(1, ...)) so the function that uses
rejectionModifier never returns 0.

---

Nitpick comments:
In `@src/core/configuration/DefaultConfig.ts`:
- Around line 377-381: The MIRV case in DefaultConfig currently returns early
(case UnitType.MIRV) which bypasses the shared caching path and
unitInfoCache.set; change the case to assign the constructed object to the
existing local variable (e.g., set info = { cost: this.costWrapper(() =>
35_000_000) }) instead of returning, then allow the common flow to call
this.unitInfoCache.set(type, info) and return the cached info like the other
cases; keep the use of this.costWrapper and the UnitType.MIRV selector
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e27d0c2b-5dcc-4533-a1fb-4900748a596f

📥 Commits

Reviewing files that changed from the base of the PR and between 97fc8b2 and dbb9fe9.

📒 Files selected for processing (1)
  • src/core/configuration/DefaultConfig.ts

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

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

3 participants