Skip to content

Implement Map voting#3018

Draft
Aotumuri wants to merge 3 commits intomainfrom
vote-map
Draft

Implement Map voting#3018
Aotumuri wants to merge 3 commits intomainfrom
vote-map

Conversation

@Aotumuri
Copy link
Member

Resolves #2999

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:

aotumuri

@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 25, 2026

Walkthrough

Adds a map-voting feature: UI modal, client storage and WebSocket vote plumbing, lobby UI integration, server-side vote collection/broadcast, and weighted dynamic map selection used by the playlist generator.

Changes

Cohort / File(s) Summary
HTML Modal
index.html
Inserts <map-vote-modal> into the modal section of the page.
Client — UI Component
src/client/MapVoteModal.ts
New MapVoteModal component: renders maps, manages selection, loads/saves local votes, emits map-vote-change and map-vote-submit, shows login banner/toast.
Client — Storage
src/client/MapVoteStorage.ts
New helpers loadStoredMapVotes() and saveStoredMapVotes() with validation, dedupe, and safe localStorage handling.
Client — Socket & Types
src/client/LobbySocket.ts
Adds vote message handling, pending vote queue, sendMapVote()/clearMapVote(), flushPendingVote() and optional callbacks (onVoteRequest, onVoteStats); broadened error handling.
Client — Lobby Integration
src/client/PublicLobby.ts, src/client/Main.ts
Wires modal into Client; PublicLobby integrates modal/CTA, login-aware flow, active vote count, event listeners for vote change/submit, sends stored votes when auth present.
Client — Localization
resources/lang/en.json
Adds public_lobby localization keys for voting UI and messages; minor JSON syntax adjustment.
Client — Map Definitions
src/core/game/PublicLobbyMaps.ts
New publicLobbyMapWeights, publicLobbyMaps list, and getPublicLobbyMapWeight() accessor for server/client weighting.
Server — Master (WS)
src/server/Master.ts
Adds map_vote message schema and auth, per-user vote tracking, connection-level vote mappings, broadcasts map_vote_request/map_vote_stats, computes aggregated map weights, integrates weights into scheduling, and clears votes after scheduling.
Server — Playlist
src/server/MapPlaylist.ts
Replaces static playlist with weighted, probabilistic selection; accepts optional mapVotes in gameConfig(mapVotes?); adds recent-map avoidance, mode sequencing, and weighted pick utilities.

Sequence Diagram

sequenceDiagram
    participant User
    participant Client
    participant Storage as LocalStorage
    participant WS as WebSocket
    participant Server
    participant Playlist

    User->>Client: Open map-vote modal
    Client->>Storage: loadStoredMapVotes()
    Storage-->>Client: stored maps
    Client->>Client: Render available maps and selections

    User->>Client: Toggle map selections
    Client->>Storage: saveStoredMapVotes(maps)
    Client-->>User: Update UI

    alt User logged in
        User->>Client: Submit vote
        Client->>WS: Send `map_vote` (token + maps)
        WS->>Server: Receive & verify vote
        Server->>Server: Update per-user votes & connections
        Server->>Server: Aggregate weights
        Server->>WS: Broadcast `map_vote_stats` / `map_vote_request`
        WS-->>Client: Deliver stats/request
    else User not logged in
        Client->>Storage: Keep votes locally (no send)
    end

    Note over Server,Playlist: When scheduling a public game
    Server->>Playlist: gameConfig(mapVoteWeights)
    Playlist->>Playlist: pickWeightedMap(weights + recency)
    Playlist-->>Server: Return game config
    Server->>WS: Broadcast new game config
    Server->>Server: Clear votes and broadcast next `map_vote_request`
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🗳️ A modal opens, a choice is made,
Votes saved local, or sent by trade.
Server counts and weights the maps,
Playlist shifts with weighted taps.
Lobbies hum with choices new — hooray for votes and changing play! 🎮

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Implement Map voting' directly and clearly summarizes the main objective of the changeset, which is to add map voting functionality to the public lobby.
Description check ✅ Passed The pull request description is related to the changeset, referencing issue #2999 and confirming that map voting functionality was implemented, UI updates were added, translations were included, and tests were created.
Linked Issues check ✅ Passed The changeset fully implements the requirements from issue #2999: map voting controls are added to the public lobby (MapVoteModal component), and vote data is collected and integrated into map selection via weighted voting in MapPlaylist.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing map voting functionality. New files, UI components, storage utilities, and server-side voting integration are all within scope of the map voting feature.

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

@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/client/MapVoteModal.ts`:
- Around line 80-89: The map tile div is not keyboard-focusable; replace the
clickable <div> with a semantic <button> (or if keeping the element, add
tabindex="0" role="button" and keydown handlers) so keyboard users can activate
it; ensure the button calls toggleMapSelection(mapValue) on click and on
Enter/Space keydown, keep the existing classes (cursor-pointer,
transition-transform, active:scale-95), and add an accessible state attribute
such as aria-pressed={this.selectedMaps.has(mapValue)} and a clear label using
the same translateText(`map.${mapKey?.toLowerCase()}`) so map-display remains
accessible.
🧹 Nitpick comments (4)
src/client/Main.ts (1)

226-226: This property is scaffolding for a future feature and can be safely removed if not needed yet.

mapVoteModal is assigned from the DOM during initialization but never read afterward. The property follows the codebase pattern of preparing modal components in advance. If the map vote feature is not yet implemented, you can remove this property and lines 279-284 to reduce unused code. If this feature is planned, consider adding a comment to clarify the intent:

// TODO: Implement map voting feature
private mapVoteModal: MapVoteModal | null = null;
src/client/MapVoteModal.ts (1)

14-18: Prefer composition over extending BaseModal.

If possible, wrap a modal element instead of inheriting from BaseModal. This keeps the component smaller and easier to reuse. Please confirm inheritance is required here.

src/server/Master.ts (1)

83-85: Make vote weight configurable (avoid debug constant).

A hardcoded 1e10 will fully swamp base weights and is marked debug. Make this configurable with a sane default so tuning is easy.

♻️ Proposed refactor
-const MAP_VOTE_WEIGHT = 10000000000; // for debug purposes, make votes very impactful 'w'
+const MAP_VOTE_WEIGHT =
+  Number(process.env.MAP_VOTE_WEIGHT ?? 1000) || 1000;
src/client/PublicLobby.ts (1)

82-96: Reduce duplicate auth + send logic.

Both methods repeat the same auth steps. A small helper keeps this cleaner.

♻️ Suggested refactor
+  private async getAuthJwt(): Promise<string | null> {
+    if (!this.isLoggedIn) return null;
+    const auth = await userAuth();
+    return auth ? auth.jwt : null;
+  }
+
   private async sendStoredVotes() {
-    if (!this.isLoggedIn) return;
-    const auth = await userAuth();
-    if (!auth) return;
-    this.lobbySocket.sendMapVote(auth.jwt, loadStoredMapVotes());
+    const jwt = await this.getAuthJwt();
+    if (!jwt) return;
+    this.lobbySocket.sendMapVote(jwt, loadStoredMapVotes());
   }
 
   private async handleMapVoteChange(
     event: CustomEvent<{ maps: GameMapType[] }>,
   ) {
-    if (!this.isLoggedIn) return;
-    const auth = await userAuth();
-    if (!auth) return;
-    this.lobbySocket.sendMapVote(auth.jwt, event.detail.maps);
+    const jwt = await this.getAuthJwt();
+    if (!jwt) return;
+    this.lobbySocket.sendMapVote(jwt, event.detail.maps);
   }

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@resources/lang/en.json`:
- Around line 334-343: The localization file is missing the key used for the
vote button aria/title (public_lobby.vote_button), so the raw key is exposed to
users and screen readers; add a new entry "public_lobby.vote_button" with a
concise accessible label (e.g., "Vote" or "Vote for maps") into the same JSON
block alongside keys like "vote_submit" and "vote_toast_submitted" so
PublicLobby can use public_lobby.vote_button for aria-label/title instead of
showing the raw key.

In `@src/client/PublicLobby.ts`:
- Around line 49-57: When handleUserMeResponse detects isLoggedIn flips to
false, send an empty/clear vote to the server (using the last known JWT) before
calling lobbySocket.clearMapVote so the server stops counting the previous vote;
locate the same logic in the other user/logout handler (the block referenced at
lines ~96-117) and apply the same change: call the LobbySocket method that sends
a vote with an empty selection and include the stored JWT, await or handle the
promise result (or fire-and-forget consistently with sendStoredVotes), then call
lobbySocket.clearMapVote and update isLoggedIn; ensure you reference
sendStoredVotes, handleUserMeResponse, and lobbySocket.clearMapVote when making
the edits.

In `@src/server/Master.ts`:
- Around line 161-169: The normalizeVotedMaps function currently iterates the
entire incoming maps array which a client can make arbitrarily large; change
normalizeVotedMaps to cap work by stopping once you've collected all possible
public maps (use publicLobbyMapSet.size) or by only iterating the first N
entries (N = publicLobbyMapSet.size) so you never process more than the number
of public maps; update the loop in normalizeVotedMaps to either slice maps to
maps.slice(0, publicLobbyMapSet.size) before iterating or break out once
unique.size === publicLobbyMapSet.size, referencing the existing
normalizeVotedMaps function, its maps parameter, and the publicLobbyMapSet to
locate the change.
- Around line 241-244: The current check rejects tokens when verifyClientToken
returns type "success" but claims is null (used for dev persistent IDs); update
the condition in Master.ts so that a verification of type "success" is accepted
even if claims is null for development/dev persistent IDs—either by gating the
relaxed check behind an environment flag (e.g. NODE_ENV==='development' or
config.allowDevPersistentIds) or by recognizing the dev persistent ID case from
the token; modify the if that uses verification.type and verification.claims
(and the log.warn message) to only return for non-success types or for success
without claims when not in the allowed dev mode.
♻️ Duplicate comments (1)
src/client/MapVoteModal.ts (1)

104-113: Make map tiles keyboard-focusable.
Clickable divs are not focusable; use a button (or add role/tabindex + key handlers) so keyboard users can vote.

Proposed fix
-              <div
-                `@click`=${() => this.toggleMapSelection(mapValue)}
-                class="cursor-pointer transition-transform duration-200 active:scale-95"
-              >
+              <button
+                type="button"
+                `@click`=${() => this.toggleMapSelection(mapValue)}
+                class="cursor-pointer transition-transform duration-200 active:scale-95"
+                aria-pressed=${this.selectedMaps.has(mapValue)}
+              >
                 <map-display
                   .mapKey=${mapKey}
                   .selected=${this.selectedMaps.has(mapValue)}
                   .translation=${translateText(`map.${mapKey?.toLowerCase()}`)}
                 ></map-display>
-              </div>
+              </button>
🧹 Nitpick comments (4)
src/server/Master.ts (2)

83-85: Avoid shipping the debug vote weight.
A fixed 10,000,000,000 weight will overpower base map weights and make a single vote dominate. Please move this to config/env or lower it to a realistic default before merge. If you want, I can help wire a config knob.


114-159: Reduce duplicate broadcast loops.
broadcastLobbies, broadcastMapVoteRequest, and broadcastMapVoteStats repeat the same open/closed cleanup. A small helper would reduce drift.

src/client/LobbySocket.ts (1)

87-97: Use a typed union for lobby WS messages.
This keeps parsing strict and makes the handler exhaustive and more idiomatic TypeScript.

Proposed refactor
 type LobbyUpdateHandler = (lobbies: GameInfo[]) => void;
 type VoteRequestHandler = () => void;
 type VoteStatsHandler = (activeVoteCount: number) => void;
+type LobbyMessage =
+  | { type: "lobbies_update"; data: { lobbies: GameInfo[] } }
+  | { type: "map_vote_request" }
+  | { type: "map_vote_stats"; data: { activeVoteCount: number } };
@@
   private handleMessage(event: MessageEvent) {
     try {
-      const message = JSON.parse(event.data as string);
-      if (message.type === "lobbies_update") {
-        this.onLobbiesUpdate(message.data?.lobbies ?? []);
-      } else if (message.type === "map_vote_request") {
-        this.onVoteRequest?.();
-      } else if (message.type === "map_vote_stats") {
-        const activeVoteCount = Number(message.data?.activeVoteCount ?? 0);
-        this.onVoteStats?.(activeVoteCount);
-      }
+      const message = JSON.parse(event.data as string) as LobbyMessage;
+      switch (message.type) {
+        case "lobbies_update":
+          this.onLobbiesUpdate(message.data?.lobbies ?? []);
+          break;
+        case "map_vote_request":
+          this.onVoteRequest?.();
+          break;
+        case "map_vote_stats": {
+          const activeVoteCount = Number(message.data?.activeVoteCount ?? 0);
+          this.onVoteStats?.(activeVoteCount);
+          break;
+        }
+      }
     } catch (error) {
src/client/MapVoteModal.ts (1)

14-16: Prefer composition over inheritance for this modal.
Consider composing BaseModal inside this component rather than extending it to keep behavior flat and easier to test.

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/Master.ts`:
- Around line 83-84: Replace the hardcoded debug constant MAP_VOTE_WEIGHT with a
configurable value sourced from configuration or an environment variable
(suggest VOTE_WEIGHT_MULTIPLIER) and update any code using MAP_VOTE_WEIGHT to
read from that config value instead; also update the MapVoteMessageSchema
definition to add an explicit array size limit by changing the maps validator to
include .max(5) (i.e., maps: z.array(z.nativeEnum(GameMapType)).max(5)) so the
maximum number of maps is enforced in the schema.
🧹 Nitpick comments (2)
src/client/PublicLobby.ts (2)

119-124: Add null check feedback for missing modal.

The querySelector may return null if the modal isn't mounted. Consider logging a warning to help debug integration issues.

Proposed fix
   private openMapVoteModal() {
     const modal = document.querySelector("map-vote-modal") as
       | (HTMLElement & { open: () => void })
       | null;
-    modal?.open();
+    if (modal) {
+      modal.open();
+    } else {
+      console.warn("map-vote-modal not found in DOM");
+    }
   }

96-117: Auth calls could be deduplicated.

sendStoredVotes, handleMapVoteChange, and handleMapVoteSubmit all follow the same pattern: check login → get auth → send vote. Consider extracting a helper:

Proposed helper
private async withAuth(
  action: (jwt: string) => void,
): Promise<void> {
  if (!this.isLoggedIn) return;
  const auth = await userAuth();
  if (!auth) return;
  action(auth.jwt);
}

Then use:

private async sendStoredVotes() {
  await this.withAuth((jwt) => {
    this.lobbySocket.sendMapVote(jwt, loadStoredMapVotes());
  });
}

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.

Implement Map voting

2 participants