Skip to content

Commit

Permalink
Implement Knockout.reorder_participants for `account_for_playoffs=T…
Browse files Browse the repository at this point in the history
…rue`
  • Loading branch information
kostrykin committed Jan 31, 2024
1 parent 471567d commit 4b436b2
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 31 deletions.
31 changes: 28 additions & 3 deletions tournaments/tournaments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand All @@ -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)
Expand Down
77 changes: 49 additions & 28 deletions tournaments/tournaments/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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):
Expand Down

0 comments on commit 4b436b2

Please sign in to comment.