Skip to content

Conversation

@Codimow
Copy link
Contributor

@Codimow Codimow commented Jan 16, 2026

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

In GameManager.tick(), when a game becomes active but hasn't started, a setTimeout for game.start() is scheduled with a 2-second delay. If the game finishes or is cancelled within those 2 seconds, game.end() is called, which clears the existing interval. However:

1.The 2-second timeout still fires. game.start() executes.
2. A NEW setInterval is created for turn execution.
3.Since the game is already ending/finished, it's removed from GameManager.games, but the interval continues to run forever in the background

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:

codimo

…g a `_hasEnded` flag and adding lifecycle tests.
@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 Jan 16, 2026

Walkthrough

A private _hasEnded flag was added to GameServer. start() now won't run if _hasEnded is true. end() sets _hasEnded = true, clears and undefines the turn interval, and is guarded against repeated calls. Tests and an npm script were added for the server tests.

Changes

Cohort / File(s) Summary
Core game lifecycle management
src/server/GameServer.ts
Added private _hasEnded: boolean = false. start() checks _hasEnded and refuses to start if true. end() sets _hasEnded = true, clears endTurnIntervalID, sets it to undefined, and guards against re-entry.
Game lifecycle test suite
tests/server/GameLifecycle.test.ts
New Vitest tests: mock config/schemas, use fake timers. Verify start() is noop after end(), interval is cleared and becomes undefined on end(), and multiple end() calls are safe.
NPM scripts
package.json
Added script "test:server": "vitest run tests/server" under scripts.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A flag is raised, the game is done,
Timers hush beneath the sun,
Start won't wake what chose to rest,
End is calm and handles stress,
Server sleeps — the logs attest 🎮✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preventing GameServer from restarting after ending by introducing a _hasEnded flag.
Description check ✅ Passed The description is directly related to the changeset, explaining the bug scenario and the fix's purpose with clear context.
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.



📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1825d4d and 5e306c1.

📒 Files selected for processing (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

✏️ Tip: You can disable this entire section by setting review_details to false in your review 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

@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

🤖 Fix all issues with AI agents
In `@src/server/GameServer.ts`:
- Around line 74-75: The file GameServer.ts has Prettier formatting issues; run
the formatter or apply Prettier rules to fix formatting so CI passes: run npx
prettier --write src/server/GameServer.ts (or run project-wide prettier) and
commit the changes; ensure formatting is consistent in the GameServer class
(e.g., around the private _hasEnded property and surrounding lines) so the
Prettier check in CI no longer fails.
🧹 Nitpick comments (2)
tests/server/GameLifecycle.test.ts (1)

6-94: Consider adding a test for the timeout scenario.

The tests cover the core fix, but the PR objective mentions a setTimeout that schedules game.start() after 2 seconds in GameManager.tick(). Since you have fake timers set up, consider adding a test that simulates this scenario:

it("should not start when delayed start fires after end()", async () => {
  const game = new GameServer(/* ... */);
  
  // Simulate the setTimeout from GameManager.tick()
  const delayedStart = setTimeout(() => game.start(), 2000);
  
  // End the game before the timeout fires
  await game.end();
  
  // Advance timers to trigger the delayed start
  vi.advanceTimersByTime(2000);
  
  // start() should have been a no-op
  expect((game as any).endTurnIntervalID).toBeUndefined();
});

This would directly mirror the bug scenario from the PR description.

src/server/GameServer.ts (1)

639-691: Consider adding re-entry guard for robustness.

While setting _hasEnded = true first helps, you could add an early return if _hasEnded is already true to skip the cleanup logic entirely on repeated calls:

♻️ Optional: Add explicit re-entry guard
 async end() {
+  if (this._hasEnded) {
+    return;
+  }
   this._hasEnded = true;
   // Close all WebSocket connections
   if (this.endTurnIntervalID) {

This is optional since the current code handles multiple calls safely (clearInterval on undefined is harmless, and iterating empty sets is fine). The explicit guard just makes the intent clearer.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2ba37e and ff2e812.

📒 Files selected for processing (2)
  • src/server/GameServer.ts
  • tests/server/GameLifecycle.test.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-12T21:37:01.156Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2874
File: src/server/MapLandTiles.ts:7-11
Timestamp: 2026-01-12T21:37:01.156Z
Learning: In this repository's OpenFrontIO deployment, inter-service HTTP calls to the master should target http://localhost:3000 (master at port 3000) as the canonical address. Apply this as the standard for all server-side TypeScript code that communicates with the master. Avoid hardcoding non-master URLs; centralize the master address (e.g., via config or env) when possible, and ensure internal service communication uses localhost:3000 in this architecture.

Applied to files:

  • src/server/GameServer.ts
  • tests/server/GameLifecycle.test.ts
📚 Learning: 2026-01-13T20:16:05.535Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:05.535Z
Learning: In code paths that return a Player-like object, prefer returning a union type (Player | TerraNullius) instead of undefined. When a function may fail to find a player, return TerraNullius for the 'not found' case and a Player for valid IDs, and check .isPlayer() (or equivalent) directly on the result instead of guarding with undefined or optional chaining. This should be enforced in Game, GameImpl, and GameView (and similar accessors) to avoid undefined checks and simplify null-safety handling.

Applied to files:

  • src/server/GameServer.ts
  • tests/server/GameLifecycle.test.ts
📚 Learning: 2026-01-09T06:10:44.499Z
Learnt from: plazmaezio
Repo: openfrontio/OpenFrontIO PR: 2716
File: tests/AllianceAcceptNukes.test.ts:85-87
Timestamp: 2026-01-09T06:10:44.499Z
Learning: In tests under tests/**, objects may stack on the same tile coordinates (e.g., MissileSilo). When writing tests that involve building/placing structures or cooldowns, assume multiple items can share coordinates; adjust assertions to account for stacking, and explicitly manage or reset the stack state where necessary.

Applied to files:

  • tests/server/GameLifecycle.test.ts
🧬 Code graph analysis (1)
tests/server/GameLifecycle.test.ts (1)
src/server/GameServer.ts (1)
  • GameServer (34-1022)
🪛 GitHub Actions: 🧪 CI
src/server/GameServer.ts

[error] 1-1: Prettier formatting check failed. Code style issues found in 'src/server/GameServer.ts'. Run 'npx prettier --write .' to fix.

🔇 Additional comments (7)
tests/server/GameLifecycle.test.ts (4)

10-28: Good test setup with fake timers and mocks.

The setup is clean. One small note: vi.clearAllTimers() in afterEach clears pending timers, but vi.useRealTimers() is not called. This is fine since vi.restoreAllMocks() handles cleanup, but consider adding vi.useRealTimers() for clarity if other tests in the suite rely on real timers.


30-50: Test covers the core fix well.

This test directly addresses the PR objective: preventing start() from running after end() has been called. The assertions correctly verify both the interval state and the hasStarted() return value.


52-77: Good coverage of normal lifecycle flow.

This test validates that start() creates the interval and end() properly cleans up. The inclusion of gameMap and gameMapSize in the config ensures start() can succeed.


79-94: Nice idempotency test.

Testing that multiple end() calls do not throw ensures robustness. The use of resolves.toBeUndefined() is the right way to assert an async function completes without error.

src/server/GameServer.ts (3)

74-75: Field declaration is clean.

The _hasEnded flag follows the same naming pattern as _hasStarted and _hasPrestarted. The pipeline failure mentions Prettier formatting—this might be related to the blank line at line 75. Run npx prettier --write src/server/GameServer.ts to fix.


536-539: Core fix is correct.

Adding this._hasEnded to the guard condition in start() directly addresses the bug. If end() has been called, any subsequent start() call (including from a pending setTimeout) will be a no-op.


639-645: Proper placement of the _hasEnded flag.

Setting _hasEnded = true as the first action in end() ensures:

  1. Any concurrent start() call will be blocked immediately
  2. Multiple end() calls are safe (idempotent cleanup)

The explicit this.endTurnIntervalID = undefined after clearInterval is good practice—it makes the state change visible and enables tests to verify cleanup.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

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

🤖 Fix all issues with AI agents
In `@package.json`:
- Line 129: The package.json file has a Prettier formatting failure; run the
formatter to fix it by executing npx prettier --write package.json (or your
project's lint-staged/precommit prettier script) and commit the updated
package.json so CI formatting checks pass; target the package.json file itself
and ensure no other unrelated changes are included in the commit.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff2e812 and 1825d4d.

📒 Files selected for processing (3)
  • package.json
  • src/server/GameServer.ts
  • tests/server/GameLifecycle.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/server/GameServer.ts
  • tests/server/GameLifecycle.test.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-09T05:03:18.745Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1761
File: src/core/ApiSchemas.ts:2-2
Timestamp: 2025-08-09T05:03:18.745Z
Learning: After migrating to Zod v4 in the OpenFrontIO project, the standard import statement `import { z } from "zod"` should be used across the entire codebase, not `import { z } from "zod/v4"`.

Applied to files:

  • package.json
🪛 GitHub Actions: 🧪 CI
package.json

[warning] 1-1: Prettier reported code style issues in package.json. Run 'npx prettier --write package.json' to fix.

🔇 Additional comments (1)
package.json (1)

15-15: Nice: targeted server test script.
Line 15 adds a focused test command, which makes server test runs clearer and faster.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 16, 2026
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.

2 participants