]> git.sesse.net Git - ultimatescore/commitdiff
Check in some scripts based on OR-tools to try to generate good group schedules.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Thu, 25 Oct 2018 15:43:45 +0000 (17:43 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Thu, 25 Oct 2018 15:43:45 +0000 (17:43 +0200)
roster/test.py [new file with mode: 0644]
roster/twofields.py [new file with mode: 0644]
roster/twofields_sat.py [new file with mode: 0644]

diff --git a/roster/test.py b/roster/test.py
new file mode 100644 (file)
index 0000000..dd90b43
--- /dev/null
@@ -0,0 +1,160 @@
+from __future__ import print_function
+import sys
+from ortools.constraint_solver import pywrapcp
+
+def print_solution(sol):
+  for i in range(num_matches):
+    home_team = collector.Value(sol, home_teams[i])
+    away_team = collector.Value(sol, away_teams[i])
+    matchnum = collector.Value(sol, matchnums[i])
+    #print("%2d. %d vs. %d   (matchnum %d)" % (i, home_team, away_team, matchnum))
+    print("%2d. %d vs. %d   (matchnum %2d, excitedness %d)" % (i, home_team, away_team, matchnum, excitedness[matchnum]))
+
+# Creates the solver.
+solver = pywrapcp.Solver("schedule_shifts")
+
+num_teams = 6
+num_matches = (num_teams * (num_teams - 1)) // 2
+
+# Create supermatch variables.
+matchnums = []
+for match_idx in range(num_matches):
+  matchnums.append(solver.IntVar(0, num_matches - 1, "matchnum(%i)" % (match_idx)))
+solver.Add(solver.AllDifferent(matchnums));
+
+# Create list of matches.
+match_num = 0
+excitedness = []
+home_teams_for_match_num = []
+away_teams_for_match_num = []
+for team_idx_1 in range(num_teams):
+  for team_idx_2 in range(num_teams):
+    if team_idx_2 > team_idx_1:
+      home_teams_for_match_num.append(team_idx_1)
+      away_teams_for_match_num.append(team_idx_2)
+      if team_idx_2 - team_idx_1 == 1:
+        excitedness.append(5)
+      elif team_idx_2 - team_idx_1 == 2:
+        excitedness.append(2)
+      else:
+        excitedness.append(0)
+      print("matchnum ", match_num , " plays: ", team_idx_1, " - ", team_idx_2, "  excited: " , excitedness[match_num])
+      match_num = match_num + 1
+
+# Create match variables.
+home_teams = []
+away_teams = []
+for match_idx in range(num_matches):
+  home_teams.append(solver.IntVar(0, num_teams - 1, "home_team_on_match(%i)" % (match_idx)))
+  away_teams.append(solver.IntVar(0, num_teams - 1, "away_team_on_match(%i)" % (match_idx)))
+matches_flat = home_teams + away_teams
+
+for match_num in range(num_matches):
+  # home_teams_var[i] = home_teams[match_num_var[i]]
+  #solver.Add(matchnums[match_num].IndexOf(home_teams) == team_idx_1)  # home_teams[matchnums[match_num]] == team_idx_1
+  #solver.Add(matchnums[match_num].IndexOf(away_teams) == team_idx_2)
+  solver.Add(home_teams[match_num] == solver.Element(home_teams_for_match_num, matchnums[match_num]))
+  solver.Add(away_teams[match_num] == solver.Element(away_teams_for_match_num, matchnums[match_num]))
+
+## A team can never play two matches in a row
+for match_idx in range(num_matches - 1):
+  solver.Add(home_teams[match_idx] != home_teams[match_idx + 1])
+  solver.Add(away_teams[match_idx] != home_teams[match_idx + 1])
+  solver.Add(home_teams[match_idx] != away_teams[match_idx + 1])
+  solver.Add(away_teams[match_idx] != away_teams[match_idx + 1])
+
+# More waiting time is good
+tired_matches = []
+for match_idx in range(num_matches - 2):
+  home_tired = (
+    (home_teams[match_idx] == home_teams[match_idx + 2]) +
+    (home_teams[match_idx] == away_teams[match_idx + 2]))
+  away_tired = (
+    (away_teams[match_idx] == home_teams[match_idx + 2]) +
+    (away_teams[match_idx] == away_teams[match_idx + 2]))
+  tired_matches.append(home_tired)
+  tired_matches.append(away_tired)
+
+  # double-tired is not cool
+  if match_idx < num_matches - 4:
+    home_doubletired = home_tired * (
+      (home_teams[match_idx] == home_teams[match_idx + 4]) +
+      (home_teams[match_idx] == away_teams[match_idx + 4]))
+    away_doubletired = away_tired * (
+      (away_teams[match_idx] == home_teams[match_idx + 4]) +
+      (away_teams[match_idx] == away_teams[match_idx + 4]))
+    tired_matches.append(home_doubletired * 100)
+    tired_matches.append(away_doubletired * 100)
+
+sum_tiredness = solver.Sum(tired_matches)
+
+## TFK can not play the first match
+solver.Add(home_teams[0] != 0)
+solver.Add(away_teams[0] != 0)
+solver.Add(home_teams[1] != 0)
+solver.Add(away_teams[1] != 0)
+
+# Group final comes last
+solver.Add(home_teams[num_matches - 1] == 0)
+solver.Add(away_teams[num_matches - 1] == 1)
+
+# Put the more excitedness last
+sum_excitedness = solver.Sum([(matchnums[match_num].IndexOf(excitedness) * match_num) for match_num in range(num_matches)])
+objective = solver.Maximize(sum_excitedness - 10 * sum_tiredness, 1)
+
+# TODO: AllowedAssignments
+# TODO: multiple fields/groups, objectives of getting TV time (esp. for interesting matches)
+# TODO: objective on getting more interesting matches last
+
+db = solver.Phase(matchnums, solver.CHOOSE_FIRST_UNBOUND,
+                  solver.ASSIGN_MIN_VALUE)
+search_log = solver.SearchLog(1000000, objective)
+#global_limit = solver.TimeLimit(1000)
+global_limit = solver.TimeLimit(100000000)
+
+## Create the solution collector.
+#solution = solver.Assignment()
+#solution.Add(matches_flat)
+#solution.Add(matchnums)
+#
+##collector = solver.AllSolutionCollector(solution)
+##collector = solver.LastSolutionCollector(solution)
+#collector = solver.FirstSolutionCollector(solution)
+#
+#solver.Solve(db, [collector, search_log, objective, global_limit])
+#print("Solutions found:", collector.SolutionCount())
+#print("Time:", solver.WallTime(), "ms")
+#print()
+## Display a few solutions picked at random.
+#a_few_solutions = [0]
+#for sol in a_few_solutions:
+#  print_solution(sol)
+#
+#os.exit(0)
+
+###All
+solver.NewSearch(db, [search_log, objective, global_limit])
+num_solutions = 0
+while solver.NextSolution():
+  #print("objective = ", sum_excitedness.Value())
+  for i in range(num_matches):
+    home_team = home_teams[i].Value()
+    away_team = away_teams[i].Value()
+    matchnum = matchnums[i].Value()
+
+    tiredness = 0
+    if i >= 2:
+      if home_team == home_teams[i - 2].Value():
+        tiredness = tiredness + 1
+      if home_team == away_teams[i - 2].Value():
+        tiredness = tiredness + 1
+      if away_team == home_teams[i - 2].Value():
+        tiredness = tiredness + 1
+      if away_team == away_teams[i - 2].Value():
+        tiredness = tiredness + 1
+    print("%2d. %d vs. %d   (matchnum %2d, excitedness %d, tiredness %d)" % (i, home_team, away_team, matchnum, excitedness[matchnum], tiredness))
+  
+  print()
+  num_solutions += 1
+solver.EndSearch()
diff --git a/roster/twofields.py b/roster/twofields.py
new file mode 100644 (file)
index 0000000..faeb266
--- /dev/null
@@ -0,0 +1,169 @@
+from __future__ import print_function
+import sys
+from ortools.constraint_solver import pywrapcp
+
+
+num_groups = 2  # NOTE: 2 is hard-coded in some places.
+num_teams_per_group = 6
+num_teams = num_teams_per_group * num_teams_per_group
+num_rounds = (num_teams_per_group * (num_teams_per_group - 1)) // 2
+num_matches = num_rounds * num_groups
+
+def excitedness_weight(match_idx):
+  field = match_idx % num_groups
+  match_order = match_idx // num_groups
+  if field == 0:
+    return match_order + 5
+  else:
+    return match_order
+
+def team_name(team_idx):
+  if team_idx < num_teams_per_group:
+    return "A%d" % (team_idx)
+  else:
+    return "B%d" % (team_idx - num_teams_per_group)
+
+solver = pywrapcp.Solver("schedule_games")
+
+# Create match variables.
+matchnums = []
+for match_idx in range(num_matches):
+  matchnums.append(solver.IntVar(0, num_matches - 1, "matchnum(%i)" % (match_idx)))
+solver.Add(solver.AllDifferent(matchnums));
+
+# Create list of matches.
+match_num = 0
+excitedness = []
+home_teams_for_match_num = []
+away_teams_for_match_num = []
+for group in range(num_groups):
+  for team_idx_1 in range(num_teams_per_group):
+    for team_idx_2 in range(num_teams_per_group):
+      if team_idx_2 > team_idx_1:
+        real_team_idx_1 = team_idx_1 + num_teams_per_group * group
+        real_team_idx_2 = team_idx_2 + num_teams_per_group * group
+        home_teams_for_match_num.append(real_team_idx_1)
+        away_teams_for_match_num.append(real_team_idx_2)
+        if team_idx_2 - team_idx_1 == 1:
+          excitedness.append(5)
+        elif team_idx_2 - team_idx_1 == 2:
+          excitedness.append(2)
+        else:
+          excitedness.append(0)
+        print("matchnum %2d: %2d vs. %2d, excited: %d" % (match_num, real_team_idx_1, real_team_idx_2, excitedness[match_num]))
+        match_num = match_num + 1
+
+# Create match variables.
+home_teams = []
+away_teams = []
+for match_idx in range(num_matches):
+  home_teams.append(solver.IntVar(0, num_teams - 1, "home_team_on_match(%i)" % (match_idx)))
+  away_teams.append(solver.IntVar(0, num_teams - 1, "away_team_on_match(%i)" % (match_idx)))
+matches_flat = home_teams + away_teams
+
+for match_num in range(num_matches):
+  solver.Add(home_teams[match_num] == solver.Element(home_teams_for_match_num, matchnums[match_num]))
+  solver.Add(away_teams[match_num] == solver.Element(away_teams_for_match_num, matchnums[match_num]))
+
+# Fields always play opposing groups (FIXME?)
+for round_idx in range(num_rounds):
+  solver.Add((matchnums[round_idx * 2 + 0] >= num_rounds) != (matchnums[round_idx * 2 + 1] >= num_rounds))
+
+# A team can never play on the same field at the same time
+#for match_idx in range(num_rounds):
+#  solver.Add(home_teams[match_idx * 2 + 0] != home_teams[match_idx * 2 + 1])
+#  solver.Add(home_teams[match_idx * 2 + 0] != away_teams[match_idx * 2 + 1])
+#  solver.Add(away_teams[match_idx * 2 + 0] != home_teams[match_idx * 2 + 1])
+#  solver.Add(away_teams[match_idx * 2 + 0] != away_teams[match_idx * 2 + 1])
+
+plays_in_round = {}
+for team_idx in range(num_teams):
+  plays_in_round[team_idx] = {}
+  for round_idx in range(num_rounds):
+    plays_in_round[team_idx][round_idx] = (
+       (home_teams[round_idx * 2 + 0] == team_idx) +
+       (home_teams[round_idx * 2 + 1] == team_idx) +
+       (away_teams[round_idx * 2 + 0] == team_idx) +
+       (away_teams[round_idx * 2 + 1] == team_idx))
+
+# A team can never play two matches in a row
+for round_idx in range(num_rounds - 1):
+  for team_idx in range(num_teams):
+    solver.Add(plays_in_round[team_idx][round_idx] + plays_in_round[team_idx][round_idx + 1] <= 1)
+
+# More waiting time is good
+tired_matches = []
+for round_idx in range(num_rounds - 2):
+  for team_idx in range(num_teams):
+    tired = plays_in_round[team_idx][round_idx] + plays_in_round[team_idx][round_idx + 2] >= 2
+    tired_matches.append(tired)
+sum_tiredness = solver.Sum(tired_matches)
+
+# Double-tired is not cool
+for round_idx in range(num_rounds - 4):
+  for team_idx in range(num_teams):
+    #doubletired = plays_in_round[team_idx][round_idx] + plays_in_round[team_idx][round_idx + 2] + plays_in_round[team_idx][round_idx + 4] >= 3
+    #tired_matches.append(doubletired * 100)
+    solver.Add(plays_in_round[team_idx][round_idx] + plays_in_round[team_idx][round_idx + 2] + plays_in_round[team_idx][round_idx + 4] < 3)
+
+
+## TFK can not play the first two matches
+solver.Add(home_teams[0] != 0)
+solver.Add(away_teams[0] != 0)
+solver.Add(home_teams[1] != 0)
+solver.Add(away_teams[1] != 0)
+solver.Add(home_teams[2] != 0)
+solver.Add(away_teams[2] != 0)
+solver.Add(home_teams[3] != 0)
+solver.Add(away_teams[3] != 0)
+
+# Group finals come last, and on the stream field.
+solver.Add((matchnums[num_matches - 2] == 0) + (matchnums[num_matches - 2] == num_rounds) >= 1)
+solver.Add((matchnums[num_matches - 4] == 0) + (matchnums[num_matches - 4] == num_rounds) >= 1)
+
+# Put the more exciting games later, and on stream fields
+sum_excitedness = solver.Sum([(matchnums[match_num].IndexOf(excitedness) * excitedness_weight(match_num)) for match_num in range(num_matches)])
+objective = solver.Maximize(sum_excitedness - 10 * sum_tiredness, 1)
+
+db = solver.Phase(matchnums, solver.CHOOSE_FIRST_UNBOUND,
+                  solver.ASSIGN_MIN_VALUE)
+search_log = solver.SearchLog(1000000, objective)
+#global_limit = solver.TimeLimit(1000)
+global_limit = solver.TimeLimit(100000000)
+
+solver.NewSearch(db, [search_log, objective, global_limit])
+while solver.NextSolution():
+  recently_played_0 = [False for team_idx in range(num_teams)]
+  recently_played_1 = [False for team_idx in range(num_teams)]
+  recently_played_2 = [False for team_idx in range(num_teams)]
+  recently_played_3 = [False for team_idx in range(num_teams)]
+  for i in range(num_matches // num_groups):
+    recently_played_4 = [False for team_idx in range(num_teams)]
+    print("%2d. " % (i), end='')
+    for g in range(num_groups):
+      j = i * num_groups + g
+      home_team = home_teams[j].Value()
+      away_team = away_teams[j].Value()
+      matchnum = matchnums[j].Value()
+
+      tiredness = 0
+      if recently_played_2[home_team]:
+        tiredness = tiredness + 1
+        if recently_played_0[home_team]:
+          tiredness = tiredness + 100
+      if recently_played_2[away_team]:
+        tiredness = tiredness + 1
+        if recently_played_0[away_team]:
+          tiredness = tiredness + 100
+      recently_played_4[home_team] = True
+      recently_played_4[away_team] = True
+      print("%s vs. %s  (matchnum %2d, excitedness %d*%2d, tiredness %3d)  " % (team_name(home_team), team_name(away_team), matchnum, excitedness[matchnum], excitedness_weight(j), tiredness), end='')
+    print()
+    recently_played_0 = recently_played_1
+    recently_played_1 = recently_played_2
+    recently_played_2 = recently_played_3
+    recently_played_3 = recently_played_4
+  
+  print()
+solver.EndSearch()
diff --git a/roster/twofields_sat.py b/roster/twofields_sat.py
new file mode 100644 (file)
index 0000000..e95a8ba
--- /dev/null
@@ -0,0 +1,275 @@
+from __future__ import print_function
+import sys
+import time
+from ortools.sat.python import cp_model
+
+
+
+num_groups = 2  # NOTE: 2 is hard-coded in some places.
+num_teams_per_group = 6
+num_teams = num_teams_per_group * num_groups
+num_rounds = (num_teams_per_group * (num_teams_per_group - 1)) // 2
+num_matches = num_rounds * num_groups
+
+class SolutionPrinterWithObjective(cp_model.CpSolverSolutionCallback):
+  def __init__(self, home_teams, away_teams, matchnums, objective):
+    cp_model.CpSolverSolutionCallback.__init__(self)
+    self.__solution_count = 0
+    self.__start_time = time.time()
+    self.__home_teams = home_teams
+    self.__away_teams = away_teams
+    self.__matchnums = matchnums
+    self.__objective = objective
+
+  def OnSolutionCallback(self):
+    current_time = time.time()
+    self.__solution_count += 1
+    print('Solution %i, time = %f s, objective = %d' %
+          (self.__solution_count, current_time - self.__start_time, self.Value(self.__objective)))
+    num_times_on_stream = [0 for team_idx in range(num_teams)]
+    num_times_tired = [0 for team_idx in range(num_teams)]
+    recently_played_0 = [False for team_idx in range(num_teams)]
+    recently_played_1 = [False for team_idx in range(num_teams)]
+    recently_played_2 = [False for team_idx in range(num_teams)]
+    recently_played_3 = [False for team_idx in range(num_teams)]
+    for i in range(num_matches // num_groups):
+      recently_played_4 = [False for team_idx in range(num_teams)]
+      print("%2d. " % (i), end='')
+      for g in range(num_groups):
+        j = i * num_groups + g
+        home_team = self.Value(self.__home_teams[j])
+        away_team = self.Value(self.__away_teams[j])
+        matchnum = self.Value(self.__matchnums[j])
+
+        if g == 0:
+          num_times_on_stream[home_team] = num_times_on_stream[home_team] + 1
+          num_times_on_stream[away_team] = num_times_on_stream[away_team] + 1
+
+        tiredness = 0
+        if recently_played_2[home_team]:
+          tiredness = tiredness + 1
+          num_times_tired[home_team] = num_times_tired[home_team] + 1
+          if recently_played_0[home_team]:
+            tiredness = tiredness + 100
+        if recently_played_2[away_team]:
+          tiredness = tiredness + 1
+          num_times_tired[away_team] = num_times_tired[away_team] + 1
+          if recently_played_0[away_team]:
+            tiredness = tiredness + 100
+        recently_played_4[home_team] = True
+        recently_played_4[away_team] = True
+   
+        print("%s vs. %s  (matchnum %2d, excitedness %d*%2d, tiredness %3d)  " % (team_name(home_team), team_name(away_team), matchnum, excitedness[matchnum], excitedness_weight(j), tiredness), end='')
+      print()
+      recently_played_0 = recently_played_1
+      recently_played_1 = recently_played_2
+      recently_played_2 = recently_played_3
+      recently_played_3 = recently_played_4
+    print()
+    print("Number of times on stream: ", end='')
+    print(", ".join(["%s %d" % (team_name(team_idx), num_times_on_stream[team_idx]) for team_idx in range(num_teams)]))
+    print("Number of times tired: ", end='')
+    print(", ".join(["%s %d" % (team_name(team_idx), num_times_tired[team_idx]) for team_idx in range(num_teams)]))
+    print("Stream opponents for non-top-teams:")
+    for team_idx in (2, 3, 4, 5, 8, 9, 10, 11):
+      opp = []
+      for i in range(num_matches // num_groups):
+        home_team = self.Value(self.__home_teams[i * 2])
+        away_team = self.Value(self.__away_teams[i * 2])
+        mark = ""
+        if abs(home_team - away_team) == 1:
+          mark = "*"
+        if team_idx == home_team:
+          opp.append(team_name(away_team) + mark)
+        if team_idx == away_team:
+          opp.append(team_name(home_team) + mark)
+      print("  %s: %s" % (team_name(team_idx), ", ".join(sorted(opp))))
+       
+
+def excitedness_weight(match_idx):
+  field = match_idx % num_groups
+  match_order = match_idx // num_groups
+  if field == 0:
+    return match_order + 5
+  else:
+    return match_order
+
+def team_name(team_idx):
+  if team_idx < num_teams_per_group:
+    return "A%d" % (team_idx)
+  else:
+    return "B%d" % (team_idx - num_teams_per_group)
+
+model = cp_model.CpModel()
+
+# Create match variables.
+matchnums = []
+for match_idx in range(num_matches):
+  matchnums.append(model.NewIntVar(0, num_matches - 1, "matchnum(%i)" % (match_idx)))
+model.AddAllDifferent(matchnums)
+
+# Create list of matches.
+match_idx = 0
+excitedness = []
+home_teams_for_match_num = []
+away_teams_for_match_num = []
+for group in range(num_groups):
+  for team_idx_1 in range(num_teams_per_group):
+    for team_idx_2 in range(num_teams_per_group):
+      if team_idx_2 > team_idx_1:
+        real_team_idx_1 = team_idx_1 + num_teams_per_group * group
+        real_team_idx_2 = team_idx_2 + num_teams_per_group * group
+        home_teams_for_match_num.append(real_team_idx_1)
+        away_teams_for_match_num.append(real_team_idx_2)
+        if team_idx_2 - team_idx_1 == 1:
+          excitedness.append(5)
+        elif team_idx_2 - team_idx_1 == 2:
+          excitedness.append(2)
+        else:
+          excitedness.append(0)
+        print("matchnum %2d: %2d vs. %2d, excited: %d" % (match_idx, real_team_idx_1, real_team_idx_2, excitedness[match_idx]))
+        match_idx = match_idx + 1
+
+# Create match variables.
+home_teams = []
+away_teams = []
+for match_idx in range(num_matches):
+  home_teams.append(model.NewIntVar(0, num_teams - 1, "home_team_match%i" % (match_idx)))
+  away_teams.append(model.NewIntVar(0, num_teams - 1, "away_team_match%i" % (match_idx)))
+matches_flat = home_teams + away_teams
+
+for match_idx in range(num_matches):
+  model.AddElement(matchnums[match_idx], home_teams_for_match_num, home_teams[match_idx])
+  model.AddElement(matchnums[match_idx], away_teams_for_match_num, away_teams[match_idx])
+
+# Boolean variables
+home_team_in_match_x_is_y = [[
+      model.NewBoolVar('home_team_in_match_%d_is_%d' % (match_idx, team_idx)) for team_idx in range(num_teams)
+] for match_idx in range(num_matches)]
+
+away_team_in_match_x_is_y = [[
+      model.NewBoolVar('away_team_in_match_%d_is_%d' % (match_idx, team_idx)) for team_idx in range(num_teams)
+] for match_idx in range(num_matches)]
+
+match_x_has_num_y = [[
+      model.NewBoolVar('match_%d_has_number_%d' % (a, b)) for a in range(num_matches)
+] for b in range(num_matches)]
+
+for match_idx in range(num_matches):
+  model.AddMapDomain(matchnums[match_idx], match_x_has_num_y[match_idx])
+  model.AddMapDomain(home_teams[match_idx], home_team_in_match_x_is_y[match_idx])
+  model.AddMapDomain(away_teams[match_idx], away_team_in_match_x_is_y[match_idx])
+
+# Fields always play opposing groups (FIXME?)
+for round_idx in range(num_rounds):
+  field_0_is_group_0 = model.NewBoolVar('field_0_round_%d_is_group_0' % (round_idx))
+  model.AddMaxEquality(field_0_is_group_0, [match_x_has_num_y[round_idx * 2 + 0][match_idx] for match_idx in range(num_rounds)])
+  field_1_is_group_0 = model.NewBoolVar('field_1_round_%d_is_group_0' % (round_idx))
+  model.AddMaxEquality(field_1_is_group_0, [match_x_has_num_y[round_idx * 2 + 1][match_idx] for match_idx in range(num_rounds)])
+  model.AddBoolXOr([field_0_is_group_0, field_1_is_group_0])
+
+# A team can never play on the same field at the same time
+#for team_idx in range(num_teams):
+#  for round_idx in range(num_rounds):
+#    plays_on_field_0 = model.NewBoolVar('plays_on_field0_t%d_r%d' % (team_idx, round_idx))
+#    model.AddMaxEquality(plays_on_field_0, [
+#        home_team_in_match_x_is_y[round_idx * 2 + 0][team_idx],
+#        away_team_in_match_x_is_y[round_idx * 2 + 0][team_idx]])
+#    plays_on_field_1 = model.NewBoolVar('plays_on_field1_t%d_r%d' % (team_idx, round_idx))
+#    model.AddMaxEquality(plays_on_field_1, [
+#        home_team_in_match_x_is_y[round_idx * 2 + 1][team_idx],
+#        away_team_in_match_x_is_y[round_idx * 2 + 1][team_idx]])
+#    model.AddBoolOr([plays_on_field_0.Not(), plays_on_field_1.Not()])
+
+plays_in_round = {}
+for team_idx in range(num_teams):
+  plays_in_round[team_idx] = {}
+  for round_idx in range(num_rounds):
+    plays_in_round[team_idx][round_idx] = model.NewBoolVar('plays_in_round_t%d_r%d' % (team_idx, round_idx))
+    model.AddMaxEquality(plays_in_round[team_idx][round_idx], [
+        home_team_in_match_x_is_y[round_idx * 2 + 0][team_idx],
+        home_team_in_match_x_is_y[round_idx * 2 + 1][team_idx],
+        away_team_in_match_x_is_y[round_idx * 2 + 0][team_idx],
+        away_team_in_match_x_is_y[round_idx * 2 + 1][team_idx]])
+
+# A team can never play two matches in a row
+for round_idx in range(num_rounds - 1):
+  for team_idx in range(num_teams):
+    model.AddBoolOr([plays_in_round[team_idx][round_idx].Not(), plays_in_round[team_idx][round_idx + 1].Not()])
+# Also, double-tired is not cool
+#for round_idx in range(num_rounds - 4):
+#  for team_idx in range(num_teams):
+#    model.AddBoolOr([plays_in_round[team_idx][round_idx].Not(), plays_in_round[team_idx][round_idx + 2].Not(), plays_in_round[team_idx][round_idx + 4].Not()])
+# 
+# More waiting time is good
+#tired_matches = []
+#for round_idx in range(num_rounds - 2):
+#  for team_idx in range(num_teams):
+#    tired = model.NewBoolVar('team_%d_is_tired_in_round_%d' % (team_idx, round_idx))
+#    model.AddMinEquality(tired, [plays_in_round[team_idx][round_idx], plays_in_round[team_idx][round_idx + 2]])
+#    tired_matches.append(tired)
+#sum_tiredness = sum(tired_matches)
+
+# Each team gets play-rest-play exactly once, for fairness
+for team_idx in range(num_teams):
+  tired_matches = []
+  for round_idx in range(num_rounds - 2):
+    tired = model.NewBoolVar('team_%d_is_tired_in_round_%d' % (team_idx, round_idx))
+    model.AddMinEquality(tired, [plays_in_round[team_idx][round_idx], plays_in_round[team_idx][round_idx + 2]])
+    tired_matches.append(tired)
+  model.Add(sum(tired_matches) <= 1)
+sum_tiredness = 0
+
+# TFK can not play the first two matches
+model.AddBoolAnd([plays_in_round[0][0].Not(), plays_in_round[0][1].Not()])
+
+# Group finals come last, and on the stream field.
+model.AddBoolOr([match_x_has_num_y[num_matches - 2][0], match_x_has_num_y[num_matches - 2][num_rounds]])
+model.AddBoolOr([match_x_has_num_y[num_matches - 4][0], match_x_has_num_y[num_matches - 4][num_rounds]])
+
+# Count how many times each team has been on stream.
+stream_penalties = []
+for team_idx in range(num_teams):
+  playing_on_stream = []
+  for round_idx in range(num_rounds):
+    s = model.NewBoolVar('team_%d_plays_on_stream_in_round_%d' % (team_idx, round_idx))
+    model.AddMaxEquality(s, [
+        home_team_in_match_x_is_y[round_idx * 2 + 0][team_idx],
+        away_team_in_match_x_is_y[round_idx * 2 + 0][team_idx]])
+    playing_on_stream.append(s)
+  times_on_stream_this_team = sum(playing_on_stream)
+  model.Add(times_on_stream_this_team >= 1)
+
+  times_stream_var = model.NewIntVar(0, num_teams_per_group, "team_%d_stream_count" % (team_idx))
+  model.Add(times_stream_var == times_on_stream_this_team)
+  #model.Add(times_on_stream_this_team <= 4)
+
+  is_n_times_on_stream = [
+    model.NewBoolVar('team_%d_is_%d_times_on_stream' % (team_idx, i)) for i in range(num_teams_per_group)
+  ]
+  model.AddMapDomain(times_stream_var, is_n_times_on_stream)
+  stream_penalties.append(is_n_times_on_stream[1] * -50)
+  stream_penalties.append(is_n_times_on_stream[4] * -10)
+  stream_penalties.append(is_n_times_on_stream[5] * -50)
+
+# Make sure each team has at least one exciting match on stream.
+#for team_idx in range(team_idx):
+#  stream_matches_for_this_team = []
+#  for round_idx in range(num_rounds):
+#    stream_matches_for_this_team.append(home_team_in_match_x_is_y[round_idx * 2 + 0][team_idx] * excitedness_weight(round_idx * 2 + 0))
+#    stream_matches_for_this_team.append(away_team_in_match_x_is_y[round_idx * 2 + 0][team_idx] * excitedness_weight(round_idx * 2 + 0))
+
+# Put the more exciting games later, and on stream fields
+excitement = []
+for round_idx in range(num_rounds):
+  for match_idx in range(match_idx):
+    excitement.append(match_x_has_num_y[round_idx * 2 + 0][match_idx] * excitedness[match_idx] * excitedness_weight(round_idx * 2 + 0))
+    excitement.append(match_x_has_num_y[round_idx * 2 + 1][match_idx] * excitedness[match_idx] * excitedness_weight(round_idx * 2 + 1))
+sum_excitement = sum(excitement)
+objective = sum_excitement - 30 * sum_tiredness + 3 * sum(stream_penalties)
+model.Maximize(objective)
+
+solver = cp_model.CpSolver()
+solution_printer = SolutionPrinterWithObjective(home_teams, away_teams, matchnums, objective)
+status = solver.SolveWithSolutionCallback(model, solution_printer)