rosedu/wouso

View on GitHub
wouso/games/challenge/models.py

Summary

Maintainability
F
1 wk
Test Coverage
import random
from datetime import datetime, time, timedelta, date
from random import shuffle
import pickle as pk
import sys
from django.db import models
from django.db.models import Q, Avg
from django.utils.translation import ugettext_noop, ugettext as _
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
from wouso.core.user.models import Player
from wouso.core.magic.manager import InsufficientAmount
from wouso.core.qpool.models import Question
from wouso.core.qpool import get_questions_with_category, register_category
from wouso.core.game.models import Game
from wouso.core import scoring, signals
from wouso.core.god import God
from wouso.interface.apps.messaging.models import Message
import logging


class ChallengeException(Exception):
    pass


class ChallengeUser(Player):
    """ Extension of the userprofile, customized for challenge """

    last_launched = models.DateTimeField(blank=True, null=True)

    def is_eligible(self):
        return God.user_is_eligible(self, ChallengeGame)

    def can_launch(self):
        """ Check if 1 challenge per day restriction apply
        """
        now = datetime.now()
        today_start = datetime.combine(now, time())
        today_end = datetime.combine(now, time(23, 59, 59))
        logging.info("today_start: %s, today_end: %s, self.last_launched: %s" % (today_start, today_end, self.last_launched))
        if self.magic.has_modifier('challenge-cannot-challenge'):
            return False
        if not self.last_launched:
            return True
        if today_start <= self.last_launched <= today_end:
            return False
        return True

    def has_enough_points(self):
        """ Check if the user has 30 points to challenge
        """
        REQ_POINTS = 30
        return self.points >= REQ_POINTS

    def in_same_division(self, user):
        from wouso.interface.top.models import TopUser
        position_diff = abs(self.get_extension(TopUser).position - user.get_extension(TopUser).position)
        if position_diff <= 20:
            return True
        return False

    def can_challenge(self, user):
        """ Check if the target user is available.
        """
        user = user.get_extension(ChallengeUser)
        if self.user == user.user:
            # Cannot challenge myself
            logging.info("User cannot challenge because it is the same user.")
            return False
        if user.magic.has_modifier('challenge-cannot-be-challenged'):
            logging.info("User cannot challenge due to magic modifier.")
            return False
        return God.user_can_interact_with(self, user, game=ChallengeGame)

    def has_one_more(self):
        return self.magic.has_modifier('challenge-one-more')

    def do_one_more(self):
        try:
            modifier = self.magic.use_modifier('challenge-one-more', 1)
        except InsufficientAmount:
            return False
        self.set_last_launched(None)
        self.save()

        signal_msg = ugettext_noop('used {artifact} to enable one more challenge.')
        signals.addActivity.send(sender=None, user_from=self,
                                 user_to=self,
                                 message=signal_msg,
                                 arguments=dict(artifact=modifier.artifact.title),
                                 game=ChallengeGame.get_instance())
        return True

    def can_play(self, challenge):
        return challenge.can_play(self)

    def launch_against(self, destination):
        destination = destination.get_extension(ChallengeUser)

        if destination.id == self.id:
            raise ChallengeException('Cannot launch against myself')

        if not self.can_launch():
            raise ChallengeException('Player cannot launch')

        if not self.in_same_division(destination):
            raise ChallengeException('You are not in the same division')

        if not self.can_challenge(destination):
            raise ChallengeException('Player cannot launch against this opponent')

        return Challenge.create(user_from=self, user_to=destination)

    def set_last_launched(self, value):
        logging.info("set last launched of %s to %s" % (hex(id(self)), value))
        self.last_launched = value
        self.save()

    def get_all_challenges(self):
        return Challenge.objects.exclude(status=u'L').filter(Q(user_from__user=self) | Q(user_to__user=self))

    def get_won_challenges(self):
        return self.get_all_challenges().filter(winner=self)

    def get_lost_challenges(self):
        return self.get_all_challenges().exclude(winner=self).exclude(status=u'R').exclude(status=u'D')

    def get_draw_challenges(self):
        return self.get_all_challenges().filter(status=u'D')

    def get_refused_challenges(self):
        return self.get_all_challenges().filter(status=u'R')

    def get_win_percentage(self):
        w = self.get_won_challenges().count()
        d = self.get_draw_challenges().count()
        l = self.get_lost_challenges().count()
        # 1 draw counts as 1/2 win, 1/2 loss
        return 0 if w + d == 0 else (w + d / 2.0) / (w + l + d) * 100

    def get_random_opponent(self):
        players = ChallengeUser.objects.exclude(user=self.user)
        players = players.exclude(race__can_play=False)
        players = [p for p in players if self.can_challenge(p) and self.in_same_division(p)]
        if not players:
            return False
        # selects the user to be challenged
        import random
        i = random.randrange(0, len(players))
        return players[i]

    def get_related_challenges(self, target_user):
        # Gets the challenges between self and target_user
        chall_total = Challenge.objects.filter(Q(user_from__user=self) |
                                               Q(user_to__user=self)).exclude(status=u'L')
        chall_total = chall_total.filter(Q(user_from__user=target_user) |
                                         Q(user_to__user=target_user)).order_by('-date')
        return chall_total

    def get_stats(self):
        chall_total = self.get_all_challenges()
        chall_won = self.get_won_challenges()
        chall_sent = chall_total.filter(user_from__user=self)
        chall_rec = chall_total.filter(user_to__user=self)

        n_chall_sent = chall_sent.count()
        n_chall_rec = chall_rec.count()
        n_chall_played = chall_sent.count() + chall_rec.count()
        n_chall_won = chall_won.count()
        n_chall_ref = self.get_refused_challenges().count()
        all_participation = Participant.objects.filter(user=self)

        opponents_from = list(set(map(lambda x: x.user_to.user, chall_sent)))
        opponents_to = list(set(map(lambda x: x.user_from.user, chall_rec)))
        opponents = list(set(opponents_from + opponents_to))

        result = []
        for op in opponents:
            chall_against_op = chall_total.filter(Q(user_to__user=op) | Q(user_from__user=op))
            won = chall_against_op.filter(Q(status=u'P') & Q(winner=self)).count()
            lost = chall_against_op.filter(Q(status=u'P') & Q(winner=op)).count()
            draw = chall_against_op.filter(Q(status=u'D')).count()
            refused = chall_against_op.filter(Q(status=u'R')).count()
            total = won + lost + draw + refused
            result.append((op, won, lost, draw, refused, total))

        # Sort results by 'total'
        result = sorted(result, key=lambda by: by[5], reverse=True)

        average_time = all_participation.aggregate(Avg('seconds_took'))['seconds_took__avg']
        average_score = all_participation.aggregate(Avg('score'))['score__avg']

        if average_time is None:
            average_time = 0
        if average_score is None:
            average_score = 0

        win_percentage = self.get_win_percentage()

        stats = dict(n_chall_played=n_chall_played, n_chall_won=n_chall_won,
                     n_chall_sent=n_chall_sent, n_chall_rec=n_chall_rec,
                     n_chall_ref=n_chall_ref, current_player=self,
                     average_time=average_time, average_score=average_score,
                     win_percentage=win_percentage, opponents=result)
        return stats

Player.register_extension('challenge', ChallengeUser)


class Participant(models.Model):
    user = models.ForeignKey(ChallengeUser)
    start = models.DateTimeField(null=True, blank=True)
    seconds_took = models.IntegerField(null=True, blank=True)
    played = models.BooleanField(default=False)
    responses = models.TextField(default='', blank=True, null=True)
    # score = models.FloatField(null=True, blank=True)
    score = models.IntegerField(null=True, blank=True)

    @property
    def challenge(self):
        try:
            return Challenge.objects.get(Q(user_from=self) | Q(user_to=self))
        except Challenge.DoesNotExist:
            return None

    def __unicode__(self):
        return unicode(self.user)


class Challenge(models.Model):
    STATUS = (
        ('L', 'Launched'),
        ('A', 'Accepted'),
        ('R', 'Refused'),
        ('P', 'Played'),
        ('D', 'Draw'),
    )
    user_from = models.ForeignKey(Participant, related_name="user_from")
    user_to = models.ForeignKey(Participant, related_name="user_to")
    date = models.DateTimeField()
    status = models.CharField(max_length=1, choices=STATUS, default='L')
    winner = models.ForeignKey(ChallengeUser, related_name="winner", null=True, blank=True)
    questions = models.ManyToManyField(Question)
    owner = models.ForeignKey(Game, null=True, blank=True)

    nr_q = 0
    LIMIT = 5
    TIME_LIMIT = 300  # seconds
    TIME_SAFE = 10    # seconds more
    WARRANTY = True   # on/off switch
    SCORING = True    # on/off switch for rewarding challenges

    @classmethod
    def create(cls, user_from, user_to, ignore_questions=False):
        """ Assigns questions, and returns the number of assigned q """
        questions = [q for q in get_questions_with_category('challenge')]
        if (len(questions) < cls.LIMIT) and not ignore_questions:
            raise ChallengeException('Too few questions')
        shuffle(questions)

        questions_qs = questions[:cls.LIMIT]
        challenge = cls.create_custom(user_from, user_to, questions_qs)

        # set last_launched
        user_from = user_from.get_extension(ChallengeUser)
        user_from.set_last_launched(datetime.now())

        return challenge

    @classmethod
    def create_custom(cls, player_from, player_to, questions_qs, game=None):
        user_from, user_to = player_from.get_extension(ChallengeUser), player_to.get_extension(ChallengeUser)
        uf, ut = Participant.objects.create(user=user_from), Participant.objects.create(user=user_to)

        c = Challenge.objects.create(user_from=uf, user_to=ut, date=datetime.now(), owner=game)
        for q in questions_qs:
            c.questions.add(q)

        c.manager.created()
        return c

    @staticmethod
    def get_expired(today):
        """
        Return all expired candidate challenges at given date.
        """
        yesterday = today + timedelta(days=-1)
        return Challenge.objects.filter(date__lt=yesterday).filter(Q(status='A') | Q(status='L'))

    @classmethod
    def exist_last_day(cls, today, user_from, user_to):
        """
        Return true if there are any challenges between the two users in the last day
        """
        yesterday = today + timedelta(days=-1)
        return Challenge.objects.filter(user_from__user=user_from, user_to__user=user_to, date__gt=yesterday).count() > 0

    @classmethod
    def last_between(cls, user_from, user_to):
        try:
            return Challenge.objects.filter(user_from__user=user_from, user_to__user=user_to).exclude(status='R').order_by('-date')[0]
        except IndexError:
            return None

    @property
    def participants(self):
        return (self.user_from, self.user_to)

    @property
    def manager(self):
        if self.owner is None or self.owner.name == 'ChallengeGame':
            return ChallengeGame.get_manager(self)
        return self.owner.get_real_object().get_manager(self)

    def participant_for_player(self, player):
        player = player.get_extension(ChallengeUser)
        if self.user_from.user == player:
            return self.user_from
        elif self.user_to.user == player:
            return self.user_to
        raise ValueError('Not participating in this challenge')

    def accept(self):
        self.status = 'A'
        self.save()
        self.manager.accept()

    def refuse(self, auto=False):
        self.status = 'R'
        self.save()
        self.manager.refuse(auto)

    def cancel(self):
        self.manager.cancel()
        self.user_from.user.set_last_launched(None)
        self.delete()

    def set_start(self, user):
        """ Update user.start and set playing time to now
        This is called when one of the participants sees the challenge for the
        first time.
        After this call, challenge will be visible to him for 5 minutes
        TODO: check it hasn't been already started in the past"""

        partic = self.participant_for_player(user)

        partic.start = datetime.now()
        partic.save()

    def set_won_by_player(self, player):
        if self.user_from.user == player:
            self.user_from.score = 1
            self.user_to.score = 0
        else:
            self.user_from.score = 0
            self.user_to.score = 1

        self.user_from.played = True
        self.user_to.played = True

        # process expired activity
        self.played()

    def set_expired(self):
        if not self.user_from.played:
            self.user_from.score = 0.0
            self.user_from.played = True

        if not self.user_to.played:
            self.user_to.score = 0.0
            self.user_to.played = True

        # send expired activity
        self.played()

    def time_for_user(self, user):
        now = datetime.now()
        partic = self.participant_for_player(user)

        tlimit = scoring.timer(user, ChallengeGame, 'chall-timer', level=user.level_no)

        return tlimit - (now - partic.start).seconds

    def is_expired(self, participant):
        """ This function assumes that seconds_took has been set.
        If the user didn't submit the challenge, this will return False
        which might be incorrect.
        TODO: fix this to first check if user has submitted, and
        if not, verify with datetime.now - participant.start.
        """
        if participant.start is None:
            return False
        if participant.seconds_took < Challenge.TIME_LIMIT + Challenge.TIME_SAFE:
            return False
        return True

    def is_expired_for_user(self, user):
        """ Check if the challenge has expired for the user
        """
        partic = self.participant_for_player(user)
        return self.is_expired(partic)

    def is_started_for_user(self, user):
        """ Check if the challenge has already started for the given user"""
        partic = self.participant_for_player(user)
        if partic.start is None:
            return False
        return True

    def extraInfo(self, user_won, user_lost):
        '''returns a string with extra info for a string such as User 1 finished the challenge in $SECONDS seconds
        (or $MINUTES minutes and seconds) and scored X points)'''

        def formatTime(seconds):
            if seconds is None:
                return _('expired')
            ret = ''
            if seconds < 60:
                ret = _("{seconds} seconds").format(seconds=seconds)
            elif seconds == 60:
                ret = _('1 minute')
            elif seconds % 60 == 0:
                ret = _('{minutes} minutes').format(minutes=(seconds/60))
            elif 60 < seconds < 120:
                ret = _('1 minute and {s} seconds').format(s=seconds % 60)
            else:
                ret = _('{m} minutes and {s} seconds').format(m=seconds / 60, s=seconds % 60)
            return _('(in {time})').format(time=ret)

        return '%dp %s - %dp %s' % (user_won.score, formatTime(user_won.seconds_took),
                                    user_lost.score, formatTime(user_lost.seconds_took))

    def played(self):
        """ Both players have played, save and score
        Notice the fact this is the only function where the scoring is affected
        """
        result = self.manager.get_result()

        if result == 'draw':
            self.status = 'D'
        else:
            self.status = 'P'
            self.user_won, self.user_lost = result
            self.winner = self.user_won.user
        self.save()

        self.manager.handle_result()
        self.manager.score()

    @classmethod
    def _calculate_points(cls, responses):
        """ Response contains a dict with question id and checked answers ids.
        Example:
            {1 : [14,], ...}, - has answered answer with id 14 at the question with id 1
        """
        points = 0.0
        results = {}
        for r, v in responses.iteritems():
            checked, missed, wrong = 0, 0, 0
            q = Question.objects.get(id=r)
            for a in q.answers.all():
                if a.correct:
                    if a.id in v:
                        checked += 1
                    else:
                        missed += 1
                elif a.id in v:
                    wrong += 1
            correct_count = len([a for a in q.answers if a.correct])
            wrong_count = len([a for a in q.answers if not a.correct])
            if correct_count == 0:
                qpoints = 1 if (len(v) == 0) else 0
            elif wrong_count == 0:
                qpoints = 1 if (len(v) == q.answers.count()) else 0
            else:
                qpoints = float(checked) / correct_count - float(wrong) / wrong_count
            qpoints = qpoints if qpoints > 0 else 0
            points += qpoints
            results[r] = ((checked, correct_count))
        return {'points': int(100.0 * points), 'results': results}

    def set_played(self, user, responses):
        """ Set user's results. If both users have played, also update self and activity. """
        user_played = self.participant_for_player(user)

        user_played.seconds_took = (datetime.now() - user_played.start).seconds
        user_played.played = True
        user_played.responses = pk.dumps(responses)
        exp = False
        if self.is_expired(user_played):
            exp = True
            user_played.score = 0.0
        else:
            results = Challenge._calculate_points(responses)
            user_played.score = results['points']
        user_played.save()

        if self.user_to.played and self.user_from.played:
            self.played()

        if exp:
            results = {}
            results['results'] = {}
            results['points'] = '0.0 (expired)'
        return results

    def can_play(self, user):
        """ Check if user can play this challenge"""
        try:
            partic = self.participant_for_player(user)
        except ValueError:
            return False

        if partic.played:
            return False

        return self.is_runnable()

    def is_launched(self):
        return self.status == 'L'

    def is_runnable(self):
        return self.status == 'A'

    def is_refused(self):
        return self.status == 'R'

    def is_draw(self):
        return self.status == 'D'

    def is_played(self):
        return self.status == 'P'

    def title(self):
        return u"%s vs %s" % (self.user_from, self.user_to)

    def __unicode__(self):
        return "%s vs %s (%s) - %s [%d] " % (
            unicode(self.user_from),
            unicode(self.user_to),
            self.date,
            self.status,
            self.questions.count())


class ChallengeManager(object):
    def __init__(self, challenge):
        self.challenge = challenge

    def created(self):
        pass

    def accept(self):
        pass

    def refuse(self):
        raise NotImplementedError

    def cancel(self):
        raise NotImplementedError

    def get_result(self):
        raise NotImplementedError

    def handle_result(self):
        """ Called after status is one of: 'D', 'P'
        """
        pass

    def score(self):
        raise NotImplementedError


class DefaultChallengeManager(ChallengeManager):
    def created(self):
        if self.challenge.WARRANTY:
            # take 3 points from user_from
            scoring.score(self.challenge.user_from.user, ChallengeGame, 'chall-warranty', external_id=self.challenge.id)

    def accept(self):
        if self.challenge.WARRANTY:
            # take warranty from user_to
            scoring.score(self.challenge.user_to.user, ChallengeGame, 'chall-warranty', external_id=self.challenge.id)

    def refuse(self, auto):
        self.challenge.user_from.user.set_last_launched(None)

        # send activity signal
        if auto:
            signal_msg = ugettext_noop('has refused challenge from {chall_from} (expired)')
        else:
            signal_msg = ugettext_noop('has refused challenge from {chall_from}')
        action_msg = 'chall-refused'
        signals.addActivity.send(sender=None,
                                 user_from=self.challenge.user_to.user,
                                 user_to=self.challenge.user_from.user,
                                 message=signal_msg,
                                 arguments=dict(chall_from=self.challenge.user_from),
                                 action=action_msg,
                                 game=ChallengeGame.get_instance())
        self.challenge.save()
        if self.challenge.WARRANTY:
            # give warranty back to initiator
            scoring.unset(self.challenge.user_from.user, ChallengeGame, 'chall-warranty', external_id=self.challenge.id)

    def cancel(self):
        if self.challenge.WARRANTY:
            # give warranty back to initiator
            scoring.unset(self.challenge.user_from.user, ChallengeGame, 'chall-warranty', external_id=self.challenge.id)

    def get_result(self):
        for u in (self.challenge.user_to, self.challenge.user_from):
            if u.user.magic.has_modifier('challenge-always-lose'):
                u.score = -1

        if self.challenge.user_to.score > self.challenge.user_from.score:
            result = (self.challenge.user_to, self.challenge.user_from)
        elif self.challenge.user_from.score > self.challenge.user_to.score:
            result = (self.challenge.user_from, self.challenge.user_to)
        else:  # draw game
            result = 'draw'

        return result

    def handle_result(self):
        if self.challenge.status == 'D':
            action_msg = "chall-draw"
            signal_msg = ugettext_noop('draw result between {user_from} and {user_to}:\n{extra}')
            signals.addActivity.send(sender=None, user_from=self.challenge.user_to.user,
                                     user_to=self.challenge.user_from.user,
                                     message=signal_msg,
                                     arguments=dict(user_to=self.challenge.user_to, user_from=self.challenge.user_from,
                                                    extra=self.challenge.extraInfo(self.challenge.user_from, self.challenge.user_to)),
                                     action=action_msg,
                                     game=ChallengeGame.get_instance())
        elif self.challenge.status == 'P':
            action_msg = "chall-won"
            signal_msg = ugettext_noop('won challenge with {user_lost}: {extra}')
            signals.addActivity.send(sender=None, user_from=self.challenge.user_won.user,
                                     user_to=self.challenge.user_lost.user,
                                     message=signal_msg,
                                     arguments=dict(user_lost=self.challenge.user_lost, id=self.challenge.id,
                                                    extra=self.challenge.extraInfo(self.challenge.user_won, self.challenge.user_lost)),
                                     action=action_msg,
                                     game=ChallengeGame.get_instance())

    def score(self):
        if not self.challenge.SCORING:
            return

        for u in (self.challenge.user_to, self.challenge.user_from):
                u.percents = 100

        if self.challenge.status == 'D':
            scoring.score(self.challenge.user_to.user, ChallengeGame, 'chall-draw', percents=self.challenge.user_to.percents)
            scoring.score(self.challenge.user_from.user, ChallengeGame, 'chall-draw', percents=self.challenge.user_from.percents)
        else:
            diff_race = self.challenge.user_won.user.race != self.challenge.user_lost.user.race
            diff_class = self.challenge.user_won.user.group != self.challenge.user_lost.user.group
            diff_race = 1 if diff_race else 0
            diff_class = 1 if diff_class else 0
            winner_points = self.challenge.user_won.user.points
            loser_points = self.challenge.user_lost.user.points

            # Check for charge
            if self.challenge.user_won.user.magic.has_modifier('challenge-affect-scoring-won'):
                self.challenge.user_won.percents += self.challenge.user_won.user.magic.modifier_percents('challenge-affect-scoring-won') - 100

            # Check for weakness
            if self.challenge.user_won.user.magic.has_modifier('challenge-affect-scoring-lost'):
                self.challenge.user_won.percents += self.challenge.user_won.user.magic.modifier_percents('challenge-affect-scoring-lost') - 100

            # Check for frenzy on the winning player
            if self.challenge.user_won.user.magic.has_modifier('challenge-affect-scoring'):
                self.challenge.user_won.percents += self.challenge.user_won.user.magic.modifier_percents('challenge-affect-scoring') - 100

            if self.challenge.WARRANTY:
                # warranty not affected by percents
                scoring.score(self.challenge.user_won.user, ChallengeGame, 'chall-warranty-return', external_id=self.challenge.id)

            scoring.score(self.challenge.user_won.user, ChallengeGame, 'chall-won',
                          external_id=self.challenge.id, percents=self.challenge.user_won.percents,
                          points=self.challenge.user_won.score, points2=self.challenge.user_lost.score,
                          different_race=diff_race, different_class=diff_class,
                          winner_points=winner_points, loser_points=loser_points)

            # Check for frenzy on the losing player
            if self.challenge.user_lost.user.magic.has_modifier('challenge-affect-scoring'):
                self.challenge.user_lost.percents += self.challenge.user_lost.user.magic.modifier_percents('challenge-affect-scoring') - 100

            # Check for spell evade
            if self.challenge.user_lost.user.magic.has_modifier('challenge-evade'):
                random.seed()
                if random.random() < 0.20:
                    # He's lucky, no penalty, return warranty
                    scoring.score(self.challenge.user_lost.user, ChallengeGame, 'chall-warranty-return', external_id=self.challenge.id)
                    Message.send(sender=None, receiver=self.challenge.user_lost.user, subject="Challenge evaded", text="You have just evaded losing points in a challenge")

            scoring.score(self.challenge.user_lost.user, ChallengeGame, 'chall-lost',
                          external_id=self.challenge.id, percents=self.challenge.user_lost.percents,
                          points=self.challenge.user_lost.score, points2=self.challenge.user_lost.score)


class ChallengeGame(Game):
    """ Each game must extend Game """
    class Meta:
        proxy = True

    QPOOL_CATEGORY = 'challenge'

    def __init__(self, *args, **kwargs):
        # Set parent's fields
        self._meta.get_field('verbose_name').default = "Challenges"
        self._meta.get_field('short_name').default = ""
        # the url field takes as value only a named url from module's urls.py
        self._meta.get_field('url').default = "challenge_index_view"
        super(ChallengeGame, self).__init__(*args, **kwargs)

    @staticmethod
    def get_active(user):
        """ Return a list of active (runnable) challenges for a user """
        user = user.get_extension(ChallengeUser)
        try:
            challs = [p.challenge for p in Participant.objects.filter(
                Q(user_id=user.id, played=False)).order_by('-id') if p.challenge.is_launched() or p.challenge.is_runnable()]
        except Participant.DoesNotExist:
            challs = []
        return challs

    @staticmethod
    def get_played(user):
        """ Return a list of played (scored TODO) challenges for a user """
        try:
            challs = [p.challenge for p in Participant.objects.filter(
                Q(user_id=user.id, played=True)).order_by('-id')]
        except Participant.DoesNotExist:
            challs = []
        return challs

    @staticmethod
    def get_expired(user):
        """ Return a list of status != L/A challenges, where played = False """
        # TODO
        return []

    @classmethod
    def get_formulas(kls):
        """ Returns a list of formulas used by qotd """
        fs = []
        chall_game = kls.get_instance()
        fs.append(dict(name='chall-won', expression='points=6+{different_race}+{different_class}',
                       owner=chall_game.game,
                       description='Points earned when winning a challenge. Arguments: different_race (int 0,1), different_class (int 0,1)'))
        fs.append(dict(name='chall-lost', expression='points=2',
                       owner=chall_game.game,
                       description='Points earned when losing a challenge'))
        fs.append(dict(name='chall-draw', expression='points=4',
                       owner=chall_game.game,
                       description='Points earned when drawing a challenge'))
        fs.append(dict(name='chall-warranty', expression='points=-3',
                       owner=chall_game.game,
                       description='Points taken as a warranty for challenge'))
        fs.append(dict(name='chall-warranty-return', expression='points=3',
                       owner=chall_game.game,
                       description='Points given back as a warranty taken for challenge'))
        fs.append(dict(name='chall-timer',
                       expression='tlimit=300 - 5 * ({level} - 1)', owner=chall_game.game,
                       description='Seconds left for a user in challenge'))
        return fs

    @classmethod
    def get_modifiers(kls):
        """
        Challenge game modifiers
        """
        return ['challenge-one-more',  # challenge twice a day, positive
                'challenge-cannot-be-challenged',  # reject incoming challenges, negative
                'challenge-cannot-challenge',  # reject outgoing challenges, negative
                'challenge-always-lose',  # lose regardless the result, negative
                'challenge-affect-scoring',  # affect scoring by positive/negative percent
                'challenge-affect-scoring-won',  # affect scoring by positive/negative percent for win challenges
                'challenge-evade',  # 33% chance player does not lose points in a challenge
                ]

    @classmethod
    def management_task(cls, now=None, stdout=sys.stdout):
        now = now if now else datetime.now()
        today = now.date()
        challenges = Challenge.get_expired(today)
        stdout.write(' Updating %d expired challenges (at %s)\n' % (len(challenges), today))
        for c in challenges:
            if c.is_launched():
                # launched before yesterday, automatically refuse
                c.refuse(auto=True)
            else:
                # launched and accepted before yesterday, but not played by both
                c.set_expired()

    @classmethod
    def get_api(kls):
        from api import ChallengesHandler, ChallengeLaunch, ChallengeHandler, ChallengeGetRandom

        return {r'^challenge/list/$': ChallengesHandler,
                r'^challenge/random/$': ChallengeGetRandom,
                r'^challenge/launch/(?P<player_id>\d+)/$': ChallengeLaunch,
                r'^challenge/(?P<challenge_id>\d+)/$': ChallengeHandler,
                r'^challenge/(?P<challenge_id>\d+)/(?P<action>refuse|cancel|accept)/$': ChallengeHandler,
                }

    @classmethod
    def get_manager(cls, challenge):
        return DefaultChallengeManager(challenge)


register_category(ChallengeGame.QPOOL_CATEGORY, ChallengeGame)


# Hack for having participants in sync
def challenge_post_delete(sender, instance, **kwargs):
    """ For some reason, this is called twice. Needs investigantion
    Also, in django devele, on_delete=cascade will fix this hack
    """
    try:
        instance.user_from.delete()
        instance.user_to.delete()
    except:
        pass
models.signals.post_delete.connect(challenge_post_delete, Challenge)