implement the bones of character management
this provides character-level operations such as character creation, logging in/out, leveling them, and penalizing them. this isn't a game, yet, but it does implement and test a lot of the core functionality, just without triggers or bulk operations Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
This commit is contained in:
parent
9540c98d18
commit
d862aa16d8
@ -18,8 +18,8 @@ Files: MANIFEST.in pyproject.toml tox.ini requirements/*
|
||||
Copyright: © 2024 Brian S. Stephan <bss@incorporeal.org>
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
# Django-generated files
|
||||
Files: idlerpg/migrations/*
|
||||
# Django-generated files and testing errata
|
||||
Files: idlerpg/migrations/* tests/fixtures/*
|
||||
Copyright: © 2024 Brian S. Stephan <bss@incorporeal.org>
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
|
11
idlerpg/admin.py
Normal file
11
idlerpg/admin.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""Manage IdleRPG models.
|
||||
|
||||
SPDX-FileCopyrightText: © 2024 Brian S. Stephan <bss@incorporeal.org>
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
from django.contrib import admin
|
||||
|
||||
from idlerpg.models import Character, Game
|
||||
|
||||
admin.site.register(Character)
|
||||
admin.site.register(Game)
|
61
idlerpg/migrations/0001_initial.py
Normal file
61
idlerpg/migrations/0001_initial.py
Normal file
@ -0,0 +1,61 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-06 05:07
|
||||
|
||||
import django.db.models.deletion
|
||||
import idlerpg.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0020_alter_alias_id_alter_botuser_id_alter_ircchannel_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Game',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('active', models.BooleanField(default=False)),
|
||||
('channel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ircbot.ircchannel')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Character',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=16)),
|
||||
('password', models.CharField(max_length=256)),
|
||||
('hostmask', models.CharField(max_length=256)),
|
||||
('status', models.CharField(choices=[('OFFLINE', 'Offline'), ('ONLINE', 'Online'), ('DISABLED', 'Disabled')], default='OFFLINE', max_length=8)),
|
||||
('character_class', models.CharField(max_length=30)),
|
||||
('level', models.PositiveIntegerField(default=0)),
|
||||
('next_level', models.DateTimeField(default=idlerpg.models.Game.level_formula)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('last_login', models.DateTimeField(blank=True, null=True)),
|
||||
('time_penalized_nick_change', models.PositiveIntegerField(default=0)),
|
||||
('time_penalized_part', models.PositiveIntegerField(default=0)),
|
||||
('time_penalized_quit', models.PositiveIntegerField(default=0)),
|
||||
('time_penalized_logout', models.PositiveIntegerField(default=0)),
|
||||
('time_penalized_kicked', models.PositiveIntegerField(default=0)),
|
||||
('time_penalized_privmsg', models.PositiveIntegerField(default=0)),
|
||||
('time_penalized_notice', models.PositiveIntegerField(default=0)),
|
||||
('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='idlerpg.game')),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='game',
|
||||
constraint=models.UniqueConstraint(models.F('active'), models.F('channel'), name='one_game_per_channel'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='character',
|
||||
constraint=models.UniqueConstraint(models.F('game'), models.F('hostmask'), name='one_player_character_per_game'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='character',
|
||||
constraint=models.UniqueConstraint(models.F('game'), models.F('name'), name='no_characters_with_same_name_per_game'),
|
||||
),
|
||||
]
|
1
idlerpg/migrations/__init__.py
Normal file
1
idlerpg/migrations/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Django-generated migrations for the IdleRPG models."""
|
214
idlerpg/models.py
Normal file
214
idlerpg/models.py
Normal file
@ -0,0 +1,214 @@
|
||||
"""Models for the IdleRPG game.
|
||||
|
||||
SPDX-FileCopyrightText: © 2024 Brian S. Stephan <bss@incorporeal.org>
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Game(models.Model):
|
||||
"""Collect game and world configuration and elements."""
|
||||
|
||||
name = models.CharField(max_length=30)
|
||||
active = models.BooleanField(default=False)
|
||||
|
||||
channel = models.ForeignKey('ircbot.IrcChannel', on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
"""Options for the Game and its objects."""
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint("active", "channel", name="one_game_per_channel"),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def level_formula(current_level: int = 0, base_time: datetime = None) -> timedelta:
|
||||
"""DRY the formula for determining when a character's next level would be.
|
||||
|
||||
Defaults are for the default Character.next_level value.
|
||||
|
||||
Args:
|
||||
current_level: current character level, which influences the next level (arg of 1 means time to level 2)
|
||||
base_time: the time to delta from, or now if None
|
||||
Returns:
|
||||
datetime of when a character as specified would next level up
|
||||
"""
|
||||
if not base_time:
|
||||
base_time = timezone.now()
|
||||
if current_level >= 60:
|
||||
# "The exponent method code had simply gotten to that point that levels were taking too long to complete"
|
||||
return base_time + timedelta(seconds=math.ceil((600*(1.16**59)) + ((60*60*24)*(current_level-59))))
|
||||
return base_time + timedelta(seconds=math.ceil(600*(1.16**current_level)))
|
||||
|
||||
|
||||
class CharacterManager(models.Manager):
|
||||
"""Query manager for creating a Character and taking operations relevant to the game."""
|
||||
|
||||
def register(self, name: str, game: Game, password: str, hostmask: str, character_class: str):
|
||||
"""Create a character, with guardrails.
|
||||
|
||||
Args:
|
||||
name: name of the character
|
||||
game: the game to associate this character to
|
||||
password: plaintext password to be hashed in the DB
|
||||
hostmask: hostmask to assign to the character initially
|
||||
character_class: class for the character
|
||||
Returns:
|
||||
the created character
|
||||
"""
|
||||
hashed_pass = make_password(password)
|
||||
character = self.create(name=name, game=game, password=hashed_pass, hostmask=hostmask,
|
||||
character_class=character_class, status=Character.CHARACTER_STATUS_ONLINE,
|
||||
last_login=timezone.now())
|
||||
logger.info("%s::%s: created and logged in, next level @ %s",
|
||||
character.game.name, character.name, character.next_level)
|
||||
return character
|
||||
|
||||
|
||||
class Character(models.Model):
|
||||
"""A character in a game."""
|
||||
|
||||
CHARACTER_STATUS_DISABLED = 'DISABLED'
|
||||
CHARACTER_STATUS_OFFLINE = 'OFFLINE'
|
||||
CHARACTER_STATUS_ONLINE = 'ONLINE'
|
||||
CHARACTER_STATUSES = {
|
||||
CHARACTER_STATUS_OFFLINE: "Offline",
|
||||
CHARACTER_STATUS_ONLINE: "Online",
|
||||
CHARACTER_STATUS_DISABLED: "Disabled",
|
||||
}
|
||||
|
||||
NICK_CHANGE_P = 30
|
||||
PART_P = 200
|
||||
QUIT_P = 20
|
||||
LOGOUT_P = 20
|
||||
KICKED_P = 250
|
||||
|
||||
name = models.CharField(max_length=16)
|
||||
password = models.CharField(max_length=256)
|
||||
hostmask = models.CharField(max_length=256)
|
||||
status = models.CharField(max_length=8, choices=CHARACTER_STATUSES, default='OFFLINE')
|
||||
|
||||
character_class = models.CharField(max_length=30)
|
||||
level = models.PositiveIntegerField(default=0)
|
||||
next_level = models.DateTimeField(default=Game.level_formula)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
last_login = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
time_penalized_nick_change = models.PositiveIntegerField(default=0)
|
||||
time_penalized_part = models.PositiveIntegerField(default=0)
|
||||
time_penalized_quit = models.PositiveIntegerField(default=0)
|
||||
time_penalized_logout = models.PositiveIntegerField(default=0)
|
||||
time_penalized_kicked = models.PositiveIntegerField(default=0)
|
||||
time_penalized_privmsg = models.PositiveIntegerField(default=0)
|
||||
time_penalized_notice = models.PositiveIntegerField(default=0)
|
||||
|
||||
game = models.ForeignKey('Game', on_delete=models.CASCADE)
|
||||
|
||||
objects = CharacterManager()
|
||||
|
||||
class Meta:
|
||||
"""Options for the Character and its objects."""
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint("game", "hostmask", name="one_player_character_per_game"),
|
||||
models.UniqueConstraint("game", "name", name="no_characters_with_same_name_per_game"),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""Provide a string representation for display, etc. purposes."""
|
||||
return f"{self.name}, level {self.level} {self.character_class}"
|
||||
|
||||
def calculate_datetime_to_next_level(self):
|
||||
"""Determine when the next level will be reached (barring no penalties or logoffs).
|
||||
|
||||
This is based on their previous timestamp, to get around any timing issues (load, bot was offline, etc.)
|
||||
that would otherwise lead to drift if we calculated based on right now.
|
||||
|
||||
Returns:
|
||||
the datetime when this character would level up
|
||||
"""
|
||||
# if the character hasn't reached the next level yet, just use that
|
||||
if self.next_level > timezone.now():
|
||||
return self.next_level
|
||||
|
||||
# character is past their old time and in the process of leveling up, calculate next based on that
|
||||
return Game.level_formula(self.level, self.next_level)
|
||||
|
||||
def level_up(self):
|
||||
"""Set this character's level and next time if they meet the necessary criteria.
|
||||
|
||||
Raises:
|
||||
ValueError: if the character can't level up right now for some reason
|
||||
"""
|
||||
# error if this is being called before it should be
|
||||
if self.next_level > timezone.now():
|
||||
raise ValueError(f"character '{self.name}' can't level, it isn't yet {self.next_level}!")
|
||||
if self.status != self.CHARACTER_STATUS_ONLINE:
|
||||
raise ValueError(f"character '{self.name}' can't level, it isn't logged in!")
|
||||
|
||||
logger.debug("leveling up %s...", str(self))
|
||||
self.next_level = self.calculate_datetime_to_next_level()
|
||||
self.level += 1
|
||||
logger.info("%s::%s: leveled up to %d, next level @ %s", self.game.name, self.name, self.level, self.next_level)
|
||||
|
||||
def log_in(self, password: str, hostmask: str):
|
||||
"""Log the character in, assuming the provided password is correct.
|
||||
|
||||
Args:
|
||||
password: the password to validate
|
||||
Raises:
|
||||
ValueError: if the provided password was incorrect, or the character isn't logged out
|
||||
"""
|
||||
if self.status != self.CHARACTER_STATUS_OFFLINE:
|
||||
raise ValueError(f"character '{self.name}' can't be logged in, isn't logged out!")
|
||||
if not check_password(password, self.password):
|
||||
raise ValueError(f"incorrect password for character '{self.name}'!")
|
||||
|
||||
logger.debug("logging %s in...", str(self))
|
||||
# we need to apply the time lost to the next level time
|
||||
self.next_level += timezone.now() - self.last_login
|
||||
self.status = self.CHARACTER_STATUS_ONLINE
|
||||
self.hostmask = hostmask
|
||||
logger.info("%s::%s: logged in, next level @ %s", self.game.name, self.name, self.next_level)
|
||||
|
||||
def log_out(self):
|
||||
"""Log out the character and stop their progress for now.
|
||||
|
||||
Raises:
|
||||
ValueError: the character isn't logged in
|
||||
"""
|
||||
if self.status != self.CHARACTER_STATUS_ONLINE:
|
||||
raise ValueError(f"character '{self.name}' can't be logged out, isn't logged in!")
|
||||
|
||||
seconds = self.penalize(self.LOGOUT_P, "logging out")
|
||||
self.time_penalized_logout += seconds
|
||||
self.status = self.CHARACTER_STATUS_OFFLINE
|
||||
self.last_login = timezone.now()
|
||||
logger.info("%s::%s: logged out", self.game.name, self.name)
|
||||
|
||||
def penalize(self, penalty: int, reason: str) -> int:
|
||||
"""Add a penalty to the character's time to level up.
|
||||
|
||||
Args:
|
||||
penalty: the base penalty (see FOO_P members) to penalize the character
|
||||
reason: the reason for this penalty
|
||||
"""
|
||||
seconds = math.ceil(penalty*(1.14**self.level))
|
||||
self.next_level += timedelta(seconds=seconds)
|
||||
logger.info("%s::%s: penalized %d seconds for %s", self.game.name, self.name, seconds, reason)
|
||||
return seconds
|
||||
|
||||
def set_password(self, new_password):
|
||||
"""Set the password for the character to the new value."""
|
||||
logger.debug("setting password for %s...", self.name)
|
||||
self.password = make_password(new_password)
|
||||
logger.info("%s::%s: password changed", self.game.name, self.name)
|
68
tests/fixtures/simple_character.json
vendored
Normal file
68
tests/fixtures/simple_character.json
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
[
|
||||
{
|
||||
"model": "ircbot.ircserver",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "default",
|
||||
"hostname": "irc.example.org",
|
||||
"port": 6667,
|
||||
"password": null,
|
||||
"nickname": "",
|
||||
"realname": "",
|
||||
"additional_addressed_nicks": "",
|
||||
"use_ssl": false,
|
||||
"use_ipv6": false,
|
||||
"post_connect": "",
|
||||
"delay_before_joins": 0,
|
||||
"xmlrpc_host": "localhost",
|
||||
"xmlrpc_port": 13132,
|
||||
"replace_irc_control_with_markdown": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ircbot.ircchannel",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "#test",
|
||||
"server": 1,
|
||||
"autojoin": false,
|
||||
"topic_msg": "",
|
||||
"topic_time": "2024-05-06T05:10:25.154Z",
|
||||
"topic_by": "",
|
||||
"markov_learn_from_channel": true,
|
||||
"discord_bridge": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "idlerpg.game",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "test",
|
||||
"active": false,
|
||||
"channel": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "idlerpg.character",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "bss",
|
||||
"password": "pbkdf2_sha256$720000$A941t4dL96zzqeldCFucrr$Pof137/IjT3p//ZR+iYNoBnGmYPG6jLbNqenwMA3hHY=",
|
||||
"hostmask": "bss!bss@bss",
|
||||
"status": "ONLINE",
|
||||
"character_class": "tester",
|
||||
"level": 0,
|
||||
"next_level": "2024-05-05T05:20:45.437Z",
|
||||
"created": "2024-05-05T05:10:45.438Z",
|
||||
"last_login": "2024-05-05T05:10:45.437Z",
|
||||
"time_penalized_nick_change": 0,
|
||||
"time_penalized_part": 0,
|
||||
"time_penalized_quit": 0,
|
||||
"time_penalized_logout": 0,
|
||||
"time_penalized_kicked": 0,
|
||||
"time_penalized_privmsg": 0,
|
||||
"time_penalized_notice": 0,
|
||||
"game": 1
|
||||
}
|
||||
}
|
||||
]
|
182
tests/test_idlerpg_character.py
Normal file
182
tests/test_idlerpg_character.py
Normal file
@ -0,0 +1,182 @@
|
||||
"""Test IdleRPG character operations.
|
||||
|
||||
SPDX-FileCopyrightText: © 2024 Brian S. Stephan <bss@incorporeal.org>
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from idlerpg.models import Character, Game
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CharacterTest(TestCase):
|
||||
"""Test the Character model."""
|
||||
|
||||
fixtures = ['tests/fixtures/simple_character.json']
|
||||
|
||||
def test_string_repr(self):
|
||||
"""Test the basic string summary."""
|
||||
char = Character.objects.get(pk=1)
|
||||
logger.debug(str(char))
|
||||
assert str(char) == "bss, level 0 tester"
|
||||
|
||||
def test_log_out(self):
|
||||
"""Test basic log out functionality and end result."""
|
||||
char = Character.objects.get(pk=1)
|
||||
# retain some data for comparison after logging out
|
||||
old_next_level = char.next_level
|
||||
|
||||
logout_time = timezone.now()
|
||||
with patch('django.utils.timezone.now', return_value=logout_time):
|
||||
char.log_out()
|
||||
|
||||
assert char.next_level == old_next_level + timedelta(seconds=20)
|
||||
assert char.status == Character.CHARACTER_STATUS_OFFLINE
|
||||
assert char.last_login == logout_time
|
||||
assert char.time_penalized_logout == 20
|
||||
|
||||
def test_cant_log_out_offline(self):
|
||||
"""Test that we error if trying to log out a character already offline."""
|
||||
char = Character.objects.get(pk=1)
|
||||
char.log_out()
|
||||
with self.assertRaises(ValueError):
|
||||
char.log_out()
|
||||
|
||||
def test_log_in(self):
|
||||
"""Test the result of logging in."""
|
||||
char = Character.objects.get(pk=1)
|
||||
|
||||
logout_time = timezone.now()
|
||||
login_time = logout_time + timedelta(seconds=300)
|
||||
# manipulate the time so that we log out and then log in 300 seconds later, which
|
||||
# adds 20 seconds to the next level time
|
||||
with patch('django.utils.timezone.now', return_value=logout_time):
|
||||
char.log_out()
|
||||
# logout has a penalty of its own, so this post-logout value is what will be altered
|
||||
old_next_level = char.next_level
|
||||
with patch('django.utils.timezone.now', return_value=login_time):
|
||||
char.log_in('bss', 'bss!bss@test_log_in')
|
||||
|
||||
assert char.next_level == old_next_level + timedelta(seconds=300)
|
||||
assert char.status == Character.CHARACTER_STATUS_ONLINE
|
||||
assert char.hostmask == 'bss!bss@test_log_in'
|
||||
|
||||
def test_cant_log_in_when_already_online(self):
|
||||
"""Test that we can't log in the character if they're already online."""
|
||||
char = Character.objects.get(pk=1)
|
||||
with self.assertRaises(ValueError):
|
||||
char.log_in('bss', 'bss!bss@test_online')
|
||||
|
||||
def test_cant_log_in_bad_password(self):
|
||||
"""Test that we can't log in the character if we don't have the right password."""
|
||||
char = Character.objects.get(pk=1)
|
||||
char.log_out()
|
||||
with self.assertRaises(ValueError):
|
||||
char.log_in('bad pass', 'bss!bss@test_bad_password')
|
||||
|
||||
def test_set_password(self):
|
||||
"""Test that the password is actually changed when requested."""
|
||||
char = Character.objects.get(pk=1)
|
||||
old_password = char.password
|
||||
char.set_password('something')
|
||||
assert old_password != char.password
|
||||
assert char.password[0:13] == 'pbkdf2_sha256'
|
||||
|
||||
def test_penalize(self):
|
||||
"""Test that penalties apply properly, including level adjustments."""
|
||||
char = Character.objects.get(pk=1)
|
||||
next_level = char.next_level
|
||||
|
||||
# level 0
|
||||
next_level += timedelta(seconds=20)
|
||||
char.penalize(20, 'test')
|
||||
assert char.next_level == next_level
|
||||
# level 1
|
||||
char.level = 1
|
||||
next_level += timedelta(seconds=23)
|
||||
char.penalize(20, 'test')
|
||||
assert char.next_level == next_level
|
||||
# level 5
|
||||
char.level = 5
|
||||
next_level += timedelta(seconds=39)
|
||||
char.penalize(20, 'test')
|
||||
assert char.next_level == next_level
|
||||
# level 25
|
||||
char.level = 25
|
||||
next_level += timedelta(seconds=530)
|
||||
char.penalize(20, 'test')
|
||||
assert char.next_level == next_level
|
||||
|
||||
def test_calculate_datetime_to_next_level(self):
|
||||
"""Test that the next level calculation behaves as expected in a number of situations."""
|
||||
char = Character.objects.get(pk=1)
|
||||
|
||||
# level 0 -> 1
|
||||
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=600)
|
||||
# level 1 -> 2
|
||||
char.level = 1
|
||||
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=696)
|
||||
# level 24 -> 25
|
||||
char.level = 24
|
||||
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=21142)
|
||||
# level 59 -> 60
|
||||
char.level = 59
|
||||
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=3812174)
|
||||
# level 60 -> 61
|
||||
char.level = 60
|
||||
assert char.calculate_datetime_to_next_level() == char.next_level + (timedelta(seconds=3812174) +
|
||||
timedelta(seconds=86400))
|
||||
# level 61 -> 62
|
||||
char.level = 61
|
||||
assert char.calculate_datetime_to_next_level() == char.next_level + (timedelta(seconds=3812174) +
|
||||
timedelta(seconds=86400*2))
|
||||
|
||||
def test_calculate_datetime_to_next_level_not_time_yet(self):
|
||||
"""Test that we just bail with the current next_level if it hasn't been reached yet."""
|
||||
char = Character.objects.get(pk=1)
|
||||
with patch('django.utils.timezone.now', return_value=char.next_level - timedelta(days=1)):
|
||||
assert char.calculate_datetime_to_next_level() == char.next_level
|
||||
|
||||
def test_register(self):
|
||||
"""Test the primary way to create a character with proper initial values."""
|
||||
game = Game.objects.get(pk=1)
|
||||
|
||||
register_time = timezone.now()
|
||||
with patch('django.utils.timezone.now', return_value=register_time):
|
||||
new_char = Character.objects.register('new', game, 'pass', 'bss!bss@test_register', 'unit tester')
|
||||
|
||||
assert new_char.status == Character.CHARACTER_STATUS_ONLINE
|
||||
assert new_char.next_level == register_time + timedelta(seconds=600)
|
||||
assert new_char.last_login == register_time
|
||||
assert new_char.password[0:13] == 'pbkdf2_sha256'
|
||||
|
||||
def test_level_up(self):
|
||||
"""Test the level up functionality."""
|
||||
char = Character.objects.get(pk=1)
|
||||
|
||||
old_level = char.level
|
||||
old_next_level = char.next_level
|
||||
char.level_up()
|
||||
|
||||
assert char.level == old_level + 1
|
||||
assert char.next_level == old_next_level + timedelta(seconds=600)
|
||||
|
||||
def test_level_up_fail_not_time(self):
|
||||
"""Test the failure condition for trying to level up before the timestamp."""
|
||||
char = Character.objects.get(pk=1)
|
||||
with patch('django.utils.timezone.now', return_value=char.next_level-timedelta(minutes=30)):
|
||||
with self.assertRaises(ValueError):
|
||||
char.level_up()
|
||||
|
||||
def test_level_up_fail_offline(self):
|
||||
"""Test the failure condition for trying to level up a character not online."""
|
||||
char = Character.objects.get(pk=1)
|
||||
char.log_out()
|
||||
with self.assertRaises(ValueError):
|
||||
char.level_up()
|
Loading…
Reference in New Issue
Block a user