Skip to content

Comments

Feature/#1032 working veto system#1071

Merged
Brutus5000 merged 86 commits intodevelopfrom
feature/#1032-working-veto-system
Nov 1, 2025
Merged

Feature/#1032 working veto system#1071
Brutus5000 merged 86 commits intodevelopfrom
feature/#1032-working-veto-system

Conversation

@K-ETFreeman
Copy link
Contributor

@K-ETFreeman K-ETFreeman commented Oct 29, 2025

the work is done, it seems

Summary by CodeRabbit

  • New Features

    • Player veto system to let players exclude maps during matchmaking
    • Map versioning added so launched games reference specific map-pool map versions
  • Improvements

    • Map selection now uses version-aware weights and initial veto-driven weighting
    • Anti-repetition algorithm improved to reduce repeated maps and respect token limits
  • Configuration Updates

    • Tuned anti-repetition thresholds and repeat weighting for better map variety

@coderabbitai
Copy link

coderabbitai bot commented Oct 29, 2025

Walkthrough

Adds a matchmaker veto system: new VetoService and PlayerVetoes, database/schema changes for veto fields and map pool map-versioning, wiring of veto_service into LobbyConnection and LadderService, map selection and anti-repetition logic updates, and extensive test and fixture updates to exercise veto behavior.

Changes

Cohort / File(s) Summary
Veto system core
server/ladder_service/veto_system.py
New module implementing PlayerVetoes, VetoService, validation helpers, veto config extraction/update, initial weight generation for matches, and dynamic token calculation.
Service wiring & public API
server/__init__.py, server/lobbyconnection.py
Export VetoService in public API; inject veto_service into LobbyConnection constructor; add command_set_player_vetoes to LobbyConnection.
Ladder integration
server/ladder_service/ladder_service.py
LadderService accepts veto_service, threads veto-related fields (veto tokens, max per map, min maps after veto, map_pool_map_version_id) through map-pool and match flow, and calls veto_service.update_pools_veto_config after queue updates; passes map_pool_map_version_id into game options.
Matchmaker & map selection
server/matchmaker/map_pool.py, server/matchmaker/matchmaker_queue.py, server/matchmaker/__init__.py
New MatchmakerQueueMapPool NamedTuple; MapPool.apply_antirepetition_adjustment added; MapPool.choose_map accepts initial_weights; MatchmakerQueue refactored to use MatchmakerQueueMapPool objects and store by map_pool.id.
Types & map-version propagation
server/types.py
Added map_pool_map_version_id to Map, NeroxisGeneratedMap, and GameLaunchOptions; new MatchmakerQueueMapPoolVetoData type; updated Neroxis factory signature to accept and carry version id.
Database model & test data
server/db/models.py, tests/data/test-data.sql
matchmaker_queue_map_pool table gains id, veto_tokens_per_player, max_tokens_per_map, minimum_maps_after_veto; map_pool_map_version inserts now include leading id column; test SQL updated to match new schema and test rows added.
Config & player state
server/config.py, server/players.py
LADDER_ANTI_REPETITION_LIMIT changed to 1; added LADDER_ANTI_REPETITION_WEIGHT_BASE_THRESHOLDS and LADDER_ANTI_REPETITION_REPEAT_COUNTS_FACTOR; Player now initializes PlayerVetoes and logging scaffolding applied.
Tests — fixtures & wiring
tests/integration_tests/conftest.py, tests/unit_tests/conftest.py, tests/integration_tests/test_server_instance.py, tests/integration_tests/test_servercontext.py
Add veto_service fixture and lifecycle; construct LadderService and LobbyConnection with veto_service; propagate overrides in server instance tests and mocks.
Tests — integration
tests/integration_tests/test_game.py, tests/integration_tests/test_matchmaker.py, tests/integration_tests/test_matchmaker_vetoes.py
Add helpers end_game_as_draw, gen_vetoes; extend matchmaking helper to accept vetoes; adapt assertions to include map_pool_map_version_id; new comprehensive test_matchmaker_vetoes.py exercising veto behaviors.
Tests — unit
tests/unit_tests/test_ladder_service.py, tests/unit_tests/test_lobbyconnection.py, tests/unit_tests/test_map_pool.py, tests/unit_tests/test_matchmaker_queue.py, tests/unit_tests/test_veto_system.py
Update tests to use MatchmakerQueueMapPool, include map_pool_map_version_id in map fixtures, add parameterized tests for apply_antirepetition_adjustment and veto calculations, and unit tests for veto helpers.

Sequence Diagram(s)

sequenceDiagram
    participant Player
    participant LobbyConnection
    participant VetoService
    participant LadderService
    participant MapPool

    Player->>LobbyConnection: set_player_vetoes(veto_payload)
    LobbyConnection->>VetoService: set_player_vetoes(player, new_vetoes)
    VetoService->>VetoService: _adjust_vetoes(new_vetoes)
    VetoService-->>LobbyConnection: notify client (vetoes_info) if adjusted

    Player->>LobbyConnection: queue_for_matchmaking(queue)
    LobbyConnection->>LadderService: start_search(player, queue)

    alt when veto-aware selection needed
        LadderService->>VetoService: generate_initial_weights_for_match(players, matchmaker_queue_map_pool)
        VetoService->>VetoService: aggregate player veto tokens, calculate dynamic caps
        VetoService-->>LadderService: initial_weights per map_version_id
        LadderService->>MapPool: choose_map(played_map_ids, initial_weights)
        MapPool->>MapPool: apply_antirepetition_adjustment(initial_weights,...)
        MapPool-->>LadderService: selected Map (includes map_pool_map_version_id)
    end

    LadderService-->>Player: game_launch_options(map_pool_map_version_id=...)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas needing extra attention:

  • server/ladder_service/veto_system.py — complex new logic (dynamic token algorithm, aggregation, validation).
  • server/ladder_service/ladder_service.py — propagation of new fields and integration points where veto updates can cancel searches.
  • server/matchmaker/map_pool.py — apply_antirepetition_adjustment math and randomness interaction.
  • Database schema changes (server/db/models.py and tests/data/test-data.sql) — ensure migrations / test fixtures remain consistent.
  • Widespread test updates — verify mocks/fixtures correctly supply veto_service and new map_version_id fields.

Possibly related PRs

Poem

🐇 I nudged a veto into play,

tokens counted, maps sway away;
weights shuffle, repeats decline,
matchmakers hum, versions align.
Hooray — this rabbit hops, "fair match, hooray!"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Feature/#1032 working veto system" clearly describes the main change in the changeset. Based on the raw summary and PR objectives, the pull request implements a comprehensive veto system that allows players to ban maps within matchmaker mappools, including new modules (veto_system.py), database schema updates, integration with ladder service and client communication, and supporting test coverage. The title is concise, uses a standard feature reference format, and avoids vague terms or unnecessary noise. A teammate scanning the commit history would readily understand this PR adds a veto system feature.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#1032-working-veto-system

📜 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 3795aff and 1f0edc3.

📒 Files selected for processing (1)
  • server/ladder_service/veto_system.py (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: K-ETFreeman
Repo: FAForever/server PR: 1033
File: server/ladder_service/veto_system.py:113-124
Timestamp: 2025-10-26T15:11:39.923Z
Learning: In server/ladder_service/veto_system.py, the `+ [-1]` appended to map_pool_map_version_ids in the extract_pools_veto_config method was dead code from an old design idea to allow players to ban "last played map" as an anti-repetition feature. This feature was never implemented and the code was removed.
Learnt from: K-ETFreeman
Repo: FAForever/server PR: 1033
File: server/ladder_service/veto_system.py:257-280
Timestamp: 2025-10-26T14:43:05.147Z
Learning: In server/ladder_service/veto_system.py, the field max_tokens_per_map is guaranteed to be non-negative through external constraints (not enforced at the database schema level in the visible code).
Learnt from: K-ETFreeman
Repo: FAForever/server PR: 1033
File: server/matchmaker/map_pool.py:80-109
Timestamp: 2025-10-26T14:43:30.226Z
Learning: In server/matchmaker/map_pool.py, the choose_map method does not need a guard against all-zero final_weights because the veto system validation logic (including minimum_maps_after_veto constraints) prevents pools from being configured where all maps can be fully banned, ensuring at least some maps always have non-zero weights.
📚 Learning: 2025-10-26T15:11:39.923Z
Learnt from: K-ETFreeman
Repo: FAForever/server PR: 1033
File: server/ladder_service/veto_system.py:113-124
Timestamp: 2025-10-26T15:11:39.923Z
Learning: In server/ladder_service/veto_system.py, the `+ [-1]` appended to map_pool_map_version_ids in the extract_pools_veto_config method was dead code from an old design idea to allow players to ban "last played map" as an anti-repetition feature. This feature was never implemented and the code was removed.

Applied to files:

  • server/ladder_service/veto_system.py
📚 Learning: 2025-10-26T14:43:05.147Z
Learnt from: K-ETFreeman
Repo: FAForever/server PR: 1033
File: server/ladder_service/veto_system.py:257-280
Timestamp: 2025-10-26T14:43:05.147Z
Learning: In server/ladder_service/veto_system.py, the field max_tokens_per_map is guaranteed to be non-negative through external constraints (not enforced at the database schema level in the visible code).

Applied to files:

  • server/ladder_service/veto_system.py
📚 Learning: 2025-10-26T14:43:30.226Z
Learnt from: K-ETFreeman
Repo: FAForever/server PR: 1033
File: server/matchmaker/map_pool.py:80-109
Timestamp: 2025-10-26T14:43:30.226Z
Learning: In server/matchmaker/map_pool.py, the choose_map method does not need a guard against all-zero final_weights because the veto system validation logic (including minimum_maps_after_veto constraints) prevents pools from being configured where all maps can be fully banned, ensuring at least some maps always have non-zero weights.

Applied to files:

  • server/ladder_service/veto_system.py
📚 Learning: 2025-10-29T18:56:18.770Z
Learnt from: K-ETFreeman
Repo: FAForever/server PR: 1071
File: server/types.py:35-40
Timestamp: 2025-10-29T18:56:18.770Z
Learning: In server/types.py and server/matchmaker/map_pool.py, max_tokens_per_map is intentionally typed as float (not int) because it can be dynamically calculated by VetoService.calculate_dynamic_tokens_per_map(), which returns fractional values (e.g., 2.4). While the database schema stores it as Integer for static configurations, the runtime type is float to support dynamic calculation when max_tokens_per_map == 0 in the configuration.

Applied to files:

  • server/ladder_service/veto_system.py
🧬 Code graph analysis (1)
server/ladder_service/veto_system.py (8)
server/core/service.py (1)
  • Service (13-54)
server/decorators.py (1)
  • with_logger (15-29)
server/exceptions.py (1)
  • ClientError (10-21)
server/matchmaker/matchmaker_queue.py (2)
  • MatchmakerQueue (46-304)
  • to_dict (283-301)
server/matchmaker/map_pool.py (1)
  • MatchmakerQueueMapPool (114-121)
server/player_service.py (2)
  • PlayerService (41-252)
  • all_players (72-73)
server/players.py (4)
  • Player (35-185)
  • to_dict (145-175)
  • write_message (133-143)
  • send_message (121-131)
server/types.py (3)
  • MatchmakerQueueMapPoolVetoData (35-40)
  • id (45-45)
  • map_pool_map_version_id (47-47)
🪛 Ruff (0.14.2)
server/ladder_service/veto_system.py

135-135: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: unit-test
🔇 Additional comments (10)
server/ladder_service/veto_system.py (10)

1-16: LGTM!

Imports and type aliases are well-organized. The type aliases improve code readability for complex nested dictionary structures.


19-37: LGTM!

The PlayerVetoes class provides a clean interface for storing and retrieving veto data with appropriate serialization.


40-47: LGTM!

Service initialization follows established patterns with proper dependency injection.


71-90: Address the TODO comment based on PR discussion.

According to the PR objectives, you've argued that server-side veto adjustment is necessary to prevent timing mismatches between client and server mappool updates. If this is the consensus, please remove the TODO comment to avoid confusion for future maintainers.


92-126: LGTM!

The configuration extraction properly validates veto settings and falls back to safe defaults when invalid, with appropriate error logging.


128-147: LGTM!

The method properly validates input and adjusts vetoes according to pool constraints, with appropriate client notifications when adjustments occur.


148-168: Address the TODO comment based on PR discussion.

This TODO echoes the concern from update_pools_veto_config. Based on your explanation in the PR objectives about the necessity of server-side adjustment due to timing mismatches, please either remove this TODO or add a comment explaining why client-side-only validation is insufficient.


170-209: LGTM!

The weight generation logic correctly aggregates player vetoes and handles the dynamic token calculation fallback. The defensive error handling for impossible configurations is appropriate.


211-254: LGTM!

The dynamic token calculation algorithm is well-documented and mathematically sound. The grouping approach efficiently finds the minimum token cap that satisfies the map availability constraint.


257-335: LGTM!

The helper functions implement the veto adjustment logic correctly. The assertions at lines 304 and 328 serve as internal invariant checks, which is appropriate since these are private functions called with controlled inputs.


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

Copy link

@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 (8)
tests/unit_tests/test_veto_system.py (1)

160-227: Consider simplifying inline test case comments.

The inline comments for each parameterized test case are very detailed, which aids understanding but makes the parameter list quite verbose. Since the test logic itself validates correctness and the docstring of calculate_dynamic_tokens_per_map explains the algorithm, you might consider either:

  • Moving detailed explanations to a module-level docstring
  • Using shorter inline comments focusing on the distinguishing feature of each case

However, the current approach is acceptable if it serves as living documentation for the algorithm's expected behavior.

server/lobbyconnection.py (1)

1392-1406: Minor inconsistency in message field access.

The code uses .get() for matchmaker_queue_map_pool_id but direct dictionary access ([]) for map_pool_map_version_id and veto_tokens_applied. Consider using consistent access patterns—either all .get() with defaults or all [] access—to make the expected message structure more explicit and handle missing fields uniformly.

Apply this diff for consistency:

     async def command_set_player_vetoes(self, message):
         assert self.player is not None
 
         vetoes = {}
         for v in message["vetoes"]:
-            matchmaker_queue_map_pool = v.get("matchmaker_queue_map_pool_id")
+            matchmaker_queue_map_pool = v["matchmaker_queue_map_pool_id"]
             map_pool_map_version_id = v["map_pool_map_version_id"]
             veto_tokens_applied = v["veto_tokens_applied"]
 
             if matchmaker_queue_map_pool not in vetoes:
                 vetoes[matchmaker_queue_map_pool] = {}
 
             vetoes[matchmaker_queue_map_pool][map_pool_map_version_id] = veto_tokens_applied
 
         await self.veto_service.set_player_vetoes(self.player, vetoes)
server/matchmaker/map_pool.py (1)

88-108: Consider reducing debug logging verbosity.

The method contains 5 debug log statements (lines 88, 94, 104, 105, 107). While debug logs are typically disabled in production, this level of verbosity could impact debuggability by creating noise. Consider consolidating these into 1-2 more structured debug statements, or using a dedicated tracing level if available.

server/ladder_service/ladder_service.py (1)

572-591: Map selection with veto integration looks correct.

The code properly:

  1. Retrieves the appropriate map pool for the players' rating
  2. Generates veto-aware initial weights using veto_service.generate_initial_weights_for_match
  3. Passes these weights to map_pool.choose_map for antirepetition-adjusted selection

Minor points for consideration:

  • Lines 576-590 contain 3 debug statements that could be consolidated
  • Static analysis suggests the RuntimeError at line 574 could use a shorter message or be abstracted to an inner function (TRY301, TRY003)
server/ladder_service/veto_system.py (4)

19-37: Consider adding a setter method for better encapsulation.

The _vetoes attribute is accessed directly throughout the codebase (lines 70, 72, 79, 139-140), which bypasses encapsulation. Consider adding a setter method or making the attribute public if direct access is intended.

For example, add a setter method:

 class PlayerVetoes:
     def __init__(self):
         self._vetoes: dict[BracketID, VetosMap] = {}
 
     def get_vetoes_for_bracket(self, bracket_id: BracketID) -> VetosMap:
         return self._vetoes.get(bracket_id, {})
+    
+    def set_vetoes(self, vetoes: dict[BracketID, VetosMap]) -> None:
+        self._vetoes = vetoes
+    
+    def get_all_vetoes(self) -> dict[BracketID, VetosMap]:
+        return self._vetoes
 
     def to_dict(self) -> dict:

Then update callers to use these methods instead of direct attribute access.


73-78: Consider extracting complex boolean logic for clarity.

This nested boolean expression is hard to read and reason about. Extracting it into a helper method would improve maintainability.

Consider refactoring to:

+                def _were_veto_tokens_reduced(
+                    original_vetoes: dict[BracketID, VetosMap],
+                    adjusted_vetoes: dict[BracketID, VetosMap],
+                    pool_maps_by_bracket: dict[BracketID, set[int]]
+                ) -> bool:
+                    """Check if any map's veto tokens were reduced during adjustment."""
+                    for bracket, bracket_vetoes in original_vetoes.items():
+                        valid_maps = pool_maps_by_bracket.get(bracket, set())
+                        for map_id, original_tokens in bracket_vetoes.items():
+                            if map_id in valid_maps:
+                                adjusted_tokens = adjusted_vetoes.get(bracket, {}).get(map_id, 0)
+                                if original_tokens > adjusted_tokens:
+                                    return True
+                    return False
+
                 if adjusted_vetoes != player.vetoes._vetoes:
-                    tokens_amount_for_some_map_was_reduced = any(
-                        map_id in pool_maps_by_bracket.get(bracket, set())
-                        and original_tokens > adjusted_vetoes.get(bracket, {}).get(map_id, 0)
-                        for bracket, bracket_vetoes in player.vetoes._vetoes.items()
-                        for map_id, original_tokens in bracket_vetoes.items()
-                    )
+                    tokens_amount_for_some_map_was_reduced = _were_veto_tokens_reduced(
+                        player.vetoes._vetoes,
+                        adjusted_vetoes,
+                        pool_maps_by_bracket
+                    )

158-158: Brittle tuple unpacking risks silent breakage.

Unpacking MatchmakerQueueMapPoolVetoData by position is fragile—if field order changes in the NamedTuple, this code breaks silently.

Use named attribute access instead:

-        for bracket_id, map_ids, total_tokens, max_per_map, _ in self.pools_veto_data:
+        for pool_data in self.pools_veto_data:
+            bracket_id = pool_data.matchmaker_queue_map_pool_id
+            map_ids = pool_data.map_pool_map_version_ids
+            total_tokens = pool_data.veto_tokens_per_player
+            max_per_map = pool_data.max_tokens_per_map
+            
             bracket_vetoes = _adjust_vetoes_for_bracket(
                 new_vetoes.get(bracket_id, {}),
                 map_ids,
                 total_tokens,
                 max_per_map,
             )

175-181: Brittle tuple unpacking risks silent breakage.

Similar to line 158, unpacking MatchmakerQueueMapPool by position is fragile. The use of *_ to capture middle fields makes this especially prone to errors if the NamedTuple structure changes.

Use named attribute access:

-        (
-            pool_id,
-            pool,
-            *_,
-            max_tokens_per_map,
-            minimum_maps_after_veto
-        ) = matchmaker_queue_map_pool
+        pool_id = matchmaker_queue_map_pool.id
+        pool = matchmaker_queue_map_pool.map_pool
+        max_tokens_per_map = matchmaker_queue_map_pool.max_tokens_per_map
+        minimum_maps_after_veto = matchmaker_queue_map_pool.minimum_maps_after_veto
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6cca9f and 3795aff.

📒 Files selected for processing (24)
  • server/__init__.py (3 hunks)
  • server/config.py (1 hunks)
  • server/db/models.py (1 hunks)
  • server/ladder_service/ladder_service.py (11 hunks)
  • server/ladder_service/veto_system.py (1 hunks)
  • server/lobbyconnection.py (4 hunks)
  • server/matchmaker/__init__.py (1 hunks)
  • server/matchmaker/map_pool.py (2 hunks)
  • server/matchmaker/matchmaker_queue.py (4 hunks)
  • server/players.py (3 hunks)
  • server/types.py (5 hunks)
  • tests/data/test-data.sql (3 hunks)
  • tests/integration_tests/conftest.py (5 hunks)
  • tests/integration_tests/test_game.py (1 hunks)
  • tests/integration_tests/test_matchmaker.py (2 hunks)
  • tests/integration_tests/test_matchmaker_vetoes.py (1 hunks)
  • tests/integration_tests/test_server_instance.py (2 hunks)
  • tests/integration_tests/test_servercontext.py (1 hunks)
  • tests/unit_tests/conftest.py (4 hunks)
  • tests/unit_tests/test_ladder_service.py (9 hunks)
  • tests/unit_tests/test_lobbyconnection.py (2 hunks)
  • tests/unit_tests/test_map_pool.py (7 hunks)
  • tests/unit_tests/test_matchmaker_queue.py (6 hunks)
  • tests/unit_tests/test_veto_system.py (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: K-ETFreeman
PR: FAForever/server#1033
File: server/ladder_service/veto_system.py:113-124
Timestamp: 2025-10-26T15:11:39.923Z
Learning: In server/ladder_service/veto_system.py, the `+ [-1]` appended to map_pool_map_version_ids in the extract_pools_veto_config method was dead code from an old design idea to allow players to ban "last played map" as an anti-repetition feature. This feature was never implemented and the code was removed.
Learnt from: K-ETFreeman
PR: FAForever/server#1033
File: server/matchmaker/map_pool.py:80-109
Timestamp: 2025-10-26T14:43:30.226Z
Learning: In server/matchmaker/map_pool.py, the choose_map method does not need a guard against all-zero final_weights because the veto system validation logic (including minimum_maps_after_veto constraints) prevents pools from being configured where all maps can be fully banned, ensuring at least some maps always have non-zero weights.
Learnt from: K-ETFreeman
PR: FAForever/server#1033
File: server/ladder_service/veto_system.py:257-280
Timestamp: 2025-10-26T14:43:05.147Z
Learning: In server/ladder_service/veto_system.py, the field max_tokens_per_map is guaranteed to be non-negative through external constraints (not enforced at the database schema level in the visible code).
📚 Learning: 2025-10-26T15:11:39.923Z
Learnt from: K-ETFreeman
PR: FAForever/server#1033
File: server/ladder_service/veto_system.py:113-124
Timestamp: 2025-10-26T15:11:39.923Z
Learning: In server/ladder_service/veto_system.py, the `+ [-1]` appended to map_pool_map_version_ids in the extract_pools_veto_config method was dead code from an old design idea to allow players to ban "last played map" as an anti-repetition feature. This feature was never implemented and the code was removed.

Applied to files:

  • tests/unit_tests/test_veto_system.py
  • tests/integration_tests/test_matchmaker_vetoes.py
  • server/players.py
  • server/db/models.py
  • server/__init__.py
  • tests/unit_tests/test_ladder_service.py
  • tests/unit_tests/test_lobbyconnection.py
  • tests/data/test-data.sql
  • tests/integration_tests/test_matchmaker.py
  • tests/unit_tests/conftest.py
  • server/lobbyconnection.py
  • tests/unit_tests/test_matchmaker_queue.py
  • server/types.py
  • tests/integration_tests/test_game.py
  • server/matchmaker/map_pool.py
  • server/ladder_service/veto_system.py
  • server/matchmaker/matchmaker_queue.py
  • tests/integration_tests/conftest.py
  • server/ladder_service/ladder_service.py
  • tests/unit_tests/test_map_pool.py
📚 Learning: 2025-10-26T14:43:05.147Z
Learnt from: K-ETFreeman
PR: FAForever/server#1033
File: server/ladder_service/veto_system.py:257-280
Timestamp: 2025-10-26T14:43:05.147Z
Learning: In server/ladder_service/veto_system.py, the field max_tokens_per_map is guaranteed to be non-negative through external constraints (not enforced at the database schema level in the visible code).

Applied to files:

  • tests/unit_tests/test_veto_system.py
  • tests/integration_tests/test_matchmaker_vetoes.py
  • server/db/models.py
  • tests/unit_tests/test_lobbyconnection.py
  • server/ladder_service/veto_system.py
  • server/ladder_service/ladder_service.py
📚 Learning: 2025-10-26T14:43:30.226Z
Learnt from: K-ETFreeman
PR: FAForever/server#1033
File: server/matchmaker/map_pool.py:80-109
Timestamp: 2025-10-26T14:43:30.226Z
Learning: In server/matchmaker/map_pool.py, the choose_map method does not need a guard against all-zero final_weights because the veto system validation logic (including minimum_maps_after_veto constraints) prevents pools from being configured where all maps can be fully banned, ensuring at least some maps always have non-zero weights.

Applied to files:

  • tests/unit_tests/test_veto_system.py
  • tests/integration_tests/test_matchmaker_vetoes.py
  • server/db/models.py
  • tests/unit_tests/test_ladder_service.py
  • tests/data/test-data.sql
  • server/types.py
  • server/matchmaker/map_pool.py
  • server/ladder_service/veto_system.py
  • server/matchmaker/matchmaker_queue.py
  • server/ladder_service/ladder_service.py
  • tests/unit_tests/test_map_pool.py
📚 Learning: 2025-10-26T14:51:28.358Z
Learnt from: K-ETFreeman
PR: FAForever/server#1033
File: server/types.py:46-47
Timestamp: 2025-10-26T14:51:28.358Z
Learning: In server/types.py, the MapPoolMap Protocol intentionally requires map_pool_map_version_id to be non-Optional (int) because maps in a map pool context must have this ID. The concrete implementations (Map and NeroxisGeneratedMap) use Optional[int] to support usage outside of map pools, but when used as MapPoolMap they must be instantiated with a non-None value.

Applied to files:

  • tests/unit_tests/test_ladder_service.py
  • tests/integration_tests/test_matchmaker.py
  • server/types.py
  • server/matchmaker/map_pool.py
  • server/matchmaker/matchmaker_queue.py
  • tests/unit_tests/test_map_pool.py
🧬 Code graph analysis (19)
tests/unit_tests/test_veto_system.py (3)
server/ladder_service/veto_system.py (3)
  • _cap_tokens (323-335)
  • _is_valid_veto_config_for_queue (257-280)
  • calculate_dynamic_tokens_per_map (211-254)
server/matchmaker/map_pool.py (2)
  • MapPool (13-111)
  • MatchmakerQueueMapPool (114-121)
tests/conftest.py (1)
  • queue_factory (373-395)
tests/integration_tests/test_matchmaker_vetoes.py (8)
server/players.py (1)
  • PlayerState (24-31)
tests/utils/event_loop.py (1)
  • fast_forward (52-69)
tests/integration_tests/conftest.py (3)
  • connect_and_sign_in (559-569)
  • lobby_server (310-311)
  • get (410-414)
tests/integration_tests/test_game.py (4)
  • client_response (124-127)
  • end_game_as_draw (171-198)
  • gen_vetoes (201-209)
  • queue_player_for_matchmaking (212-226)
tests/conftest.py (2)
  • player_service (307-310)
  • database (198-200)
server/player_service.py (1)
  • get_player (189-190)
server/ladder_service/veto_system.py (1)
  • to_dict (26-37)
server/ladder_service/ladder_service.py (1)
  • update_data (100-154)
server/players.py (3)
server/decorators.py (1)
  • with_logger (15-29)
server/ladder_service/veto_system.py (1)
  • PlayerVetoes (19-37)
server/factions.py (1)
  • Faction (10-30)
server/__init__.py (3)
tests/integration_tests/conftest.py (2)
  • ladder_service (51-67)
  • veto_service (79-83)
tests/unit_tests/conftest.py (2)
  • ladder_service (65-81)
  • veto_service (93-97)
server/ladder_service/veto_system.py (1)
  • VetoService (41-254)
tests/unit_tests/test_ladder_service.py (4)
server/matchmaker/map_pool.py (2)
  • MapPool (13-111)
  • MatchmakerQueueMapPool (114-121)
server/matchmaker/matchmaker_queue.py (2)
  • MatchmakerQueue (46-304)
  • add_map_pool (83-87)
server/ladder_service/ladder_service.py (1)
  • LadderService (71-795)
server/types.py (5)
  • Map (55-73)
  • map_pool_map_version_id (47-47)
  • NeroxisGeneratedMap (79-150)
  • of (93-117)
  • id (45-45)
tests/unit_tests/test_lobbyconnection.py (3)
tests/integration_tests/conftest.py (2)
  • ladder_service (51-67)
  • veto_service (79-83)
tests/unit_tests/conftest.py (2)
  • ladder_service (65-81)
  • veto_service (93-97)
server/ladder_service/veto_system.py (1)
  • VetoService (41-254)
tests/integration_tests/test_servercontext.py (2)
tests/integration_tests/conftest.py (1)
  • veto_service (79-83)
tests/unit_tests/conftest.py (1)
  • veto_service (93-97)
tests/integration_tests/test_server_instance.py (2)
tests/integration_tests/conftest.py (1)
  • veto_service (79-83)
tests/unit_tests/conftest.py (1)
  • veto_service (93-97)
tests/unit_tests/conftest.py (5)
tests/integration_tests/conftest.py (3)
  • ladder_service (51-67)
  • violation_service (71-75)
  • veto_service (79-83)
server/ladder_service/veto_system.py (1)
  • VetoService (41-254)
tests/conftest.py (3)
  • player_service (307-310)
  • database (198-200)
  • game_service (346-361)
server/player_service.py (2)
  • PlayerService (41-252)
  • initialize (52-56)
server/ladder_service/ladder_service.py (2)
  • LadderService (71-795)
  • initialize (96-98)
server/lobbyconnection.py (3)
tests/integration_tests/conftest.py (3)
  • ladder_service (51-67)
  • veto_service (79-83)
  • get (410-414)
tests/unit_tests/conftest.py (2)
  • ladder_service (65-81)
  • veto_service (93-97)
server/ladder_service/veto_system.py (2)
  • VetoService (41-254)
  • set_player_vetoes (128-146)
tests/unit_tests/test_matchmaker_queue.py (2)
server/matchmaker/map_pool.py (2)
  • MapPool (13-111)
  • MatchmakerQueueMapPool (114-121)
server/matchmaker/matchmaker_queue.py (1)
  • add_map_pool (83-87)
tests/integration_tests/test_game.py (3)
tests/integration_tests/conftest.py (4)
  • send_message (630-631)
  • lobby_server (310-311)
  • connect_and_sign_in (559-569)
  • read_until_command (532-545)
e2e_tests/websocket_protocol.py (1)
  • send_message (41-48)
e2e_tests/fafclient.py (2)
  • send_message (44-49)
  • read_until_command (61-67)
server/matchmaker/__init__.py (2)
server/matchmaker/map_pool.py (2)
  • MapPool (13-111)
  • MatchmakerQueueMapPool (114-121)
server/matchmaker/matchmaker_queue.py (1)
  • MatchmakerQueue (46-304)
server/matchmaker/map_pool.py (1)
server/types.py (7)
  • weight (50-50)
  • Map (55-73)
  • id (45-45)
  • map_pool_map_version_id (47-47)
  • get_map (52-52)
  • get_map (72-73)
  • get_map (129-150)
server/ladder_service/veto_system.py (7)
server/core/service.py (1)
  • Service (13-54)
server/decorators.py (1)
  • with_logger (15-29)
server/exceptions.py (1)
  • ClientError (10-21)
server/matchmaker/map_pool.py (1)
  • MatchmakerQueueMapPool (114-121)
server/player_service.py (1)
  • all_players (72-73)
server/players.py (3)
  • Player (35-185)
  • write_message (133-143)
  • send_message (121-131)
server/types.py (3)
  • MatchmakerQueueMapPoolVetoData (35-40)
  • id (45-45)
  • map_pool_map_version_id (47-47)
server/matchmaker/matchmaker_queue.py (1)
server/matchmaker/map_pool.py (2)
  • MapPool (13-111)
  • MatchmakerQueueMapPool (114-121)
tests/integration_tests/conftest.py (2)
server/ladder_service/veto_system.py (1)
  • VetoService (41-254)
server/ladder_service/ladder_service.py (2)
  • LadderService (71-795)
  • initialize (96-98)
server/ladder_service/ladder_service.py (8)
tests/integration_tests/conftest.py (3)
  • ladder_service (51-67)
  • violation_service (71-75)
  • veto_service (79-83)
server/ladder_service/veto_system.py (3)
  • VetoService (41-254)
  • update_pools_veto_config (49-90)
  • generate_initial_weights_for_match (170-209)
server/matchmaker/map_pool.py (3)
  • MapPool (13-111)
  • MatchmakerQueueMapPool (114-121)
  • choose_map (80-108)
server/matchmaker/matchmaker_queue.py (3)
  • MatchmakerQueue (46-304)
  • shutdown (279-281)
  • get_map_pool_for_rating (89-97)
tests/conftest.py (4)
  • players (298-303)
  • database (198-200)
  • game_service (346-361)
  • game (209-210)
server/players.py (1)
  • Player (35-185)
server/types.py (8)
  • GameLaunchOptions (23-32)
  • Map (55-73)
  • MapPoolMap (43-52)
  • NeroxisGeneratedMap (79-150)
  • id (45-45)
  • map_pool_map_version_id (47-47)
  • of (93-117)
  • weight (50-50)
server/player_service.py (1)
  • all_players (72-73)
tests/unit_tests/test_map_pool.py (2)
server/types.py (6)
  • Map (55-73)
  • map_pool_map_version_id (47-47)
  • weight (50-50)
  • NeroxisGeneratedMap (79-150)
  • of (93-117)
  • id (45-45)
server/matchmaker/map_pool.py (2)
  • choose_map (80-108)
  • apply_antirepetition_adjustment (29-78)
🪛 Ruff (0.14.2)
tests/integration_tests/test_game.py

173-173: Avoid specifying long messages outside the exception class

(TRY003)

server/matchmaker/map_pool.py

86-86: Avoid specifying long messages outside the exception class

(TRY003)


108-108: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)

server/ladder_service/veto_system.py

135-135: Avoid specifying long messages outside the exception class

(TRY003)

server/ladder_service/ladder_service.py

574-574: Abstract raise to an inner function

(TRY301)


574-574: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: unit-test
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (66)
tests/data/test-data.sql (2)

78-142: LGTM!

The new test users and their leaderboard ratings are properly configured to test different rating brackets (951, 952, 1151, 1152), which aligns well with the veto system's rating-based map pool selection.


304-324: LGTM!

The test data correctly reflects the new schema changes. The veto configurations are well-designed to test different scenarios:

  • Rating-based map pool brackets with varying veto rules
  • Different max_tokens_per_map values including 0 (likely unlimited)
server/types.py (3)

32-32: LGTM!

Adding map_pool_map_version_id to GameLaunchOptions is appropriate for tracking which map pool map version was selected for a game.


46-47: LGTM!

The protocol requires map_pool_map_version_id as non-Optional while Map uses Optional[int], which is correct based on learnings. When used in map pool contexts, instances must have a non-None value.

Based on learnings.

Also applies to: 61-61


85-85: LGTM!

The map_pool_map_version_id field is consistently added throughout NeroxisGeneratedMap and properly propagated through the generation flow.

Also applies to: 93-93, 116-116, 149-149, 157-157

tests/integration_tests/test_servercontext.py (1)

62-62: LGTM!

Adding veto_service mock to the test fixture follows the established pattern for other service dependencies.

server/matchmaker/__init__.py (1)

7-7: LGTM!

Exposing MatchmakerQueueMapPool in the public API is appropriate for the veto system integration.

Also applies to: 15-15

tests/integration_tests/test_server_instance.py (1)

36-36: LGTM!

The veto_service wiring follows the established pattern for service dependency injection in integration tests.

Also applies to: 53-54

tests/integration_tests/test_matchmaker.py (1)

48-64: LGTM!

The test correctly validates that map_pool_map_version_id is included in game launch messages and has an expected value from the test data map pool.

server/players.py (2)

5-5: LGTM!

The logging setup using the @with_logger decorator follows the established pattern in the codebase.

Also applies to: 9-9, 11-11, 34-34, 36-36


59-61: LGTM!

The lazy import of PlayerVetoes avoids potential circular dependency issues, and initializing vetoes for all players is a reasonable trade-off for simplicity.

server/db/models.py (1)

298-306: Verify that a corresponding Flyway migration exists in the external database repository.

This project uses Flyway for database migrations, but migration files are maintained in an external repository (not in FAForever/server). The schema change to matchmaker_queue_map_pool adds three non-nullable columns without defaults, which requires a corresponding Flyway migration (.sql file) to handle existing data.

Confirm that a V*.sql migration file has been created in the FAForever/db repository to accompany this schema change. The test data in this PR has been correctly updated with values for these new columns.

tests/unit_tests/test_veto_system.py (2)

12-121: LGTM!

The test comprehensively validates _is_valid_veto_config_for_queue across various configurations including edge cases such as minimum_maps_after_veto exceeding pool size, zero values, and different token-per-map settings.


123-158: LGTM!

The test thoroughly validates _cap_tokens behavior across all relevant scenarios, including edge cases where caps are zero or constraints are binding.

tests/integration_tests/conftest.py (3)

51-66: LGTM!

The ladder_service fixture is correctly updated to accept and wire veto_service into the LadderService constructor, matching the updated service signature.


78-83: LGTM!

The veto_service fixture follows standard async fixture patterns with proper initialization and shutdown lifecycle management.


151-186: LGTM!

The veto_service is correctly wired through the lobby_server_factory fixture, added to the service override dictionary to ensure it's available during lobby server initialization.

tests/unit_tests/test_lobbyconnection.py (2)

18-18: LGTM!

The import of VetoService is correctly added to support the mock creation in the test fixture.


83-109: LGTM!

The veto_service mock is correctly added to the lobbyconnection fixture using create_autospec, following the established pattern for other service dependencies in unit tests.

tests/unit_tests/conftest.py (4)

13-15: LGTM!

The imports for VetoService and PlayerService are correctly added to support the new fixture implementations.


64-81: LGTM!

The ladder_service fixture is correctly updated to accept and wire veto_service into the LadderService constructor, with all parameters explicitly listed for clarity.


92-97: LGTM!

The veto_service fixture follows standard async fixture patterns with proper dependency injection and lifecycle management.


20-61: No issues found—the PlayerService mock-to-real transition is necessary and correct.

The change is intentional and aligns with the codebase architecture:

  • VetoService requires a real PlayerService instance in its constructor (confirmed in server/ladder_service/veto_system.py)
  • The parent fixture player_service in tests/conftest.py already creates a real PlayerService(database) instance
  • ladder_and_game_service_context now creates a real PlayerService for consistency, enabling veto_service initialization
  • Tests do not mock or assert on player_service behavior—they rely on derived fixtures (ladder_service, veto_service)
  • Proper lifecycle management is maintained with initialize() and shutdown() calls

The transition is a necessary architectural fix to support the veto system integration, not a regression risk.

server/config.py (1)

140-142: LGTM! Ensure these values are coordinated with the implementation.

The configuration changes support the new veto and anti-repetition system:

  • LADDER_ANTI_REPETITION_LIMIT reduced from 2 to 1 (tighter restriction on map repetition)
  • New threshold list and repeat factor parameters for weight-based anti-repetition logic

Verify that these values align with the implementation in server/matchmaker/map_pool.py and related modules.

server/__init__.py (3)

129-129: LGTM!

The import of VetoService is correctly placed and follows the existing pattern for service imports.


161-161: LGTM!

Adding VetoService to __all__ appropriately exposes it as part of the public API, consistent with other services like LadderService, PlayerService, etc.


200-210: LGTM!

The dependency injection of veto_service into LobbyConnection follows the established pattern for other services and correctly wires the veto system into the connection factory.

server/lobbyconnection.py (1)

56-56: LGTM!

The integration of VetoService into LobbyConnection follows the established dependency injection pattern used for other services like game_service, ladder_service, etc.

Also applies to: 113-113, 123-123

tests/integration_tests/test_game.py (3)

171-199: LGTM!

The end_game_as_draw helper correctly simulates a draw outcome by transitioning all clients to launching state, awaiting launch confirmation, then broadcasting sequential draw results for each army followed by GameEnded.

Note: The static analysis hint about "long messages outside exception class" at line 173 can be safely ignored—the message is clear and appropriate for this test utility function.


201-209: LGTM!

The gen_vetoes helper provides a clean way to generate veto payloads for testing. The format matches the expected structure for the set_player_vetoes command.


212-226: LGTM!

The integration of veto support into queue_player_for_matchmaking is well-designed. Sending the set_player_vetoes message before starting the matchmaking search correctly simulates the veto flow.

tests/unit_tests/test_map_pool.py (2)

27-36: LGTM!

The test updates correctly reflect the introduction of map_pool_map_version_id across Map and NeroxisGeneratedMap structures. The changes are consistent and align with the broader map versioning system introduced in this PR.

Also applies to: 42-51, 65-65, 92-101, 112-124, 129-163, 169-175


185-294: LGTM!

The parametrized test provides comprehensive coverage of the antirepetition adjustment logic with well-chosen scenarios including:

  • Equal play distribution
  • Proportional weight redistribution
  • Threshold-based behavior
  • Repeat factor effects
  • Edge cases (empty history, maps not in pool)
  • Order independence

The use of pytest.approx for floating-point comparisons is appropriate.

tests/unit_tests/test_matchmaker_queue.py (2)

11-16: LGTM!

The import of MatchmakerQueueMapPool is correctly added and follows the existing import style.


294-301: LGTM!

The refactoring to use the MatchmakerQueueMapPool wrapper simplifies the API and encapsulates pool-to-rating bounds and veto configuration. The changes are consistent across all tests and the default veto values are appropriate for tests that don't exercise veto logic.

Also applies to: 310-317, 329-336, 348-355, 376-391

server/matchmaker/map_pool.py (3)

1-11: LGTM!

The new imports support the antirepetition adjustment logic and the MatchmakerQueueMapPool type.


29-78: Antirepetition algorithm looks solid.

The weight transfer logic correctly redistributes probability from played maps to unplayed or less-played maps using threshold-based targeting. The algorithm handles edge cases appropriately, and the use of notzero_weights ensures division-by-zero is avoided at line 68.


114-121: LGTM!

Clean public API type that encapsulates map pool configuration with veto-related metadata.

tests/integration_tests/test_matchmaker_vetoes.py (7)

13-33: Test logic is sound.

The veto assignment test correctly verifies that vetoes are adjusted according to pool constraints. The ping/pong workaround at lines 23-25 is an acceptable pattern for ensuring no vetoes_info message is sent when vetoes match expectations.


36-58: LGTM!

The test correctly verifies that vetoed maps are excluded from selection across 20 iterations.


60-81: LGTM!

The test verifies dynamic token calculation correctly selects map 8 when other maps are vetoed.


83-108: LGTM!

The test correctly verifies that partial vetoes allow probabilistic selection of non-fully-vetoed maps. The assertion at line 107 ensures both maps 10 and 11 are selected over 20 iterations.


110-136: LGTM!

The test correctly verifies veto behavior in team matchmaking (2v2), ensuring map 10 is selected when maps 9 and 11 are vetoed by player pairs.


138-161: LGTM!

The test correctly verifies that pool configuration changes (reducing veto tokens) trigger forced updates and cancel ongoing searches. The try/finally block ensures database state is restored.


163-186: LGTM!

The test correctly verifies that map pool changes (map removal) trigger silent veto updates without canceling searches. Database cleanup in the finally block is appropriate.

tests/unit_tests/test_ladder_service.py (4)

24-53: LGTM!

The test correctly includes veto_service in LadderService initialization, matching the updated constructor signature.


55-117: LGTM!

The test assertions correctly navigate the new MatchmakerQueueMapPool structure using queue.map_pools[pool_id][1] to access the MapPool, and verify map_pool_map_version_id values.


442-477: LGTM!

The test correctly uses MatchmakerQueueMapPool to wrap the map pool configuration, providing all necessary fields including map_pool_map_version_id.


854-1025: LGTM!

The map selection tests correctly use mocker to patch choose_map, and wrap map pools in MatchmakerQueueMapPool with appropriate fields. The Map instances don't include map_pool_map_version_id since they're mocked and never actually used, which is acceptable for these unit tests.

server/matchmaker/matchmaker_queue.py (3)

49-70: LGTM!

The constructor correctly accepts MatchmakerQueueMapPool instances and stores them by map_pool.id.


83-87: LGTM!

The add_map_pool method correctly accepts and stores MatchmakerQueueMapPool instances.


89-97: LGTM!

The unpacking pattern correctly extracts map_pool, min_rating, and max_rating from MatchmakerQueueMapPool while ignoring the id and veto-related fields.

server/ladder_service/ladder_service.py (5)

79-92: LGTM!

The constructor correctly accepts and stores veto_service, integrating the veto system into the ladder service.


100-155: LGTM!

The update_data method correctly integrates veto system updates. After updating map pools, it calls veto_service.update_pools_veto_config and cancels searches for players whose vetoes were force-adjusted due to configuration changes. This ensures consistency between player vetoes and pool configurations.


156-220: LGTM!

The fetch_map_pools method correctly retrieves map_pool_map_version_id from the database and propagates it through Map and NeroxisGeneratedMap construction.


222-280: LGTM!

The fetch_matchmaker_queues method correctly retrieves veto configuration fields (veto_tokens_per_player, max_tokens_per_map, minimum_maps_after_veto) from the database and includes them in the queue map pool tuples.


635-644: LGTM!

The make_game_options function correctly includes map_pool_map_version_id in GameLaunchOptions, enabling version-aware map loading during game launch.

server/ladder_service/veto_system.py (9)

1-16: LGTM: Clean imports and type aliases.

The imports are well-organized and the type aliases improve code readability throughout the module.


40-47: LGTM: Standard service initialization.

The service follows the standard pattern with proper dependency injection and logger setup.


69-69: TODO: Should force adjustment logic be reconsidered?

The TODO suggests exploring alternatives to force-adjusting player veto selections. This might impact user experience when pool configs change.

Is this TODO intended to be addressed in this PR or tracked separately? The current implementation removes players from matchmaking if their veto tokens are reduced, which seems reasonable, but the comment suggests there might be concerns about this approach.


92-126: LGTM: Robust config extraction with validation.

The method properly validates veto configurations and falls back to safe defaults when invalid setups are detected, with appropriate error logging.


128-146: LGTM: Proper validation and adjustment flow.

The method correctly validates incoming veto data, adjusts it to fit constraints, and notifies the player if adjustments were made. The inline error message at line 135 is acceptable despite the static analysis hint—the message is clear and this simple case doesn't warrant a custom exception class.


152-156: TODO: Consider validating instead of adjusting vetoes.

The TODO suggests that client-side validation might be preferable to server-side adjustment. This would provide better user feedback about invalid selections.

Should this design decision be addressed now or tracked separately? The current auto-adjustment approach might hide validation errors from users, making it harder for them to understand why their veto selections were changed.


183-209: LGTM: Correct weight calculation with fallback handling.

The veto accumulation and weight calculation logic is sound. The dynamic token calculation fallback (lines 192-201) and error handling for impossible setups provide good resilience.

Based on learnings: The validation logic ensures at least some maps always have non-zero weights, so the error case at line 195 should indeed be rare or impossible.


211-254: LGTM: Complex but well-documented algorithm.

The dynamic token calculation is mathematically sound. The algorithm correctly groups maps by veto token count and finds the minimum threshold T that satisfies the constraint. The docstring clearly explains the approach, and the special case handling (zero tokens → return 1) is appropriate.


257-335: LGTM: Comprehensive validation and adjustment helpers.

All helper functions implement correct validation and adjustment logic:

  • _is_valid_veto_config_for_queue properly checks configuration constraints
  • _is_valid_vetoes validates data structure and types
  • _adjust_vetoes_for_bracket correctly applies token limits
  • _cap_tokens properly constrains token values

Based on learnings: The assertions about non-negative values are guaranteed by external constraints.

@ssiccha
Copy link

ssiccha commented Oct 30, 2025

Hi @K-ETFreeman, I can have a go at reviewing your CR this weekend. I'm a bit busy and might forget about this. If this happens please do ping me on discord (@GuteAlteKanone) next week.

Since I'm new to this repository could you do the following things until the weekend?

  • write a high level summary (4 - 5 sentences) of what this PR attempts to do.
    • where in the test cases is the intended behavior tested?
  • review the sequence diagram that coderabbit created
    • what is accurate in the sequence diagram, what is irrelevant, is something missing?

@K-ETFreeman
Copy link
Contributor Author

K-ETFreeman commented Oct 31, 2025

Hi @K-ETFreeman, I can have a go at reviewing your CR this weekend. I'm a bit busy and might forget about this. If this happens please do ping me on discord (@GuteAlteKanone) next week.

Since I'm new to this repository could you do the following things until the weekend?

  • write a high level summary (4 - 5 sentences) of what this PR attempts to do.

    • where in the test cases is the intended behavior tested?
  • review the sequence diagram that coderabbit created

    • what is accurate in the sequence diagram, what is irrelevant, is something missing?

This PR adds veto system to the server.
What it does: players now can ban maps they dont like in a matchmaker mappool.
The system is flexible and provides features like partial vetoes or dynamic max_tokens_for_map for each particular match.

i attach the guide to the matchmaker team (its a bit outdated in anti-repetition part tho), so you can better to understand the context
veto-system-tutorial.html

tests are written in C:\FreemanProduction\faf\server\tests\integration_tests\test_matchmaker_vetoes.py
and C:\FreemanProduction\faf\server\tests\unit_tests\test_veto_system

diagram looks fine.
TL;DR; logic in files:

  1. lobbyconnection + players.py -> players set vetoes for themselves
  2. ladder_service
    -> server fetches the mappool veto params from servers in update_data, cancels search of the players if some of their veto tokens were removed due to how settings of the mappool was changed
    -> generates initial map weights in _start_game
  3. map_pool.py -> takes the initial map weights, applies antirepetition adjustment to it (so players dont play same map twice when it possible / makes sense) -> applies map inner weight (not related to the veto system, map could have inner weights even before this pr) -> chooses the map at random

all the inner logic is hidden in veto_system.py

@K-ETFreeman
Copy link
Contributor Author

K-ETFreeman commented Oct 31, 2025

Also about # TODO comments (by Askaholic):
He talks about can-we-not-adjust-player-vetoes on the server
I think the answer is NO: client can be modified easily so client checks isnt enough
we just have to have some sorta server validation isnt it

even if we try to remove immediate check (and only check when match found) that will lead to some confusion:

  • client fetches the map pool params each time maplist is open
  • server fetches the map pool params only each 10 minutes

so if we start to only checking vetoes on match start, this will lead to:
mm team changes mappool settings -> client sees new settings and bans some map -> client finds a match -> his vetoes are cancelled by server (cuz server still has old veto settings) and he plays a map he just banned (as he saw it)

so i think current approach is correct. if you agree with me, free to delete those TODO comments

return f"MapPool({self.id}, {self.name}, {list(self.maps.values())})"


class MatchmakerQueueMapPool(NamedTuple):
Copy link

@ssiccha ssiccha Nov 1, 2025

Choose a reason for hiding this comment

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

It's not obvious to me what this class is supposed to do. I think to understand that I'd have to read the full PR first.

It seems to connect a map pool with information that the veto system needs. Why do we need this class? Why is this not either a part of the MapPool or of the MatchmakerQueue itself?

edit: I just saw that matchmaker_queue_map_pool in db/models.py already existed before your PR.

If we think we need this class, then please improve:

  • The naming of the class. The current name concatenates the names of two existing classes. But how does this relate to MatchmakerQueue-s? That's not obvious to me. edit: if this stays the only finding of my review, let's not change the name. If I ask you to change other parts of the CR too and you need to test that anyways, then I'd say let's also rename this thing.
  • give it a docstring

Copy link

@ssiccha ssiccha Nov 1, 2025

Choose a reason for hiding this comment

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

I've pasted some parts of your PR into Claude sonnet and asked it to come up with more concrete suggestions. Here's the output. Please double-check whether this even makes sense and is not completely hallucinated. The LLMs are good at generating stuff that sounds convincing but might be subtly or even fundamentally wrong. Often they're subtly wrong and it's easy to miss that.

Suggested change
class MatchmakerQueueMapPool(NamedTuple):
class QueueMapPoolConfig(NamedTuple):
"""
Configuration for using a map pool in a matchmaker queue.
Defines rating-based restrictions and veto system parameters for how
a map pool should be used within a specific queue. A single MapPool
can be reused across multiple queues with different configurations.
This corresponds to a row in the `matchmaker_queue_map_pool` database table.
Attributes:
id: Database ID of this configuration (matchmaker_queue_map_pool.id)
map_pool: The map pool to use
min_rating: Minimum displayed rating to use this map pool (None = no limit)
max_rating: Maximum displayed rating to use this map pool (None = no limit)
veto_tokens_per_player: Number of maps each player can veto
max_tokens_per_map: Maximum veto tokens that can be applied to any single map
minimum_maps_after_veto: Minimum number of maps that must remain selectable after vetoes
"""

Copy link
Contributor Author

@K-ETFreeman K-ETFreeman Nov 1, 2025

Choose a reason for hiding this comment

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

yes as you saw later this is just representation of the DB entity
https://faforever.github.io/db/tables/matchmaker_queue_map_pool.html

map_pool too
https://faforever.github.io/db/tables/map_pool.html

the name is intentionally the same to link them, i dont understand why it should be changed
MapPool has the same name as its DB representation, so MatchmakerQueueMapPool should be MatchmakerQueueMapPool as well, not QueueMapPoolConfig

why needed? to not have code repetition
its used in multiple places, if you remove that -> you'll need to write tuple of 7 elements instead of single word of this class

Copy link

Choose a reason for hiding this comment

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

why needed? to not have code repetition
its used in multiple places, if you remove that -> you'll need to write tuple of 7 elements instead of single word of this class

I meant "what purpose does it serve"? :) I think I got it now though.

Column("map_pool_id", Integer, ForeignKey("map_pool.id"), nullable=False),
Column("min_rating", Integer),
Column("max_rating", Integer),
Column("id", Integer, primary_key=True),
Copy link

Choose a reason for hiding this comment

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

Regarding my earlier comment. I just saw that this was not introduced by your PR but instead already existed in the code base.

I pasted your version of this definition into claude sonnet and prompted to generate a comment for the review, based on my earlier comment. This is its output:

The table name matchmaker_queue_map_pool makes it look like a simple join table, but it's actually a first-class entity with its own attributes (rating ranges, veto configuration). This deserves a more descriptive name that reflects what it represents in the domain: a configured assignment of a map pool to a queue.
Consider renaming to something like queue_map_pool_config or map_pool_assignment to better communicate its purpose. At minimum, please add a comment explaining what this table represents.

Code Suggestion:

# Configures how a map pool is used within a matchmaker queue.
# Each row represents an assignment of a map pool to a queue with specific
# parameters for rating eligibility and the veto system. A queue can have
# multiple assignments (e.g., different map pools for different rating ranges),
# and a map pool can be assigned to multiple queues with different configurations.
matchmaker_queue_map_pool = Table(
    "matchmaker_queue_map_pool", metadata,
    Column("id",                       Integer, primary_key=True),
    Column("matchmaker_queue_id",      Integer, ForeignKey("matchmaker_queue.id"), nullable=False),
    Column("map_pool_id",              Integer, ForeignKey("map_pool.id"),         nullable=False),
    Column("min_rating",               Integer),  # Minimum rating to use this map pool assignment
    Column("max_rating",               Integer),  # Maximum rating to use this map pool assignment
    Column("veto_tokens_per_player",   Integer, nullable=False),  # Number of maps each player can veto
    Column("max_tokens_per_map",       Integer, nullable=False),  # Max veto tokens applicable to one map
    Column("minimum_maps_after_veto",  Float,   nullable=False),  # Minimum maps remaining after vetoes
)

Copy link

Choose a reason for hiding this comment

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

Since this already existed before your PR I'd say let's not rename this, but definitely do add the source comment. First check whether it makes sense though. The LLMs are good at generating stuff that sounds convincing but might be subtly or even fundamentally wrong.

Copy link
Contributor Author

@K-ETFreeman K-ETFreeman Nov 1, 2025

Choose a reason for hiding this comment

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

https://faforever.github.io/db/tables/matchmaker_queue_map_pool.html

this is just database entity
i am not sure why any comment is needed here because no other table has this sorta comments either

Copy link

Choose a reason for hiding this comment

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

Because the other database entities have descriptive names. This name is not helpful. Instead of renaming it, I'd suggest simply adding a source comment what this is for.

Comment on lines +119 to +121
veto_tokens_per_player: int = 0
max_tokens_per_map: float = 0
minimum_maps_after_veto: float = 1
Copy link

Choose a reason for hiding this comment

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

This is duplicated with

class MatchmakerQueueMapPoolVetoData(NamedTuple):
...
    veto_tokens_per_player: int
    max_tokens_per_map: float
    minimum_maps_after_veto: float

Can we get rid of one of them?

Copy link

Choose a reason for hiding this comment

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

First thought: Maybe remove this from MatchmakerQueueMapPoolVetoData and there only keep
matchmaker_queue_map_pool_id and map_pool_map_version_ids?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

First thought: Maybe remove this from MatchmakerQueueMapPoolVetoData and there only keep matchmaker_queue_map_pool_id and map_pool_map_version_ids?

no, this wont work
MatchmakerQueueMapPoolVetoData is a data structure that extracts only the veto-relevant information from MatchmakerQueueMapPool. It's used by the VetoService for efficient veto validation and adjustment.

you can look at line 57 in veto_system.py:

        pools_vetodata = self.extract_pools_veto_config(queues)

        if self.pools_veto_data != pools_vetodata:
          ... update stuff

pools_vetodata is list of MatchmakerQueueMapPoolVetoData
and when we do self.pools_veto_data != pools_vetodata we check, if anything veto-related was changed in the pool. If was changed -> we're doing some actions, if not -> we're not

These are fundamental to MatchmakerQueueMapPoolVetoData and cannot be removed. You cannot directly replace it with MatchmaerQueueMapPool either without overcomplicating everything:

is it possible to replace it with MatchmakerQueueMapPool by any chance? if yes, what fixes that will require to preserve the same behaviour?
⎿ ⧉ Selected 1 lines from server\ladder_service\veto_system.py in Visual Studio Code

● Looking at the code, I can see why MatchmakerQueueMapPoolVetoData exists as a separate structure. Let me analyze if replacement is feasible.

● Search(pattern: "self.pools_veto_data", path: "server\ladder_service\veto_system.py", output_mode: "content")
⎿ Found 5 lines (ctrl+o to expand)

● Short answer: It's technically possible but not recommended. The current design is intentional and solves several important problems.

Key Issues with Using MatchmakerQueueMapPool

  1. Equality Comparison Breaks (line 59)

if self.pools_veto_data != pools_vetodata:

Problem: MatchmakerQueueMapPool contains a MapPool object, which has:

  • A mutable dict: self.maps = {map_.id: map_ for map_ in maps}
  • This makes equality comparison unreliable

Impact: The change detection would fail because:

  • Two different MapPool instances are never equal (even with same data)
  • The condition would always be True, triggering unnecessary updates every time
  • All players would get forced veto adjustments on every config refresh
  1. Tuple Unpacking Requires Major Refactoring (line 158)

Current: unpacks 5 fields in specific order

for bracket_id, map_ids, total_tokens, max_per_map, _ in self.pools_veto_data:

Would become: unpacks 7 fields, different order, need to extract map IDs

for pool_config in self.pools_veto_data:
bracket_id = pool_config.id
map_ids = [m.map_pool_map_version_id for m in pool_config.map_pool.maps.values()]
total_tokens = pool_config.veto_tokens_per_player
max_per_map = pool_config.max_tokens_per_map

  1. Map ID Extraction Everywhere (line 64)

Current: direct access

pool_maps_by_bracket = {
pool_data.matchmaker_queue_map_pool_id: set(pool_data.map_pool_map_version_ids)
for pool_data in self.pools_veto_data
}

Would become: repeated extraction

pool_maps_by_bracket = {
pool_data.id: set(m.map_pool_map_version_id for m in pool_data.map_pool.maps.values())
for pool_data in self.pools_veto_data
}

  1. Memory and Complexity
  • Stores unnecessary fields (min_rating, max_rating)
  • Keeps full MapPool objects in memory (with all map data)
  • Creates tight coupling between veto logic and map pool structure

What Would Be Required

If you really wanted to do this, you'd need:

  1. Replace equality check with manual field comparison:
    def _veto_configs_equal(old_pools, new_pools):
    if len(old_pools) != len(new_pools):
    return False
    for old, new in zip(old_pools, new_pools):
    if (old.id != new.id or
    old.veto_tokens_per_player != new.veto_tokens_per_player or
    old.max_tokens_per_map != new.max_tokens_per_map or
    old.minimum_maps_after_veto != new.minimum_maps_after_veto or
    set(m.map_pool_map_version_id for m in old.map_pool.maps.values()) !=
    set(m.map_pool_map_version_id for m in new.map_pool.maps.values())):
    return False
    return True

if not _veto_configs_equal(self.pools_veto_data, pools_vetodata):

  1. Update all tuple unpacking
  2. Extract map IDs everywhere they're needed
  3. Update type hints and field access patterns

Why Current Design is Better

The MatchmakerQueueMapPoolVetoData structure is a value object pattern that:

  • ✅ Is immutable and comparable
  • ✅ Contains only veto-relevant data
  • ✅ Enables reliable change detection
  • ✅ Decouples veto logic from MapPool implementation
  • ✅ Is lightweight for caching

Verdict: The current design is correct. The data duplication is intentional and beneficial. Replacing it would add complexity, reduce
reliability, and violate separation of concerns.

Copy link
Contributor Author

@K-ETFreeman K-ETFreeman Nov 1, 2025

Choose a reason for hiding this comment

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

Basically, MatchmakerQueueMapPool exists because its representation of what we get from the DB (full info about the mappool + his matchmaker-and-veto settings), MatchmakerQueueMapPoolVetoData is distilled version of it which contains only veto-related entities which simplifies a bunch of stuff

Copy link

Choose a reason for hiding this comment

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

Interesting. Thanks for the explanation. :)

Comment on lines 13 to 16
BracketID = int
MapPoolMapVersionId = int
VetoTokensApplied = int
VetosMap = dict[MapPoolMapVersionId, VetoTokensApplied]
Copy link

Choose a reason for hiding this comment

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

nit: consider turning these into classes such that type checking can distinguish between them. Since e.g. the first three are int, I assume mypy can't distinguish them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You right that mypy can't distinguish them

But these are just for the readability purposes
Can be straight up deleted but reading dict[int, dict[int,int]] can be not very pleasant experience
Same pattern is already used in typedefs.py

turning these into classes - you mean NewType?
i dont see it used anywhere in the codebase, and looks like its an overkill here

more importantly: why its "VetosMap" when it should be "VetoesMap"? the real bug found

Copy link

Choose a reason for hiding this comment

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

I mean something like

class BracketID(int):
    pass

The change is trivial and the advantage is as I said that mypy can help you find errors early.

from server.players import Player
from server.types import MatchmakerQueueMapPoolVetoData

BracketID = int
Copy link

Choose a reason for hiding this comment

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

what is a bracket? docstring please

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You right, its not obvious
I guess i'll just replace it with MatchmakerQueueMapPoolId

Copy link
Contributor Author

Choose a reason for hiding this comment

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

or maybe not. bracket is simpler. ugh

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 guess i just explain the connection in the doc
its "Bracket" because MatchmakerQueueMapPoolId has range of ratings (and maplist), so like "Rating bracket" -> bracket

Copy link

Choose a reason for hiding this comment

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

Yeah, I think the whole matchmaker_queue_map_pool thing in the DB was badly named in the beginning. As you say, it should have been Bracket all along. That makes way more sense. But let's not go ahead and do all kinds of renaming stuff. That's not worth it. Since the documentation you wrote/updated explains this, we're fine.

@@ -0,0 +1,335 @@
import logging
Copy link

Choose a reason for hiding this comment

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

Please add a reference to https://github.com/user-attachments/files/23255986/veto-system-tutorial.html as a module docstring. Also from how I read that document, what we're implementing actually isn't a veto system. A veto is an "unilaterally stopping an action". We don't do that here. Naming it a "veto system" is confusing, I'd change that in the announcement. Ideally also in the code.

This is more of a preference system were players can influence which maps will be made less likely. But they don't have an actual veto vote. Which totally makes sense, given that our matchmaking queues don't have hundreds of players.

Copy link
Member

@Brutus5000 Brutus5000 left a comment

Choose a reason for hiding this comment

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

Even though I don't understand it in detail, this PR looks solid to me.

Please fix the 2 minor things, then I am willing to merge it.
In case further review findings of other reviewers, we can still fix them afterwards.

@K-ETFreeman
Copy link
Contributor Author

veto-system-developer-guide.html

i will put it here for future generations

@Brutus5000 Brutus5000 merged commit d2295fd into develop Nov 1, 2025
10 checks passed

self.LADDER_1V1_OUTCOME_OVERRIDE = True
self.LADDER_ANTI_REPETITION_LIMIT = 2
self.LADDER_ANTI_REPETITION_LIMIT = 1
Copy link

Choose a reason for hiding this comment

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

You said there were issues with this being 2. Add a unit test which: enforces this being 1 until the underlying issue is fixed. Add a short description to the unit test of what the issue was.

@ssiccha
Copy link

ssiccha commented Nov 2, 2025

veto-system-developer-guide.html

Do add a link to this to

server/ladder_service/veto_system.py. It's a lot less likely that folks will find it in the PR when they're debugging / looking at the veto system code.

@K-ETFreeman K-ETFreeman deleted the feature/#1032-working-veto-system branch November 2, 2025 14:41
@K-ETFreeman K-ETFreeman mentioned this pull request Nov 2, 2025
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.

4 participants