Generate a Match Schedule with Python 3
Question:
I’m struggling with generating a match schedule between teams.
The conditions are:
- 14 teams play 1 game per day (7 games per day) over 26 days.
- Every team play against each other twice. 1 home game and 1 away game.
- Every team play a Home game and an Away game every other day.
- A team can not play against itself. So each team got 13/14 opponents.
I’ve replaced my dates with day 1-26 as key, and my teams with ID’s to illustrate simpler.
Example of outcome:
[
{1: [((7, 8), (6, 9), (5, 10), (4, 11), (3, 12), (2, 13), (1, 14)])]},
{2: [((8, 1), (9, 7), (10, 6), (11, 5), (12, 4), (13, 3), (14, 2)])]},
{3: [((9, 8), (8, 9), (7, 10), (6, 11), (5, 12), (4, 13), (3, 14)])]},
...
]
My current code looks like this:
from collections import deque, OrderedDict
teams_all = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
teams = teams_all[:int(len(teams_all)/2)]
matches = {}
for index, team in enumerate(teams):
# Remove self to not match against self.
opponents = list(teams_all)
opponents.pop(index)
# We reverse and rotate the opponents to give a
# different start opponent for each team.
opponents = deque(opponents)
opponents.reverse()
opponents.rotate(-index)
start_day = 1
end_day = 26
# We only loop 13 times instead of 26, because we schedule
# 2 matches at a time.
for i in range(0, 13):
opponent = opponents[i]
# Init lists
if matches.get(start_day, None) is None:
matches[start_day] = []
if matches.get(end_day, None) is None:
matches[end_day] = []
# We create both the home and away match at the same time
# but with different dates, opposite side of the schedule.
matches[start_day].insert(0, (team, opponent))
matches[end_day].insert(0, (opponent, team))
start_day += 2
end_day -= 2
# Print to console to check result.
od = OrderedDict(sorted(matches.items()))
for key, match in od.items():
print(key, match)
The code above generates the first line correct, but the next rows it always gets 1 duplicate (1 team play 2 matches in 1 day) and because of the duplicate, one team is missing completely.
I’m pretty sure my problem is with how I use opponents.rotate()
. However I’m not sure exactly what I’m doing wrong.
Answers:
Here is a simplified version:
from pprint import pprint as pp
def make_day(num_teams, day):
# using circle algorithm, https://en.wikipedia.org/wiki/Round-robin_tournament#Scheduling_algorithm
assert not num_teams % 2, "Number of teams must be even!"
# generate list of teams
lst = list(range(1, num_teams + 1))
# rotate
day %= (num_teams - 1) # clip to 0 .. num_teams - 2
if day: # if day == 0, no rotation is needed (and using -0 as list index will cause problems)
lst = lst[:1] + lst[-day:] + lst[1:-day]
# pair off - zip the first half against the second half reversed
half = num_teams // 2
return list(zip(lst[:half], lst[half:][::-1]))
def make_schedule(num_teams):
"""
Produce a double round-robin schedule
"""
# number of teams must be even
if num_teams % 2:
num_teams += 1 # add a dummy team for padding
# build first round-robin
schedule = [make_day(num_teams, day) for day in range(num_teams - 1)]
# generate second round-robin by swapping home,away teams
swapped = [[(away, home) for home, away in day] for day in schedule]
return schedule + swapped
def main():
num_teams = int(input("How many teams? "))
schedule = make_schedule(num_teams)
pp(schedule)
if __name__ == "__main__":
main()
which runs like
How many teams? 14
[[(1, 14), (2, 13), (3, 12), (4, 11), (5, 10), (6, 9), (7, 8)],
[(1, 13), (14, 12), (2, 11), (3, 10), (4, 9), (5, 8), (6, 7)],
[(1, 12), (13, 11), (14, 10), (2, 9), (3, 8), (4, 7), (5, 6)],
[(1, 11), (12, 10), (13, 9), (14, 8), (2, 7), (3, 6), (4, 5)],
[(1, 10), (11, 9), (12, 8), (13, 7), (14, 6), (2, 5), (3, 4)],
[(1, 9), (10, 8), (11, 7), (12, 6), (13, 5), (14, 4), (2, 3)],
[(1, 8), (9, 7), (10, 6), (11, 5), (12, 4), (13, 3), (14, 2)],
[(1, 7), (8, 6), (9, 5), (10, 4), (11, 3), (12, 2), (13, 14)],
[(1, 6), (7, 5), (8, 4), (9, 3), (10, 2), (11, 14), (12, 13)],
[(1, 5), (6, 4), (7, 3), (8, 2), (9, 14), (10, 13), (11, 12)],
[(1, 4), (5, 3), (6, 2), (7, 14), (8, 13), (9, 12), (10, 11)],
[(1, 3), (4, 2), (5, 14), (6, 13), (7, 12), (8, 11), (9, 10)],
[(1, 2), (3, 14), (4, 13), (5, 12), (6, 11), (7, 10), (8, 9)],
[(14, 1), (13, 2), (12, 3), (11, 4), (10, 5), (9, 6), (8, 7)],
[(13, 1), (12, 14), (11, 2), (10, 3), (9, 4), (8, 5), (7, 6)],
[(12, 1), (11, 13), (10, 14), (9, 2), (8, 3), (7, 4), (6, 5)],
[(11, 1), (10, 12), (9, 13), (8, 14), (7, 2), (6, 3), (5, 4)],
[(10, 1), (9, 11), (8, 12), (7, 13), (6, 14), (5, 2), (4, 3)],
[(9, 1), (8, 10), (7, 11), (6, 12), (5, 13), (4, 14), (3, 2)],
[(8, 1), (7, 9), (6, 10), (5, 11), (4, 12), (3, 13), (2, 14)],
[(7, 1), (6, 8), (5, 9), (4, 10), (3, 11), (2, 12), (14, 13)],
[(6, 1), (5, 7), (4, 8), (3, 9), (2, 10), (14, 11), (13, 12)],
[(5, 1), (4, 6), (3, 7), (2, 8), (14, 9), (13, 10), (12, 11)],
[(4, 1), (3, 5), (2, 6), (14, 7), (13, 8), (12, 9), (11, 10)],
[(3, 1), (2, 4), (14, 5), (13, 6), (12, 7), (11, 8), (10, 9)],
[(2, 1), (14, 3), (13, 4), (12, 5), (11, 6), (10, 7), (9, 8)]]
Based on Hugh’s answer I was able to generate the complete code. It seems like it is almost always generating alternating Home and Away matches, there are a few exception and I believe this has to do with the rotation of the list – but in my use case almost perfect is pretty good as well.
from pprint import pprint as pp
def make_day(teams, day):
day %= (len(teams)-1)
home_teams = list(teams)
if day:
home_teams = home_teams[:1] + home_teams[-day:] + home_teams[1:-day]
half = len(teams)//2
return list(zip(home_teams[:half], home_teams[half:][::-1]))
def make_schedule(teams):
matches = {day: make_day(teams, day) for day in get_even_days()}
swapped_matches = reschedule_reversed(matches, get_uneven_days())
return {**matches, **swapped_matches}
def reschedule_reversed(matches, days):
schedule = {}
for day in days:
schedule[day] = [(away, home) for home, away in matches[day+1]]
return schedule
def get_even_days():
start_day = 2
days = [start_day+2*i for i in range(13)]
return days
def get_uneven_days():
start_day = 1
days = [start_day+2*i for i in range(13)]
return days
def main():
teams = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
schedule = make_schedule(teams)
pp(schedule)
print(f"{len(schedule)} days")
if __name__ == "__main__":
main()
A rewrite of Marcus’s last complete code in Typescript.
import {zip} from "lodash";
const teams = [ "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T",];
function getEvenDays() {
let startDay = 2;
let days: number[] = [];
for (let i = 0; i < 19; i++) {
days[i] = startDay + 2 * i;
}
return days;
}
function getUnEvenDays() {
let startDay = 1;
let days: number[] = [];
for (let i = 0; i < 19; i++) {
days[i] = startDay + 2 * i;
}
return days;
}
function makeDay(teams, day) {
day %= teams.length - 1;
let homeTeams = teams;
if (day) {
homeTeams = [
...homeTeams.slice(0, 1),
...homeTeams.slice(-day),
...homeTeams.slice(1, -day),
];
}
const half = teams.length / 2;
return zip(homeTeams.slice(0, half), homeTeams.slice(half).reverse());
}
type Game = []
function rescheduleReversed(matches: {}, days: number[]) {
let schedule = {}
for(let day of days){
const games: Game[][] = []
matches[day + 1].forEach((match)=>{
let game = [match[1], match[0]]
games.push(game)
})
schedule[day] = games
}
return schedule
}
function makeSchedule(teams: string[]) {
let matches = {};
getEvenDays().forEach((day) => {
matches[day] = makeDay(teams, day);
});
let swapMatches = rescheduleReversed(matches,getUnEvenDays())
return {...matches, ...swapMatches}
}
const rounds = makeSchedule(teams)
for(let round in rounds){
console.table(rounds[round])
}
I’m struggling with generating a match schedule between teams.
The conditions are:
- 14 teams play 1 game per day (7 games per day) over 26 days.
- Every team play against each other twice. 1 home game and 1 away game.
- Every team play a Home game and an Away game every other day.
- A team can not play against itself. So each team got 13/14 opponents.
I’ve replaced my dates with day 1-26 as key, and my teams with ID’s to illustrate simpler.
Example of outcome:
[
{1: [((7, 8), (6, 9), (5, 10), (4, 11), (3, 12), (2, 13), (1, 14)])]},
{2: [((8, 1), (9, 7), (10, 6), (11, 5), (12, 4), (13, 3), (14, 2)])]},
{3: [((9, 8), (8, 9), (7, 10), (6, 11), (5, 12), (4, 13), (3, 14)])]},
...
]
My current code looks like this:
from collections import deque, OrderedDict
teams_all = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
teams = teams_all[:int(len(teams_all)/2)]
matches = {}
for index, team in enumerate(teams):
# Remove self to not match against self.
opponents = list(teams_all)
opponents.pop(index)
# We reverse and rotate the opponents to give a
# different start opponent for each team.
opponents = deque(opponents)
opponents.reverse()
opponents.rotate(-index)
start_day = 1
end_day = 26
# We only loop 13 times instead of 26, because we schedule
# 2 matches at a time.
for i in range(0, 13):
opponent = opponents[i]
# Init lists
if matches.get(start_day, None) is None:
matches[start_day] = []
if matches.get(end_day, None) is None:
matches[end_day] = []
# We create both the home and away match at the same time
# but with different dates, opposite side of the schedule.
matches[start_day].insert(0, (team, opponent))
matches[end_day].insert(0, (opponent, team))
start_day += 2
end_day -= 2
# Print to console to check result.
od = OrderedDict(sorted(matches.items()))
for key, match in od.items():
print(key, match)
The code above generates the first line correct, but the next rows it always gets 1 duplicate (1 team play 2 matches in 1 day) and because of the duplicate, one team is missing completely.
I’m pretty sure my problem is with how I use opponents.rotate()
. However I’m not sure exactly what I’m doing wrong.
Here is a simplified version:
from pprint import pprint as pp
def make_day(num_teams, day):
# using circle algorithm, https://en.wikipedia.org/wiki/Round-robin_tournament#Scheduling_algorithm
assert not num_teams % 2, "Number of teams must be even!"
# generate list of teams
lst = list(range(1, num_teams + 1))
# rotate
day %= (num_teams - 1) # clip to 0 .. num_teams - 2
if day: # if day == 0, no rotation is needed (and using -0 as list index will cause problems)
lst = lst[:1] + lst[-day:] + lst[1:-day]
# pair off - zip the first half against the second half reversed
half = num_teams // 2
return list(zip(lst[:half], lst[half:][::-1]))
def make_schedule(num_teams):
"""
Produce a double round-robin schedule
"""
# number of teams must be even
if num_teams % 2:
num_teams += 1 # add a dummy team for padding
# build first round-robin
schedule = [make_day(num_teams, day) for day in range(num_teams - 1)]
# generate second round-robin by swapping home,away teams
swapped = [[(away, home) for home, away in day] for day in schedule]
return schedule + swapped
def main():
num_teams = int(input("How many teams? "))
schedule = make_schedule(num_teams)
pp(schedule)
if __name__ == "__main__":
main()
which runs like
How many teams? 14
[[(1, 14), (2, 13), (3, 12), (4, 11), (5, 10), (6, 9), (7, 8)],
[(1, 13), (14, 12), (2, 11), (3, 10), (4, 9), (5, 8), (6, 7)],
[(1, 12), (13, 11), (14, 10), (2, 9), (3, 8), (4, 7), (5, 6)],
[(1, 11), (12, 10), (13, 9), (14, 8), (2, 7), (3, 6), (4, 5)],
[(1, 10), (11, 9), (12, 8), (13, 7), (14, 6), (2, 5), (3, 4)],
[(1, 9), (10, 8), (11, 7), (12, 6), (13, 5), (14, 4), (2, 3)],
[(1, 8), (9, 7), (10, 6), (11, 5), (12, 4), (13, 3), (14, 2)],
[(1, 7), (8, 6), (9, 5), (10, 4), (11, 3), (12, 2), (13, 14)],
[(1, 6), (7, 5), (8, 4), (9, 3), (10, 2), (11, 14), (12, 13)],
[(1, 5), (6, 4), (7, 3), (8, 2), (9, 14), (10, 13), (11, 12)],
[(1, 4), (5, 3), (6, 2), (7, 14), (8, 13), (9, 12), (10, 11)],
[(1, 3), (4, 2), (5, 14), (6, 13), (7, 12), (8, 11), (9, 10)],
[(1, 2), (3, 14), (4, 13), (5, 12), (6, 11), (7, 10), (8, 9)],
[(14, 1), (13, 2), (12, 3), (11, 4), (10, 5), (9, 6), (8, 7)],
[(13, 1), (12, 14), (11, 2), (10, 3), (9, 4), (8, 5), (7, 6)],
[(12, 1), (11, 13), (10, 14), (9, 2), (8, 3), (7, 4), (6, 5)],
[(11, 1), (10, 12), (9, 13), (8, 14), (7, 2), (6, 3), (5, 4)],
[(10, 1), (9, 11), (8, 12), (7, 13), (6, 14), (5, 2), (4, 3)],
[(9, 1), (8, 10), (7, 11), (6, 12), (5, 13), (4, 14), (3, 2)],
[(8, 1), (7, 9), (6, 10), (5, 11), (4, 12), (3, 13), (2, 14)],
[(7, 1), (6, 8), (5, 9), (4, 10), (3, 11), (2, 12), (14, 13)],
[(6, 1), (5, 7), (4, 8), (3, 9), (2, 10), (14, 11), (13, 12)],
[(5, 1), (4, 6), (3, 7), (2, 8), (14, 9), (13, 10), (12, 11)],
[(4, 1), (3, 5), (2, 6), (14, 7), (13, 8), (12, 9), (11, 10)],
[(3, 1), (2, 4), (14, 5), (13, 6), (12, 7), (11, 8), (10, 9)],
[(2, 1), (14, 3), (13, 4), (12, 5), (11, 6), (10, 7), (9, 8)]]
Based on Hugh’s answer I was able to generate the complete code. It seems like it is almost always generating alternating Home and Away matches, there are a few exception and I believe this has to do with the rotation of the list – but in my use case almost perfect is pretty good as well.
from pprint import pprint as pp
def make_day(teams, day):
day %= (len(teams)-1)
home_teams = list(teams)
if day:
home_teams = home_teams[:1] + home_teams[-day:] + home_teams[1:-day]
half = len(teams)//2
return list(zip(home_teams[:half], home_teams[half:][::-1]))
def make_schedule(teams):
matches = {day: make_day(teams, day) for day in get_even_days()}
swapped_matches = reschedule_reversed(matches, get_uneven_days())
return {**matches, **swapped_matches}
def reschedule_reversed(matches, days):
schedule = {}
for day in days:
schedule[day] = [(away, home) for home, away in matches[day+1]]
return schedule
def get_even_days():
start_day = 2
days = [start_day+2*i for i in range(13)]
return days
def get_uneven_days():
start_day = 1
days = [start_day+2*i for i in range(13)]
return days
def main():
teams = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
schedule = make_schedule(teams)
pp(schedule)
print(f"{len(schedule)} days")
if __name__ == "__main__":
main()
A rewrite of Marcus’s last complete code in Typescript.
import {zip} from "lodash";
const teams = [ "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T",];
function getEvenDays() {
let startDay = 2;
let days: number[] = [];
for (let i = 0; i < 19; i++) {
days[i] = startDay + 2 * i;
}
return days;
}
function getUnEvenDays() {
let startDay = 1;
let days: number[] = [];
for (let i = 0; i < 19; i++) {
days[i] = startDay + 2 * i;
}
return days;
}
function makeDay(teams, day) {
day %= teams.length - 1;
let homeTeams = teams;
if (day) {
homeTeams = [
...homeTeams.slice(0, 1),
...homeTeams.slice(-day),
...homeTeams.slice(1, -day),
];
}
const half = teams.length / 2;
return zip(homeTeams.slice(0, half), homeTeams.slice(half).reverse());
}
type Game = []
function rescheduleReversed(matches: {}, days: number[]) {
let schedule = {}
for(let day of days){
const games: Game[][] = []
matches[day + 1].forEach((match)=>{
let game = [match[1], match[0]]
games.push(game)
})
schedule[day] = games
}
return schedule
}
function makeSchedule(teams: string[]) {
let matches = {};
getEvenDays().forEach((day) => {
matches[day] = makeDay(teams, day);
});
let swapMatches = rescheduleReversed(matches,getUnEvenDays())
return {...matches, ...swapMatches}
}
const rounds = makeSchedule(teams)
for(let round in rounds){
console.table(rounds[round])
}