Scheduling games in a tournament #3444
-
Hello, please bear with me - I'm quite new to or-tools/constraint programming. :) Problem StatementI need to schedule a set of games (consisting of 2 teams each) on 1 or more fields in a tournament. I have already solved the 1st part of the problem by splitting a list of teams into groups, then pairing each team in the group with each other (a match) and then placing these matches in different fields where they need to be played. Teams might be assigned to play on more than 1 field. All matches have the same length time-wise. ConstraintsThe part missing is ordering / scheduling the games such that: Objective(s)Further more I need an objective that optimizes how/when a team is playing, such that each particular team are as evenly distributed as possible throughout the day (this is regardless of which field the team plays on). This is to avoid having a team play all their e.g. 4 matches in the morning, and then have the rest of the day off. We need to optimize that the teams are playing at a somewhat evenly interval. This should also minimize the risk of a team playing 2 games in consecutive time slots, but if I'm mistaken and that's not implicitly incorporated in that objective I would also need another objective to try and minimize the risk of that happening. I've tried come up with a solution for this for many days now, but I'm struggling with actually making it work. Here is the current code I have made: Python code#!/usr/bin/python3
from ortools.sat.python import cp_model
import argparse
import json
import pprint
pp = pprint.PrettyPrinter(indent=4)
# 1st dimension = field
# 2nd dimension = a match/game of 2 teams
# Simpler tournament
# team_games = [
# [
# [0, 1],
# [0, 2],
# [0, 3],
# [5, 6],
# [5, 7],
# [7, 6],
# ],
# [
# [4, 5],
# [4, 6],
# [4, 7],
# [1, 2],
# [1, 3],
# [2, 3],
# ],
# ]
# Advanced tournament
team_games = [
[
[0, 1],
[0, 2],
[0, 3],
[3, 1],
[2, 1],
[3, 4],
[2, 3],
[1, 4],
[2, 4],
[0, 4],
[5, 6],
[5, 7],
[5, 8],
[9, 10],
[11, 10],
[9, 12],
[11, 12],
[9, 11],
[12, 10]
],
[
[13, 14],
[15, 14],
[13, 16],
[17, 16],
[17, 14],
[16, 14],
[17, 13],
[15, 17],
[15, 16],
[15, 13],
[6, 7],
[7, 8],
[6, 8],
[18, 19],
[18, 20],
[19, 20],
[21, 18],
[21, 19],
[21, 20]
]
]
num_fields = len(team_games)
all_fields = range(num_fields)
all_teams = list(
set([team for field in team_games for teams in field for team in teams]))
model = cp_model.CpModel()
# Make variable for each fields' slots's possible games
# games[(f, s, g)] = 1, that particular game g is to be played on field f in time slot s
max_num_games = 0
games = {}
for f in all_fields:
num_games = len(team_games[f])
all_game_slots = range(num_games)
all_games = range(num_games)
max_num_games = max(max_num_games, num_games)
for s in all_game_slots:
for g in all_games:
games[(f, s, g)] = model.NewBoolVar(f'game_{f}_{s}_{g}')
# Make sure the time slot s has exactly 1 game scheduled to be played.
for s in all_game_slots:
model.AddExactlyOne(games[(f, s, g)] for g in all_games)
# Make sure the game g is assigned/scheduled to exactly 1 time slot.
for g in all_games:
model.AddExactlyOne(games[(f, s, g)] for s in all_game_slots)
# Create pairs of the same time-slots on the different fields.
# We should use it to make sure only a team is not assigned to that more than on 1 of those time slots (regardless of the field)
field_games_pairs = []
for field in all_fields:
num_games = len(team_games[field])
all_game_slots = range(num_games)
all_games = range(num_games)
for game_slot in all_game_slots:
unique_teams = []
for f in all_fields:
if game_slot < len(team_games[f]):
unique_teams.append((f, game_slot))
if unique_teams not in field_games_pairs:
field_games_pairs.append(unique_teams)
# Create a mapping between the team to the game/match they are playing in.
games_by_team = {}
for f in all_fields:
for s, (home, away) in enumerate(team_games[f]):
if not home in games_by_team:
games_by_team[home] = []
if not away in games_by_team:
games_by_team[away] = []
games_by_team[home].append((f, s))
games_by_team[away].append((f, s))
# Create variables for each team,
# which links it together with the game's field+time-slots
teams_assigned_vars = {}
for t in all_teams:
for f in all_fields:
num_games = len(team_games[f])
all_game_slots = range(num_games)
all_games = range(num_games)
for s in all_game_slots:
team_assign_var = model.NewBoolVar(f'team_{t}_{f}_{s}')
model.Add(team_assign_var == sum(
games[(f, s, g)] for g in all_games))
teams_assigned_vars[(t, f, s)] = team_assign_var
# Constrain team to maximally play 1 game in
# the possible (field, time slot) pairs
for pairs in field_games_pairs:
for t in all_teams:
possible_pairs = []
for f, s in pairs:
assigned_fields = set([t_f for t_f, t_s in games_by_team[t]])
if len(assigned_fields) > 1 and f in assigned_fields:
possible_pairs.append((f, s))
if len(possible_pairs) == 0:
continue
# We are down here if we know that the team t, is supposed to play on multiple fields.
team_restriction_vars = {}
for f, s in possible_pairs:
for g in range(len(team_games[f])):
# Setup relationship between the specific game played on (f, s)
# and the team assigned on (f, s)
# If this is the case, v==True.
v = model.NewBoolVar('v')
model.AddMultiplicationEquality(
v,
[
games[(f, s, g)],
teams_assigned_vars[(t, f, s)]
]
)
team_restriction_vars[(t, f, s)] = v
# Based on the intermediary booleans, we can constrain those
# to add most 1 is true for the particular team on the possible pairs of (f, s)
model.AddAtMostOne(
team_restriction_vars[(t, f, s)] for f, s in possible_pairs
)
# Objective
# Minimize sum of each game index,
# this should try and equally divide all games into the slots/indexes
max_index_balancing_integers = []
for team, game_indexes in games_by_team.items():
max_index_balancing_integers.append(
sum([x + 1 for x in range(max_num_games - len(game_indexes), max_num_games)]))
max_index_balancing_integer = max(max_index_balancing_integers)
index_balancing_vars = []
for team, game_indexes in games_by_team.items():
index_sum_balancing = []
for f in all_fields:
index_sum_var = model.NewIntVar(0, max_index_balancing_integer, 'v')
sum_index = []
for s in range(len(team_games[f])):
for g in game_indexes:
if (f, s, g) in games:
sum_index.append(games[(f, s, g)] * (1 + s))
model.Add(index_sum_var == sum(sum_index))
index_sum_balancing.append(index_sum_var)
max_field_index_balancing_var = model.NewIntVar(
0, max_index_balancing_integer, 'max_index_balancing')
model.AddMaxEquality(max_field_index_balancing_var, index_sum_balancing)
model.Minimize(sum(index_balancing_vars))
solver = cp_model.CpSolver()
# solver.parameters.linearization_level = 1
status = solver.Solve(model)
if (status != cp_model.OPTIMAL) and (status != cp_model.FEASIBLE):
exit()
result = {}
for f in all_fields:
num_games = len(team_games[f])
all_game_slots = range(num_games)
all_games = range(num_games)
if f not in result:
result[f] = []
for s in all_game_slots:
for g in all_games:
if solver.Value(games[(f, s, g)]) > 0:
result[f].append(team_games[f][g])
pp.pprint(result) Test run resultResult:
As you can see this produced at least one double-scheduled team (6) which is both assigned a time to play in f0 s11 and f1 s11. (match Please let me know if you have any suggestion to this a better way, as I'm not feeling like I'm getting good results. :) Thank you for your helping me! :) |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
recall me https://github.com/Mizux/tournament |
Beta Was this translation helpful? Give feedback.
-
have you looked at: https://github.com/google/or-tools/blob/stable/examples/cpp/sports_scheduling_sat.cc |
Beta Was this translation helpful? Give feedback.
have you looked at: https://github.com/google/or-tools/blob/stable/examples/cpp/sports_scheduling_sat.cc