# -*- encoding: utf-8 -*- # TODO: Rankings archiv Flag erstellen, womit sie nicht mehr neuberechnet # werden dürfen. from __future__ import division from datetime import date from django.conf import settings from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models from django.utils import timezone from django.utils.translation import ugettext as _ from events.models import Event from . import KYU_RANKS, DAN_RANKS, DAN_RANKS_DICT, logger, set_dirty from . import managers kyu_dan_rankings = set() ladder_rankings = set() ladder_seasons = set() class EventRanking(models.Model): """ Event Rankings funktionieren genauso wie Season Rankings. Sie beschränken sich aber auf einen Event und werden nur dann angestossen, wenn der Event als Turnier markiert wurde. """ user = models.ForeignKey(settings.AUTH_USER_MODEL) event = models.ForeignKey(Event) placement = models.PositiveIntegerField(blank=True, null=True) avg_placement = models.FloatField(default=4) avg_score = models.FloatField(default=0) hanchan_count = models.PositiveIntegerField(default=0) good_hanchans = models.PositiveIntegerField(default=0) won_hanchans = models.PositiveIntegerField(default=0) class Meta(object): ordering = ('placement', 'avg_placement', '-avg_score',) def get_absolute_url(self): return reverse('event-ranking', args=[self.event_id]) def recalculate(self): """ Berechnet die durschnittliche Platzierung und Punkte, u.v.m. neu. Diese Daten werden benötigt um die Platzierung zu erstellen. Sie können zwar sehr leicht errechnet werden, es macht trotzdem Sinn sie zwischen zu speichern. """ logger.info( u'Recalculate EventRanking for Player %s in %s', self.user, self.event.name ) sum_placement = 0.0 sum_score = 0.0 event_hanchans = Hanchan.objects.confirmed_hanchans( user=self.user_id, event=self.event_id ) self.hanchan_count = event_hanchans.count() self.won_hanchans = 0 self.good_hanchans = 0 if self.hanchan_count <= 0: self.delete() else: for hanchan in event_hanchans: hanchan.get_playerdata(user=self.user) sum_placement += hanchan.placement sum_score += hanchan.game_score self.won_hanchans += 1 if hanchan.placement == 1 else 0 self.good_hanchans += 1 if hanchan.placement == 2 else 0 self.avg_placement = sum_placement / self.hanchan_count self.avg_score = sum_score / self.hanchan_count self.save(force_update=True) class Hanchan(models.Model): """ Ein komplette Runde Mahjong, die aus genau 4 Spielern bestehen muss. Es werden aber noch andere Tests durchgeführt, ob sie gültig ist. Außerdem gehört jede Hanchan zu einer Veranstaltung. """ event = models.ForeignKey(Event) start = models.DateTimeField( _('Start'), help_text=_('This is crucial to get the right Hanchans that scores') ) player1 = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='user_hanchan+', verbose_name=_('Player 1')) player1_input_score = models.IntegerField(_('Score')) player1_game_score = models.PositiveIntegerField( _('Score'), default=0, editable=False) player1_placement = models.PositiveSmallIntegerField( default=0, editable=False) player1_kyu_points = models.SmallIntegerField( blank=True, null=True, editable=False) player1_dan_points = models.SmallIntegerField( blank=True, null=True, editable=False) player1_bonus_points = models.SmallIntegerField( blank=True, null=True, editable=False) player1_comment = models.CharField( _('Comment'), blank=True, max_length=255, editable=False) player2 = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='user_hanchan+', verbose_name=_('Player 2')) player2_input_score = models.IntegerField(_('Score')) player2_game_score = models.PositiveIntegerField( _('Score'), default=0, editable=False) player2_placement = models.PositiveSmallIntegerField( default=0, editable=False) player2_kyu_points = models.SmallIntegerField( blank=True, null=True, editable=False) player2_dan_points = models.SmallIntegerField( blank=True, null=True, editable=False) player2_bonus_points = models.SmallIntegerField( blank=True, null=True, editable=False) player2_comment = models.CharField( _('Comment'), blank=True, max_length=255, editable=False) player3 = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='user_hanchan+', verbose_name=_('Player 3')) player3_input_score = models.IntegerField(_('Score')) player3_game_score = models.PositiveIntegerField( _('Score'), default=0, editable=False) player3_placement = models.PositiveSmallIntegerField( default=0, editable=False) player3_kyu_points = models.SmallIntegerField( blank=True, null=True, editable=False) player3_dan_points = models.SmallIntegerField( blank=True, null=True, editable=False) player3_bonus_points = models.SmallIntegerField( blank=True, null=True, editable=False) player3_comment = models.CharField( _('Comment'), blank=True, max_length=255, editable=False) player4 = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='user_hanchan+', verbose_name=_('Player 4')) player4_input_score = models.IntegerField(_('Score')) player4_game_score = models.PositiveIntegerField( _('Score'), default=0, editable=False) player4_placement = models.PositiveSmallIntegerField( default=0, editable=False) player4_kyu_points = models.SmallIntegerField( blank=True, null=True, editable=False) player4_dan_points = models.SmallIntegerField( blank=True, null=True, editable=False) player4_bonus_points = models.SmallIntegerField( blank=True, null=True, editable=False) player4_comment = models.CharField( _('Comment'), blank=True, max_length=255, editable=False) comment = models.TextField(_('Comment'), blank=True) confirmed = models.BooleanField(_('Has been Confirmed'), default=True, help_text=_( 'Only valid and confirmed Hanchans ' 'will be counted in the rating.') ) player_names = models.CharField(max_length=255, editable=False) season = models.PositiveSmallIntegerField( _('Season'), editable=False, db_index=True) objects = managers.HanchanManager() class Meta(object): ordering = ('-start',) verbose_name = _(u'Hanchan') verbose_name_plural = _(u'Hanchans') def __str__(self): return _("Hanchan from {0:%Y-%m-%d} at {0:%H:%M} with {1}").format( self.start, self.player_names ) def clean(self): """ Check if 4 different Players are attending the Game, if the Game between the opening hours and the Hanchan has a total of 100.000 Points. """ errors = {} score_sum = 0 player_set = set() try: self.clean_fields() except ValidationError as e: print e.update_error_dict(errors) return for i in xrange(1, 5): player = getattr(self, 'player%d' % i) input_score = getattr(self, 'player%d_input_score' % i) score_sum += input_score game_score = input_score if input_score > 0 else 0 if player in player_set: errors['player%d' % i] = _( "%s can't attend the same game multiple times") % player else: player_set.add(player) setattr(self, 'player%d_game_score' % i, game_score) # Check if the game was played during the event if self.start > timezone.now(): errors['start'] = _( "Games in the future may not be added, Dr. Brown") elif not (self.event.start <= self.start <= self.event.end): errors['start'] = _("Only games during the event are allowed") elif score_sum < 100000: errors['player4_input_score'] = _( 'Gamescore is lower then 100.000 Pt.') elif score_sum > 100000: errors['player4_input_score'] = _('Gamescore is over 100.000 Pt.') if errors: raise ValidationError(errors) def compute_player_placements(self): u""" Bestimmt die Platzierung eines der Spieler einer Hanchan und speichert diese beim jeweiligen Spieler ab. """ logger.debug("Berechne die Platzierungen neu...") player_names = [] other_player_placement = 0 other_player_game_score = 0 placement = 1 player_nr = 1 for player in self.player_list: if player['game_score'] == other_player_game_score: player['placement'] = other_player_placement else: player['placement'] = placement setattr(self, "player%d" % player_nr, player['user']) setattr(self, "player%d_input_score" % player_nr, player['input_score']) setattr(self, "player%d_game_score" % player_nr, player['game_score']) setattr(self, "player%d_placement" % player_nr, player['placement']) setattr(self, "player%d_kyu_points" % player_nr, player['kyu_points']) setattr(self, "player%d_dan_points" % player_nr, player['dan_points']) setattr(self, "player%d_bonus_points" % player_nr, player['bonus_points']) setattr(self, "player%d_comment" % player_nr, player['comment']) other_player_placement = player['placement'] other_player_game_score = player['game_score'] player_names.append(player['user'].username) player_nr += 1 placement += 1 self.player_names = ', '.join(player_names) def get_absolute_url(self): return "{url:s}#{id:d}".format( url=reverse('event-hanchan-list', kwargs={'event': self.event_id}), id=self.pk ) def get_playerdata(self, user): """small workaround to access score, placement and points of a specific user prominent in the user templates""" for player in ('player1', 'player2', 'player3', 'player4'): if getattr(self, player) == user: self.user = user self.input_score = getattr(self, '%s_input_score' % player) self.game_score = getattr(self, '%s_game_score' % player) self.placement = getattr(self, '%s_placement' % player) self.kyu_points = getattr(self, '%s_kyu_points' % player) self.dan_points = getattr(self, '%s_dan_points' % player) self.bonus_points = getattr(self, '%s_bonus_points' % player) self.player_comment = getattr(self, '%s_comment' % player) def update_playerdata(self, user, **kwargs): """i small workaround to access score, placement of a specific user prominent from a in the user templates""" for player in ('player1', 'player2', 'player3', 'player4'): if getattr(self, player) == user: setattr(self, '%s_input_score' % player, self.input_score) setattr(self, '%s_game_score' % player, self.game_score) setattr(self, '%s_placement' % player, self.placement) setattr(self, '%s_kyu_points' % player, self.kyu_points) setattr(self, '%s_dan_points' % player, self.dan_points) setattr(self, '%s_bonus_points' % player, self.bonus_points) setattr(self, '%s_comment' % player, self.player_comment) @property def player_list(self): player_list = [] for i in xrange(1, 5): player_list.append({ 'user': getattr(self, 'player%d' % i), 'input_score': getattr(self, 'player%d_input_score' % i), 'game_score': getattr(self, 'player%d_game_score' % i), 'placement': getattr(self, 'player%d_placement' % i), 'kyu_points': getattr(self, 'player%d_kyu_points' % i), 'dan_points': getattr(self, 'player%d_dan_points' % i), 'bonus_points': getattr(self, 'player%d_bonus_points' % i), 'comment': getattr(self, 'player%d_comment' % i), }) # sort player by Score: return sorted(player_list, key=lambda player: player['input_score'], reverse=True) def save(self, **kwargs): self.season = self.event.mahjong_season or self.start.year self.full_clean() self.compute_player_placements() return models.Model.save(self, **kwargs) class KyuDanRanking(models.Model): u""" Die Einstufung des Spielers im Kyu bzw. Dan System. Im Gegensatz zum Ladder Ranking ist das nicht Saison gebunden. Deswegen läuft es getrennt. """ user = models.OneToOneField(settings.AUTH_USER_MODEL) dan = models.PositiveSmallIntegerField(blank=True, null=True) dan_points = models.PositiveIntegerField(default=0) kyu = models.PositiveSmallIntegerField(default=10, blank=True, null=True) kyu_points = models.PositiveIntegerField(default=0) won_hanchans = models.PositiveIntegerField(default=0) good_hanchans = models.PositiveIntegerField(default=0) hanchan_count = models.PositiveIntegerField(default=0) legacy_date = models.DateField(blank=True, null=True) legacy_hanchan_count = models.PositiveIntegerField(default=0) legacy_dan_points = models.PositiveIntegerField(default=0) legacy_kyu_points = models.PositiveIntegerField(default=0) wins_in_a_row = 0 class Meta(object): ordering = ('-dan', '-dan_points', '-kyu_points',) verbose_name = _(u'Kyū/Dan Ranking') verbose_name_plural = _(u'Kyū/Dan Rankings') def __unicode__(self): if self.dan_points is not None: return u"%s - %d. Dan" % (self.user.username, self.dan or 1) else: return u"%s - %d. Kyu" % (self.user.username, self.kyu or 10) def append_3_in_a_row_bonuspoints(self, hanchan): u""" Wenn der Spieler 3 Siege in Folge hatte, bekommt er so viele Punkte das er einen Dan Rang aufsteigt. Dies wird als Kommentar abgespeichert, um es besser nachvollziehen zu können. """ if self.dan and hanchan.placement == 1: self.wins_in_a_row += 1 else: self.wins_in_a_row = 0 if self.dan and self.wins_in_a_row >= 3 and self.dan < 9: logger.info('adding bonuspoints for 3 wins in a row for %s', self.user) new_dan_rank = self.dan + 1 new_dan_points = DAN_RANKS_DICT[new_dan_rank] + 1 bonus_points = new_dan_points - self.dan_points logger.debug("Stats for %s:", self.user) logger.debug("current dan_points: %d", self.dan_points) logger.debug("current dan: %d", self.dan) logger.debug("min required points for the next dan: %d", new_dan_points) logger.debug("bonus points to add: %d", bonus_points) hanchan.dan_points += bonus_points hanchan.bonus_points += bonus_points hanchan.player_comment += '3 Siege in Folge: +%d Punkte auf den ' \ '%d. Dan. ' % ( bonus_points, new_dan_rank) self.dan_points += bonus_points self.wins_in_a_row = 0 # TODO: Komplett Überabreiten! def append_tournament_bonuspoints(self, hanchan): """ Prüft ob es die letzte Hanchan in einem Turnier war. Wenn ja werden bei Bedarf Bonuspunkte vergeben, falls der Spieler das Turnier gewonnen hat. :param hanchan: Ein Player Objekt """ bonus_points = 0 hanchans_this_event = Hanchan.objects.user_hanchans( user=self.user, event=hanchan.event ).order_by('-start') last_hanchan_this_event = hanchans_this_event[0] if hanchan != last_hanchan_this_event: return False # Das braucht nur am Ende eines Turnieres gemacht werden. else: event_ranking = EventRanking.objects.get( user=self.user, event=hanchan.event ) if event_ranking.placement == 1: bonus_points += 4 hanchan.player_comment += u'+4 Punkte Turnier gewonnen. ' if event_ranking.avg_placement == 1: bonus_points += 8 hanchan.player_comment += u'+8 Pkt: alle Spiele des Turnieres gewonnen. ' if bonus_points and self.dan: hanchan.dan_points += bonus_points self.dan_points += bonus_points elif bonus_points: hanchan.kyu_points += bonus_points self.kyu_points += bonus_points hanchan.bonus_points += bonus_points return True def get_absolute_url(self): if self.dan or self.dan_points > 0: return reverse('player-dan-score', args=[self.user.username]) else: return reverse('player-kyu-score', args=[self.user.username]) def recalculate(self): """ Fetches all valid Hanchans from this Player and recalculates his Kyu/Dan Ranking. """ self.dan = None self.dan_points = self.legacy_dan_points or 0 self.kyu = None self.kyu_points = self.legacy_kyu_points or 0 self.hanchan_count = self.legacy_hanchan_count or 0 self.good_hanchans = 0 self.won_hanchans = 0 logger.info("recalculating Kyu/Dan points for %s...", self.user) self.update_rank() valid_hanchans = Hanchan.objects.confirmed_hanchans(user=self.user) valid_hanchans = valid_hanchans.order_by('start') if self.legacy_date: valid_hanchans = valid_hanchans.filter(start__gt=self.legacy_date) self.hanchan_count += valid_hanchans.count() for hanchan in valid_hanchans: hanchan.get_playerdata(self.user) hanchan.bonus_points = 0 hanchan.player_comment = u"" self.update_hanchan_points(hanchan) if hanchan.event.mahjong_tournament: self.append_tournament_bonuspoints(hanchan) self.update_rank() self.append_3_in_a_row_bonuspoints(hanchan) self.update_rank() self.won_hanchans += 1 if hanchan.placement == 1 else 0 self.good_hanchans += 1 if hanchan.placement == 2 else 0 hanchan.update_playerdata(self.user) hanchan.save(force_update=True) self.save(force_update=True) def update_hanchan_points(self, hanchan): """ Berechne die Kyu bzw. Dan Punkte für eine Hanchan neu. :type hanchan: Hanchan :param hanchan: Das Player Objekt das neuberechnet werden soll. """ hanchan.kyu_points = None hanchan.dan_points = None if hanchan.event.mahjong_tournament: # Für Turniere gelten andere Regeln zur Punktevergabe: # 1. Platz 4 Punkte # 2. Platz 3 Punkte # 3. Platz 2 Punkte # 4. Platz 1 Punkt tourney_points = 4 - hanchan.placement if self.dan: hanchan.dan_points = tourney_points else: hanchan.kyu_points = tourney_points elif self.dan: if hanchan.game_score >= 60000: hanchan.dan_points = 3 elif hanchan.game_score == 0: hanchan.dan_points = -3 elif hanchan.placement == 1: hanchan.dan_points = 2 elif hanchan.placement == 2: hanchan.dan_points = 1 elif hanchan.placement == 3: hanchan.dan_points = -1 elif hanchan.placement == 4: hanchan.dan_points = -2 elif hanchan.game_score >= 60000: hanchan.kyu_points = 3 elif hanchan.game_score >= 30000: hanchan.kyu_points = 1 elif hanchan.input_score < 10000: hanchan.kyu_points = -1 else: hanchan.kyu_points = 0 # Add the hanchans points to the players points if self.dan: # Only substract so much points that player has 0 Points: if self.dan_points + hanchan.dan_points < 0: hanchan.dan_points -= (self.dan_points + hanchan.dan_points) self.dan_points += hanchan.dan_points else: # Only substract so much points that player has 0 Points: if self.kyu_points + hanchan.kyu_points < 0: hanchan.kyu_points -= (self.kyu_points + hanchan.kyu_points) self.kyu_points += hanchan.kyu_points # TODO: Merkwürdige Methode die zwar funktioniert aber nicht sehr # aussagekräfig ist. Überarbeiten? def update_rank(self): if self.dan and self.dan_points < 0: self.dan_points = 0 self.dan = 1 elif self.dan or self.dan_points > 0: old_dan = self.dan for min_points, dan_rank in DAN_RANKS: if self.dan_points > min_points: self.dan = dan_rank break if self.dan > old_dan: self.wins_in_a_row = 0 elif self.kyu_points < 1: self.kyu_points = 0 self.kyu = 10 elif self.kyu_points > 50: self.dan = 1 self.kyu = None self.dan_points = 0 self.kyu_points = 0 else: for min_points, kyu_rank in KYU_RANKS: if self.kyu_points > min_points: self.kyu = kyu_rank break class SeasonRanking(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL) season = models.PositiveSmallIntegerField(_('Season')) placement = models.PositiveIntegerField(blank=True, null=True) avg_placement = models.FloatField(blank=True, null=True) avg_score = models.FloatField(blank=True, null=True) hanchan_count = models.PositiveIntegerField(default=0) good_hanchans = models.PositiveIntegerField(default=0) won_hanchans = models.PositiveIntegerField(default=0) objects = managers.SeasonRankingManager() class Meta(object): ordering = ('placement', 'avg_placement', '-avg_score',) def get_absolute_url(self): return reverse('player-ladder-score', args=[self.user.username]) def recalculate(self): season_hanchans = Hanchan.objects.season_hanchans( user=self.user, season=self.season) sum_placement = 0 sum_score = 0 self.placement = None self.hanchan_count = season_hanchans.count() self.good_hanchans = 0 self.won_hanchans = 0 logger.info( u'Recalculate LadderRanking for Player %s in Season %s', self.user, self.season) for hanchan in season_hanchans: sum_placement += hanchan.placement sum_score += hanchan.game_score self.won_hanchans += 1 if hanchan.placement == 1 else 0 self.good_hanchans += 1 if hanchan.placement == 2 else 0 try: self.avg_placement = sum_placement / self.hanchan_count self.avg_score = sum_score / self.hanchan_count except ZeroDivisionError: self.avg_placement = None self.avg_score = None self.save(force_update=True) def update_ranking(sender, instance, **kwargs): for user in ( instance.player1, instance.player2, instance.player3, instance.player4): logger.debug("marking %s's kyu/dan for recalculation", user) set_dirty(user=user.id) logger.debug( "marking event %s for recalculation.", instance.event) set_dirty(event=instance.event_id, user=user.id) if instance.season: logger.debug( "marking %s's ladder %i season for recalculation.", user, instance.season) set_dirty(user=user.id, season=instance.season) set_dirty(season=instance.season) models.signals.pre_delete.connect(update_ranking, sender=Hanchan) models.signals.post_save.connect(update_ranking, sender=Hanchan)