Skip to content

Fix: prevent 2-3 public lobbies showing the same map/max players/number of teams#3567

Open
VariableVince wants to merge 9 commits intomainfrom
prevent-samelobbies
Open

Fix: prevent 2-3 public lobbies showing the same map/max players/number of teams#3567
VariableVince wants to merge 9 commits intomainfrom
prevent-samelobbies

Conversation

@VariableVince
Copy link
Copy Markdown
Contributor

@VariableVince VariableVince commented Apr 3, 2026

Description:

Try to prevent two or three of the public lobbies having the same map/number of players/number of teams. Six tries max before it just goes with what is has. We could make it e.g. three tries if six is two much.

Because it is (relatively) quite frequently happening that public lobbies have:

  • one map showing up in two or even three lobbies at the same time. For example all 3 having the Montreal map. If players have a dislike of that map, it may be hard to fill up 2-3 lobbies with the map. Also it simply gives less choice.
  • for teams games, the Special Mix lobby has 42 Humans vs 42 Nations and the Teams lobby also has 42 Humans vs 42 Nations. Harder to fill up if both are the same while there's a group waiting for a non-Nations team game or disliking Humans vs Nations.
  • two or three lobbies with the same number of players. For example two 125 players. Making it harder to fill up because of their sheer size, but again also giving less choice especially if players don't like big lobbies. Same goes for medium or small lobbies. Also, same maxPlayers leads to same team sizes if the number of teams is the same.

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:

tryout33

…or Team games) number of teams.

It was relatively quite frequently happening that one map showed up in two or even three lobbies at the same time. Same for teams games, the Special Mix lobby would have 42 Humans vs 42 Nations and the Teams lobby would show the exact same configuration. Or there would be two lobbies with 125 players.
@VariableVince VariableVince self-assigned this Apr 3, 2026
@VariableVince VariableVince added the Refactor Code cleanup, technical debt, refactoring, and architecture improvements. label Apr 3, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

Important

Review skipped

This PR was authored by the user configured for CodeRabbit reviews. CodeRabbit does not review PRs authored by this user. It's recommended to use a dedicated user account to post CodeRabbit review feedback.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3453282-daa9-4323-99dc-0542cd631cde

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

MapPlaylist now provides gameConfigNotInUse(type, isInUse) which peeks and retries up to six maps, rotating rejected maps and only consuming when accepted. MasterLobbyService requests a non-conflicting config, records it as in-use immediately, and sends it to the worker to create the game.

Changes

Cohort / File(s) Summary
Config Selection & Playlist
src/server/MapPlaylist.ts
Replaced gameConfig(type) with gameConfigNotInUse(type, isInUse) that peeks the next map, retries up to 6 attempts, cycles rejected maps (cycleNextMap) and only consumes on accept (consumeNextMap). Added peekNextMap, ensurePlaylistPopulated, and split builders into buildConfig(type, map) and buildSpecialConfig(map).
Lobby Scheduling / Conflict Avoidance
src/server/MasterLobbyService.ts
maybeScheduleLobby() now collects next-lobby in-use keys (gameMap, serialized playerTeams, maxPlayers), calls playlist.gameConfigNotInUse(type, predicate) to avoid conflicts, records the returned config immediately via recordInUse, and sends it to the worker with createGame.

Sequence Diagram

sequenceDiagram
    participant MLS as MasterLobbyService
    participant MP as MapPlaylist
    participant IS as InUseStore
    participant WK as Worker

    MLS->>IS: collect in-use keys (map / teams / maxPlayers)
    MLS->>MP: gameConfigNotInUse(type, predicate)
    loop up to 6 attempts
        MP->>MP: ensurePlaylistPopulated(type) / peekNextMap(type)
        MP->>MP: buildConfig(type, map) or buildSpecialConfig(map)
        MP->>IS: predicate(config) -> isInUse?
        alt not in use
            MP->>MP: consumeNextMap(type) — remove chosen map
            MP-->>MLS: return GameConfig
        else in use
            MP->>MP: cycleNextMap(type) — rotate or regenerate
            MP-->>MP: retry
        end
    end
    MLS->>IS: recordInUse(selected config)
    MLS->>WK: send createGame(selected config)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

Peek the maps, give each a spin,
Six soft tries to find the win.
Master marks the chosen place,
Sends the match to take its space.
Playlists hum — the game begins.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: preventing multiple public lobbies from displaying identical map, max players, or team configurations.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem motivations and implementation approach for preventing lobby configuration duplicates.
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.


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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/server/MapPlaylist.ts (1)

152-171: Method name does not match behavior after fallback.

After 6 attempts, this method returns a config regardless of whether isInUse(config) is true. The name gameConfigNotInUse suggests a guarantee that is not provided. Consider renaming to something clearer like gameConfigAvoidingCollision or tryGetUniqueGameConfig, or add a doc comment explaining the fallback behavior.

The do { ... } while (true) with an internal return works, but you could also express the limit more directly:

♻️ Suggested rewrite for clarity
 public async gameConfigNotInUse(
   type: PublicGameType,
   isInUse: (config: GameConfig) => boolean,
 ): Promise<GameConfig> {
   const maxAttempts = 6;
-  let attempts = 0;
-
-  do {
+  for (let attempts = 0; attempts < maxAttempts; attempts++) {
     const map = this.tryFirstMap(type);
     const config = await this.buildConfig(type, map);
-    attempts++;

-    if (!isInUse(config) || attempts >= maxAttempts) {
+    if (!isInUse(config) || attempts === maxAttempts - 1) {
       this.useFirstMap(type);
       return config;
     }

     this.firstMapToLast(type);
-  } while (true);
+  }
+  // Unreachable, but satisfies TypeScript
+  throw new Error("Unexpected: maxAttempts exhausted without return");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/MapPlaylist.ts` around lines 152 - 171, The method
gameConfigNotInUse incorrectly promises a config "not in use" but after
maxAttempts returns whichever config was last built even if isInUse(config) is
true; either rename the method to something clearer (e.g.,
tryGetUniqueGameConfig or gameConfigAvoidingCollision) and update all callers,
or change the implementation to explicitly reflect the fallback: loop up to
maxAttempts (use a for loop over attempts), call tryFirstMap -> buildConfig and
check isInUse(config), returning immediately if not in use; if loop exits
without finding a free config, call useFirstMap(type) once and return the last
built config; reference the functions tryFirstMap, buildConfig, useFirstMap,
firstMapToLast, and the isInUse callback when making the change and add a short
doc comment describing the fallback behavior.
🤖 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/server/MasterLobbyService.ts`:
- Around line 151-157: The loop only calls recordInUse on lobbies[0], so other
existing lobbies per type (e.g., lobbies[1], lobbies[2]) are never marked in
usedMaps/usedTeamTypes/usedMaxPlayers; change the iteration over lobbyTypes to
iterate every lobby in lobbiesByType[type] (e.g., for each const lobby of
lobbies) and call recordInUse(lobby.gameConfig) when gameConfig is present,
ensuring all existing lobbies are recorded and preventing collisions; update
references to lobbyTypes, lobbiesByType, and recordInUse accordingly.

---

Nitpick comments:
In `@src/server/MapPlaylist.ts`:
- Around line 152-171: The method gameConfigNotInUse incorrectly promises a
config "not in use" but after maxAttempts returns whichever config was last
built even if isInUse(config) is true; either rename the method to something
clearer (e.g., tryGetUniqueGameConfig or gameConfigAvoidingCollision) and update
all callers, or change the implementation to explicitly reflect the fallback:
loop up to maxAttempts (use a for loop over attempts), call tryFirstMap ->
buildConfig and check isInUse(config), returning immediately if not in use; if
loop exits without finding a free config, call useFirstMap(type) once and return
the last built config; reference the functions tryFirstMap, buildConfig,
useFirstMap, firstMapToLast, and the isInUse callback when making the change and
add a short doc comment describing the fallback behavior.
🪄 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: 21ca9ff7-de42-40e1-80b0-fde667a38950

📥 Commits

Reviewing files that changed from the base of the PR and between 21c2861 and c7ea1be.

📒 Files selected for processing (2)
  • src/server/MapPlaylist.ts
  • src/server/MasterLobbyService.ts

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Apr 3, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 3, 2026
@github-project-automation github-project-automation bot moved this from Development to Final Review in OpenFront Release Management Apr 3, 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.

Actionable comments posted: 1

🤖 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/server/MapPlaylist.ts`:
- Around line 438-440: firstMapToLast currently moves the first element using a
raw push(this.playlists[type].shift()!) which can introduce consecutive
duplicates; instead, after tryFirstMap(type) remove the head value and attempt
to reinsert it using the same non-consecutive rule (call
addNextMapNonConsecutive(type, removedMap) or equivalent) and if that
reinsertion fails, rebuild the tail using generateNewPlaylist(type) (or a
dedicated rebuildTail method) so the non-consecutive invariant maintained by
generateNewPlaylist is preserved; update firstMapToLast to locate the head via
this.playlists[type].shift(), attempt addNextMapNonConsecutive and fallback to
generateNewPlaylist on failure.
🪄 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: 90f4cfe3-d305-486d-ac9c-774d40e4f2b9

📥 Commits

Reviewing files that changed from the base of the PR and between c7ea1be and f73c73b.

📒 Files selected for processing (1)
  • src/server/MapPlaylist.ts

@github-project-automation github-project-automation bot moved this from Final Review to Development in OpenFront Release Management Apr 3, 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.

♻️ Duplicate comments (1)
src/server/MapPlaylist.ts (1)

438-440: ⚠️ Potential issue | 🟠 Major

Raw push() here can recreate back-to-back maps.

generateNewPlaylist() avoids reusing any of the last five maps, but Line 440 bypasses that rule. A valid cycle like [A, ..., A] becomes [..., A, A] after one rejection, so this retry path can reintroduce the same-map problem this PR is trying to reduce. Please reinsert the rejected map with addNextMapNonConsecutive() and rebuild the tail when that is not possible.

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

In `@src/server/MapPlaylist.ts` around lines 438 - 440, The current firstMapToLast
method moves the rejected first map back into playlists[type] with a raw push
which can create back-to-back duplicates; instead call
addNextMapNonConsecutive(type, rejectedMap) to reinsert the rejected map so
non-consecutive rules are respected, and if addNextMapNonConsecutive cannot
place it (returns false or throws), rebuild the tail for that playlist by
invoking generateNewPlaylist(type) (or the existing tail-rebuild helper) after
tryFirstMap(type) to guarantee the non-consecutive constraint is preserved; keep
tryFirstMap(type) as the rejection check and replace the
playlists[type].push(this.playlists[type].shift()!) step with this safer
reinsertion + fallback rebuild logic.
🤖 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/server/MapPlaylist.ts`:
- Around line 438-440: The current firstMapToLast method moves the rejected
first map back into playlists[type] with a raw push which can create
back-to-back duplicates; instead call addNextMapNonConsecutive(type,
rejectedMap) to reinsert the rejected map so non-consecutive rules are
respected, and if addNextMapNonConsecutive cannot place it (returns false or
throws), rebuild the tail for that playlist by invoking
generateNewPlaylist(type) (or the existing tail-rebuild helper) after
tryFirstMap(type) to guarantee the non-consecutive constraint is preserved; keep
tryFirstMap(type) as the rejection check and replace the
playlists[type].push(this.playlists[type].shift()!) step with this safer
reinsertion + fallback rebuild logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4408923b-d973-493e-bc2b-b72e6ceafcc5

📥 Commits

Reviewing files that changed from the base of the PR and between f73c73b and 4589958.

📒 Files selected for processing (1)
  • src/server/MapPlaylist.ts

coderabbitai[bot]

This comment was marked as off-topic.

coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 3, 2026
@github-project-automation github-project-automation bot moved this from Development to Final Review in OpenFront Release Management Apr 3, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 3, 2026
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 3, 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.

♻️ Duplicate comments (1)
src/server/MapPlaylist.ts (1)

446-450: ⚠️ Potential issue | 🟠 Major

Rebuild the fallback tail with the current suffix, not a standalone playlist.

If addNextMapNonConsecutive() fails here, appending a fresh generateNewPlaylist() can still put one of the current last five maps right at the boundary, so the non-consecutive rule comes back later in the cycle. This also drops the rejected map from the current rotation.

♻️ One way to keep the invariant intact
 private cycleNextMap(type: PublicGameType): void {
   const map = this.consumeNextMap(type);
 
-  if (!this.addNextMapNonConsecutive(this.playlists[type], [map])) {
-    this.playlists[type].push(...this.generateNewPlaylist(type));
+  if (this.addNextMapNonConsecutive(this.playlists[type], [map])) {
+    return;
+  }
+
+  const rebuilt = [...this.playlists[type], map];
+  this.playlists[type] = [];
+  while (rebuilt.length > 0) {
+    if (!this.addNextMapNonConsecutive(this.playlists[type], rebuilt)) {
+      this.playlists[type] = this.generateNewPlaylist(type);
+      break;
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/MapPlaylist.ts` around lines 446 - 450, The current cycleNextMap
drops the consumed map and appends a fresh generateNewPlaylist() which can
reintroduce a recent map at the boundary; instead rebuild the fallback tail
using the actual current suffix (including the rejected map) so
addNextMapNonConsecutive can validate against the real last-N maps. Change the
failure branch in cycleNextMap: compute the suffix = last N entries of
this.playlists[type] concatenated with [map] (use the same window size that
addNextMapNonConsecutive enforces), then generate or extend a replacement tail
that is validated by addNextMapNonConsecutive(suffix, tail) (or call
addNextMapNonConsecutive(this.playlists[type], tail) while ensuring tail was
built from that suffix); loop/regenerate until addNextMapNonConsecutive returns
true so the rejected map remains in rotation and the non-consecutive invariant
holds (use functions consumeNextMap, addNextMapNonConsecutive,
generateNewPlaylist as reference points when implementing).
🤖 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/server/MapPlaylist.ts`:
- Around line 446-450: The current cycleNextMap drops the consumed map and
appends a fresh generateNewPlaylist() which can reintroduce a recent map at the
boundary; instead rebuild the fallback tail using the actual current suffix
(including the rejected map) so addNextMapNonConsecutive can validate against
the real last-N maps. Change the failure branch in cycleNextMap: compute the
suffix = last N entries of this.playlists[type] concatenated with [map] (use the
same window size that addNextMapNonConsecutive enforces), then generate or
extend a replacement tail that is validated by addNextMapNonConsecutive(suffix,
tail) (or call addNextMapNonConsecutive(this.playlists[type], tail) while
ensuring tail was built from that suffix); loop/regenerate until
addNextMapNonConsecutive returns true so the rejected map remains in rotation
and the non-consecutive invariant holds (use functions consumeNextMap,
addNextMapNonConsecutive, generateNewPlaylist as reference points when
implementing).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f815bd24-579c-44e3-a5e6-eb6eb55d1f17

📥 Commits

Reviewing files that changed from the base of the PR and between 5f97b0f and 1aff3ba.

📒 Files selected for processing (2)
  • src/server/MapPlaylist.ts
  • src/server/MasterLobbyService.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems a bit over complicated to me. could we just do something like:

activeMaps => new Set(lobbies.map(l => l.map))

while(true) {
const config => mapPlaylist.config()
if(!activeMaps.has(config.map)) {
continue
}

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

Labels

Refactor Code cleanup, technical debt, refactoring, and architecture improvements.

Projects

Status: Final Review

Development

Successfully merging this pull request may close these issues.

2 participants