Module server.games.game_results
Functions
def resolve_game(team_outcomes: list[set[ArmyOutcome]]) ‑> list[GameOutcome]
-
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.
Params
team_outcomes
: list ofGameOutcomes
Errors
Throws
GameResolutionError
if outcomes are inconsistent or ambiguous.Returns
A list of ranks as to be used with trueskill
Classes
class ArmyOutcome (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
The resolved outcome for an army. Each army has only one of these.
Expand source code
class ArmyOutcome(Enum): """ The resolved outcome for an army. Each army has only one of these. """ VICTORY = "VICTORY" DEFEAT = "DEFEAT" DRAW = "DRAW" UNKNOWN = "UNKNOWN" CONFLICTING = "CONFLICTING"
Ancestors
- enum.Enum
Class variables
var CONFLICTING
var DEFEAT
var DRAW
var UNKNOWN
var VICTORY
class ArmyReportedOutcome (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
A reported outcome for an army. Each army may have several of these.
Expand source code
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)
Ancestors
- enum.Enum
Class variables
var DEFEAT
var DRAW
var MUTUAL_DRAW
var VICTORY
Methods
def to_resolved(self) ‑> ArmyOutcome
-
Convert this result to the resolved version. For when all reported results agree.
class ArmyResult (player_id: int, army: Optional[int], army_outcome: str, metadata: list[str])
-
Broadcast in the end of game rabbitmq message.
Expand source code
class ArmyResult(NamedTuple): """ Broadcast in the end of game rabbitmq message. """ player_id: int army: Optional[int] army_outcome: str metadata: list[str]
Ancestors
- builtins.tuple
Instance variables
var army : Optional[int]
-
Alias for field number 1
var army_outcome : str
-
Alias for field number 2
var metadata : list[str]
-
Alias for field number 3
var player_id : int
-
Alias for field number 0
class GameOutcome (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
An enumeration.
Expand source code
@unique class GameOutcome(Enum): VICTORY = "VICTORY" DEFEAT = "DEFEAT" DRAW = "DRAW" UNKNOWN = "UNKNOWN"
Ancestors
- enum.Enum
Class variables
var DEFEAT
var DRAW
var UNKNOWN
var VICTORY
class GameResolutionError (*args, **kwargs)
-
Common base class for all non-exit exceptions.
Expand source code
class GameResolutionError(Exception): pass
Ancestors
- builtins.Exception
- builtins.BaseException
class GameResultReport (reporter: int, army: int, outcome: ArmyReportedOutcome, score: int, metadata: frozenset[str] = frozenset())
-
These are sent from each player's FA when they quit the game. 'Score' depends on the number of ACUs killed, whether the player died, maybe other factors.
Expand source code
class GameResultReport(NamedTuple): """ These are sent from each player's FA when they quit the game. 'Score' depends on the number of ACUs killed, whether the player died, maybe other factors. """ reporter: int army: int outcome: ArmyReportedOutcome score: int metadata: frozenset[str] = frozenset()
Ancestors
- builtins.tuple
Instance variables
var army : int
-
Alias for field number 1
var metadata : frozenset[str]
-
Alias for field number 4
var outcome : ArmyReportedOutcome
-
Alias for field number 2
var reporter : int
-
Alias for field number 0
var score : int
-
Alias for field number 3
class GameResultReports (game_id: int)
-
Collects all results from a single game. Allows to determine results for an army and game as a whole. Supports a dict-like access to lists of results for each army, but don't modify these.
Expand source code
@with_logger class GameResultReports(Mapping): """ Collects all results from a single game. Allows to determine results for an army and game as a whole. Supports a dict-like access to lists of results for each army, but don't modify these. """ def __init__(self, game_id: int): Mapping.__init__(self) self._game_id = game_id # Just for logging self._back: dict[int, list[GameResultReport]] = {} # Outcome caching self._outcomes: dict[int, ArmyOutcome] = {} self._dirty_armies: set[int] = set() def __getitem__(self, key: int) -> list[GameResultReport]: return self._back[key] def __iter__(self) -> Iterator[int]: return iter(self._back) def __len__(self) -> int: return len(self._back) def add(self, result: GameResultReport) -> None: army_results = self._back.setdefault(result.army, []) army_results.append(result) self._dirty_armies.add(result.army) def outcome(self, army: int) -> ArmyOutcome: """ 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 ArmyOutcome.UNKNOWN voters = defaultdict(set) for report in self[army]: voters[report.outcome].add(report.reporter) if len(voters) == 0: return ArmyOutcome.UNKNOWN if len(voters) == 1: unique_outcome = voters.popitem()[0] return unique_outcome.to_resolved() sorted_outcomes = sorted( voters.keys(), reverse=True, key=lambda outcome: (len(voters[outcome]), outcome.value), ) 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].to_resolved() else: decision = ArmyOutcome.CONFLICTING self._logger.info( "Multiple outcomes for game %s army %s resolved to %s. Reports are: %s", self._game_id, army, decision, voters, ) return decision def metadata(self, army: int) -> list[str]: """ If any users have sent metadata tags in their messages about this army this function will compare those tags across all messages trying to find common ones to return. """ if army not in self: return [] all_metadata = [report.metadata for report in self[army]] metadata_count = Counter(all_metadata).most_common() if len(metadata_count) == 1: # Everyone agrees! return sorted(list(metadata_count[0][0])) most_common, next_most_common, *_ = metadata_count if most_common[1] > next_most_common[1]: resolved_to = sorted(list(most_common[0])) self._logger.info( "Conflicting metadata for game %s army %s resolved to %s. Reports are: %s", self._game_id, army, resolved_to, all_metadata, ) return resolved_to # We have a tie self._logger.info( "Conflicting metadata for game %s army %s, unable to resolve. Reports are: %s", self._game_id, army, all_metadata, ) return [] 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 no results for a given army. """ if army not in self: return 0 scores = Counter(r.score for r in self[army]) if len(scores) == 1: return scores.popitem()[0] self._logger.info( "Conflicting scores (%s) reported for game %s", scores, self._game_id ) score, _ = max(scores.items(), key=lambda kv: kv[::-1]) return score def victory_only_score(self, army: int) -> int: """ Calculate our own score depending *only* on victory. """ if army not in self: return 0 if self.outcome(army) is ArmyOutcome.VICTORY: return 1 else: return 0 @classmethod async def from_db(cls, database, game_id): results = cls(game_id) async with database.acquire() as conn: result = await conn.execute( select( game_player_stats.c.place, game_player_stats.c.score, game_player_stats.c.result ).where(game_player_stats.c.gameId == game_id) ) for row in result: # FIXME: Assertion about startspot == army with contextlib.suppress(ValueError): outcome = ArmyReportedOutcome(row.result.value) report = GameResultReport(0, row.place, outcome, row.score) results.add(report) return results
Ancestors
- collections.abc.Mapping
- collections.abc.Collection
- collections.abc.Sized
- collections.abc.Iterable
- collections.abc.Container
Static methods
async def from_db(database, game_id)
Methods
def add(self, result: GameResultReport) ‑> None
def metadata(self, army: int) ‑> list[str]
-
If any users have sent metadata tags in their messages about this army this function will compare those tags across all messages trying to find common ones to return.
def outcome(self, army: int) ‑> ArmyOutcome
-
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.
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 no results for a given army.
def victory_only_score(self, army: int) ‑> int
-
Calculate our own score depending only on victory.