[Back to page on Round-Robin schedules]
The Wikipedia article on Round-robin tournament describes the standard Circle algorithm for generating Round Robin schedules. I wrote this Python program to implement the Circle algorithm.
################[BEGIN SOURCE CODE]################
# -*- coding: utf-8 -*- """ round_robin.py written by Stephen R. Ferg placed in the public domain 2021-10-01 A Python program to generate round-robin schedules for up to NUMBER_OF_TEAMS teams. It uses the Circle algorithm described in https://en.wikipedia.org/wiki/Round-robin_tournament Output is written to the console. """ import random ################################################## # CUSTOMIZABLE SETTINGS ################################################## # Customize the value for NUMBER_OF_TEAMS. # Specify an even number, not an odd number. NUMBER_OF_TEAMS = 16 ################################################## # CUSTOMIZABLE SETTINGS: end ################################################## COMPETITIONS = [] TERRAINS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" HEADINGS = ["WIN?", "OUR SCORE", "OPPONENTS", "DIFF"] UNDERSCORES = "_" * 4 COLUMN_SEPARATOR = " " * 2 COLUMN_HEAD = "" RESULTS_LINE = "" COLUMN_WIDTH = 9 NEW_PAGE_SYMBOL = "[]" BANNER_CHARACTER = "#" BANNER_LINE_LEN = 65 BANNER_LINE = BANNER_CHARACTER * BANNER_LINE_LEN trash, there_is_an_odd_number_of_teams = divmod(NUMBER_OF_TEAMS, 2) if there_is_an_odd_number_of_teams: raise Exception("Number of teams must be an even number. Number of teams: " + str(NUMBER_OF_TEAMS)) def paren(*args): s = strr(*args) return strr("(", s, ")") def competition_description(numberOfTeams): return strr("Round Robin :: ", numberOfTeams, " teams") def round_header(round_id, list_of_rounds): number_of_rounds = len (list_of_rounds) return strr("Round ", round_id, " of ", number_of_rounds) def round_row_header(round_id): s = strr(round_id).rjust(2) return strr("Round ", s, ":") def terrain_bracket(*args): s = strr(*args) return strr("{", s, "}") def left_header_column(*args): s = strr(*args) return s.ljust(23) def print_banner(*args , line2=None ): say(NEW_PAGE_SYMBOL) say(BANNER_LINE) line1 = strr(*args) for line in [line1, line2]: if not line: continue s = line.center(BANNER_LINE_LEN - 2) s = strr(BANNER_CHARACTER, s, BANNER_CHARACTER) say(s) say(BANNER_LINE) def strr(*args): x = [str(arg) for arg in args] y = "".join(x) return y def say(*args): print(strr(*args)) def generate_column_lines(): global COLUMN_HEAD, RESULTS_LINE s = "" for heading in HEADINGS: contents = heading.center(COLUMN_WIDTH) s = s + contents + COLUMN_SEPARATOR COLUMN_HEAD = s s = "" for heading in HEADINGS: contents = "_" * COLUMN_WIDTH s = s + contents + COLUMN_SEPARATOR RESULTS_LINE = s def format_team_header(team): return strr("TEAM ", team.team_id, ".") ############################################################ # class Competition ############################################################ class Competition: def __init__(self, numberOfTeams): self.numberOfTeams = numberOfTeams self.numberOfRounds = self.numberOfTeams - 1 self.gamesPerRound = self.numberOfTeams // 2 self.numberOfTerrains, remainder = divmod(numberOfTeams, 2) self.game_terrains = list(TERRAINS[0:self.numberOfTerrains]) self.competition_header = competition_description(self.numberOfTeams) self.teams = [] self.rounds = [] self.generate_teams_in_competition() self.generate_rounds_in_competition() def generate_teams_in_competition(self): self.teams = [] for i in range(self.numberOfTeams): team_id = i + 1 team = Team(team_id, self) self.teams.append(team) def generate_rounds_in_competition(self): self.rounds = [] for i in range(self.numberOfRounds): round_id = i + 1 round = Round(round_id, self) self.rounds.append(round) self.turn() def turn(self): first = self.teams[0] last = self.teams[-1] middle = self.teams[1:-1] newList = [first, last] newList.extend(middle) self.teams = newList def print_competition_header_page(self): print_banner("* FORMS FOR A COMPETITION OF " , self.numberOfTeams, " TEAMS") def print_rounds_and_matches_header(self): competition_info = competition_description(self.numberOfTeams) print_banner("ROUNDS AND TEAM MATCH-UPS" , line2 = competition_info ) def print_team_results_forms(self): for team in self.teams: print_banner("TEAM RESULTS FORMS FOR " + format_team_header(team) , line2 = self.competition_header ) say(left_header_column(), COLUMN_HEAD) for round in team.get_opponents_for_rounds(): say(round) say(left_header_column("TEAM TOTALS"), RESULTS_LINE) def print_rounds_in_competition(self): ### say() for round in self.rounds: say() say(round_header(round.round_id, self.rounds)) for game in round.games: say(game.toString()) def print_control_table_form(self): for team in self.teams: print_banner(strr("CONTROL TABLE RESULTS FOR ", format_team_header(team)) , line2 = self.competition_header ) say(left_header_column(), COLUMN_HEAD) for round in team.get_opponents_for_rounds(): say(round) say(left_header_column("TEAM TOTALS"), RESULTS_LINE) ############################################################ # class Round ############################################################ class Round: def __init__(self, round_id, competition): self.round_id = round_id self.competition = competition self.unassigned_terrain_ids = [x for x in competition.game_terrains] self.games = [] teams = self.competition.teams for i in range(self.competition.gamesPerRound): # make one game game_id = i + 1 # get terrain id terrain_id = random.choice(self.unassigned_terrain_ids) self.unassigned_terrain_ids.remove(terrain_id) round = self game = Game(self.competition, round, game_id, terrain_id) offsetFromStartOfTeamsList = i offsetFromEndOfTeamsList = len(teams) - 1 - offsetFromStartOfTeamsList team1 = teams[offsetFromStartOfTeamsList] team2 = teams[offsetFromEndOfTeamsList] game.assign_teams_to_game(team1, team2) self.games.append(game) self.games = sorted(self.games) # keep the list sorted def get_printable_games_in_round(self): x = [] for game in self.games: s = game.toString() x.append(s) return x def print_games_in_round(self): say() say(round_row_header(self.round_id)) for printable_game in self.get_printable_games_in_round(): say(printable_game) ############################################################ # class Game ############################################################ class Game: def __init__(self, competition, round, game_id, terrain_id): self.game_id = game_id self.terrain_id = terrain_id self.competition = competition self.round = round self.team1 = None self.team2 = None def assign_teams_to_game(self, team1, team2): # aesthetics: put lowest team id number on left if team1.team_id < team2.team_id: self.team1, self.team2 = team1, team2 else: self.team1, self.team2 = team2, team1 team1.add_game_to_team_schedule(self) team2.add_game_to_team_schedule(self) def __lt__(self, other): if self.team1.team_id < other.team1.team_id: return True else: return False def toString(self): VERSUS = " : " team1 = str(self.team1.team_id) team2 = str(self.team2.team_id) s = strr(team1.rjust(3) , VERSUS, team2.ljust(3) , " ", terrain_bracket("terrain " + self.terrain_id)) return s ############################################################ # class Team ############################################################ class Team: def __init__(self, id, competition): self.team_id = id self.competition = competition self.rounds = [] self.opponents_dict = {} self.game_terrains = {} def add_game_to_team_schedule(self, game): # we assume that rounds will be added sequentially round_id = game.round.round_id if game.team1.team_id == self.team_id: opponent = game.team2 else: opponent = game.team1 if round_id in self.opponents_dict: pass else: self.opponents_dict[round_id] = opponent self.game_terrains[round_id] = game.terrain_id self.rounds.append(round_id) def get_opponents_for_rounds(self): x = [] for round_id in self.rounds: terrain = self.game_terrains[round_id] opponent = self.opponents_dict[round_id] s = strr(round_row_header(round_id) , " team ", str(opponent.team_id).rjust(2) , " ", terrain_bracket(terrain) , " " , RESULTS_LINE ) x.append(s) return x def __lt__(self, other): if self.team_id < other.team_id: return True else: return False ############################################################ # subroutines ############################################################ def generate_1_competition(numberOfTeams): competition = Competition(numberOfTeams) competition.generate_rounds_in_competition() COMPETITIONS.append(competition) def generate_competitions(): for numberOfTeams in range(1, NUMBER_OF_TEAMS + 1): if numberOfTeams 0: # an odd number of teams continue else: generate_1_competition(numberOfTeams) if __name__ == "__main__": generate_competitions() generate_column_lines() say("Empty square brackets [] indicate page breaks.") say("An asterisk * indicates start of forms for a competition.") for competition in COMPETITIONS: competition.print_rounds_and_matches_header() competition.print_rounds_in_competition() print_banner("End of competitions list.") for competition in COMPETITIONS: competition.print_competition_header_page() competition.print_rounds_and_matches_header() competition.print_rounds_in_competition() competition.print_control_table_form() competition.print_team_results_forms() exit(0)
################[END SOURCE CODE]################