Files
kasu/src/mahjong_ranking/models.py

608 lines
25 KiB
Python

# -*- 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)