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 2020-10-30

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

# terrain identifiers
TERRAINS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

PRINT_DETAILED_FORMS = True
COMPETITIONS = []
UNDERSCORES = "_" * 4
SCORE_HEADINGS = SCORE_FORMS = ""
HEADINGS = ["WON", "LOST", "POINTS", "OPPONENT", "DIFF"]
COLUMN_WIDTH = 8
COLUMN_SPACER = " " * 2
HEADER_WIDTH = 72
BAR = "*" * HEADER_WIDTH
COMPETITION_SECTION_SEPARATOR = "+"
CONTROL_TABLE_SECTION_SEPARATOR = "@"
TEAM_SECTION_SEPARATOR = "&"

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 round_bracket(round_id):
    s = strr(round_id).rjust(2)
    return strr("Round ", s, ":")


def terrain_bracket(*args):
    s = strr(*args)
    return strr("{", s, "}")


def left_column(*args):
    s = strr(*args)
    return s.ljust(23)


def round_column(*args):
    s = strr(*args)
    return s.ljust(12)

def print_section_header(*args
        , line2=None
        , line3=None
        , section_separator):
    stars = "*"
    text_len = len(BAR) - (len(stars)*2)
    say()
    say(section_separator)
    say(BAR)
    s = strr(*args)
    s = s.center(text_len)
    s = strr(stars, s, stars)
    say(s)
    for line in [line2, line3]:
        if not line: continue
        s = line.center(text_len)
        s = strr(stars, s, stars)
        say(s)

    say(BAR)


def strr(*args):
    x = [str(arg) for arg in args]
    y = "".join(x)
    return y


def say(*args):
    print(strr(*args))


def create_score_lines():
    global SCORE_HEADINGS, SCORE_FORMS
    s = ""
    for heading in HEADINGS:
        contents = heading.center(COLUMN_WIDTH)
        s = s + contents + COLUMN_SPACER
    SCORE_HEADINGS = s

    s = ""
    for heading in HEADINGS:
        contents = "_" * COLUMN_WIDTH
        s = s + contents + COLUMN_SPACER
    SCORE_FORMS = s


############################################################
# 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 = strr(
            "Round Robin competition for ", self.numberOfTeams, " teams")

        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(self):
        terrains_comma_list = " ".join(self.game_terrains)
        sep = " / "
        s = strr(self.numberOfTeams, " teams"
            , sep, self.numberOfRounds, " rounds"
            , sep, "terrains: ", terrains_comma_list)
        print_section_header(self.competition_header
                             , line2 = "COMPETITION ORGANIZATION"
                             , line3=s
                             , section_separator=COMPETITION_SECTION_SEPARATOR
                             )

    def format_team_header(self, team):
        return strr("TEAM ", team.team_id, ".")


    def print_team_schedules(self):
        for team in self.teams:
            s = "RESULTS RECORD FOR " + self.format_team_header(team)
            print_section_header(self.competition_header
                                 , line2=s
                                 , section_separator=TEAM_SECTION_SEPARATOR
                                 )
            say(left_column(), SCORE_HEADINGS)
            for round in team.get_opponents_for_rounds():
                say(round)
            say(left_column("TEAM TOTALS"), SCORE_FORMS)

    def print_rounds_in_competition(self):
        say()
        for round in self.rounds:
            say()
            say(round_bracket(round.round_id))
            for game in round.games:
                say(game.toString())

    def print_control_table_form(self):
        print_section_header(self.competition_header
            , line2="CONTROL TABLE RESULTS RECORD"
            , section_separator=CONTROL_TABLE_SECTION_SEPARATOR
            )
        for team in self.teams:
            say()
            if True:
                s = round_column("Team ", team.team_id)
                s = s + SCORE_HEADINGS
                say(s)
            for round in self.rounds:
                s = round_column(round_bracket(round.round_id))
                s = s + SCORE_FORMS
                say(s)
            say(round_column("TEAM TOTALS"), SCORE_FORMS)

        say()
        say(CONTROL_TABLE_SECTION_SEPARATOR)


############################################################
# 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_bracket(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_bracket(round_id)
                     , " team ", str(opponent.team_id).ljust(2)
                     , " ", terrain_bracket(terrain)
                     , "  "
                     )
            if PRINT_DETAILED_FORMS:
                s = strr(s, SCORE_FORMS)

            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()
    create_score_lines()

    for competition in COMPETITIONS:
        say()
        competition.print_competition_header()
        competition.print_rounds_in_competition()
        competition.print_control_table_form()
        competition.print_team_schedules()

    exit(0)


################[END SOURCE CODE]################