From 90a31a77084cba955791c17190e16cd250e2a186 Mon Sep 17 00:00:00 2001 From: Askaholic Date: Sun, 28 Feb 2021 02:22:16 -0900 Subject: [PATCH] Refactor GameOutcome enum into several enums We can now distinguish between results in their various stages: 1. Results reported by an individual for a specific army 2. Accepted result for an army based on the individual reports 3. Accepted result for the whole team based on the accepted army results For instance, it doesn't make sense for a player's game to report the status of a particular result as "conflicting" since conflicts only arise when multiple players report different results for the same army. The Enum's now reflect that, and you can no longer report a "conflicting" or "unknown" result directly (as you could have before). As a consequence, any result string that is not present in the ArmyReportedOutcome will be ignored. Hence, the ACU kills reported via "score" results will no longer affect the army score calculation (previously these could turn a -10 score from a defeat into a positive number if the player got any acu kills). --- server/games/game.py | 14 ++- server/games/game_results.py | 123 +++++++++++++------- server/games/ladder_game.py | 7 +- server/stats/game_stats_service.py | 6 +- tests/unit_tests/test_game.py | 32 ++--- tests/unit_tests/test_game_rating.py | 1 - tests/unit_tests/test_game_resolution.py | 103 ++++++---------- tests/unit_tests/test_game_results.py | 44 +++++++ tests/unit_tests/test_game_stats_service.py | 4 +- 9 files changed, 195 insertions(+), 139 deletions(-) create mode 100644 tests/unit_tests/test_game_results.py diff --git a/server/games/game.py b/server/games/game.py index 2778acb31..f203c9a7d 100644 --- a/server/games/game.py +++ b/server/games/game.py @@ -17,6 +17,8 @@ matchmaker_queue_game ) from server.games.game_results import ( + ArmyOutcome, + ArmyReportedOutcome, GameOutcome, GameResolutionError, GameResultReport, @@ -286,9 +288,13 @@ async def add_result( return try: - outcome = GameOutcome(result_type.upper()) + outcome = ArmyReportedOutcome(result_type.upper()) except ValueError: - outcome = GameOutcome.UNKNOWN + self._logger.debug( + "Ignoring result reported by %s for army %s: %s %s", + reporter, army, result_type, score + ) + return result = GameResultReport(reporter, army, outcome, score) self._results.add(result) @@ -819,10 +825,10 @@ async def mark_invalid(self, new_validity_state: ValidityState): def get_army_score(self, army): return self._results.score(army) - def get_player_outcome(self, player): + def get_player_outcome(self, player: Player) -> ArmyOutcome: army = self.get_player_option(player.id, "Army") if army is None: - return GameOutcome.UNKNOWN + return ArmyOutcome.UNKNOWN return self._results.outcome(army) diff --git a/server/games/game_results.py b/server/games/game_results.py index 1b134ff97..33b6f082b 100644 --- a/server/games/game_results.py +++ b/server/games/game_results.py @@ -1,20 +1,51 @@ +import contextlib from collections import Counter, defaultdict from collections.abc import Mapping from enum import Enum -from typing import List, NamedTuple, Set +from typing import Dict, Iterator, List, NamedTuple, Set from server.decorators import with_logger -class GameOutcome(Enum): +class ArmyOutcome(Enum): + """ + The resolved outcome for an army. Each army has only one of these. + """ VICTORY = "VICTORY" DEFEAT = "DEFEAT" DRAW = "DRAW" - MUTUAL_DRAW = "MUTUAL_DRAW" UNKNOWN = "UNKNOWN" CONFLICTING = "CONFLICTING" +class ArmyReportedOutcome(Enum): + """ + A reported outcome for an army. Each army may have several of these. + """ + VICTORY = "VICTORY" + DEFEAT = "DEFEAT" + DRAW = "DRAW" + # This doesn't seem to be reported by the game anymore + MUTUAL_DRAW = "MUTUAL_DRAW" + + def to_resolved(self) -> ArmyOutcome: + """ + Convert this result to the resolved version. For when all reported + results agree. + """ + value = self.value + if value == "MUTUAL_DRAW": + value = "DRAW" + return ArmyOutcome(value) + + +class GameOutcome(Enum): + VICTORY = "VICTORY" + DEFEAT = "DEFEAT" + DRAW = "DRAW" + UNKNOWN = "UNKNOWN" + + class GameResultReport(NamedTuple): """ These are sent from each player's FA when they quit the game. 'Score' @@ -24,7 +55,7 @@ class GameResultReport(NamedTuple): reporter: int army: int - outcome: GameOutcome + outcome: ArmyReportedOutcome score: int @@ -36,25 +67,29 @@ class GameResultReports(Mapping): for each army, but don't modify these. """ - def __init__(self, game_id): + def __init__(self, game_id: int): Mapping.__init__(self) self._game_id = game_id # Just for logging - self._back = {} + self._back: Dict[int, List[GameResultReport]] = {} + # Outcome caching + self._outcomes: Dict[int, ArmyOutcome] = {} + self._dirty_armies: Set[int] = set() - def __getitem__(self, key: int): + def __getitem__(self, key: int) -> List[GameResultReport]: return self._back[key] - def __iter__(self): + def __iter__(self) -> Iterator[int]: return iter(self._back) - def __len__(self): + def __len__(self) -> int: return len(self._back) - def add(self, result: GameResultReport): + def add(self, result: GameResultReport) -> None: army_results = self._back.setdefault(result.army, []) army_results.append(result) + self._dirty_armies.add(result.army) - def is_mutually_agreed_draw(self, player_armies): + def is_mutually_agreed_draw(self, player_armies) -> bool: # Can't tell if we have no results if not self: return False @@ -62,33 +97,38 @@ def is_mutually_agreed_draw(self, player_armies): for army in player_armies: if army not in self: continue - if any(r.outcome is not GameOutcome.MUTUAL_DRAW for r in self[army]): + if any(r.outcome is not ArmyReportedOutcome.MUTUAL_DRAW for r in self[army]): return False return True - def outcome(self, army: int) -> GameOutcome: + def outcome(self, army: int) -> ArmyOutcome: """ - Determines what the game outcome was for a given army. + Determines what the outcome was for a given army. Returns the unique reported outcome if all players agree, or the majority outcome if only a few reports disagree. Otherwise returns CONFLICTING if there is too much disagreement or UNKNOWN if no reports were filed. """ + if army not in self._outcomes or army in self._dirty_armies: + self._outcomes[army] = self._compute_outcome(army) + self._dirty_armies.discard(army) + + return self._outcomes[army] + + def _compute_outcome(self, army: int) -> ArmyOutcome: if army not in self: - return GameOutcome.UNKNOWN + return ArmyOutcome.UNKNOWN voters = defaultdict(set) - for report in filter( - lambda r: r.outcome is not GameOutcome.UNKNOWN, self[army] - ): + for report in self[army]: voters[report.outcome].add(report.reporter) if len(voters) == 0: - return GameOutcome.UNKNOWN + return ArmyOutcome.UNKNOWN if len(voters) == 1: unique_outcome = voters.popitem()[0] - return unique_outcome + return unique_outcome.to_resolved() sorted_outcomes = sorted( voters.keys(), @@ -99,9 +139,9 @@ def outcome(self, army: int) -> GameOutcome: top_votes = len(voters[sorted_outcomes[0]]) runner_up_votes = len(voters[sorted_outcomes[1]]) if top_votes > 1 >= runner_up_votes or top_votes >= runner_up_votes + 3: - decision = sorted_outcomes[0] + decision = sorted_outcomes[0].to_resolved() else: - decision = GameOutcome.CONFLICTING + decision = ArmyOutcome.CONFLICTING self._logger.info( "Multiple outcomes for game %s army %s resolved to %s. Reports are: %s", @@ -109,7 +149,7 @@ def outcome(self, army: int) -> GameOutcome: ) return decision - def score(self, army: int): + def score(self, army: int) -> int: """ Pick and return most frequently reported score for an army. If multiple scores are most frequent, pick the largest one. Returns 0 if there are @@ -128,14 +168,14 @@ def score(self, army: int): score, _ = max(scores.items(), key=lambda kv: kv[::-1]) return score - def victory_only_score(self, army: int): + def victory_only_score(self, army: int) -> int: """ Calculate our own score depending *only* on victory. """ if army not in self: return 0 - if any(r.outcome is GameOutcome.VICTORY for r in self[army]): + if self.outcome(army) is ArmyOutcome.VICTORY: return 1 else: return 0 @@ -154,9 +194,10 @@ async def from_db(cls, database, game_id): async for row in rows: startspot, score = row[0], row[1] # FIXME: Assertion about startspot == army - outcome = GameOutcome[row[2]] - result = GameResultReport(0, startspot, outcome, score) - results.add(result) + with contextlib.suppress(ValueError): + outcome = ArmyReportedOutcome(row[2]) + result = GameResultReport(0, startspot, outcome, score) + results.add(result) return results @@ -164,9 +205,9 @@ class GameResolutionError(Exception): pass -def resolve_game(team_outcomes: List[Set[GameOutcome]]) -> List[GameOutcome]: +def resolve_game(team_outcomes: List[Set[ArmyOutcome]]) -> List[GameOutcome]: """ - Takes a list of length two containing sets of GameOutcomes + Takes a list of length two containing sets of ArmyOutcome for individual players on a team and converts a list of two GameOutcomes, either VICTORY and DEFEAT or DRAW and DRAW. @@ -179,8 +220,8 @@ def resolve_game(team_outcomes: List[Set[GameOutcome]]) -> List[GameOutcome]: "Will not resolve game with other than two parties." ) - victory0 = GameOutcome.VICTORY in team_outcomes[0] - victory1 = GameOutcome.VICTORY in team_outcomes[1] + victory0 = ArmyOutcome.VICTORY in team_outcomes[0] + victory1 = ArmyOutcome.VICTORY in team_outcomes[1] both_claim_victory = victory0 and victory1 someone_claims_victory = victory0 or victory1 if both_claim_victory: @@ -191,20 +232,14 @@ def resolve_game(team_outcomes: List[Set[GameOutcome]]) -> List[GameOutcome]: elif someone_claims_victory: return [ GameOutcome.VICTORY - if GameOutcome.VICTORY in outcomes + if ArmyOutcome.VICTORY in outcomes else GameOutcome.DEFEAT for outcomes in team_outcomes ] # Now know that no-one has GameOutcome.VICTORY - draw0 = ( - GameOutcome.DRAW in team_outcomes[0] - or GameOutcome.MUTUAL_DRAW in team_outcomes[0] - ) - draw1 = ( - GameOutcome.DRAW in team_outcomes[1] - or GameOutcome.MUTUAL_DRAW in team_outcomes[1] - ) + draw0 = ArmyOutcome.DRAW in team_outcomes[0] + draw1 = ArmyOutcome.DRAW in team_outcomes[1] both_claim_draw = draw0 and draw1 someone_claims_draw = draw0 or draw1 if both_claim_draw: @@ -212,15 +247,15 @@ def resolve_game(team_outcomes: List[Set[GameOutcome]]) -> List[GameOutcome]: elif someone_claims_draw: raise GameResolutionError( "Cannot resolve game with unilateral draw. " - f" Team outcomes: {team_outcomes}" + f"Team outcomes: {team_outcomes}" ) # Now know that the only results are DEFEAT or UNKNOWN/CONFLICTING # Unrank if there are any players with unknown result all_outcomes = team_outcomes[0] | team_outcomes[1] if ( - GameOutcome.UNKNOWN in all_outcomes - or GameOutcome.CONFLICTING in all_outcomes + ArmyOutcome.UNKNOWN in all_outcomes + or ArmyOutcome.CONFLICTING in all_outcomes ): raise GameResolutionError( "Cannot resolve game with ambiguous outcome. " diff --git a/server/games/ladder_game.py b/server/games/ladder_game.py index 0843ce1cf..2a2546647 100644 --- a/server/games/ladder_game.py +++ b/server/games/ladder_game.py @@ -6,7 +6,8 @@ from server.players import Player from server.rating import RatingType -from .game import Game, GameOutcome, GameType +from .game import Game, GameType +from .game_results import ArmyOutcome, GameOutcome from .typedefs import FeaturedModType logger = logging.getLogger(__name__) @@ -27,8 +28,8 @@ def __init__(self, id_, *args, **kwargs): new_kwargs.update(kwargs) super().__init__(id_, *args, **new_kwargs) - def is_winner(self, player: Player): - return self.get_player_outcome(player) is GameOutcome.VICTORY + def is_winner(self, player: Player) -> bool: + return self.get_player_outcome(player) is ArmyOutcome.VICTORY def get_army_score(self, army: int) -> int: """ diff --git a/server/stats/game_stats_service.py b/server/stats/game_stats_service.py index 7a15e0038..895b5517b 100644 --- a/server/stats/game_stats_service.py +++ b/server/stats/game_stats_service.py @@ -3,7 +3,7 @@ from server.config import config from server.core import Service from server.games import FeaturedModType, Game -from server.games.game_results import GameOutcome +from server.games.game_results import ArmyOutcome from server.players import Player from server.stats.achievement_service import * from server.stats.event_service import * @@ -48,7 +48,7 @@ async def process_game_stats(self, player: Player, game: Game, army_stats_list: return army_result = game.get_player_outcome(player) - if army_result is GameOutcome.UNKNOWN: + if army_result is ArmyOutcome.UNKNOWN: self._logger.warning("No army result available for player %s", player.login) return @@ -61,7 +61,7 @@ async def process_game_stats(self, player: Player, game: Game, army_stats_list: e_queue = [] self._logger.debug("Army result for %s => %s ", player, army_result) - survived = army_result is GameOutcome.VICTORY + survived = army_result is ArmyOutcome.VICTORY blueprint_stats = stats["blueprints"] unit_stats = stats["units"] scored_highest = highest_scorer == player.login diff --git a/tests/unit_tests/test_game.py b/tests/unit_tests/test_game.py index b702bdae3..a76828f1d 100644 --- a/tests/unit_tests/test_game.py +++ b/tests/unit_tests/test_game.py @@ -19,7 +19,7 @@ Victory, VisibilityState ) -from server.games.game_results import GameOutcome +from server.games.game_results import ArmyOutcome from server.rating import InclusiveRange, RatingType from tests.unit_tests.conftest import ( add_connected_player, @@ -135,7 +135,7 @@ async def test_add_result_unknown(game, game_add_players): players = game_add_players(game, 5, team=1) await game.launch() await game.add_result(0, 1, "something invalid", 5) - assert game.get_player_outcome(players[0]) is GameOutcome.UNKNOWN + assert game.get_player_outcome(players[0]) is ArmyOutcome.UNKNOWN async def test_ffa_not_rated(game, game_add_players): @@ -516,10 +516,10 @@ async def test_game_get_player_outcome_ignores_unknown_results( await game.add_result(0, 0, "defeat", 0) await game.add_result(0, 0, "score", 0) - assert game.get_player_outcome(players[0]) is GameOutcome.DEFEAT + assert game.get_player_outcome(players[0]) is ArmyOutcome.DEFEAT await game.add_result(0, 1, "score", 0) - assert game.get_player_outcome(players[1]) is GameOutcome.UNKNOWN + assert game.get_player_outcome(players[1]) is ArmyOutcome.UNKNOWN async def test_on_game_end_single_player_gives_unknown_result(game): @@ -659,9 +659,9 @@ async def test_persist_results_called_with_two_players(game, game_add_players): assert game.get_army_score(1) == 5 for player in game.players: if game.get_player_option(player.id, "Army") == 1: - assert game.get_player_outcome(player) is GameOutcome.VICTORY + assert game.get_player_outcome(player) is ArmyOutcome.VICTORY else: - assert game.get_player_outcome(player) is GameOutcome.UNKNOWN + assert game.get_player_outcome(player) is ArmyOutcome.UNKNOWN async def test_persist_results_called_for_unranked(game, game_add_players): @@ -677,17 +677,17 @@ async def test_persist_results_called_for_unranked(game, game_add_players): assert len(game.players) == 2 for player in game.players: if game.get_player_option(player.id, "Army") == 1: - assert game.get_player_outcome(player) is GameOutcome.VICTORY + assert game.get_player_outcome(player) is ArmyOutcome.VICTORY else: - assert game.get_player_outcome(player) is GameOutcome.UNKNOWN + assert game.get_player_outcome(player) is ArmyOutcome.UNKNOWN await game.load_results() assert game.get_army_score(1) == 5 for player in game.players: if game.get_player_option(player.id, "Army") == 1: - assert game.get_player_outcome(player) is GameOutcome.VICTORY + assert game.get_player_outcome(player) is ArmyOutcome.VICTORY else: - assert game.get_player_outcome(player) is GameOutcome.UNKNOWN + assert game.get_player_outcome(player) is ArmyOutcome.UNKNOWN async def test_get_army_score_conflicting_results_clear_winner( @@ -823,8 +823,8 @@ async def test_game_outcomes(game: Game, database, players): host_outcome = game.get_player_outcome(players.hosting) guest_outcome = game.get_player_outcome(players.joining) - assert host_outcome is GameOutcome.VICTORY - assert guest_outcome is GameOutcome.DEFEAT + assert host_outcome is ArmyOutcome.VICTORY + assert guest_outcome is ArmyOutcome.DEFEAT default_values_before_end = {(players.hosting.id, 0), (players.joining.id, 0)} assert await game_player_scores(database, game) == default_values_before_end @@ -845,8 +845,8 @@ async def test_game_outcomes_no_results(game: Game, database, players): host_outcome = game.get_player_outcome(players.hosting) guest_outcome = game.get_player_outcome(players.joining) - assert host_outcome is GameOutcome.UNKNOWN - assert guest_outcome is GameOutcome.UNKNOWN + assert host_outcome is ArmyOutcome.UNKNOWN + assert guest_outcome is ArmyOutcome.UNKNOWN await game.on_game_end() expected_scores = {(players.hosting.id, 0), (players.joining.id, 0)} @@ -868,8 +868,8 @@ async def test_game_outcomes_conflicting(game: Game, database, players): host_outcome = game.get_player_outcome(players.hosting) guest_outcome = game.get_player_outcome(players.joining) - assert host_outcome is GameOutcome.CONFLICTING - assert guest_outcome is GameOutcome.CONFLICTING + assert host_outcome is ArmyOutcome.CONFLICTING + assert guest_outcome is ArmyOutcome.CONFLICTING # No guarantees on scores for conflicting results. diff --git a/tests/unit_tests/test_game_rating.py b/tests/unit_tests/test_game_rating.py index 43a3327ac..b15270f35 100644 --- a/tests/unit_tests/test_game_rating.py +++ b/tests/unit_tests/test_game_rating.py @@ -157,7 +157,6 @@ async def test_rating_summary_missing_team_raises_game_error(game, players): async def test_resolve_game_fails_if_not_launched(custom_game, players): - rating_service = custom_game.game_service._rating_service custom_game.state = GameState.LOBBY add_connected_players(custom_game, [players.hosting, players.joining]) custom_game.set_player_option(players.hosting.id, "Team", 2) diff --git a/tests/unit_tests/test_game_resolution.py b/tests/unit_tests/test_game_resolution.py index a9cd1b6b0..beab45784 100644 --- a/tests/unit_tests/test_game_resolution.py +++ b/tests/unit_tests/test_game_resolution.py @@ -1,6 +1,7 @@ import pytest from server.games.game_results import ( + ArmyOutcome, GameOutcome, GameResolutionError, resolve_game @@ -8,9 +9,13 @@ def test_only_rate_with_two_parties(): - one_party = [{GameOutcome.VICTORY}] - two_parties = [{GameOutcome.VICTORY}, {GameOutcome.DEFEAT}] - three_parties = [{GameOutcome.VICTORY}, {GameOutcome.DEFEAT}, {GameOutcome.DEFEAT}] + one_party = [{ArmyOutcome.VICTORY}] + two_parties = [{ArmyOutcome.VICTORY}, {ArmyOutcome.DEFEAT}] + three_parties = [ + {ArmyOutcome.VICTORY}, + {ArmyOutcome.DEFEAT}, + {ArmyOutcome.DEFEAT} + ] with pytest.raises(GameResolutionError): resolve_game(one_party) @@ -22,7 +27,7 @@ def test_only_rate_with_two_parties(): def testresolve(): - team_outcomes = [{GameOutcome.VICTORY}, {GameOutcome.DEFEAT}] + team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.DEFEAT}] ranks = resolve_game(team_outcomes) @@ -33,97 +38,73 @@ def test_ranks_all_1v1_possibilities(): """ Document expectations for all outcomes of 1v1 games. Assumes that the order of teams doesn't matter. - With six possible outcomes there are 21 possibilities. + With five possible outcomes there are 15 possibilities. """ - team_outcomes = [{GameOutcome.VICTORY}, {GameOutcome.VICTORY}] + team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.VICTORY}] with pytest.raises(GameResolutionError): resolve_game(team_outcomes) - team_outcomes = [{GameOutcome.VICTORY}, {GameOutcome.DEFEAT}] + team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.DEFEAT}] ranks = resolve_game(team_outcomes) assert ranks == [GameOutcome.VICTORY, GameOutcome.DEFEAT] - team_outcomes = [{GameOutcome.VICTORY}, {GameOutcome.DRAW}] + team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.DRAW}] resolve_game(team_outcomes) assert ranks == [GameOutcome.VICTORY, GameOutcome.DEFEAT] - team_outcomes = [{GameOutcome.VICTORY}, {GameOutcome.MUTUAL_DRAW}] - ranks = resolve_game(team_outcomes) - assert ranks == [GameOutcome.VICTORY, GameOutcome.DEFEAT] - - team_outcomes = [{GameOutcome.VICTORY}, {GameOutcome.UNKNOWN}] + team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.UNKNOWN}] ranks = resolve_game(team_outcomes) assert ranks == [GameOutcome.VICTORY, GameOutcome.DEFEAT] - team_outcomes = [{GameOutcome.VICTORY}, {GameOutcome.CONFLICTING}] + team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.CONFLICTING}] ranks = resolve_game(team_outcomes) assert ranks == [GameOutcome.VICTORY, GameOutcome.DEFEAT] - team_outcomes = [{GameOutcome.DEFEAT}, {GameOutcome.DEFEAT}] + team_outcomes = [{ArmyOutcome.DEFEAT}, {ArmyOutcome.DEFEAT}] ranks = resolve_game(team_outcomes) assert ranks == [GameOutcome.DRAW, GameOutcome.DRAW] - team_outcomes = [{GameOutcome.DEFEAT}, {GameOutcome.DRAW}] - with pytest.raises(GameResolutionError): - resolve_game(team_outcomes) - - team_outcomes = [{GameOutcome.DEFEAT}, {GameOutcome.MUTUAL_DRAW}] - with pytest.raises(GameResolutionError): - resolve_game(team_outcomes) - - team_outcomes = [{GameOutcome.DEFEAT}, {GameOutcome.UNKNOWN}] - with pytest.raises(GameResolutionError): - resolve_game(team_outcomes) - - team_outcomes = [{GameOutcome.DEFEAT}, {GameOutcome.CONFLICTING}] + team_outcomes = [{ArmyOutcome.DEFEAT}, {ArmyOutcome.DRAW}] with pytest.raises(GameResolutionError): resolve_game(team_outcomes) - team_outcomes = [{GameOutcome.DRAW}, {GameOutcome.DRAW}] - ranks = resolve_game(team_outcomes) - assert ranks == [GameOutcome.DRAW, GameOutcome.DRAW] - - team_outcomes = [{GameOutcome.DRAW}, {GameOutcome.MUTUAL_DRAW}] - ranks = resolve_game(team_outcomes) - assert ranks == [GameOutcome.DRAW, GameOutcome.DRAW] - - team_outcomes = [{GameOutcome.DRAW}, {GameOutcome.UNKNOWN}] + team_outcomes = [{ArmyOutcome.DEFEAT}, {ArmyOutcome.UNKNOWN}] with pytest.raises(GameResolutionError): resolve_game(team_outcomes) - team_outcomes = [{GameOutcome.DRAW}, {GameOutcome.CONFLICTING}] + team_outcomes = [{ArmyOutcome.DEFEAT}, {ArmyOutcome.CONFLICTING}] with pytest.raises(GameResolutionError): resolve_game(team_outcomes) - team_outcomes = [{GameOutcome.MUTUAL_DRAW}, {GameOutcome.MUTUAL_DRAW}] + team_outcomes = [{ArmyOutcome.DRAW}, {ArmyOutcome.DRAW}] ranks = resolve_game(team_outcomes) assert ranks == [GameOutcome.DRAW, GameOutcome.DRAW] - team_outcomes = [{GameOutcome.MUTUAL_DRAW}, {GameOutcome.UNKNOWN}] + team_outcomes = [{ArmyOutcome.DRAW}, {ArmyOutcome.UNKNOWN}] with pytest.raises(GameResolutionError): resolve_game(team_outcomes) - team_outcomes = [{GameOutcome.MUTUAL_DRAW}, {GameOutcome.CONFLICTING}] + team_outcomes = [{ArmyOutcome.DRAW}, {ArmyOutcome.CONFLICTING}] with pytest.raises(GameResolutionError): resolve_game(team_outcomes) - team_outcomes = [{GameOutcome.UNKNOWN}, {GameOutcome.UNKNOWN}] + team_outcomes = [{ArmyOutcome.UNKNOWN}, {ArmyOutcome.UNKNOWN}] with pytest.raises(GameResolutionError): resolve_game(team_outcomes) - team_outcomes = [{GameOutcome.UNKNOWN}, {GameOutcome.CONFLICTING}] + team_outcomes = [{ArmyOutcome.UNKNOWN}, {ArmyOutcome.CONFLICTING}] with pytest.raises(GameResolutionError): resolve_game(team_outcomes) - team_outcomes = [{GameOutcome.CONFLICTING}, {GameOutcome.CONFLICTING}] + team_outcomes = [{ArmyOutcome.CONFLICTING}, {ArmyOutcome.CONFLICTING}] with pytest.raises(GameResolutionError): resolve_game(team_outcomes) def test_team_outcome_ignores_unknown(): team_outcomes = [ - {GameOutcome.VICTORY, GameOutcome.UNKNOWN}, - {GameOutcome.DEFEAT, GameOutcome.UNKNOWN}, + {ArmyOutcome.VICTORY, ArmyOutcome.UNKNOWN}, + {ArmyOutcome.DEFEAT, ArmyOutcome.UNKNOWN}, ] ranks = resolve_game(team_outcomes) @@ -132,18 +113,8 @@ def test_team_outcome_ignores_unknown(): def test_team_outcome_throws_if_unilateral_draw(): team_outcomes = [ - {GameOutcome.DRAW, GameOutcome.DEFEAT}, - {GameOutcome.DEFEAT, GameOutcome.UNKNOWN}, - ] - - with pytest.raises(GameResolutionError): - resolve_game(team_outcomes) - - -def test_team_outcome_throws_if_unilateral_mutual_draw(): - team_outcomes = [ - {GameOutcome.MUTUAL_DRAW, GameOutcome.DEFEAT}, - {GameOutcome.DEFEAT, GameOutcome.UNKNOWN}, + {ArmyOutcome.DRAW, ArmyOutcome.DEFEAT}, + {ArmyOutcome.DEFEAT, ArmyOutcome.UNKNOWN}, ] with pytest.raises(GameResolutionError): @@ -152,8 +123,8 @@ def test_team_outcome_throws_if_unilateral_mutual_draw(): def test_team_outcome_victory_has_priority_over_defeat(): team_outcomes = [ - {GameOutcome.VICTORY, GameOutcome.DEFEAT}, - {GameOutcome.DEFEAT, GameOutcome.DEFEAT}, + {ArmyOutcome.VICTORY, ArmyOutcome.DEFEAT}, + {ArmyOutcome.DEFEAT, ArmyOutcome.DEFEAT}, ] ranks = resolve_game(team_outcomes) @@ -163,8 +134,8 @@ def test_team_outcome_victory_has_priority_over_defeat(): def test_team_outcome_victory_has_priority_over_draw(): team_outcomes = [ - {GameOutcome.VICTORY, GameOutcome.DRAW}, - {GameOutcome.DRAW, GameOutcome.DEFEAT}, + {ArmyOutcome.VICTORY, ArmyOutcome.DRAW}, + {ArmyOutcome.DRAW, ArmyOutcome.DEFEAT}, ] ranks = resolve_game(team_outcomes) @@ -174,8 +145,8 @@ def test_team_outcome_victory_has_priority_over_draw(): def test_team_outcome_no_double_victory(): team_outcomes = [ - {GameOutcome.VICTORY, GameOutcome.VICTORY}, - {GameOutcome.VICTORY, GameOutcome.DEFEAT}, + {ArmyOutcome.VICTORY, ArmyOutcome.VICTORY}, + {ArmyOutcome.VICTORY, ArmyOutcome.DEFEAT}, ] with pytest.raises(GameResolutionError): @@ -184,8 +155,8 @@ def test_team_outcome_no_double_victory(): def test_team_outcome_unranked_if_ambiguous(): team_outcomes = [ - {GameOutcome.UNKNOWN, GameOutcome.DEFEAT}, - {GameOutcome.DEFEAT, GameOutcome.DEFEAT}, + {ArmyOutcome.UNKNOWN, ArmyOutcome.DEFEAT}, + {ArmyOutcome.DEFEAT, ArmyOutcome.DEFEAT}, ] with pytest.raises(GameResolutionError): diff --git a/tests/unit_tests/test_game_results.py b/tests/unit_tests/test_game_results.py new file mode 100644 index 000000000..c72d96c37 --- /dev/null +++ b/tests/unit_tests/test_game_results.py @@ -0,0 +1,44 @@ +import mock +import pytest + +from server.games.game_results import ( + ArmyOutcome, + ArmyReportedOutcome, + GameResultReport, + GameResultReports +) + + +@pytest.fixture +def game_results(): + return GameResultReports(game_id=42) + + +def test_reported_result_to_resolved(): + assert ArmyReportedOutcome.VICTORY.to_resolved() is ArmyOutcome.VICTORY + assert ArmyReportedOutcome.DEFEAT.to_resolved() is ArmyOutcome.DEFEAT + assert ArmyReportedOutcome.DRAW.to_resolved() is ArmyOutcome.DRAW + assert ArmyReportedOutcome.MUTUAL_DRAW.to_resolved() is ArmyOutcome.DRAW + + # Make sure our test hits every enum variant in case new ones are added + for variant in ArmyReportedOutcome: + assert variant.to_resolved() in ArmyOutcome + + +def test_outcome_cache(game_results): + game_results._compute_outcome = mock.Mock( + side_effect=game_results._compute_outcome + ) + game_results.add(GameResultReport(1, 1, ArmyReportedOutcome.DEFEAT, -10)) + + assert game_results.outcome(1) is ArmyOutcome.DEFEAT + game_results._compute_outcome.assert_called_once_with(1) + assert game_results.outcome(1) is ArmyOutcome.DEFEAT + game_results._compute_outcome.assert_called_once_with(1) + game_results._compute_outcome.reset_mock() + + game_results.add(GameResultReport(1, 1, ArmyReportedOutcome.VICTORY, -10)) + assert game_results.outcome(1) is ArmyOutcome.CONFLICTING + game_results._compute_outcome.assert_called_once_with(1) + assert game_results.outcome(1) is ArmyOutcome.CONFLICTING + game_results._compute_outcome.assert_called_once_with(1) diff --git a/tests/unit_tests/test_game_stats_service.py b/tests/unit_tests/test_game_stats_service.py index e69b1eb39..9d58ba74d 100644 --- a/tests/unit_tests/test_game_stats_service.py +++ b/tests/unit_tests/test_game_stats_service.py @@ -8,7 +8,7 @@ from server.factions import Faction from server.games import Game from server.games.game_results import ( - GameOutcome, + ArmyReportedOutcome, GameResultReport, GameResultReports ) @@ -49,7 +49,7 @@ def game(database, game_stats_service, player): game = Game(1, database, Mock(), game_stats_service) game._player_options[player.id] = {"Army": 1} game._results = GameResultReports(1) - game._results.add(GameResultReport(1, 1, GameOutcome.VICTORY, 0)) + game._results.add(GameResultReport(1, 1, ArmyReportedOutcome.VICTORY, 0)) return game