rosedu/wouso

View on GitHub
wouso/core/scoring/sm.py

Summary

Maintainability
C
1 day
Test Coverage
# TODO: why isn't this implemented as a class singleton, the same as God ?
import logging
from django.utils.translation import ugettext_noop
from django.db import models
from django.contrib.auth.models import User
from wouso.core import signals
from wouso.core.user.models import Player
from wouso.core.scoring.models import Coin, Formula, History
from wouso.core.god import God
from wouso.core.game import get_games, Game


class NotSetupError(Exception):
    pass


class InvalidFormula(Exception):
    pass


class FormulaParsingError(Exception):
    def __init__(self, formula):
        super(FormulaParsingError, self).__init__()
        self.formula = formula

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


class InvalidScoreCall(Exception):
    pass


CORE_POINTS = ('points', 'gold', 'penalty')

# Utility functions
PHI = (1 + 5**0.5) / 2


def fib(n):
    return int(round((PHI**n - (1-PHI)**n) / 5**0.5))


# Setup
def check_setup():
    """ Check if the module has been setup """

    if Coin.get('points') is None:
        return False
    return True


def setup_scoring():

    """ Prepare database for Scoring """
    for cc in CORE_POINTS:
        if not Coin.get(cc):
            Coin.add(cc)
    # special case, gold is integer
    gold = Coin.get('gold')
    gold.integer = True
    gold.save()

    # iterate through games and register formulas
    for game in get_games():
        for formula in game.get_formulas():
            if not Formula.get(formula):
                Formula.add(formula)
    # add wouso formulas
    for formula in God.get_system_formulas():
        if not Formula.get(formula):
            Formula.add(formula)


def calculate(formula, **params):
    """ Calculate formula and return a dictionary of coin and amounts """
    formula = Formula.get(formula)
    if formula is None:
        raise InvalidFormula(formula)

    if not formula.expression:
        return {}

    return calculate_expression(formula.expression, formula, **params)


def calculate_expression(expression, formula=None, **params):
    """
    Calculate a formula defintion. Example of such defintions are:
        * points=50
        * points=10+{{level}}*100
        * gold=3;points=100

    Returns a dictionary with coins and values, example:
        {'points': 30, 'gold': 100}
    """
    ret = {}
    try:
        frml = expression.format(**params)
        # Python does not allow assignments inside eval
        # Using this workaround for now
        ass = frml.split(';')
        for a in ass:
            asp = a.split('=')
            coin = asp[0].strip()
            expr = '='.join(asp[1:])
            try:
                result = eval(expr)
            except ZeroDivisionError as e:
                result = 0
            ret[coin] = result
    except Exception as e:
        #logging.exception(e)
        raise FormulaParsingError(formula)
    return ret


def score(user, game, formula, external_id=None, percents=100, **params):
    """ Give amount of coin specified by the formula to the player.
     The amount can be affected by percents/100.
    """
    ret = calculate(formula, **params)

    if isinstance(ret, dict):
        for coin, amount in ret.items():
            score_simple(user, coin, amount, game, formula, external_id, percents=percents)


def timer(user, game, formula, default=300, **params):
    """ Compute a timer value, or return default
    """
    formula = Formula.get(formula)
    if formula is None:
        raise InvalidFormula(formula)
    if not formula.expression:
        return default

    values = calculate(formula, **params)
    if 'tlimit' in values:
        return values['tlimit']
    return default


def unset(user, game, formula, external_id=None, **params):
    """ Remove all history records by the external_id, formula and game given to the user """
    formula = Formula.get(formula)
    user = user.user.get_profile()  # make sure you are working on fresh Player
    for history in History.objects.filter(user=user, game=game.get_instance(), formula=formula, external_id=external_id):
        if history.coin.name == 'points':
            user.points -= history.amount
        history.delete()
    user.save()
    update_points(user, game)


def update_points(player, game):
    level = God.get_level_for_points(player.points)

    if level == player.level_no:
        return

    if level < player.level_no:
        action_msg = 'level-downgrade'
        signal_msg = ugettext_noop("downgraded to level {level}")
        signals.addActivity.send(sender=None, user_from=player,
                                 user_to=player, message=signal_msg,
                                 arguments=dict(level=level),
                                 game=game, action=action_msg)
    else:
        action_msg = 'level-upgrade'
        arguments = dict(level=level)
        # Check if the user has previously reached this level
        if level > player.max_level:
            # Update the maximum reached level
            player.max_level = level
            # Offer the corresponding amount of gold
            score(player, None, 'level-gold', external_id=level, level=level)

            signal_msg = ugettext_noop("upgraded to level {level} and received {amount} gold")
            amount = calculate('level-gold', level=level).get('gold', 0)
            arguments['amount'] = amount
        else:
            # The user should not receive additional gold
            signal_msg = ugettext_noop("upgraded back to level {level}")

        signals.addActivity.send(sender=None, user_from=player,
                                 user_to=player, message=signal_msg,
                                 arguments=arguments, game=None,
                                 action=action_msg)
    player.level_no = level
    player.save()


def score_simple(player, coin, amount, game=None, formula=None,
                 external_id=None, percents=100):

    """ Give amount of coin to the player.
    """
    if not isinstance(game, Game) and game is not None:
        game = game.get_instance()

    if not isinstance(player, Player):
        raise InvalidScoreCall()

    user = player.user
    player = user.get_profile()
    user = player.user

    coin = Coin.get(coin)
    formula = Formula.get(formula)

    computed_amount = 1.0 * amount * percents / 100
    hs = History.add(user=user, coin=coin, amount=computed_amount,
                     game=game, formula=formula, external_id=external_id, percents=percents)

    # update user.points asap
    if coin.name == 'points':
        if player.magic.has_modifier('top-disguise'):
            computed_amount = 1.0 * computed_amount * player.magic.modifier_percents('top-disguise') / 100

        player.points += computed_amount
        player.save()
        update_points(player, game)

    logging.debug("Scored %s with %f %s" % (user, computed_amount, coin))
    return hs


def history_for(user, game, external_id=None, formula=None, coin=None):
    """ Return all history entries for given (user, game) pair.
    """
    # TODO: check usage
    fltr = {}
    if external_id:
        fltr['external_id'] = external_id
    if formula:
        fltr['formula'] = Formula.get(formula)
    if coin:
        fltr['coin'] = Coin.get(coin)

    if not isinstance(game, Game):
        game = game.get_instance()

    if not isinstance(user, User):
        user = user.user

    try:
        return History.objects.filter(user=user, game=game, **fltr)
    except History.DoesNotExist:
        return None


def user_coins(user):
    """ Returns a dictionary with user coins """
    if not isinstance(user, User):
        user = user.user
    return History.user_coins(user)


def real_points(player):
    coin = Coin.get('points')
    result = History.objects.filter(user=player.user, coin=coin).aggregate(total=models.Sum('amount'))
    return result['total'] if result['total'] is not None else 0


def sync_user(player):
    """ Synchronise user points with database
    """
    coin = Coin.get('points')
    result = History.objects.filter(user=player.user, coin=coin).aggregate(total=models.Sum('amount'))
    points = result['total'] if result['total'] is not None else 0
    if player.points != points and not player.magic.has_modifier('top-disguise'):
        logging.debug('%s had %d instead of %d points' % (player, player.points, points))
        player.points = points
        player.level_no = God.get_level_for_points(player.points)
        player.save()


def sync_all_user_points():
    """ Synchronise points amounts for all players """
    for player in Player.objects.all():
        sync_user(player)


def first_login_check(sender, **kwargs):
    """ Callback function for addActivity signal """
    action = kwargs.get('action', None)
    player = kwargs['user_from']
    if action != 'login':
        return

    if player.activity_from.count() == 0:
        # kick some activity
        signal_msg = ugettext_noop('joined the game.')

        signals.addActivity.send(sender=None, user_from=player,
                                 user_to=player,
                                 message=signal_msg,
                                 game=None)

        # give some bonus points
        try:
            score(player, None, 'start-points')
        except InvalidFormula:
            logging.error('Formula start points is missing')

signals.addActivity.connect(first_login_check)