Skip to content
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

Merged
merged 7 commits into from
Dec 9, 2019

Conversation

Wesmania
Copy link

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.

@@ -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:
Copy link
Member

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__.

Copy link
Author

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.

Copy link
Member

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):
Copy link
Member

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?

Copy link
Author

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.).

Copy link
Member

@cleborys cleborys Sep 21, 2019

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)

Copy link
Author

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.

Copy link
Member

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 :)

@Brutus5000
Copy link
Member

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:
Copy link
Member

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?

Copy link
Author

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:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can drop the else

Copy link
Author

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.

Copy link
Collaborator

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(
Copy link
Collaborator

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

Copy link
Author

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.

server/games/game_results.py Show resolved Hide resolved
@Askaholic
Copy link
Collaborator

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

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

Copy link
Collaborator

@Askaholic Askaholic left a 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.

server/games/game_results.py Show resolved Hide resolved
server/games/game_results.py Outdated Show resolved Hide resolved
@@ -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
Copy link
Collaborator

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.

Copy link
Author

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:
Copy link
Collaborator

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:

except KeyError:

and
except KeyError:

It even mentions it in the docstring
: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

Copy link
Author

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.

Copy link
Collaborator

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?

Copy link
Author

@Wesmania Wesmania left a 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.

server/games/game_results.py Outdated Show resolved Hide resolved
@@ -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
Copy link
Author

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:
Copy link
Author

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.

@Askaholic
Copy link
Collaborator

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 -1 in the database. This is mostly so that the next time someone changes the code and accidentally modifies that behaviour, at least they will know.

Also I think you have a few FIXME' comments that can be removed now, as well as a now extraneous else statement.

Igor Kotrasinski added 3 commits November 24, 2019 00:50
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>
@Wesmania
Copy link
Author

Now that I look at the old code, I'm somewhat confused. Originally -1 was entered when get_army_score threw a KeyError, but I can't see how that could happen in old code. It seems to me we never hit that '-1' branch, and that I should modify the new get_army_score to just return 0 when there are no results.

Igor Kotrasinski added 4 commits November 27, 2019 00:11
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>
@Askaholic Askaholic merged commit e6ac71f into FAForever:develop Dec 9, 2019
Brutus5000 pushed a commit that referenced this pull request Jan 27, 2020
* 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>
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