Skip to content

Commit

Permalink
Only count the best replay by each user for ranking.
Browse files Browse the repository at this point in the history
  • Loading branch information
n-rook committed Dec 29, 2024
1 parent ff96d37 commit 9125e5d
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 15 deletions.
128 changes: 128 additions & 0 deletions project/thscoreboard/replays/migrations/0042_unique_rank_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Generated by Django 4.2.4 on 2024-11-29 19:44

from django.db import migrations


class Migration(migrations.Migration):
"""Change rank view to only include the best replay for each player."""
dependencies = [
("replays", "0041_replay_rank_view"),
]

operations = [
migrations.RunSQL(
sql="""
CREATE OR REPLACE VIEW replays_rank
AS
SELECT
row_number() over () as id,
replay,
score,
shot_id,
difficulty,
route_id,
category,
place
FROM
(
SELECT
replay,
score,
shot_id,
difficulty,
route_id,
category,
created,
rank() OVER (
PARTITION BY shot_id,
difficulty,
route_id,
category
ORDER BY
score DESC,
created,
replay
) as place
FROM
(
SELECT
replay,
score,
shot_id,
difficulty,
route_id,
category,
created,
user_id
FROM
(
SELECT
id as replay,
score,
shot_id,
difficulty,
route_id,
category,
created,
user_id,
rank() OVER (
PARTITION BY shot_id,
difficulty,
route_id,
category,
user_id
ORDER BY
score DESC,
created,
id
) as per_user_place
FROM
replays_replay
WHERE
replay_type = 1 -- FULL_GAME
AND category = 1 -- STANDARD
) AS replays_ranked_per_user
WHERE
per_user_place = 1
) AS top_replays_per_user
) AS top_replays
WHERE
place <= 3
ORDER BY
shot_id,
difficulty,
route_id,
category,
place DESC;
""",
# old:
# """
# CREATE OR REPLACE VIEW replays_rank
# AS
# SELECT row_number() over () as id, replay, score, shot_id, difficulty, route_id, category, place
# FROM (
# SELECT id as replay, score, shot_id, difficulty, route_id, category, rank() OVER (PARTITION BY shot_id, difficulty, route_id, category ORDER BY score DESC, created, id) as place
# FROM replays_replay
# WHERE replay_type = 1 -- FULL_GAME
# AND category = 1 -- STANDARD
# ) AS ranked
# WHERE place <= 3
# ORDER BY shot_id, difficulty, route_id, category, place desc
# ;
# """,
reverse_sql="""
CREATE OR REPLACE VIEW replays_rank
AS
SELECT row_number() over () as id, replay, score, shot_id, difficulty, route_id, category, place
FROM (
SELECT id as replay, score, shot_id, difficulty, route_id, category, rank() OVER (PARTITION BY shot_id, difficulty, route_id, category ORDER BY score DESC, created, id) as place
FROM replays_replay
WHERE replay_type = 1 -- FULL_GAME
AND category = 1 -- STANDARD
) AS ranked
WHERE place <= 3
ORDER BY shot_id, difficulty, route_id, category, place desc
;
""",
)
]
4 changes: 3 additions & 1 deletion project/thscoreboard/replays/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,9 @@ def GetRank(self) -> int | None:
Only the top 3 replays (in a given field: the ranking is divided
by game, shot, difficulty, and so on) have a rank; for the others,
None is returned.
None is returned. Only the best replay from a specific user is ranked;
if someone has the top 3 runs, their best replay will get the
number-one slot, but the next two will go unranked.
This method is instant if "rank_view" is selected, which is strongly
recommended.
Expand Down
96 changes: 82 additions & 14 deletions project/thscoreboard/replays/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
class ReplayRankTest(test_case.ReplayTestCase):
def setUp(self):
super().setUp()
self.author = self.createUser("author")
self.viewer = self.createUser("viewer")
self.user1 = self.createUser("user1")
self.user2 = self.createUser("user2")
self.user3 = self.createUser("user3")

# Ease testing by artificially giving every replay a different unique hash,
# so we can reuse the same filename in tests.
Expand All @@ -41,17 +42,17 @@ def _ReturnIncrementing(replay_file_contents):
def testRanks(self):
test_replays.CreateAsPublishedReplay(
filename="th6_extra",
user=self.author,
user=self.user1,
score=1_000_000_000,
)
test_replays.CreateAsPublishedReplay(
filename="th6_extra",
user=self.author,
user=self.user2,
score=900_000_000,
)
test_replays.CreateAsPublishedReplay(
filename="th7_extra",
user=self.author,
user=self.user3,
score=800_000_000,
)

Expand All @@ -66,7 +67,7 @@ def testRanks(self):
def testRanksTasReplay(self):
test_replays.CreateAsPublishedReplay(
filename="th6_extra",
user=self.author,
user=self.user1,
score=1_000_000_000,
category=models.Category.TAS,
)
Expand All @@ -78,23 +79,23 @@ def testRanksTasReplay(self):
def testRanksBreakTiesUsingUploadDate(self):
test_replays.CreateAsPublishedReplay(
filename="th17_lunatic",
user=self.author,
user=self.user1,
score=9_999_999_990,
created_timestamp=datetime.datetime(
2001, 1, 1, tzinfo=datetime.timezone.utc
),
)
test_replays.CreateAsPublishedReplay(
filename="th17_lunatic",
user=self.author,
user=self.user2,
score=9_999_999_990,
created_timestamp=datetime.datetime(
2003, 3, 3, tzinfo=datetime.timezone.utc
),
)
test_replays.CreateAsPublishedReplay(
filename="th17_lunatic",
user=self.author,
user=self.user3,
score=9_999_999_990,
created_timestamp=datetime.datetime(
2002, 2, 2, tzinfo=datetime.timezone.utc
Expand All @@ -112,7 +113,7 @@ def testRanksBreakTiesUsingUploadDate(self):
def testStagePracticeReplaysAreUnranked(self) -> None:
test_replays.CreateAsPublishedReplay(
filename="th6_extra",
user=self.author,
user=self.user1,
replay_type=models.ReplayType.STAGE_PRACTICE,
)

Expand All @@ -125,21 +126,21 @@ def testRankCountsTasSeparately(self):
)

test_replays.CreateReplayWithoutFile(
user=self.author,
user=self.user1,
difficulty=1,
shot=th05_mima,
score=10000,
category=models.Category.STANDARD,
)
test_replays.CreateReplayWithoutFile(
user=self.author,
user=self.user1,
difficulty=1,
shot=th05_mima,
score=7500,
category=models.Category.STANDARD,
)
test_replays.CreateReplayWithoutFile(
user=self.author,
user=self.user1,
difficulty=1,
shot=th05_mima,
score=20000,
Expand All @@ -159,10 +160,77 @@ def testRankCountsTasSeparately(self):
self.assertEqual(returned_standard_1.category, models.Category.STANDARD)
self.assertEqual(returned_standard_1.GetRank(), 1)
self.assertEqual(returned_standard_2.category, models.Category.STANDARD)
self.assertEqual(returned_standard_2.GetRank(), 2)
self.assertIsNone(returned_standard_2.GetRank())
self.assertEqual(returned_tas.category, models.Category.TAS)
self.assertIsNone(returned_tas.GetRank())

def testOnlyBestRecordFromAPlayerIsRanked(self):
th05_mima = models.Shot.objects.get(
game_id=game_ids.GameIDs.TH05, shot_id="Mima"
)

test_replays.CreateReplayWithoutFile(
user=self.user1,
difficulty=1,
shot=th05_mima,
score=5,
category=models.Category.STANDARD,
)
test_replays.CreateReplayWithoutFile(
user=self.user1,
difficulty=1,
shot=th05_mima,
score=4,
category=models.Category.STANDARD,
)
test_replays.CreateReplayWithoutFile(
user=self.user2,
difficulty=1,
shot=th05_mima,
score=3,
category=models.Category.STANDARD,
)
test_replays.CreateReplayWithoutFile(
user=self.user2,
difficulty=1,
shot=th05_mima,
score=2,
category=models.Category.STANDARD,
)
test_replays.CreateReplayWithoutFile(
user=self.user3,
difficulty=1,
shot=th05_mima,
score=1,
category=models.Category.STANDARD,
)

replays = (
models.Replay.objects.filter(
category__in=[models.Category.STANDARD, models.Category.TAS]
)
.select_related("rank_view")
.order_by("created")
.all()
)

self.assertEqual(len(replays), 5)

first = replays[0]
self.assertEqual(first.score, 5)
self.assertEqual(first.GetRank(), 1)

second = replays[2]
self.assertEqual(second.score, 3)
self.assertEqual(second.GetRank(), 2)

third = replays[4]
self.assertEqual(third.score, 1)
self.assertEqual(third.GetRank(), 3)

self.assertIsNone(replays[1].GetRank())
self.assertIsNone(replays[3].GetRank())


class ReplayTest(test_case.ReplayTestCase):
def setUp(self):
Expand Down

0 comments on commit 9125e5d

Please sign in to comment.