-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Clean up game outcome handling #495
Conversation
@@ -462,7 +406,7 @@ def host_left_lobby() -> bool: | |||
await self.mark_invalid(ValidityState.MUTUAL_DRAW) | |||
return | |||
|
|||
if len(self._results) == 0: | |||
if not self._results: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When is this True
? Does this maybe rely on collections.abc.Mapping.__len__
being nonzero or GameResults.__len__
being nonzero? Or just self._results
not being None
? I am uncertain how trucity/casting to bool is handled for user-defined class if there is no __bool__
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to Python docs, in absence of __bool__
truthiness is decided by __len__
being non-zero. So, it's perfectly fine to idiomatically check for non-emptiness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know, thanks!
self._logger.info("Conflicting scores (%s) reported for game %s", scores, self) | ||
score, _votes = max(scores.items(), key=lambda kv: kv[::-1]) | ||
return score | ||
return self._results.score(army, self.id) | ||
|
||
def get_army_result(self, player): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be called get_player_outcome
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does return a score (a 'score' value from the game) instead of an outcome ('victory', 'defeat' etc.).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I meant get_army_result
, which takes a Player
as argument and returns a GameOutcome
(not get_army_score
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't want to touch Game
class public methods, though I could rename that if you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It just looked off to me, so I'm all for it. I can do it in my later PR if you don't want to :)
If you want to clean up game outcomes please write into the db table player_stats into a new column an enum of type VICTORY, DRAW, DEFEAT. That would save me froma lot of trouble in the future |
return False | ||
return True | ||
|
||
def outcome(self, army: int) -> GameOutcome: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe get_outcome
, making it a verb?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It returns a value without side effects, so I think it's up to personal taste.
|
||
if len(outcomes) == 1: | ||
return outcomes.pop() | ||
else: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can drop the else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The else clause is a one-liner, so I thought it's clearer to keep it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that it's not a one liner you should probably get rid of the else. It's just an unnecessary level of indentation
async def from_db(cls, database, game_id): | ||
results = cls() | ||
async with database.acquire() as conn: | ||
rows = await conn.execute( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could rewrite this with sqlalchemy models
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I kept it to make it easier to verify that the code is equivalent, we can change that in a separate commit.
1577fd2
to
53cb44a
Compare
Yea, we should really add that. Otherwise the only way of knowing how the server rated a game, is to run the exact version of the rating code that was deployed at the time against the same game results. And as we know, that rating code is flawed at the moment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need at least a few test cases for persist_results
, and also a test for this code path:
https://coveralls.io/builds/25850926/source?filename=server%2Fgames%2Fgame.py#L771.
I think one solution to the unknown game results could be to simply make the game unranked if we are in a situation where the winner can't be determined.
tests/unit_tests/test_game.py
Outdated
@@ -405,6 +406,8 @@ def custom_game(event_loop, database, game_service, game_stats_service): | |||
assert new_rating != Rating(*player.ratings[RatingType.GLOBAL]) | |||
|
|||
|
|||
# FIXME - discuss correct get_army_result behaviour |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this test now fails because the first result (with one reported victory and one reported defeat) now is reported as an unknown outcome? It might be a good idea to unrank the game due to UNKNOWN_RESULT
in this case as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll change the test to do that.
Pick and return most frequently reported score for an army. If multiple | ||
scores are most frequent, pick the largest one. | ||
""" | ||
if army not in self: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think some pieces of the code actually expect this to raise a KeyError
. Have a look at:
Line 515 in 6b9aec7
except KeyError: |
and
Line 897 in 6b9aec7
except KeyError: |
It even mentions it in the docstring
Line 841 in 6b9aec7
:raise KeyError |
We need some tests for persist_results
because I don't think it ever writes -1
into the database any more. As a matter of fact that code path is not covered by the tests: https://coveralls.io/builds/25850926/source?filename=server%2Fgames%2Fgame.py#L449
which is probably why I missed this when I rewrote that function in the first place
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I won't restore these KeyErrors and will just fix these catch blocks - we have good neutral values (0
, and GameOutcome.UNKNOWN
) for when an army isn't there. It also makes sense in that it's not an exceptional condition - we might have not received any results for a given army yet, so we need a sensible value for an army with no results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I agree with that approach. Like I mentioned, the code used to write -1
for unknown scores, although I'm not convinced that this is unambiguous since negative scores are possible. Is it possible to have a score of 0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add some unit tests tomorrow, as you asked.
tests/unit_tests/test_game.py
Outdated
@@ -405,6 +406,8 @@ def custom_game(event_loop, database, game_service, game_stats_service): | |||
assert new_rating != Rating(*player.ratings[RatingType.GLOBAL]) | |||
|
|||
|
|||
# FIXME - discuss correct get_army_result behaviour |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll change the test to do that.
Pick and return most frequently reported score for an army. If multiple | ||
scores are most frequent, pick the largest one. | ||
""" | ||
if army not in self: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I won't restore these KeyErrors and will just fix these catch blocks - we have good neutral values (0
, and GameOutcome.UNKNOWN
) for when an army isn't there. It also makes sense in that it's not an exceptional condition - we might have not received any results for a given army yet, so we need a sensible value for an army with no results.
53cb44a
to
8507a70
Compare
I think this PR is very close to being ready, but I'd still like to see a regression test that verifies unknown results are reported as score Also I think you have a few |
This slims down the Game class a bit and lets us see more clearly how we're correlating game results. Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com>
Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com>
Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com>
8507a70
to
726b697
Compare
Now that I look at the old code, I'm somewhat confused. Originally -1 was entered when |
726b697
to
27f8009
Compare
Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com>
We'll keep the strict method, but we'll also add logging to gauge how often we get conflicting results. Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com>
Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com>
Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com>
e9788ad
to
7a1b59e
Compare
* Move game outcome logic to a separate class This slims down the Game class a bit and lets us see more clearly how we're correlating game results. Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com> * Use only player id as game result reporter Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com> * Use one method for deciding player's game outcome Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com> * Remove unused exception catch Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com> * Agree on game outcome calculating method We'll keep the strict method, but we'll also add logging to gauge how often we get conflicting results. Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com> * Remove obsolete FIXMEs Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com> * Add tests for player scores, consistently use 0 as default Signed-off-by: Igor Kotrasinski <i.kotrasinsk@gmail.com>
This moves the logic determining game's outcome for players into a separate class. Methods for choosing an outcome from multiple reports are merged into one. TODO: discuss if the method chosen is appropriate.
The PR is based on top of #494, so it has some extra commits.