Skip to content

Commit e573963

Browse files
committed
Add first draft of recall waiving limit requirements for ranking custom games
1 parent 1e203cd commit e573963

File tree

6 files changed

+79
-65
lines changed

6 files changed

+79
-65
lines changed

server/games/custom_game.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from server.rating import RatingType
44

55
from .game import Game
6+
from .game_results import ArmyOutcome
67
from .typedefs import GameType, InitMode, ValidityState
78

89

@@ -18,9 +19,25 @@ def __init__(self, id, *args, **kwargs):
1819
new_kwargs.update(kwargs)
1920
super().__init__(id, *args, **new_kwargs)
2021

21-
async def _run_pre_rate_validity_checks(self):
22+
async def _run_pre_rate_validity_checks(self, team_army_outcomes: list[set[ArmyOutcome]]):
2223
assert self.launched_at is not None
2324

25+
# if there were connection issues, they are likely to show up early in
26+
# the match and manifest as players quitting out - in these cases, we
27+
# grant a grace period to custom games before we count them as ranked
2428
limit = len(self.players) * 60
25-
if not self.enforce_rating and time.time() - self.launched_at < limit:
29+
duration = time.time() - self.launched_at
30+
31+
# As we only get extremely limited data (the army results), our lens of
32+
# what can look like "quitting out" is rather large (i.e. the player is
33+
# "defeated").
34+
# In other words, only if a team unaminously recalled would we know
35+
# there weren't any connection issues.
36+
looks_like_quitting = {ArmyOutcome.DEFEAT, ArmyOutcome.UNKNOWN, ArmyOutcome.CONFLICTING}
37+
all_outcomes = {}
38+
for outcome in team_army_outcomes:
39+
all_outcomes |= outcome
40+
possible_conn_issues = len(all_outcomes & looks_like_quitting) > 0
41+
42+
if not self.enforce_rating and (possible_conn_issues and duration < limit):
2643
await self.mark_invalid(ValidityState.TOO_SHORT)

server/games/game.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ async def on_game_finish(self):
487487

488488
self.game_service.mark_dirty(self)
489489

490-
async def _run_pre_rate_validity_checks(self):
490+
async def _run_pre_rate_validity_checks(self, team_army_results: list[set[ArmyOutcome]]):
491491
pass
492492

493493
async def process_game_results(self):
@@ -504,8 +504,6 @@ async def resolve_game_results(self) -> EndedGameInfo:
504504
if self.state not in (GameState.LIVE, GameState.ENDED):
505505
raise GameError("Cannot rate game that has not been launched.")
506506

507-
await self._run_pre_rate_validity_checks()
508-
509507
basic_info = self.get_basic_info()
510508

511509
team_army_results = [
@@ -518,6 +516,8 @@ async def resolve_game_results(self) -> EndedGameInfo:
518516
{self.get_player_outcome(player) for player in team}
519517
for team in basic_info.teams
520518
]
519+
520+
await self._run_pre_rate_validity_checks(team_player_partial_outcomes)
521521

522522
try:
523523
# TODO: Remove override once game result messages are reliable

server/games/game_results.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class ArmyOutcome(Enum):
1818
"""
1919
VICTORY = "VICTORY"
2020
DEFEAT = "DEFEAT"
21+
RECALL = "RECALL" # treated the same as `DEFEAT` except for conn issues
2122
DRAW = "DRAW"
2223
UNKNOWN = "UNKNOWN"
2324
CONFLICTING = "CONFLICTING"
@@ -29,6 +30,7 @@ class ArmyReportedOutcome(Enum):
2930
"""
3031
VICTORY = "VICTORY"
3132
DEFEAT = "DEFEAT"
33+
RECALL = "RECALL"
3234
DRAW = "DRAW"
3335
# This doesn't seem to be reported by the game anymore
3436
MUTUAL_DRAW = "MUTUAL_DRAW"

tests/unit_tests/test_custom_game.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,29 @@ async def custom_game(database, game_service, game_stats_service):
1212
return CustomGame(42, database, game_service, game_stats_service)
1313

1414

15+
16+
async def test_rate_game_early_recall(
17+
custom_game: CustomGame,
18+
player_factory
19+
):
20+
custom_game.state = GameState.LOBBY
21+
players = [
22+
player_factory("Dostya", player_id=1, global_rating=(1500, 500)),
23+
player_factory("Rhiza", player_id=2, global_rating=(1500, 500)),
24+
]
25+
add_connected_players(custom_game, players)
26+
custom_game.set_player_option(1, "Team", 2)
27+
custom_game.set_player_option(2, "Team", 3)
28+
await custom_game.launch()
29+
await custom_game.add_result(0, 0, "victory", 5)
30+
await custom_game.add_result(0, 1, "recall", -5)
31+
32+
custom_game.launched_at = time.time() - 60 # seconds
33+
34+
await custom_game.on_game_finish()
35+
assert custom_game.validity == ValidityState.VALID
36+
37+
1538
async def test_rate_game_early_abort_no_enforce(
1639
custom_game: CustomGame,
1740
player_factory

tests/unit_tests/test_game_resolution.py

Lines changed: 31 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -37,68 +37,39 @@ def testresolve():
3737
def test_ranks_all_1v1_possibilities():
3838
"""
3939
Document expectations for all outcomes of 1v1 games.
40-
Assumes that the order of teams doesn't matter.
41-
With five possible outcomes there are 15 possibilities.
40+
With six possible outcomes there are 36 possibilities.
4241
"""
43-
team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.VICTORY}]
44-
with pytest.raises(GameResolutionError):
45-
resolve_game(team_outcomes)
46-
47-
team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.DEFEAT}]
48-
ranks = resolve_game(team_outcomes)
49-
assert ranks == [GameOutcome.VICTORY, GameOutcome.DEFEAT]
50-
51-
team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.DRAW}]
52-
resolve_game(team_outcomes)
53-
assert ranks == [GameOutcome.VICTORY, GameOutcome.DEFEAT]
54-
55-
team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.UNKNOWN}]
56-
ranks = resolve_game(team_outcomes)
57-
assert ranks == [GameOutcome.VICTORY, GameOutcome.DEFEAT]
5842

59-
team_outcomes = [{ArmyOutcome.VICTORY}, {ArmyOutcome.CONFLICTING}]
60-
ranks = resolve_game(team_outcomes)
61-
assert ranks == [GameOutcome.VICTORY, GameOutcome.DEFEAT]
62-
63-
team_outcomes = [{ArmyOutcome.DEFEAT}, {ArmyOutcome.DEFEAT}]
64-
ranks = resolve_game(team_outcomes)
65-
assert ranks == [GameOutcome.DRAW, GameOutcome.DRAW]
66-
67-
team_outcomes = [{ArmyOutcome.DEFEAT}, {ArmyOutcome.DRAW}]
68-
with pytest.raises(GameResolutionError):
69-
resolve_game(team_outcomes)
70-
71-
team_outcomes = [{ArmyOutcome.DEFEAT}, {ArmyOutcome.UNKNOWN}]
72-
with pytest.raises(GameResolutionError):
73-
resolve_game(team_outcomes)
74-
75-
team_outcomes = [{ArmyOutcome.DEFEAT}, {ArmyOutcome.CONFLICTING}]
76-
with pytest.raises(GameResolutionError):
77-
resolve_game(team_outcomes)
78-
79-
team_outcomes = [{ArmyOutcome.DRAW}, {ArmyOutcome.DRAW}]
80-
ranks = resolve_game(team_outcomes)
81-
assert ranks == [GameOutcome.DRAW, GameOutcome.DRAW]
82-
83-
team_outcomes = [{ArmyOutcome.DRAW}, {ArmyOutcome.UNKNOWN}]
84-
with pytest.raises(GameResolutionError):
85-
resolve_game(team_outcomes)
86-
87-
team_outcomes = [{ArmyOutcome.DRAW}, {ArmyOutcome.CONFLICTING}]
88-
with pytest.raises(GameResolutionError):
89-
resolve_game(team_outcomes)
90-
91-
team_outcomes = [{ArmyOutcome.UNKNOWN}, {ArmyOutcome.UNKNOWN}]
92-
with pytest.raises(GameResolutionError):
93-
resolve_game(team_outcomes)
94-
95-
team_outcomes = [{ArmyOutcome.UNKNOWN}, {ArmyOutcome.CONFLICTING}]
96-
with pytest.raises(GameResolutionError):
97-
resolve_game(team_outcomes)
98-
99-
team_outcomes = [{ArmyOutcome.CONFLICTING}, {ArmyOutcome.CONFLICTING}]
100-
with pytest.raises(GameResolutionError):
101-
resolve_game(team_outcomes)
43+
ERROR = 0
44+
DRAW = 1
45+
WIN = 2
46+
LOSS = 3
47+
# Victory Defeat Recall Draw Unknown Conflicting
48+
grid = [[ERROR, WIN, WIN, WIN, WIN, WIN ] # Victory
49+
[LOSS, DRAW, DRAW, ERROR, ERROR, ERROR] # Defeat
50+
[LOSS, DRAW, DRAW, ERROR, ERROR, ERROR] # Recall
51+
[LOSS, ERROR, ERROR, DRAW, ERROR, ERROR] # Draw
52+
[LOSS, ERROR, ERROR, ERROR, ERROR, ERROR] # Unknown
53+
[LOSS, ERROR, ERROR, ERROR, ERROR, ERROR]] # Conflicting
54+
outcome_list = [ArmyOutcome.VICTORY, ArmyOutcome.DEFEAT, ArmyOutcome.RECALL,
55+
ArmyOutcome.DRAW, ArmyOutcome.UNKNOWN, ArmyOutcome.CONFLICTING]
56+
57+
win_resolution = [GameOutcome.VICTORY, GameOutcome.DEFEAT]
58+
draw_resolution = [GameOutcome.DRAW, GameOutcome.DRAW]
59+
loss_resolution = [GameOutcome.DEFEAT, GameOutcome.VICTORY]
60+
61+
for outcome1, row in grid:
62+
for outcome2, resolution in row:
63+
team_outcomes = [{outcome_list[outcome1]}, {outcome_list[outcome2]}]
64+
if resolution == ERROR:
65+
with pytest.raises(GameResolutionError):
66+
resolve_game(team_outcomes)
67+
elif resolution == WIN:
68+
assert resolve_game(team_outcomes) == win_resolution
69+
elif resolution == DRAW:
70+
assert resolve_game(team_outcomes) == draw_resolution
71+
elif resolution == LOSS:
72+
assert resolve_game(team_outcomes) == loss_resolution
10273

10374

10475
def test_team_outcome_ignores_unknown():

tests/unit_tests/test_game_results.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def game_results():
1919
def test_reported_result_to_resolved():
2020
assert ArmyReportedOutcome.VICTORY.to_resolved() is ArmyOutcome.VICTORY
2121
assert ArmyReportedOutcome.DEFEAT.to_resolved() is ArmyOutcome.DEFEAT
22+
assert ArmyReportedOutcome.RECALL.to_resolved() is ArmyOutcome.RECALL
2223
assert ArmyReportedOutcome.DRAW.to_resolved() is ArmyOutcome.DRAW
2324
assert ArmyReportedOutcome.MUTUAL_DRAW.to_resolved() is ArmyOutcome.DRAW
2425

0 commit comments

Comments
 (0)