import math
from django.db import models
from django.db.models import Q, F
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.core.exceptions import MultipleObjectsReturned
from django.urls import reverse, reverse_lazy
# from registration.models import AbstractFormat
# Create your models here.
[docs]class KumiteMatchPerson(models.Model):
eventlink = models.ForeignKey('registration.EventLink', on_delete=models.PROTECT)
points = models.PositiveSmallIntegerField(default=0)
warnings = models.PositiveSmallIntegerField(default=0)
disqualified = models.BooleanField(default=False)
is_first_match = models.BooleanField(default=False)
def __str__(self):
return self.eventlink.name
@property
def name(self):
return self.eventlink.name
@property
def kumitematch(self):
return KumiteMatch.objects.get(Q(aka=self.id) | Q(shiro=self.id))
[docs] def is_swappable(self):
if not isinstance(self.kumitematch.bracket, KumiteElim1Bracket):
return False
return self.is_first_match and not self.kumitematch.done
[docs] def is_aka(self):
return self.kumitematch.aka == self
[docs] def is_shiro(self):
return self.kumitematch.shiro == self
[docs] @staticmethod
def same_person(p1,p2):
return (p1 is None and p2 is None) or (p1 is not None and p2 is not None and p1.eventlink == p2.eventlink)
[docs]class KumiteMatch(models.Model):
class Meta:
ordering = ['-round', 'order']
round = models.SmallIntegerField()
order = models.SmallIntegerField()
bracket_elim1 = models.ForeignKey('KumiteElim1Bracket', on_delete=models.CASCADE, null=True)
bracket_rr = models.ForeignKey('KumiteRoundRobinBracket', on_delete=models.CASCADE, null=True)
bracket_2people = models.ForeignKey('Kumite2PeopleBracket', on_delete=models.CASCADE, null=True)
winner_match = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL, related_name='+')
consolation_match = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL, related_name='+')
aka = models.OneToOneField(KumiteMatchPerson, blank=True, null=True, on_delete=models.PROTECT, related_name='match_aka')
shiro = models.OneToOneField(KumiteMatchPerson, blank=True, null=True, on_delete=models.PROTECT, related_name='match_shiro')
swap = models.BooleanField(default=False) # Swap aka and shiro for display
done = models.BooleanField(default=False)
aka_won = models.BooleanField(default=False)
def __str__(self):
name = ""
if self.is_final():
name = ", final"
elif self.is_consolation():
name = ", consolation"
if self.bracket.division:
prefix = self.bracket.division.name
else:
prefix = ''
return "{}, round {}, match {}{}".format(prefix, self.round, self.order, name)
[docs] def get_display_short_name(self):
if self.is_final():
name = "Final"
elif self.is_consolation():
name = "Consolation Final"
elif self.round == 1:
name = "Semi-finals, Match {}".format(self.order + 1)
elif self.round == 2:
name = "Quarter-finals, Match {}".format(self.order + 1)
elif self.bracket_rr is not None:
name = "Round robin, Match {}".format(self.order + 1)
else:
# Also works for 2 person bracket
name = "Round of {:.0f}, Match {}".format(math.pow(2,self.round + 1), self.order + 1)
return name
@property
def bracket_field(self):
fields = ('bracket_elim1', 'bracket_rr', 'bracket_2people')
for f in fields:
val = getattr(self, f)
if val is not None:
return f
return None
@property
def bracket(self):
fields = ('bracket_elim1', 'bracket_rr', 'bracket_2people')
for f in fields:
val = getattr(self, f)
if val is not None:
return val
return None
@bracket.setter
def bracket(self, val):
if val is not None:
self.bracket_elim1 = None
self.bracket_rr = None
self.bracket_2people = None
setattr(self, val.kumite_match_bracket_field, val)
[docs] def get_absolute_url(self):
return reverse('kumite:match', args=[self.id])
[docs] def people(self):
return [self.aka, self.shiro]
[docs] def get_aka_display(self):
return self.aka if not self.swap else self.shiro
[docs] def get_shiro_display(self):
return self.shiro if not self.swap else self.aka
[docs] def winner(self):
if self.done:
km = self.aka if self.aka_won else self.shiro
if km.disqualified:
from registration.models import EventLink
return EventLink.get_disqualified_singleton(km.eventlink.event)
else:
return km.eventlink
else:
return None
[docs] def infer_winner(self):
if self.done:
if self.aka.points == self.shiro.points and not self.aka.disqualified and not self.shiro.disqualified:
raise ValueError("Ties not permitted.")
self.aka_won = self.aka.points > self.shiro.points and not self.aka.disqualified or self.shiro.disqualified
[docs] def loser(self):
if self.done:
km = self.shiro if self.aka_won else self.aka
if km.disqualified:
from registration.models import EventLink
return EventLink.get_disqualified_singleton(km.eventlink.event)
else:
return km.eventlink
else:
return None
[docs] def is_final(self):
return self.bracket_elim1 is not None and self.round == 0 and self.order == 0
[docs] def is_consolation(self):
return self.bracket_elim1 is not None and self.round == 0 and self.order == -1
[docs] def is_ready(self):
"""Returns true if the match is ready to be run for the first time."""
return not self.done and self.is_editable()
[docs] def is_editable(self):
"""Returns true if the match outcome can be changed without invalidating other completed matches."""
return (len(self.prev_matches.filter(done=False)) == 0
and (self.winner_match is None or not self.winner_match.done)
and (self.consolation_match is None or not self.consolation_match.done))
[docs] def save(self, *args, **kwargs):
if self.done and not self.is_editable():
raise ValueError("Can't complete match if predecessor isn't complete.")
super(KumiteMatch, self).save(*args, **kwargs)
self.bracket.match_callback(self)
@property
def prev_matches(self):
id_or_none = lambda x: x.id if x is not None else None
return KumiteMatch.objects.filter(
bracket_elim1__id=id_or_none(self.bracket_elim1),
bracket_2people__id=id_or_none(self.bracket_2people)
).filter(
Q(winner_match__id=self.id) | Q(consolation_match__id=self.id))
[docs]@receiver(post_delete, sender=KumiteMatch)
def kumite_match_post_delete(sender, instance, **kwargs):
if instance.aka is not None:
instance.aka.delete()
if instance.shiro is not None:
instance.shiro.delete()
[docs]class KumiteElim1Bracket(models.Model):
name = models.CharField(max_length=250)
rounds = models.PositiveSmallIntegerField(default=0)
division = models.ForeignKey('registration.Division', on_delete=models.PROTECT, related_name='+', null=True)
kumite_match_bracket_field = 'bracket_elim1'
def __str__(self):
s = str(self.division) if self.division is not None else "Unbound"
return s + " - Elim 1"
@property
def final_match(self):
m = self.kumitematch_set.filter(round=0, order=0)
if len(m) == 0:
return None
elif len(m) == 1:
return m[0]
else:
raise MultipleObjectsReturned('Multiple final matches.')
@property
def consolation_match(self):
m = self.kumitematch_set.filter(round=0, order=-1)
if len(m) == 0:
return None
elif len(m) == 1:
return m[0]
else:
raise MultipleObjectsReturned('Multiple consolation matches.')
[docs] def get_next_match(self):
m = self.kumitematch_set.filter(done=False)
if len(m) == 0:
return None
m = m[0]
assert m.is_ready(), "Next match {} isn't ready.".format(m)
return m
[docs] def get_on_deck_match(self):
m = self.kumitematch_set.filter(done=False)
if len(m) < 2:
return None
return m[1]
[docs] def get_winners(self):
return ((1, self.final_match.winner()),
(2, self.final_match.loser()),
(3, self.consolation_match.winner()))
[docs] def get_absolute_url(self):
return reverse('kumite:bracket-n', args=[self.id])
[docs] def build(self, people):
if len(self.kumitematch_set.all()) > 0:
raise Exception("Bracket has already been built.")
# Consolation Match
consolation = KumiteMatch()
consolation.bracket = self
consolation.str = "consolation"
consolation.round = 0
consolation.order = -1
consolation.save()
# Build tree recursively
def build_helper(bracket, round, match, parent, order):
if len(order) > 2:
# Add another round
m = KumiteMatch()
m.bracket = self
m.str = str(round)
m.round = round
m.order = match
m.winner_match = parent
m.save()
split = len(order) // 2
build_helper(bracket, round + 1, 2 * match, m, order[:split])
build_helper(bracket, round + 1, 2 * match + 1, m, order[split:])
elif len(order) == 2:
if order[1] >= len(people):
# Competetor gets a buy
m = KumiteMatchPerson(eventlink=people[order[0]], is_first_match=True)
m.save()
if match % 2 == 0:
parent.aka = m
else:
parent.shiro = m
parent.save()
else:
m = KumiteMatch()
m.bracket = self
m.str = str(round)
m.round = round
m.order = match
m.winner_match = parent
p = KumiteMatchPerson(eventlink=people[order[0]], is_first_match=True)
p.save()
m.aka = p
p = KumiteMatchPerson(eventlink=people[order[1]], is_first_match=True)
p.save()
m.shiro = p
m.save()
else:
raise Exception("Bracket is too big for number of participants.")
if round == 1:
# Connect consolation match
m.consolation_match = consolation
m.save()
return m
n_person = len(people)
if n_person < 4:
raise ValueError("Minimum 4 competetors.")
self.rounds = math.ceil(math.log2(n_person))
order = self.get_seed_order()
round = 0
m = build_helper(self, round, 0, None, order)
self.save()
[docs] def get_swappable_match_people(self):
return KumiteMatchPerson.objects.filter(is_first_match=True).filter(
Q(match_aka__bracket_elim1=self) & Q(match_aka__done=False)
| Q(match_shiro__bracket_elim1=self) & Q(match_shiro__done=False))
[docs] def get_seed_order(self, rounds=None):
"""
Returns order for assigning participants to the initial round.
For example, get_seed_order(2) will return the seed orders for a 2
round bracket (4 competetors) as [0 3 1 2]. This means that the first
match is between competetors 0 and 4 and the second match is between
competetors 2 and 1.
Args:
rounds (optional): The number of rounds. Defaults to self.rounds.
Returns:
List of orders.
"""
if rounds is None:
rounds = self.rounds
if rounds // 1 != rounds or rounds <= 0:
raise ValueError('Rounds should be a positive integer. Got {}.'.format(rounds))
order = [0, 1]
for round in range(rounds-1):
order = [2 * x for x in order] + [2 * x + 1 for x in reversed(order)]
order = sorted(range(len(order)), key=lambda k: order[k])
return order
[docs] def get_num_match_in_round(self, round=None):
if round is None:
# Can't pass argument from template. Return array.
return [ int(math.pow(2, round)) for round in range(0,self.rounds) ]
else:
return int(math.pow(2, round))
[docs] def get_match(self, round, match_i):
"""
Returns a match specified by round and match index (order).
If the bracket is incomplete, some matches will not exist. In this
case, None is returned.
Args:
round: The round index. 0 is finals, preceeding rounds are
positive numbers.
match_i: The match index. 0 to 2^round-1. The consolation match
is -1 in round 0.
Returns:
The specified match or None.
"""
# 2 1 0
# 0 ____
# 1 \____
# 2 ----\____/ \
# 3 ----/ \____
# 4 ----\____ /
# 5 ----/ \____/
# 6 ____/
# 7
# -1 ____
if round // 1 != round or round < 0 or self.rounds <= round:
raise ValueError("Invalid round {}.".format(round))
if round == 0 and match_i == -1:
return self.consolation_match
if match_i // 1 != match_i or match_i < 0 or self.get_num_match_in_round(round) <= match_i:
raise ValueError("Invalid match index {}.".format(match_i))
split = -1
m = self.final_match
for i_round in range(round):
split = self.get_num_match_in_round(round - i_round - 1)
if match_i < split:
# want top
m = self.prev_match_aka(m)
elif match_i < 2 * split:
# want bottom
match_i -= split
m = self.prev_match_shiro(m)
if m is None:
break
return m
[docs] def match_callback(self, match):
if match.winner_match:
self.claim_people(match.winner_match)
if match.consolation_match:
self.claim_people(match.consolation_match)
[docs] def claim_people(self, match):
curr_ids = [x.id for x in match.people() if x is not None]
attr_name = 'aka'
for m in [self.prev_match_aka(match), self.prev_match_shiro(match)]:
if m is not None:
if m.done:
if m.winner_match == match:
el = m.winner()
elif m.consolation_match == match:
el = m.loser()
else:
el = None
curr_p = getattr(match, attr_name)
curr_el = curr_p.eventlink if curr_p is not None else None
if curr_el is not el:
if match.done:
raise ValueError("Can't modify people if the match is done.")
if el is not None:
p = KumiteMatchPerson(eventlink=el, disqualified=el.disqualified)
p.save()
else:
p = None
setattr(match, attr_name, p)
match.save()
if curr_p is not None:
curr_p.delete()
attr_name = 'shiro'
[docs] def prev_match_aka(self, match):
# m = self.prev_matches.annotate(ordermod2=F('order') % 2).filter(ordermod2=0)
m = [m for m in match.prev_matches if m.order % 2 == 0]
if len(m) == 0:
return None
elif len(m) == 1:
return m[0]
else:
print(str(match.order))
for a in m:
print(a)
from django.core import exceptions
raise MultipleObjectsReturned()
[docs] def prev_match_shiro(self, match):
# m = self.prev_matches.annotate(ordermod2=F('order') % 2).filter(ordermod2=1)
m = [m for m in match.prev_matches if m.order % 2 == 1]
if len(m) == 0:
return None
elif len(m) == 1:
return m[0]
else:
raise MultipleObjectsReturned()
[docs]class Kumite2PeopleBracket(models.Model):
division = models.ForeignKey('registration.Division', on_delete=models.PROTECT, related_name='+', null=True)
winner = models.ForeignKey('registration.EventLink', on_delete=models.PROTECT, related_name='+', null=True)
loser = models.ForeignKey('registration.EventLink', on_delete=models.PROTECT, related_name='+', null=True)
rounds = 1
kumite_match_bracket_field = 'bracket_2people'
def __str__(self):
s = str(self.division) if self.division is not None else "Unbound"
return s + " - 2 People"
[docs] def get_winners(self):
return ((1, self.winner), (2, self.loser))
[docs] def get_absolute_url(self):
return reverse('kumite:bracket-2', args=[self.id,])
[docs] def get_next_match(self):
m = self.kumitematch_set.filter(done=False)
if len(m) == 0:
return None
else:
return m[0]
[docs] def build(self, people):
if len(people) != 2:
raise ValueError("Kumite2PeopleBracket only supports 2 competetors.")
for i in range(2):
m = KumiteMatch(bracket=self, round=0, order=i)
p = KumiteMatchPerson(eventlink=people[i % 2])
p.save()
m.aka = p
p = KumiteMatchPerson(eventlink=people[(i+1) % 2])
p.save()
m.shiro = p
if i == 0:
m_prev = m
else:
m.save()
m_prev.winner_match = m
m_prev.consolation_match = m
m_prev.save()
[docs] def get_swappable_match_people(self):
return KumiteMatchPrson.objects.filter(
Q(match_aka__bracket_2people=self) & Q(match_aka__done=False)
| Q(match_shiro__bracket_2people=self) & Q(match_shirt__done=False))
[docs] def match_callback(self, match):
# Cases
# - First 2 matches are still in progress.
# - First 2 matches done and no tie => assign winner.
# - First 2 matches tied => Create new match.
# - First 2 matches no longer tied => Delete extra match and assign winner.
# - One person disqualified
# - Both people diqualified
matches = self.kumitematch_set.all()
p1 = matches[0].aka.eventlink
p2 = matches[0].shiro.eventlink
wins = {p1: 0, p2: 0}
points = {p1: 0, p2: 0}
disqualifieds = {p1: 0, p2: 0}
all_done = True
have_winner = False
for im, m in enumerate(matches):
all_done = all_done and m.done
if m.done:
if not m.winner().disqualified:
wins[m.winner()] += 1
points[m.aka.eventlink] += m.aka.points
disqualifieds[m.aka.eventlink] += m.aka.disqualified
points[m.shiro.eventlink] += m.shiro.points
disqualifieds[m.shiro.eventlink] += m.shiro.disqualified
if not all_done:
break
have_winner = disqualifieds[p1] != 0 or disqualifieds[p2] != 0 \
or im >= 1 and wins[p1] != wins[p2] \
or im >= 2 and points[p1] != points[p2]
# After 2 matchs, decide based on number of wins
# After 3 matchs, can decide based on total points too. However,
# since we don't allow a match to tie, this doesn't actually get used.
if disqualifieds[p1]:
wins[p1] = -1
points[p1] = -1
if disqualifieds[p2]:
wins[p2] = -1
points[p2] = -1
if have_winner:
if wins[p1] != wins[p2]:
winner = p1 if wins[p1] > wins[p2] else p2
else:
winner = p1 if points[p1] > points[p2] else p2
from registration.models import EventLink
if disqualifieds[winner] > 0:
winner = EventLink.get_disqualified_singleton(winner.event)
loser = p2 if winner == p1 else p1
if disqualifieds[loser] > 0:
loser = EventLink.get_disqualified_singleton(loser.event)
for id in range(im+1, len(matches)):
matches[id].delete()
break
if all_done and have_winner:
self.winner = winner
self.loser = loser
else:
self.winner = None
self.loser = None
if all_done:
# Create a tie break match
m2 = KumiteMatch(bracket=self, round=0, order=im+1)
p = KumiteMatchPerson(eventlink=m.shiro.eventlink)
p.save()
m2.aka = p
p = KumiteMatchPerson(eventlink=m.aka.eventlink)
p.save()
m2.shiro = p
m2.save()
m.winner_match = m2
m.consolation_match = m2
m.save()
self.save()
[docs] def get_num_match_in_round(self, round=None):
n = len(self.kumitematch_set.all())
if round is None:
# Can't pass argument from template. Return array.
return [n]
else:
if round == 0:
return n
elif round == 1:
return n * 2
else:
raise ValueError('Invalid round number {}.'.format(n))
[docs] def get_match(self, round, match_i):
if round != 0:
raise ValueError("Only 1 round")
return self.kumitematch_set.get(order=match_i)
[docs]class KumiteRoundRobinBracket(models.Model):
# Round robin format for three people.
division = models.ForeignKey('registration.Division', on_delete=models.PROTECT, related_name='+', null=True)
gold = models.ForeignKey('registration.EventLink', on_delete=models.PROTECT, related_name='+', null=True, blank=True)
silver = models.ForeignKey('registration.EventLink', on_delete=models.PROTECT, related_name='+', null=True, blank=True)
bronze = models.ForeignKey('registration.EventLink', on_delete=models.PROTECT, related_name='+', null=True, blank=True)
rounds = 1
kumite_match_bracket_field = 'bracket_rr'
def __str__(self):
s = str(self.division) if self.division is not None else "Unbound"
return s + " - Round Robin"
[docs] def get_winners(self):
return ((1, self.gold), (2, self.silver), (3, self.bronze))
[docs] def get_absolute_url(self):
return reverse('kumite:bracket-rr', args=[self.id])
[docs] def get_next_match(self):
m = self.kumitematch_set.filter(done=False)
if len(m) == 0:
return None
else:
return m[0]
[docs] def get_on_deck_match(self):
m = self.kumitematch_set.filter(done=False)
if len(m) < 2:
return None
else:
return m[1]
[docs] def build(self, people):
if len(people) != 3:
raise ValueError("KumiteRoundRobinBracket only supports 3 people for now.")
for i in range(len(people)):
m = KumiteMatch(bracket=self, round=0, order=i)
p = KumiteMatchPerson(eventlink=people[i])
p.save()
m.aka = p
p = KumiteMatchPerson(eventlink=people[(i+1) % len(people)])
p.save()
m.shiro = p
m.save()
[docs] def get_people(self):
from registration.models import EventLink
people = EventLink.objects.filter(disqualified=False).filter(
Q(kumitematchperson__match_aka__bracket_rr=self)
| Q(kumitematchperson__match_shiro__bracket_rr=self)).distinct()
return people
[docs] def get_swappable_match_people(self):
return KumiteMatchPrson.objects.filter(
Q(match_aka__bracket_rr=self) & Q(match_aka__done=False)
| Q(match_shiro__bracket_rr=self) & Q(match_shirt__done=False))
[docs] def match_callback(self, match):
from registration.models import EventLink
# Propagate aka disqualification status to other matches
if not match.aka.eventlink.disqualified:
other_match = self.kumitematch_set.filter(order=(match.order - 1) % 3)
# During construction of the bracket, this function is called but the other match doesn't exist
if len(other_match) > 0 and not other_match[0].done:
other_match = other_match[0]
disqualified = match.aka.disqualified and match.done
if disqualified:
el = EventLink.get_disqualified_singleton(match.aka.eventlink.event)
else:
el = match.aka.eventlink
if other_match.shiro.eventlink != el:
other_match.shiro.eventlink = el
other_match.shiro.disqualified = disqualified
other_match.shiro.save()
match.winner_match = other_match if disqualified else None
match.save()
# Propagate shiro disqualification status to other matches
if not match.shiro.eventlink.disqualified:
other_match = self.kumitematch_set.filter(order=(match.order + 1) % 3)
# During construction of the bracket, this function is called but the other match doesn't exist
if len(other_match) > 0 and not other_match[0].done:
other_match = other_match[0]
disqualified = match.shiro.disqualified and match.done
if disqualified:
el = EventLink.get_disqualified_singleton(match.shiro.eventlink.event)
else:
el = match.shiro.eventlink
if other_match.aka.eventlink != el:
other_match.aka.eventlink = el
other_match.aka.disqualified = disqualified
other_match.aka.save()
match.consolation_match = other_match if disqualified else None
match.save()
# Figure out the winner
matches = self.kumitematch_set.all()
points = {p: [0, 0, 0, 0] for p in self.get_people()}
# points = [-disqualifications, wins, point differential, total points]
DISQUALIFIED = 0
WINS = 1
DIFF = 2
TOTAL = 3
all_done = True
for im, m in enumerate(matches):
all_done = all_done and m.done
if m.done:
diff = m.aka.points - m.shiro.points
if not m.aka.eventlink.disqualified:
points[m.aka.eventlink][DISQUALIFIED] -= m.aka.disqualified
if m.aka_won:
points[m.aka.eventlink][WINS] += 1
points[m.aka.eventlink][DIFF] += diff
points[m.aka.eventlink][TOTAL] += m.aka.points
if not m.shiro.eventlink.disqualified:
points[m.shiro.eventlink][DISQUALIFIED] -= m.shiro.disqualified
if not m.aka_won:
points[m.shiro.eventlink][WINS] += 1
points[m.shiro.eventlink][DIFF] -= diff
points[m.shiro.eventlink][TOTAL] += m.shiro.points
if not all_done:
break
if all_done:
points = [(p, point) for p, point in points.items()]
points = sorted(points, key=lambda x: x[1], reverse=True)
ties = []
ranks = (x for x in ("gold", "silver", "bronze"))
prev_point = None
prev_person = None
for (p, point) in points:
if prev_person != None:
if point != prev_point:
setattr(self, ranks.__next__(), prev_person)
else:
ties.append(p)
if len(ties) > 0:
raise Exception("Ties not implemented.")
prev_point = point
if point[DISQUALIFIED] == 0:
prev_person = p
else:
prev_person = EventLink.get_disqualified_singleton(p.event)
setattr(self, ranks.__next__(), prev_person)
self.save()
else:
if self.gold is not None or self.silver is not None or self.bronze is not None:
self.gold = None
self.silver = None
self.bronze = None
self.save()
# if all_done:
# # Create a tie break match
# m2 = KumiteMatch(bracket=self, round=0, order=im+1)
# p = KumiteMatchPerson(eventlink=m.shiro.eventlink)
# p.save()
# m2.aka = p
# p = KumiteMatchPerson(eventlink=m.aka.eventlink)
# p.save()
# m2.shiro = p
# m2.save()
#
# m.winner_match = m2
# m.consolation_match = m2
# m.save()
# self.save()
[docs] def get_num_match_in_round(self, round=None):
n = len(self.kumitematch_set.all())
if round is None:
# Can't pass argument from template. Return array.
return [n]
else:
if round == 0:
return n
elif round == 1:
return n * 2
else:
raise ValueError('Invalid round number {}.'.format(n))
[docs] def get_match(self, round, match_i):
if round != 0:
raise ValueError("Only 1 round")
return self.kumitematch_set.get(order=match_i)