Skip to content

Commit

Permalink
feat: implement swiss pairing algorithm for tournaments
Browse files Browse the repository at this point in the history
- move tournament pairing functions to pairings.py
- implement random and swiss algorithms
- set swiss algorithm to default
- improve chunking algorithm using iterators
  • Loading branch information
krishnans2006 committed Feb 18, 2024
1 parent 9a8b2d6 commit 7cc7373
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 22 deletions.
79 changes: 79 additions & 0 deletions othello/apps/tournaments/pairings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import random
from typing import List, Tuple

from othello.apps.tournaments.models import TournamentPlayer
from othello.apps.tournaments.utils import get_updated_ranking, logger, chunks

Players = List[TournamentPlayer]
Pairings = List[Tuple[TournamentPlayer, ...]]


def random_pairing(players: Players, bye_player: TournamentPlayer) -> Pairings:
randomized: Players = random.sample(players, len(players))
if len(randomized) % 2 == 1:
randomized.append(bye_player)

return list(chunks(randomized, 2))


def swiss_pairing(players: Players, bye_player: TournamentPlayer) -> Pairings:
tournament = players[0].tournament

# Default to danish pairing if there are enough rounds
if tournament.num_rounds >= len(players) - 1:
return danish_pairing(players, bye_player)

matches: Pairings = []
players = sorted(players, key=get_updated_ranking, reverse=True)
if len(players) % 2 == 1:
players.append(bye_player)

logger.info(players)

tournament_matches = set()
for game in tournament.games.all():
tournament_matches.add((game.black.id, game.white.id))
tournament_matches.add((game.white.id, game.black.id))

for i in range(0, len(players)):
if i + 1 >= len(players):
break
for j in range(i + 1, len(players)):
if (players[i].id, players[j].id) not in tournament_matches:
black = random.choice((players[i], players[j]))
white = players[i] if black == players[j] else players[j]
logger.info(f"RANK: {black}({black.ranking}), {white}({white.ranking})")
matches.append((black, white))
break
else:
return danish_pairing(players, bye_player)

return matches


def danish_pairing(players: Players, bye_player: TournamentPlayer) -> Pairings:
matches = []
players = sorted(players, key=get_updated_ranking, reverse=True)

for i in range(0, len(players), 2):
if i + 1 >= len(players):
players.append(bye_player)
black = random.choice((players[i], players[i + 1]))
white = players[i] if black == players[i + 1] else players[i + 1]
logger.info(f"RANK: {black}({black.ranking}), {white}({white.ranking})")
matches.append((black, white))

return matches


algorithms = {
"random": random_pairing,
"swiss": swiss_pairing,
"danish": danish_pairing,
}


def pair(players: Players, bye_player: TournamentPlayer, algorithm: str = "swiss") -> Pairings:
if algorithm not in algorithms:
raise ValueError(f"Invalid pairing algorithm: {algorithm}")
return algorithms[algorithm](players, bye_player)
5 changes: 3 additions & 2 deletions othello/apps/tournaments/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from ..games.tasks import Player, run_game
from .emails import email_send
from .models import Tournament, TournamentGame, TournamentPlayer
from .utils import chunks, get_updated_ranking, make_pairings
from .utils import chunks, get_updated_ranking
from .pairings import pair

logger = logging.getLogger("othello")

Expand Down Expand Up @@ -88,7 +89,7 @@ def run_tournament(tournament_id: int) -> None:
bye_player = TournamentPlayer.objects.create(tournament=t, submission=t.bye_player)

for round_num in range(t.num_rounds):
matches: List[Tuple[TournamentPlayer, TournamentPlayer]] = make_pairings(submissions, bye_player)
matches: List[Tuple[TournamentPlayer, ...]] = pair(submissions, bye_player)
t.refresh_from_db()
if t.terminated:
t.delete()
Expand Down
24 changes: 4 additions & 20 deletions othello/apps/tournaments/utils.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
import logging
import random
from typing import Generator, List, Tuple, TypeVar
from typing import TypeVar, Iterator, Tuple, Iterable

from .models import TournamentPlayer

T = TypeVar("T")
logger = logging.getLogger("othello")


def chunks(v: List[T], n: int) -> Generator[List[T], None, None]:
for i in range(0, len(v), n):
yield v[i: i + n]
def chunks(v: Iterable[T], n: int) -> Iterator[Tuple[T, ...]]:
# From https://stackoverflow.com/a/1625023
return zip(*(iter(v),) * n)


def get_updated_ranking(player: TournamentPlayer) -> float:
player.refresh_from_db()
return player.ranking


def make_pairings(players: List[TournamentPlayer], bye_player: TournamentPlayer) -> List[Tuple[TournamentPlayer, TournamentPlayer]]:
matches = []
players = sorted(players, key=get_updated_ranking, reverse=True)

for i in range(0, len(players), 2):
if i + 1 >= len(players):
players.append(bye_player)
logger.info(f"RANK: {players[i]}({players[i].ranking}), {players[i+1]}({players[i+1].ranking})")
black = random.choice((players[i], players[i + 1]))
white = players[i] if black == players[i + 1] else players[i + 1]
matches.append((black, white))

return matches

0 comments on commit 7cc7373

Please sign in to comment.