From 4b436b242594167901f442bfa21fb4b1852993fa Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 31 Jan 2024 10:49:55 +0100 Subject: [PATCH] Implement `Knockout.reorder_participants` for `account_for_playoffs=True` --- tournaments/tournaments/models.py | 31 +++++++++++-- tournaments/tournaments/tests.py | 77 ++++++++++++++++++++----------- 2 files changed, 77 insertions(+), 31 deletions(-) diff --git a/tournaments/tournaments/models.py b/tournaments/tournaments/models.py index 1e09349..de1f785 100644 --- a/tournaments/tournaments/models.py +++ b/tournaments/tournaments/models.py @@ -496,7 +496,32 @@ def clean(self): raise ValidationError('Double elimination is not implemented yet.') @staticmethod - def reorder_participants(participants): + def reorder_participants(participants, account_for_playoffs): + """ + Re-order the participants to establish a fair ordering. + + The participants are re-ordered so that the first (highest ranked) are matched against the last (lowest ranked). + If the number of participants is not a power of 2, playoff matches are required (incomplete levels of the binary tree). + The order of participants will account for that if `account_for_playoffs` is True, ensuring that the playoffs will be filled up with the very last participants (lowest ranked). + """ + if len(participants) == 0: return list() + + # Check whether the number of participants is a power of 2. + power_of_two_floor = 1 << (len(participants).bit_length() - 1) + power_of_two = (power_of_two_floor == len(participants)) + + # Account for playoffs. + if account_for_playoffs and not power_of_two: + + # Number of participants allocated for the playoffs. + n = min((2 * (len(participants) - power_of_two_floor), len(participants))) + + # Allocate the participants. + playoffs_part = Knockout.reorder_participants(participants[-n:], account_for_playoffs = False) + complete_part = Knockout.reorder_participants(participants[:-n], account_for_playoffs = False) + return playoffs_part + complete_part + + # Establish the order so that the first are matched against the last. result = [None] * len(participants) participants = list(participants) for pidx in range(len(participants)): @@ -509,8 +534,8 @@ def create_fixtures(self, participants): assert len(participants) >= 2 levels = math.ceil(math.log2(len(participants))) - # Re-order the participants so that the first (highest ranked) are matched against the last (lowest ranked). - participants = Knockout.reorder_participants(participants) + # Re-order the participants so that the first (highest ranked) are matched against the last (lowest ranked), also accounting for playoffs. + participants = Knockout.reorder_participants(participants, account_for_playoffs = True) # Identify fixtures by their path (which, in a binary tree, corresponds to the index of the node in binary representation, starting from `1` for the root). remaining_participants = list(participants) diff --git a/tournaments/tournaments/tests.py b/tournaments/tournaments/tests.py index bfb1350..a0a9964 100644 --- a/tournaments/tournaments/tests.py +++ b/tournaments/tournaments/tests.py @@ -692,11 +692,19 @@ def test_required_confirmations_count(self): class KnockoutTest(ModeTestBase, TestCase): def test_reorder_participants(self): - self.assertEqual(Knockout.reorder_participants([1]), [1]) - self.assertEqual(Knockout.reorder_participants([1, 2]), [1, 2]) - self.assertEqual(Knockout.reorder_participants([1, 2, 3]), [1, 3, 2]) - self.assertEqual(Knockout.reorder_participants([1, 2, 3, 4, 5, 6]), [1, 6, 2, 5, 3, 4]) - self.assertEqual(Knockout.reorder_participants([1, 2, 3, 4, 5, 6, 7]), [1, 7, 2, 6, 3, 5, 4]) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = False, participants = []), []) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = False, participants = [1]), [1]) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = False, participants = [1, 2]), [1, 2]) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = False, participants = [1, 2, 3]), [1, 3, 2]) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = False, participants = [1, 2, 3, 4, 5, 6]), [1, 6, 2, 5, 3, 4]) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = False, participants = [1, 2, 3, 4, 5, 6, 7]), [1, 7, 2, 6, 3, 5, 4]) + + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = True, participants = []), []) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = True, participants = [1]), [1]) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = True, participants = [1, 2]), [1, 2]) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = True, participants = [1, 2, 3]), [2, 3, 1]) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = True, participants = [1, 2, 3, 4, 5, 6]), [3, 6, 4, 5, 1, 2]) + self.assertEqual(Knockout.reorder_participants(account_for_playoffs = True, participants = [1, 2, 3, 4, 5, 6, 7]), [2, 7, 3, 6, 4, 5, 1]) def test_create_fixtures_2participants(self): mode = Knockout.objects.create(tournament = self.tournament) @@ -751,6 +759,19 @@ def test_create_fixtures_6participants(self): } self.assertEqual(actual_fixtures, expected_fixtures) + def test_create_fixtures_7participants(self): + mode = Knockout.objects.create(tournament = self.tournament) + mode.create_fixtures(self.participants[:7]) + + # Verify fixtures. + actual_fixtures = self.group_fixtures_by_level(mode) + expected_fixtures = { + 0: [(5, 4), (6, 3), (7, 2)], + 1: [(None, None), (None, 1)], + 2: [(None, None)] + } + self.assertEqual(actual_fixtures, expected_fixtures) + def test_create_fixtures_8participants(self): mode = Knockout.objects.create(tournament = self.tournament) mode.create_fixtures(self.participants[:8]) @@ -798,7 +819,7 @@ def test_propagate(self): mode = self.test_create_fixtures_5participants() playoff = mode.current_fixtures.get() - # Propagate play-off (user-5 vs. user-1). + # Propagate play-off (user-5 vs. user-4). playoff.score = (10, 12) playoff.save() propagate_ret = mode.propagate(playoff) @@ -807,14 +828,14 @@ def test_propagate(self): # Verify fixtures after play-off. actual_fixtures = self.group_fixtures_by_level(mode) expected_fixtures = { - 0: [(5, 1)], - 1: [(1, 3), (4, 2)], + 0: [(5, 4)], + 1: [(4, 2), (3, 1)], 2: [(None, None)] } self.assertEqual(actual_fixtures, expected_fixtures) - # Propagate 1st seminfal (user-1 vs. user-3). - semifinal1 = mode.fixtures.get(player1 = User.objects.get(id = 1), player2 = User.objects.get(id = 3)) + # Propagate 1st seminfal (user-4 vs. user-2). + semifinal1 = mode.fixtures.get(player1 = User.objects.get(id = 4), player2 = User.objects.get(id = 2)) semifinal1.score = (12, 10) semifinal1.save() propagate_ret = mode.propagate(semifinal1) @@ -823,14 +844,14 @@ def test_propagate(self): # Verify fixtures after 1st semifinal. actual_fixtures = self.group_fixtures_by_level(mode) expected_fixtures = { - 0: [(5, 1)], - 1: [(1, 3), (4, 2)], - 2: [(1, None)] + 0: [(5, 4)], + 1: [(4, 2), (3, 1)], + 2: [(4, None)] } self.assertEqual(actual_fixtures, expected_fixtures) - # Propagate 2nd seminfal (user-4 vs. user-2). - semifinal2 = mode.fixtures.get(player1 = User.objects.get(id = 4), player2 = User.objects.get(id = 2)) + # Propagate 2nd seminfal (user-3 vs. user-1). + semifinal2 = mode.fixtures.get(player1 = User.objects.get(id = 3), player2 = User.objects.get(id = 1)) semifinal2.score = (12, 10) semifinal2.save() propagate_ret = mode.propagate(semifinal2) @@ -843,14 +864,14 @@ def test_propagate(self): # Verify fixtures after 2nd semifinal. actual_fixtures = self.group_fixtures_by_level(mode) expected_fixtures = { - 0: [(5, 1)], - 1: [(1, 3), (4, 2)], - 2: [(1, 4)] + 0: [(5, 4)], + 1: [(4, 2), (3, 1)], + 2: [(4, 3)] } self.assertEqual(actual_fixtures, expected_fixtures) - # Propagate final (user-1 vs. user-4). - final = mode.fixtures.get(player1 = User.objects.get(id = 1), player2 = User.objects.get(id = 4)) + # Propagate final (user-4 vs. user-3). + final = mode.fixtures.get(player1 = User.objects.get(id = 4), player2 = User.objects.get(id = 3)) final.score = (12, 10) final.save() propagate_ret = mode.propagate(final) @@ -871,7 +892,7 @@ def test_current_fixtures(self): fixture = mode.current_fixtures.get() self.assertEqual(fixture.player1.id, 5) - self.assertEqual(fixture.player2.id, 1) + self.assertEqual(fixture.player2.id, 4) self.confirm_fixture(fixture) @@ -880,10 +901,10 @@ def test_current_fixtures(self): self.assertEqual(mode.current_fixtures.count(), 2) fixtures = mode.current_fixtures.all() - self.assertEqual(fixtures[0].player1.id, 1) - self.assertEqual(fixtures[0].player2.id, 3) - self.assertEqual(fixtures[1].player1.id, 4) - self.assertEqual(fixtures[1].player2.id, 2) + self.assertEqual(fixtures[0].player1.id, 4) + self.assertEqual(fixtures[0].player2.id, 2) + self.assertEqual(fixtures[1].player1.id, 3) + self.assertEqual(fixtures[1].player2.id, 1) self.confirm_fixture(fixtures[0]) self.assertEqual(mode.current_level, 1) @@ -894,8 +915,8 @@ def test_current_fixtures(self): self.assertEqual(mode.current_fixtures.count(), 1) fixture = mode.current_fixtures.get() - self.assertEqual(fixture.player1.id, 1) - self.assertEqual(fixture.player2.id, 4) + self.assertEqual(fixture.player1.id, 4) + self.assertEqual(fixture.player2.id, 3) self.confirm_fixture(fixture) @@ -906,7 +927,7 @@ def test_current_fixtures(self): def test_placements(self): mode = self.test_propagate() actual_placements = [user.id for user in mode.placements] - expected_placements = [1, 4, 3, 2, 5] + expected_placements = [4, 3, 2, 1, 5] self.assertEqual(actual_placements, expected_placements) def test_placements_empty(self):