Round-Robin Python program

[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]################