Generate a good canoe polo draw.
Runs on Python 3, with click
installed (pip install click
).
$ python solve.py input.json output.json
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# | |
# solve.py | |
# rod-polo-draw | |
# | |
import random | |
import json | |
from collections import Counter, namedtuple, defaultdict | |
import click | |
Time = namedtuple('Time', 'date game_no') | |
Team = namedtuple('Team', 'name division') | |
EmptySlot = namedtuple('EmptySlot', 'time type') | |
FilledSlot = namedtuple('FilledSlot', 'time type team') | |
DrawConfig = namedtuple('DrawConfig', 'teams slots divisions') | |
EmptySlot.fill = lambda self, t: FilledSlot(self.time, self.type, t) | |
@click.command() | |
@click.argument('draw_file', type=click.Path(exists=True)) | |
@click.argument('output_file', type=click.Path()) | |
def main(draw_file, output_file): | |
"Generate a draw given a JSON draw config." | |
config = _load_config(draw_file) | |
filled_slots = _solve(_heuristic, config) | |
_print_stats(filled_slots) | |
_save_draw(filled_slots, output_file) | |
def _load_config(draw_file): | |
data = json.load(open(draw_file)) | |
teams = _fetch_teams(data) | |
divisions = set(t.division for t in teams) | |
slots = _generate_empty_slots(data) | |
# change the team order arounde each time | |
random.shuffle(teams) | |
return DrawConfig(teams, slots, divisions) | |
def _fetch_teams(data): | |
teams = [] | |
for division in data['divisions']: | |
for team in division['teams']: | |
teams.append(Team(team['name'], team['div'])) | |
return teams | |
def _generate_empty_slots(data): | |
slots = [] | |
for block in data['datesAndTimes']['jsonDatesAndTimes']: | |
n_games = block['games'] | |
date = block['date'] | |
for i in range(1, n_games + 1): | |
t = Time(date, i) | |
slots.append(EmptySlot(t, 'game')) | |
slots.append(EmptySlot(t, 'game')) | |
slots.append(EmptySlot(t, 'referee')) | |
start = Time(date, 1) | |
end = Time(date, n_games) | |
slots.append(EmptySlot(Time(date, start), 'duty')) | |
slots.append(EmptySlot(Time(date, end), 'duty')) | |
return slots | |
def _solve(heuristic, config): | |
filled = [] | |
for s in config.slots: | |
best_team = min(config.teams, | |
key=lambda t: heuristic(t, s, filled, config)) | |
f = s.fill(best_team) | |
filled.append(f) | |
return filled | |
def _heuristic(team, slot, filled, config): | |
return sum( | |
f(team, slot, filled, config) | |
for f in [ | |
_team_can_only_do_one_thing_at_once, | |
_divisions_get_equal_play, | |
_teams_get_equal_play, | |
_teams_get_equal_refereeing, | |
_teams_get_equal_duties, | |
_teams_referee_their_own_division, | |
_games_have_same_division, | |
] | |
) | |
def _team_can_only_do_one_thing_at_once(team, slot, filled, config): | |
same_time = [s.team for s in filled if slot.time == s.time] | |
if team in same_time: | |
return 1000000 | |
return 0 | |
def _games_have_same_division(team, slot, filled, config): | |
if slot.type == 'game': | |
same_time = [s.team for s in filled | |
if slot.time == s.time and s.type == 'game'] | |
if same_time: | |
return 1000000 * (same_time[0].division != team.division) | |
return 0 | |
def _divisions_get_equal_play(team, slot, filled, config): | |
if slot.type != 'game': | |
return 0 | |
counts = Counter(s.team.division for s in filled if s.type == 'game') | |
counts[team.division] += 1 | |
min_ = min(counts[d] for d in config.divisions) | |
max_ = max(counts[d] for d in config.divisions) | |
return (max_ - min_) * 200 | |
def _teams_get_equal_play(team, slot, filled, config): | |
if slot.type != 'game': | |
return 0 | |
counts = Counter([s.team for s in filled if s.type == 'game']) | |
counts[team] += 1 | |
min_ = min(counts[t] for t in config.teams) | |
max_ = max(counts[t] for t in config.teams) | |
assert not filled or max_ > 0 | |
return (max_ - min_) * 1000 | |
def _teams_get_equal_refereeing(team, slot, filled, config): | |
if slot.type != 'referee': | |
return 0 | |
counts = Counter([s.team for s in filled if s.type == 'referee']) | |
counts[team] += 1 | |
min_ = min(counts[t] for t in config.teams) | |
max_ = max(counts[t] for t in config.teams) | |
assert not filled or max_ > 0 | |
return (max_ - min_) * 10 | |
def _teams_get_equal_duties(team, slot, filled, config): | |
if slot.type != 'duty': | |
return 0 | |
counts = Counter([s.team for s in filled if s.type == 'duty']) | |
counts[team] += 1 | |
min_ = min(counts[t] for t in config.teams) | |
max_ = max(counts[t] for t in config.teams) | |
assert not filled or max_ > 0 | |
return (max_ - min_) * 10 | |
def _teams_referee_their_own_division(team, slot, filled, config): | |
if slot.type == 'referee': | |
teams = [s.team for s in filled if s.time == slot.time] | |
return 5 * (teams[0].division != team.division) | |
return 0 | |
def _is_same_game(s1, s2): | |
return s1['date'] == s2['date'] and s1['game_no'] == s2['game_no'] | |
def _print_stats(slots): | |
teams = defaultdict(Counter) | |
for s in slots: | |
teams[s.team][s.type] += 1 | |
games = [t['game'] for t in teams.values()] | |
referees = [t['referee'] for t in teams.values()] | |
duty = [t['duty'] for t in teams.values()] | |
print('Draw statistics') | |
print() | |
print('Games:', games) | |
print('Referees:', referees) | |
print('Duties:', duty) | |
def _save_draw(slots, output_file): | |
draw = [] | |
for s in slots: | |
draw.append({ | |
'date': s.time.date, | |
'game_no': s.time.game_no, | |
'type': s.type, | |
'team': { | |
'name': s.team.name, | |
'div': s.team.division, | |
}, | |
}) | |
json.dump(draw, open(output_file, 'w'), indent=2) | |
if __name__ == '__main__': | |
main() |