]> git.sesse.net Git - ultimatescore/blob - roster/twofields_sat.py
e95a8ba080952f876bb6acafd5d256027b161dd5
[ultimatescore] / roster / twofields_sat.py
1 from __future__ import print_function
2 import sys
3 import time
4 from ortools.sat.python import cp_model
5
6
7
8 num_groups = 2  # NOTE: 2 is hard-coded in some places.
9 num_teams_per_group = 6
10 num_teams = num_teams_per_group * num_groups
11 num_rounds = (num_teams_per_group * (num_teams_per_group - 1)) // 2
12 num_matches = num_rounds * num_groups
13
14 class SolutionPrinterWithObjective(cp_model.CpSolverSolutionCallback):
15   def __init__(self, home_teams, away_teams, matchnums, objective):
16     cp_model.CpSolverSolutionCallback.__init__(self)
17     self.__solution_count = 0
18     self.__start_time = time.time()
19     self.__home_teams = home_teams
20     self.__away_teams = away_teams
21     self.__matchnums = matchnums
22     self.__objective = objective
23
24   def OnSolutionCallback(self):
25     current_time = time.time()
26     self.__solution_count += 1
27     print('Solution %i, time = %f s, objective = %d' %
28           (self.__solution_count, current_time - self.__start_time, self.Value(self.__objective)))
29     num_times_on_stream = [0 for team_idx in range(num_teams)]
30     num_times_tired = [0 for team_idx in range(num_teams)]
31     recently_played_0 = [False for team_idx in range(num_teams)]
32     recently_played_1 = [False for team_idx in range(num_teams)]
33     recently_played_2 = [False for team_idx in range(num_teams)]
34     recently_played_3 = [False for team_idx in range(num_teams)]
35     for i in range(num_matches // num_groups):
36       recently_played_4 = [False for team_idx in range(num_teams)]
37       print("%2d. " % (i), end='')
38       for g in range(num_groups):
39         j = i * num_groups + g
40         home_team = self.Value(self.__home_teams[j])
41         away_team = self.Value(self.__away_teams[j])
42         matchnum = self.Value(self.__matchnums[j])
43
44         if g == 0:
45           num_times_on_stream[home_team] = num_times_on_stream[home_team] + 1
46           num_times_on_stream[away_team] = num_times_on_stream[away_team] + 1
47
48         tiredness = 0
49         if recently_played_2[home_team]:
50           tiredness = tiredness + 1
51           num_times_tired[home_team] = num_times_tired[home_team] + 1
52           if recently_played_0[home_team]:
53             tiredness = tiredness + 100
54         if recently_played_2[away_team]:
55           tiredness = tiredness + 1
56           num_times_tired[away_team] = num_times_tired[away_team] + 1
57           if recently_played_0[away_team]:
58             tiredness = tiredness + 100
59         recently_played_4[home_team] = True
60         recently_played_4[away_team] = True
61    
62         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='')
63       print()
64       recently_played_0 = recently_played_1
65       recently_played_1 = recently_played_2
66       recently_played_2 = recently_played_3
67       recently_played_3 = recently_played_4
68     print()
69     print("Number of times on stream: ", end='')
70     print(", ".join(["%s %d" % (team_name(team_idx), num_times_on_stream[team_idx]) for team_idx in range(num_teams)]))
71     print("Number of times tired: ", end='')
72     print(", ".join(["%s %d" % (team_name(team_idx), num_times_tired[team_idx]) for team_idx in range(num_teams)]))
73     print("Stream opponents for non-top-teams:")
74     for team_idx in (2, 3, 4, 5, 8, 9, 10, 11):
75       opp = []
76       for i in range(num_matches // num_groups):
77         home_team = self.Value(self.__home_teams[i * 2])
78         away_team = self.Value(self.__away_teams[i * 2])
79         mark = ""
80         if abs(home_team - away_team) == 1:
81           mark = "*"
82         if team_idx == home_team:
83           opp.append(team_name(away_team) + mark)
84         if team_idx == away_team:
85           opp.append(team_name(home_team) + mark)
86       print("  %s: %s" % (team_name(team_idx), ", ".join(sorted(opp))))
87        
88
89 def excitedness_weight(match_idx):
90   field = match_idx % num_groups
91   match_order = match_idx // num_groups
92   if field == 0:
93     return match_order + 5
94   else:
95     return match_order
96
97 def team_name(team_idx):
98   if team_idx < num_teams_per_group:
99     return "A%d" % (team_idx)
100   else:
101     return "B%d" % (team_idx - num_teams_per_group)
102
103 model = cp_model.CpModel()
104
105 # Create match variables.
106 matchnums = []
107 for match_idx in range(num_matches):
108   matchnums.append(model.NewIntVar(0, num_matches - 1, "matchnum(%i)" % (match_idx)))
109 model.AddAllDifferent(matchnums)
110
111 # Create list of matches.
112 match_idx = 0
113 excitedness = []
114 home_teams_for_match_num = []
115 away_teams_for_match_num = []
116 for group in range(num_groups):
117   for team_idx_1 in range(num_teams_per_group):
118     for team_idx_2 in range(num_teams_per_group):
119       if team_idx_2 > team_idx_1:
120         real_team_idx_1 = team_idx_1 + num_teams_per_group * group
121         real_team_idx_2 = team_idx_2 + num_teams_per_group * group
122         home_teams_for_match_num.append(real_team_idx_1)
123         away_teams_for_match_num.append(real_team_idx_2)
124         if team_idx_2 - team_idx_1 == 1:
125           excitedness.append(5)
126         elif team_idx_2 - team_idx_1 == 2:
127           excitedness.append(2)
128         else:
129           excitedness.append(0)
130         print("matchnum %2d: %2d vs. %2d, excited: %d" % (match_idx, real_team_idx_1, real_team_idx_2, excitedness[match_idx]))
131         match_idx = match_idx + 1
132
133 # Create match variables.
134 home_teams = []
135 away_teams = []
136 for match_idx in range(num_matches):
137   home_teams.append(model.NewIntVar(0, num_teams - 1, "home_team_match%i" % (match_idx)))
138   away_teams.append(model.NewIntVar(0, num_teams - 1, "away_team_match%i" % (match_idx)))
139 matches_flat = home_teams + away_teams
140
141 for match_idx in range(num_matches):
142   model.AddElement(matchnums[match_idx], home_teams_for_match_num, home_teams[match_idx])
143   model.AddElement(matchnums[match_idx], away_teams_for_match_num, away_teams[match_idx])
144
145 # Boolean variables
146 home_team_in_match_x_is_y = [[
147       model.NewBoolVar('home_team_in_match_%d_is_%d' % (match_idx, team_idx)) for team_idx in range(num_teams)
148 ] for match_idx in range(num_matches)]
149
150 away_team_in_match_x_is_y = [[
151       model.NewBoolVar('away_team_in_match_%d_is_%d' % (match_idx, team_idx)) for team_idx in range(num_teams)
152 ] for match_idx in range(num_matches)]
153
154 match_x_has_num_y = [[
155       model.NewBoolVar('match_%d_has_number_%d' % (a, b)) for a in range(num_matches)
156 ] for b in range(num_matches)]
157
158 for match_idx in range(num_matches):
159   model.AddMapDomain(matchnums[match_idx], match_x_has_num_y[match_idx])
160   model.AddMapDomain(home_teams[match_idx], home_team_in_match_x_is_y[match_idx])
161   model.AddMapDomain(away_teams[match_idx], away_team_in_match_x_is_y[match_idx])
162
163 # Fields always play opposing groups (FIXME?)
164 for round_idx in range(num_rounds):
165   field_0_is_group_0 = model.NewBoolVar('field_0_round_%d_is_group_0' % (round_idx))
166   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)])
167   field_1_is_group_0 = model.NewBoolVar('field_1_round_%d_is_group_0' % (round_idx))
168   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)])
169   model.AddBoolXOr([field_0_is_group_0, field_1_is_group_0])
170
171 # A team can never play on the same field at the same time
172 #for team_idx in range(num_teams):
173 #  for round_idx in range(num_rounds):
174 #    plays_on_field_0 = model.NewBoolVar('plays_on_field0_t%d_r%d' % (team_idx, round_idx))
175 #    model.AddMaxEquality(plays_on_field_0, [
176 #        home_team_in_match_x_is_y[round_idx * 2 + 0][team_idx],
177 #        away_team_in_match_x_is_y[round_idx * 2 + 0][team_idx]])
178 #    plays_on_field_1 = model.NewBoolVar('plays_on_field1_t%d_r%d' % (team_idx, round_idx))
179 #    model.AddMaxEquality(plays_on_field_1, [
180 #        home_team_in_match_x_is_y[round_idx * 2 + 1][team_idx],
181 #        away_team_in_match_x_is_y[round_idx * 2 + 1][team_idx]])
182 #    model.AddBoolOr([plays_on_field_0.Not(), plays_on_field_1.Not()])
183
184 plays_in_round = {}
185 for team_idx in range(num_teams):
186   plays_in_round[team_idx] = {}
187   for round_idx in range(num_rounds):
188     plays_in_round[team_idx][round_idx] = model.NewBoolVar('plays_in_round_t%d_r%d' % (team_idx, round_idx))
189     model.AddMaxEquality(plays_in_round[team_idx][round_idx], [
190         home_team_in_match_x_is_y[round_idx * 2 + 0][team_idx],
191         home_team_in_match_x_is_y[round_idx * 2 + 1][team_idx],
192         away_team_in_match_x_is_y[round_idx * 2 + 0][team_idx],
193         away_team_in_match_x_is_y[round_idx * 2 + 1][team_idx]])
194
195 # A team can never play two matches in a row
196 for round_idx in range(num_rounds - 1):
197   for team_idx in range(num_teams):
198     model.AddBoolOr([plays_in_round[team_idx][round_idx].Not(), plays_in_round[team_idx][round_idx + 1].Not()])
199  
200 # Also, double-tired is not cool
201 #for round_idx in range(num_rounds - 4):
202 #  for team_idx in range(num_teams):
203 #    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()])
204
205 # More waiting time is good
206 #tired_matches = []
207 #for round_idx in range(num_rounds - 2):
208 #  for team_idx in range(num_teams):
209 #    tired = model.NewBoolVar('team_%d_is_tired_in_round_%d' % (team_idx, round_idx))
210 #    model.AddMinEquality(tired, [plays_in_round[team_idx][round_idx], plays_in_round[team_idx][round_idx + 2]])
211 #    tired_matches.append(tired)
212 #sum_tiredness = sum(tired_matches)
213
214 # Each team gets play-rest-play exactly once, for fairness
215 for team_idx in range(num_teams):
216   tired_matches = []
217   for round_idx in range(num_rounds - 2):
218     tired = model.NewBoolVar('team_%d_is_tired_in_round_%d' % (team_idx, round_idx))
219     model.AddMinEquality(tired, [plays_in_round[team_idx][round_idx], plays_in_round[team_idx][round_idx + 2]])
220     tired_matches.append(tired)
221   model.Add(sum(tired_matches) <= 1)
222 sum_tiredness = 0
223
224 # TFK can not play the first two matches
225 model.AddBoolAnd([plays_in_round[0][0].Not(), plays_in_round[0][1].Not()])
226
227 # Group finals come last, and on the stream field.
228 model.AddBoolOr([match_x_has_num_y[num_matches - 2][0], match_x_has_num_y[num_matches - 2][num_rounds]])
229 model.AddBoolOr([match_x_has_num_y[num_matches - 4][0], match_x_has_num_y[num_matches - 4][num_rounds]])
230
231 # Count how many times each team has been on stream.
232 stream_penalties = []
233 for team_idx in range(num_teams):
234   playing_on_stream = []
235   for round_idx in range(num_rounds):
236     s = model.NewBoolVar('team_%d_plays_on_stream_in_round_%d' % (team_idx, round_idx))
237     model.AddMaxEquality(s, [
238         home_team_in_match_x_is_y[round_idx * 2 + 0][team_idx],
239         away_team_in_match_x_is_y[round_idx * 2 + 0][team_idx]])
240     playing_on_stream.append(s)
241   times_on_stream_this_team = sum(playing_on_stream)
242   model.Add(times_on_stream_this_team >= 1)
243
244   times_stream_var = model.NewIntVar(0, num_teams_per_group, "team_%d_stream_count" % (team_idx))
245   model.Add(times_stream_var == times_on_stream_this_team)
246   #model.Add(times_on_stream_this_team <= 4)
247
248   is_n_times_on_stream = [
249     model.NewBoolVar('team_%d_is_%d_times_on_stream' % (team_idx, i)) for i in range(num_teams_per_group)
250   ]
251   model.AddMapDomain(times_stream_var, is_n_times_on_stream)
252   stream_penalties.append(is_n_times_on_stream[1] * -50)
253   stream_penalties.append(is_n_times_on_stream[4] * -10)
254   stream_penalties.append(is_n_times_on_stream[5] * -50)
255
256 # Make sure each team has at least one exciting match on stream.
257 #for team_idx in range(team_idx):
258 #  stream_matches_for_this_team = []
259 #  for round_idx in range(num_rounds):
260 #    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))
261 #    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))
262
263 # Put the more exciting games later, and on stream fields
264 excitement = []
265 for round_idx in range(num_rounds):
266   for match_idx in range(match_idx):
267     excitement.append(match_x_has_num_y[round_idx * 2 + 0][match_idx] * excitedness[match_idx] * excitedness_weight(round_idx * 2 + 0))
268     excitement.append(match_x_has_num_y[round_idx * 2 + 1][match_idx] * excitedness[match_idx] * excitedness_weight(round_idx * 2 + 1))
269 sum_excitement = sum(excitement)
270 objective = sum_excitement - 30 * sum_tiredness + 3 * sum(stream_penalties)
271 model.Maximize(objective)
272
273 solver = cp_model.CpSolver()
274 solution_printer = SolutionPrinterWithObjective(home_teams, away_teams, matchnums, objective)
275 status = solver.SolveWithSolutionCallback(model, solution_printer)