Compare commits
6 Commits
14a1c5ceb6
...
56b554f6f1
Author | SHA1 | Date | |
---|---|---|---|
56b554f6f1 | |||
731e434c8d | |||
f22ca875e3 | |||
4a8babf39e | |||
0ad687669e | |||
e9b985bd79 |
@ -32,6 +32,70 @@ class IdleRPG(Plugin):
|
|||||||
self.seen_hostmasks = set()
|
self.seen_hostmasks = set()
|
||||||
super().__init__(bot, connection, event)
|
super().__init__(bot, connection, event)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Set up the handlers and start processing game state."""
|
||||||
|
self.connection.add_global_handler('join', self.handle_join, -20)
|
||||||
|
self.connection.add_global_handler('kick', self.handle_kick_penalty, -20)
|
||||||
|
self.connection.add_global_handler('nick', self.handle_nick_penalty, -20)
|
||||||
|
self.connection.add_global_handler('part', self.handle_part_penalty, -20)
|
||||||
|
self.connection.add_global_handler('pubmsg', self.handle_message_penalty, -20)
|
||||||
|
self.connection.add_global_handler('pubnotice', self.handle_message_penalty, -20)
|
||||||
|
self.connection.add_global_handler('quit', self.handle_quit_penalty, -20)
|
||||||
|
self.connection.add_global_handler('whoreply', self.handle_whoreply_response, -20)
|
||||||
|
|
||||||
|
self.connection.reactor.add_global_regex_handler(['privmsg'], self.LOGIN_COMMAND_PATTERN,
|
||||||
|
self.handle_login, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['privmsg'], self.LOGOUT_COMMAND_PATTERN,
|
||||||
|
self.handle_logout, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['privmsg'], self.REGISTER_COMMAND_PATTERN,
|
||||||
|
self.handle_register, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['privmsg'], self.REMOVEME_COMMAND_PATTERN,
|
||||||
|
self.handle_remove, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['privmsg'], self.STATUS_COMMAND_PATTERN,
|
||||||
|
self.handle_status, -20)
|
||||||
|
|
||||||
|
# get a list of who is in the channel and auto-log them in
|
||||||
|
logger.info("Automatically logging in users already in the channel(s).")
|
||||||
|
for game in Game.objects.filter(active=True):
|
||||||
|
self.connection.who(game.channel.name)
|
||||||
|
|
||||||
|
# start the thread to check for level ups
|
||||||
|
self.check_for_level_ups = True
|
||||||
|
level_t = threading.Thread(target=self.level_thread)
|
||||||
|
level_t.daemon = True
|
||||||
|
level_t.start()
|
||||||
|
logger.info("Started up the level up checker thread.")
|
||||||
|
|
||||||
|
super(IdleRPG, self).start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Tear down handlers, stop checking for new levels, and return to a stable game state."""
|
||||||
|
self.check_for_level_ups = False
|
||||||
|
|
||||||
|
self.connection.remove_global_handler('join', self.handle_join)
|
||||||
|
self.connection.remove_global_handler('kick', self.handle_kick_penalty)
|
||||||
|
self.connection.remove_global_handler('nick', self.handle_nick_penalty)
|
||||||
|
self.connection.remove_global_handler('part', self.handle_part_penalty)
|
||||||
|
self.connection.remove_global_handler('pubmsg', self.handle_message_penalty)
|
||||||
|
self.connection.remove_global_handler('pubnotice', self.handle_message_penalty)
|
||||||
|
self.connection.remove_global_handler('quit', self.handle_quit_penalty)
|
||||||
|
self.connection.remove_global_handler('whoreply', self.handle_whoreply_response)
|
||||||
|
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['privmsg'], self.handle_login)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['privmsg'], self.handle_logout)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['privmsg'], self.handle_register)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['privmsg'], self.handle_remove)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['privmsg'], self.handle_status)
|
||||||
|
|
||||||
|
# log everyone out (no penalty)
|
||||||
|
Character.objects.log_out_everyone()
|
||||||
|
|
||||||
|
# unset the hostmask tracking
|
||||||
|
self.seen_hostmasks = set()
|
||||||
|
logger.info("Reset the set of seen hostmasks.")
|
||||||
|
|
||||||
|
super(IdleRPG, self).stop()
|
||||||
|
|
||||||
def handle_join(self, connection, event):
|
def handle_join(self, connection, event):
|
||||||
"""Track a character as online if their player joins the game channel."""
|
"""Track a character as online if their player joins the game channel."""
|
||||||
self.seen_hostmasks.add(event.source)
|
self.seen_hostmasks.add(event.source)
|
||||||
@ -108,7 +172,49 @@ class IdleRPG(Plugin):
|
|||||||
self.bot.reply(None, f"{character}, is now online from nickname {nick}. "
|
self.bot.reply(None, f"{character}, is now online from nickname {nick}. "
|
||||||
f"Next level at {character.next_level_str()}.",
|
f"Next level at {character.next_level_str()}.",
|
||||||
explicit_target=character.game.channel.name)
|
explicit_target=character.game.channel.name)
|
||||||
return self.bot.reply(event, f"{character} has been successfully logged in.")
|
return self.bot.reply(event, f"{character}, has been successfully logged in.")
|
||||||
|
|
||||||
|
def handle_logout(self, connection, event, match):
|
||||||
|
"""Log out a character when requested."""
|
||||||
|
hostmask = event.source
|
||||||
|
try:
|
||||||
|
character = Character.objects.get(enabled=True, hostmask=hostmask)
|
||||||
|
except Character.DoesNotExist:
|
||||||
|
return self.bot.reply(event, "You do not have a character in a running game.")
|
||||||
|
|
||||||
|
if character.status != Character.CHARACTER_STATUS_LOGGED_IN:
|
||||||
|
return self.bot.reply(event, f"Cannot log out {character.name}, either they already are, "
|
||||||
|
"or they are in a state that cannot be modified.")
|
||||||
|
|
||||||
|
character.log_out()
|
||||||
|
penalty = character.penalize(20, "logging out")
|
||||||
|
character.time_penalized_logout += penalty
|
||||||
|
character.save()
|
||||||
|
return self.bot.reply(event, f"{character}, has been successfully logged out.")
|
||||||
|
|
||||||
|
def handle_register(self, connection, event, match):
|
||||||
|
"""Register a character for a user."""
|
||||||
|
hostmask = event.source
|
||||||
|
nick = irc.client.NickMask(hostmask).nick
|
||||||
|
try:
|
||||||
|
game = Game.objects.get(active=True)
|
||||||
|
except Game.DoesNotExist:
|
||||||
|
return self.bot.reply(event, "No active game exists.")
|
||||||
|
|
||||||
|
if hostmask not in self.seen_hostmasks:
|
||||||
|
return self.bot.reply(event, f"Please join {game.channel.name} before registering.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
character = Character.objects.register(match.group('name'), game, match.group('password'),
|
||||||
|
hostmask, match.group('char_class'))
|
||||||
|
except IntegrityError:
|
||||||
|
return self.bot.reply(event, "Registration failed; either you already have a character, "
|
||||||
|
"or the character name is already taken.")
|
||||||
|
|
||||||
|
self.bot.reply(None, f"{nick}'s newest character, {character.name}, the {character.character_class}, "
|
||||||
|
f"has been summoned! Next level at {character.next_level_str()}.",
|
||||||
|
explicit_target=game.channel.name)
|
||||||
|
return self.bot.reply(event, f"You have registered {character}, and been automatically logged in.")
|
||||||
|
|
||||||
def handle_remove(self, connection, event, match):
|
def handle_remove(self, connection, event, match):
|
||||||
"""Handle the disabling of a character."""
|
"""Handle the disabling of a character."""
|
||||||
@ -162,20 +268,19 @@ class IdleRPG(Plugin):
|
|||||||
"""Check for characters who have leveled up, and log as such in the channel."""
|
"""Check for characters who have leveled up, and log as such in the channel."""
|
||||||
while self.check_for_level_ups:
|
while self.check_for_level_ups:
|
||||||
time.sleep(self.SLEEP_BETWEEN_LEVEL_CHECKS)
|
time.sleep(self.SLEEP_BETWEEN_LEVEL_CHECKS)
|
||||||
self._level_up_games()
|
self._level_up_characters()
|
||||||
|
|
||||||
def _level_up_games(self):
|
def _level_up_characters(self):
|
||||||
"""Find games under management and level up characters within them, updating their channel."""
|
"""Level up characters in the active game, updating their channel."""
|
||||||
logger.debug("checking for level ups in games")
|
game = Game.objects.get(active=True)
|
||||||
for game in Game.objects.filter(active=True):
|
logger.debug("checking for level ups in %s", game)
|
||||||
logger.debug("checking for level ups in %s", game)
|
for character in Character.objects.levelable(game):
|
||||||
for character in Character.objects.levelable(game):
|
logger.debug("going to try to level up %s", character)
|
||||||
logger.debug("going to try to level up %s", character)
|
character.level_up()
|
||||||
character.level_up()
|
self.bot.reply(None, f"{character.name}, the {character.character_class}, has attained level "
|
||||||
self.bot.reply(None, f"{character.name}, the {character.character_class}, has attained level "
|
f"{character.level}! Next level at {character.next_level_str()}.",
|
||||||
f"{character.level}! Next level at {character.next_level_str()}.",
|
explicit_target=game.channel.name)
|
||||||
explicit_target=game.channel.name)
|
character.save()
|
||||||
character.save()
|
|
||||||
|
|
||||||
|
|
||||||
plugin = IdleRPG
|
plugin = IdleRPG
|
||||||
|
18
idlerpg/migrations/0008_game_one_enabled_at_a_time.py
Normal file
18
idlerpg/migrations/0008_game_one_enabled_at_a_time.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.5 on 2024-05-18 14:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('idlerpg', '0007_remove_character_one_player_character_per_game_and_more'),
|
||||||
|
('ircbot', '0020_alter_alias_id_alter_botuser_id_alter_ircchannel_id_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='game',
|
||||||
|
constraint=models.UniqueConstraint(models.F('active'), condition=models.Q(('active', True)), name='one_enabled_at_a_time'),
|
||||||
|
),
|
||||||
|
]
|
@ -26,7 +26,8 @@ class Game(models.Model):
|
|||||||
"""Options for the Game and its objects."""
|
"""Options for the Game and its objects."""
|
||||||
|
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint("active", "channel", name="one_game_per_channel"),
|
models.UniqueConstraint('active', 'channel', name='one_game_per_channel'),
|
||||||
|
models.UniqueConstraint('active', condition=models.Q(active=True), name='one_enabled_at_a_time'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -47,10 +48,9 @@ class Game(models.Model):
|
|||||||
"""
|
"""
|
||||||
if not base_time:
|
if not base_time:
|
||||||
base_time = timezone.now()
|
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"
|
# this is an logarithmic curve that starts peaking at 10ish days per level
|
||||||
return base_time + timedelta(seconds=math.ceil((600*(1.16**59)) + ((60*60*24)*(current_level-59))))
|
return base_time + timedelta(seconds=int(24*60*60*1.4*(5+math.log(0.01*(current_level+1)))))
|
||||||
return base_time + timedelta(seconds=math.ceil(600*(1.16**current_level)))
|
|
||||||
|
|
||||||
|
|
||||||
class CharacterManager(models.Manager):
|
class CharacterManager(models.Manager):
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# pip-compile --extra=dev --output-file=requirements/requirements-dev.txt
|
# pip-compile --extra=dev --output-file=requirements/requirements-dev.txt
|
||||||
#
|
#
|
||||||
annotated-types==0.6.0
|
annotated-types==0.7.0
|
||||||
# via pydantic
|
# via pydantic
|
||||||
asgiref==3.8.1
|
asgiref==3.8.1
|
||||||
# via django
|
# via django
|
||||||
@ -12,7 +12,7 @@ authlib==1.3.0
|
|||||||
# via safety
|
# via safety
|
||||||
autocommand==2.2.2
|
autocommand==2.2.2
|
||||||
# via jaraco-text
|
# via jaraco-text
|
||||||
backports-tarfile==1.1.1
|
backports-tarfile==1.2.0
|
||||||
# via jaraco-context
|
# via jaraco-context
|
||||||
bandit==1.7.8
|
bandit==1.7.8
|
||||||
# via dr.botzo-idlerpg (pyproject.toml)
|
# via dr.botzo-idlerpg (pyproject.toml)
|
||||||
@ -26,7 +26,7 @@ build==1.2.1
|
|||||||
# via pip-tools
|
# via pip-tools
|
||||||
cachetools==5.3.3
|
cachetools==5.3.3
|
||||||
# via tox
|
# via tox
|
||||||
certifi==2024.2.2
|
certifi==2024.6.2
|
||||||
# via requests
|
# via requests
|
||||||
cffi==1.16.0
|
cffi==1.16.0
|
||||||
# via cryptography
|
# via cryptography
|
||||||
@ -44,13 +44,13 @@ click==8.1.7
|
|||||||
# typer
|
# typer
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
# via tox
|
# via tox
|
||||||
coverage[toml]==7.5.1
|
coverage[toml]==7.5.3
|
||||||
# via pytest-cov
|
# via pytest-cov
|
||||||
cryptography==42.0.7
|
cryptography==42.0.7
|
||||||
# via authlib
|
# via authlib
|
||||||
distlib==0.3.8
|
distlib==0.3.8
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
django==5.0.5
|
django==5.0.6
|
||||||
# via dr.botzo-idlerpg (pyproject.toml)
|
# via dr.botzo-idlerpg (pyproject.toml)
|
||||||
dlint==0.14.1
|
dlint==0.14.1
|
||||||
# via dr.botzo-idlerpg (pyproject.toml)
|
# via dr.botzo-idlerpg (pyproject.toml)
|
||||||
@ -153,7 +153,7 @@ pbr==6.0.0
|
|||||||
# via stevedore
|
# via stevedore
|
||||||
pip-tools==7.4.1
|
pip-tools==7.4.1
|
||||||
# via dr.botzo-idlerpg (pyproject.toml)
|
# via dr.botzo-idlerpg (pyproject.toml)
|
||||||
platformdirs==4.2.1
|
platformdirs==4.2.2
|
||||||
# via
|
# via
|
||||||
# tox
|
# tox
|
||||||
# virtualenv
|
# virtualenv
|
||||||
@ -165,11 +165,11 @@ pycodestyle==2.11.1
|
|||||||
# via flake8
|
# via flake8
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==2.7.1
|
pydantic==2.7.2
|
||||||
# via
|
# via
|
||||||
# safety
|
# safety
|
||||||
# safety-schemas
|
# safety-schemas
|
||||||
pydantic-core==2.18.2
|
pydantic-core==2.18.3
|
||||||
# via pydantic
|
# via pydantic
|
||||||
pydocstyle==6.3.0
|
pydocstyle==6.3.0
|
||||||
# via flake8-docstrings
|
# via flake8-docstrings
|
||||||
@ -183,7 +183,7 @@ pyproject-hooks==1.1.0
|
|||||||
# via
|
# via
|
||||||
# build
|
# build
|
||||||
# pip-tools
|
# pip-tools
|
||||||
pytest==8.2.0
|
pytest==8.2.1
|
||||||
# via
|
# via
|
||||||
# dr.botzo-idlerpg (pyproject.toml)
|
# dr.botzo-idlerpg (pyproject.toml)
|
||||||
# pytest-cov
|
# pytest-cov
|
||||||
@ -200,7 +200,7 @@ pytz==2024.1
|
|||||||
# tempora
|
# tempora
|
||||||
pyyaml==6.0.1
|
pyyaml==6.0.1
|
||||||
# via bandit
|
# via bandit
|
||||||
requests==2.31.0
|
requests==2.32.3
|
||||||
# via safety
|
# via safety
|
||||||
reuse==3.0.2
|
reuse==3.0.2
|
||||||
# via dr.botzo-idlerpg (pyproject.toml)
|
# via dr.botzo-idlerpg (pyproject.toml)
|
||||||
@ -233,11 +233,11 @@ tempora==5.5.1
|
|||||||
# jaraco-logging
|
# jaraco-logging
|
||||||
tox==4.15.0
|
tox==4.15.0
|
||||||
# via dr.botzo-idlerpg (pyproject.toml)
|
# via dr.botzo-idlerpg (pyproject.toml)
|
||||||
typeguard==4.2.1
|
typeguard==4.3.0
|
||||||
# via inflect
|
# via inflect
|
||||||
typer==0.12.3
|
typer==0.12.3
|
||||||
# via safety
|
# via safety
|
||||||
typing-extensions==4.11.0
|
typing-extensions==4.12.1
|
||||||
# via
|
# via
|
||||||
# inflect
|
# inflect
|
||||||
# pydantic
|
# pydantic
|
||||||
@ -250,7 +250,7 @@ urllib3==2.2.1
|
|||||||
# via
|
# via
|
||||||
# requests
|
# requests
|
||||||
# safety
|
# safety
|
||||||
virtualenv==20.26.1
|
virtualenv==20.26.2
|
||||||
# via tox
|
# via tox
|
||||||
wheel==0.43.0
|
wheel==0.43.0
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
|
@ -8,9 +8,9 @@ asgiref==3.8.1
|
|||||||
# via django
|
# via django
|
||||||
autocommand==2.2.2
|
autocommand==2.2.2
|
||||||
# via jaraco-text
|
# via jaraco-text
|
||||||
backports-tarfile==1.1.1
|
backports-tarfile==1.2.0
|
||||||
# via jaraco-context
|
# via jaraco-context
|
||||||
django==5.0.5
|
django==5.0.6
|
||||||
# via dr.botzo-idlerpg (pyproject.toml)
|
# via dr.botzo-idlerpg (pyproject.toml)
|
||||||
inflect==7.2.1
|
inflect==7.2.1
|
||||||
# via jaraco-text
|
# via jaraco-text
|
||||||
@ -49,9 +49,9 @@ tempora==5.5.1
|
|||||||
# via
|
# via
|
||||||
# irc
|
# irc
|
||||||
# jaraco-logging
|
# jaraco-logging
|
||||||
typeguard==4.2.1
|
typeguard==4.3.0
|
||||||
# via inflect
|
# via inflect
|
||||||
typing-extensions==4.11.0
|
typing-extensions==4.12.1
|
||||||
# via
|
# via
|
||||||
# inflect
|
# inflect
|
||||||
# typeguard
|
# typeguard
|
||||||
|
11
tests/fixtures/simple_character.json
vendored
11
tests/fixtures/simple_character.json
vendored
@ -37,7 +37,7 @@
|
|||||||
"model": "ircbot.ircchannel",
|
"model": "ircbot.ircchannel",
|
||||||
"pk": 2,
|
"pk": 2,
|
||||||
"fields": {
|
"fields": {
|
||||||
"name": "#level_test",
|
"name": "#duplicate_test",
|
||||||
"server": 1,
|
"server": 1,
|
||||||
"autojoin": false,
|
"autojoin": false,
|
||||||
"topic_msg": "",
|
"topic_msg": "",
|
||||||
@ -56,15 +56,6 @@
|
|||||||
"channel": 1
|
"channel": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"model": "idlerpg.game",
|
|
||||||
"pk": 2,
|
|
||||||
"fields": {
|
|
||||||
"name": "level test",
|
|
||||||
"active": true,
|
|
||||||
"channel": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"model": "idlerpg.character",
|
"model": "idlerpg.character",
|
||||||
"pk": 1,
|
"pk": 1,
|
||||||
|
@ -135,24 +135,22 @@ class CharacterTest(TestCase):
|
|||||||
char = Character.objects.get(pk=1)
|
char = Character.objects.get(pk=1)
|
||||||
|
|
||||||
# level 0 -> 1
|
# level 0 -> 1
|
||||||
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=600)
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=47758)
|
||||||
# level 1 -> 2
|
# level 1 -> 2
|
||||||
char.level = 1
|
char.level = 1
|
||||||
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=696)
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=131601)
|
||||||
# level 24 -> 25
|
# level 24 -> 25
|
||||||
char.level = 24
|
char.level = 24
|
||||||
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=21142)
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=437113)
|
||||||
# level 59 -> 60
|
# level 59 -> 60
|
||||||
char.level = 59
|
char.level = 59
|
||||||
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=3812174)
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=543010)
|
||||||
# level 60 -> 61
|
# level 60 -> 61
|
||||||
char.level = 60
|
char.level = 60
|
||||||
assert char.calculate_datetime_to_next_level() == char.next_level + (timedelta(seconds=3812174) +
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=545009)
|
||||||
timedelta(seconds=86400))
|
|
||||||
# level 61 -> 62
|
# level 61 -> 62
|
||||||
char.level = 61
|
char.level = 61
|
||||||
assert char.calculate_datetime_to_next_level() == char.next_level + (timedelta(seconds=3812174) +
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=546976)
|
||||||
timedelta(seconds=86400*2))
|
|
||||||
|
|
||||||
def test_calculate_datetime_to_next_level_not_time_yet(self):
|
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."""
|
"""Test that we just bail with the current next_level if it hasn't been reached yet."""
|
||||||
@ -169,7 +167,7 @@ class CharacterTest(TestCase):
|
|||||||
new_char = Character.objects.register('new', game, 'pass', 'bss!bss@test_register', 'unit tester')
|
new_char = Character.objects.register('new', game, 'pass', 'bss!bss@test_register', 'unit tester')
|
||||||
|
|
||||||
assert new_char.status == Character.CHARACTER_STATUS_LOGGED_IN
|
assert new_char.status == Character.CHARACTER_STATUS_LOGGED_IN
|
||||||
assert new_char.next_level == register_time + timedelta(seconds=600)
|
assert new_char.next_level == register_time + timedelta(seconds=47758)
|
||||||
assert new_char.last_login == register_time
|
assert new_char.last_login == register_time
|
||||||
assert new_char.password[0:13] == 'pbkdf2_sha256'
|
assert new_char.password[0:13] == 'pbkdf2_sha256'
|
||||||
|
|
||||||
@ -203,7 +201,7 @@ class CharacterTest(TestCase):
|
|||||||
char.level_up()
|
char.level_up()
|
||||||
|
|
||||||
assert char.level == old_level + 1
|
assert char.level == old_level + 1
|
||||||
assert char.next_level == old_next_level + timedelta(seconds=600)
|
assert char.next_level == old_next_level + timedelta(seconds=47758)
|
||||||
|
|
||||||
def test_level_up_fail_not_time(self):
|
def test_level_up_fail_not_time(self):
|
||||||
"""Test the failure condition for trying to level up before the timestamp."""
|
"""Test the failure condition for trying to level up before the timestamp."""
|
||||||
|
@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from ircbot.models import IrcChannel
|
||||||
|
|
||||||
from idlerpg.models import Game
|
from idlerpg.models import Game
|
||||||
|
|
||||||
@ -22,3 +25,16 @@ class GameTest(TestCase):
|
|||||||
game = Game.objects.get(pk=1)
|
game = Game.objects.get(pk=1)
|
||||||
logger.debug(str(game))
|
logger.debug(str(game))
|
||||||
assert str(game) == "test in #test on default (active)"
|
assert str(game) == "test in #test on default (active)"
|
||||||
|
|
||||||
|
def test_cant_have_two_active(self):
|
||||||
|
"""Test that if we create another game, it's disabled, and can't be active until the first is disabled."""
|
||||||
|
channel = IrcChannel.objects.get(pk=2)
|
||||||
|
game = Game.objects.get(pk=1)
|
||||||
|
new_game = Game.objects.create(name='new one', channel=channel)
|
||||||
|
|
||||||
|
assert game.active is True
|
||||||
|
assert new_game.active is False
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
with transaction.atomic():
|
||||||
|
new_game.active = True
|
||||||
|
new_game.save()
|
||||||
|
@ -32,6 +32,18 @@ class IrcPluginTest(TestCase):
|
|||||||
self.plugin = IdleRPG(self.mock_bot, self.mock_connection, mock.MagicMock())
|
self.plugin = IdleRPG(self.mock_bot, self.mock_connection, mock.MagicMock())
|
||||||
self.game = Game.objects.get(pk=1)
|
self.game = Game.objects.get(pk=1)
|
||||||
|
|
||||||
|
def test_start_stop(self):
|
||||||
|
"""Test that handlers are registered, roughly, as expected."""
|
||||||
|
self.plugin.start()
|
||||||
|
self.plugin.stop()
|
||||||
|
|
||||||
|
assert self.mock_connection.add_global_handler.call_count == 8
|
||||||
|
assert (self.mock_connection.add_global_handler.call_count ==
|
||||||
|
self.mock_connection.remove_global_handler.call_count)
|
||||||
|
assert self.mock_connection.reactor.add_global_regex_handler.call_count == 5
|
||||||
|
assert (self.mock_connection.reactor.add_global_regex_handler.call_count ==
|
||||||
|
self.mock_connection.reactor.remove_global_regex_handler.call_count)
|
||||||
|
|
||||||
def test_kick_penalty(self):
|
def test_kick_penalty(self):
|
||||||
"""Test that if a character is kicked from the game channel, they get penalized."""
|
"""Test that if a character is kicked from the game channel, they get penalized."""
|
||||||
mock_event = mock.MagicMock()
|
mock_event = mock.MagicMock()
|
||||||
@ -257,13 +269,176 @@ class IrcPluginTest(TestCase):
|
|||||||
|
|
||||||
self.plugin.seen_hostmasks.remove('bss!bss@test_login')
|
self.plugin.seen_hostmasks.remove('bss!bss@test_login')
|
||||||
|
|
||||||
|
def test_login(self):
|
||||||
|
"""Check that the LOGIN command actually sets the character as logged in."""
|
||||||
|
mock_event = mock.MagicMock()
|
||||||
|
mock_event.source = 'bss!bss@bss_login'
|
||||||
|
mock_event.recursing = False
|
||||||
|
game = Game.objects.get(pk=1)
|
||||||
|
test_char = Character.objects.register('test_login', game, 'test', 'bss!bss@bss_login', 'tester')
|
||||||
|
test_char.log_out()
|
||||||
|
test_char.next_level = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00')
|
||||||
|
test_char.last_login = test_char.next_level
|
||||||
|
test_char.save()
|
||||||
|
self.plugin.seen_hostmasks.add('bss!bss@bss_login')
|
||||||
|
|
||||||
|
# manipulate login so it doesn't add time to next_level and so we have a consistent
|
||||||
|
# value to test against below
|
||||||
|
with mock.patch('django.utils.timezone.now', return_value=test_char.next_level):
|
||||||
|
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'LOGIN test_login test')
|
||||||
|
self.plugin.handle_login(self.mock_connection, mock_event, match)
|
||||||
|
|
||||||
|
# refetch
|
||||||
|
refetch = Character.objects.get(name='test_login')
|
||||||
|
# should have been penalized
|
||||||
|
assert refetch.status == Character.CHARACTER_STATUS_LOGGED_IN
|
||||||
|
# the ordering of this surprises me... keep an eye on it
|
||||||
|
self.mock_bot.reply.assert_has_calls([
|
||||||
|
mock.call(None, "test_login, the level 0 tester, is now online from nickname bss. "
|
||||||
|
"Next level at 2024-05-17 17:00:00 UTC.", explicit_target='#test'),
|
||||||
|
mock.call(mock_event, "test_login, the level 0 tester, has been successfully logged in."),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_logout_character_not_found(self):
|
||||||
|
"""Test the LOGOUT command sent to the bot."""
|
||||||
|
mock_event = mock.MagicMock()
|
||||||
|
mock_event.source = 'bss!bss@test_login'
|
||||||
|
mock_event.recursing = False
|
||||||
|
|
||||||
|
match = re.match(IdleRPG.LOGOUT_COMMAND_PATTERN, 'LOGOUT')
|
||||||
|
self.plugin.handle_logout(self.mock_connection, mock_event, match)
|
||||||
|
|
||||||
|
self.mock_bot.reply.assert_called_once_with(mock_event, "You do not have a character in a running game.")
|
||||||
|
|
||||||
|
def test_logout_character_not_logged_in(self):
|
||||||
|
"""Test the LOGOUT command sent to the bot."""
|
||||||
|
mock_event = mock.MagicMock()
|
||||||
|
mock_event.source = 'bss2!bss@bss'
|
||||||
|
mock_event.recursing = False
|
||||||
|
|
||||||
|
match = re.match(IdleRPG.LOGOUT_COMMAND_PATTERN, 'LOGOUT')
|
||||||
|
self.plugin.handle_logout(self.mock_connection, mock_event, match)
|
||||||
|
|
||||||
|
self.mock_bot.reply.assert_called_once_with(
|
||||||
|
mock_event,
|
||||||
|
"Cannot log out bss2, either they already are, or they are in a state that cannot be modified."
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_logout(self):
|
||||||
|
"""Check that the LOGOUT command actually sets the character offline."""
|
||||||
|
mock_event = mock.MagicMock()
|
||||||
|
mock_event.source = 'bss!bss@bss_logout'
|
||||||
|
mock_event.recursing = False
|
||||||
|
game = Game.objects.get(pk=1)
|
||||||
|
test_char = Character.objects.register('test_logout', game, 'test', 'bss!bss@bss_logout', 'tester')
|
||||||
|
|
||||||
|
match = re.match(IdleRPG.LOGOUT_COMMAND_PATTERN, 'LOGOUT')
|
||||||
|
self.plugin.handle_logout(self.mock_connection, mock_event, match)
|
||||||
|
|
||||||
|
# refetch
|
||||||
|
refetch = Character.objects.get(name='test_logout')
|
||||||
|
# should have been penalized
|
||||||
|
assert refetch.next_level > test_char.next_level
|
||||||
|
assert refetch.status == Character.CHARACTER_STATUS_OFFLINE
|
||||||
|
self.mock_bot.reply.assert_called_once_with(
|
||||||
|
mock_event, "test_logout, the level 0 tester, has been successfully logged out."
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_register(self):
|
||||||
|
"""Test the ability to register a new character."""
|
||||||
|
mock_event = mock.MagicMock()
|
||||||
|
mock_event.source = 'bss!bss@bss_register'
|
||||||
|
mock_event.recursing = False
|
||||||
|
|
||||||
|
self.plugin.seen_hostmasks.add('bss!bss@bss_register')
|
||||||
|
now = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00')
|
||||||
|
with mock.patch('django.utils.timezone.now', return_value=now):
|
||||||
|
match = re.match(IdleRPG.REGISTER_COMMAND_PATTERN, 'REGISTER reg_test test tester')
|
||||||
|
self.plugin.handle_register(self.mock_connection, mock_event, match)
|
||||||
|
|
||||||
|
self.mock_bot.reply.assert_has_calls([
|
||||||
|
mock.call(None, "bss's newest character, reg_test, the tester, "
|
||||||
|
"has been summoned! Next level at 2024-05-18 06:15:58 UTC.",
|
||||||
|
explicit_target='#test'),
|
||||||
|
mock.call(mock_event, "You have registered reg_test, the level 0 tester, "
|
||||||
|
"and been automatically logged in."),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_register_no_active(self):
|
||||||
|
"""Test the behavior of a failed registration because there's no active game."""
|
||||||
|
mock_event = mock.MagicMock()
|
||||||
|
mock_event.source = 'bss!bss@bss_register'
|
||||||
|
mock_event.recursing = False
|
||||||
|
|
||||||
|
# manipulate the game temporarily
|
||||||
|
game = Game.objects.get(active=True)
|
||||||
|
game.active = False
|
||||||
|
game.save()
|
||||||
|
self.plugin.seen_hostmasks.add('bss!bss@bss_register')
|
||||||
|
now = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00')
|
||||||
|
with mock.patch('django.utils.timezone.now', return_value=now):
|
||||||
|
match = re.match(IdleRPG.REGISTER_COMMAND_PATTERN, 'REGISTER reg_test test tester')
|
||||||
|
self.plugin.handle_register(self.mock_connection, mock_event, match)
|
||||||
|
|
||||||
|
self.mock_bot.reply.assert_has_calls([
|
||||||
|
mock.call(mock_event, "No active game exists."),
|
||||||
|
])
|
||||||
|
|
||||||
|
# undo manipulation
|
||||||
|
game.active = True
|
||||||
|
game.save()
|
||||||
|
|
||||||
|
def test_register_not_in_channel(self):
|
||||||
|
"""Test the behavior of a failed registration because the user isn't in the game channel."""
|
||||||
|
mock_event = mock.MagicMock()
|
||||||
|
mock_event.source = 'bss!bss@bss_register'
|
||||||
|
mock_event.recursing = False
|
||||||
|
|
||||||
|
now = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00')
|
||||||
|
with mock.patch('django.utils.timezone.now', return_value=now):
|
||||||
|
match = re.match(IdleRPG.REGISTER_COMMAND_PATTERN, 'REGISTER reg_test test tester')
|
||||||
|
self.plugin.handle_register(self.mock_connection, mock_event, match)
|
||||||
|
|
||||||
|
self.mock_bot.reply.assert_has_calls([
|
||||||
|
mock.call(mock_event, "Please join #test before registering.")
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_register_cant_double_register(self):
|
||||||
|
"""Test the ability to register a new character fails if you try to have two."""
|
||||||
|
mock_event = mock.MagicMock()
|
||||||
|
mock_event.source = 'bss!bss@bss_dupe_register'
|
||||||
|
mock_event.recursing = False
|
||||||
|
|
||||||
|
self.plugin.seen_hostmasks.add('bss!bss@bss_dupe_register')
|
||||||
|
now = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00')
|
||||||
|
with mock.patch('django.utils.timezone.now', return_value=now):
|
||||||
|
match = re.match(IdleRPG.REGISTER_COMMAND_PATTERN, 'REGISTER reg_test test tester')
|
||||||
|
self.plugin.handle_register(self.mock_connection, mock_event, match)
|
||||||
|
|
||||||
|
self.mock_bot.reply.assert_has_calls([
|
||||||
|
mock.call(None, "bss's newest character, reg_test, the tester, "
|
||||||
|
"has been summoned! Next level at 2024-05-18 06:15:58 UTC.",
|
||||||
|
explicit_target='#test'),
|
||||||
|
mock.call(mock_event, "You have registered reg_test, the level 0 tester, "
|
||||||
|
"and been automatically logged in."),
|
||||||
|
])
|
||||||
|
|
||||||
|
with mock.patch('django.utils.timezone.now', return_value=now):
|
||||||
|
match = re.match(IdleRPG.REGISTER_COMMAND_PATTERN, 'REGISTER reg_test2 test tester')
|
||||||
|
self.plugin.handle_register(self.mock_connection, mock_event, match)
|
||||||
|
|
||||||
|
self.mock_bot.reply.assert_has_calls([
|
||||||
|
mock.call(mock_event, "Registration failed; either you already have a character, "
|
||||||
|
"or the character name is already taken.")
|
||||||
|
])
|
||||||
|
|
||||||
def test_remove(self):
|
def test_remove(self):
|
||||||
"""Test the remove command."""
|
"""Test the remove command."""
|
||||||
mock_event = mock.MagicMock()
|
mock_event = mock.MagicMock()
|
||||||
mock_event.source = 'bss!bss@bss_remove'
|
mock_event.source = 'bss!bss@bss_remove'
|
||||||
mock_event.recursing = False
|
mock_event.recursing = False
|
||||||
|
|
||||||
game = Game.objects.get(pk=2)
|
game = Game.objects.get(pk=1)
|
||||||
test_char = Character.objects.register('test_remove', game, 'test', 'bss!bss@bss_remove', 'tester')
|
test_char = Character.objects.register('test_remove', game, 'test', 'bss!bss@bss_remove', 'tester')
|
||||||
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'REMOVEME')
|
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'REMOVEME')
|
||||||
self.plugin.handle_remove(self.mock_connection, mock_event, match)
|
self.plugin.handle_remove(self.mock_connection, mock_event, match)
|
||||||
@ -281,7 +456,7 @@ class IrcPluginTest(TestCase):
|
|||||||
mock_event.source = 'bss2!bss@bss_remove'
|
mock_event.source = 'bss2!bss@bss_remove'
|
||||||
mock_event.recursing = False
|
mock_event.recursing = False
|
||||||
|
|
||||||
game = Game.objects.get(pk=2)
|
game = Game.objects.get(pk=1)
|
||||||
test_char = Character.objects.register('test_remove', game, 'test', 'bss!bss@bss_remove', 'tester')
|
test_char = Character.objects.register('test_remove', game, 'test', 'bss!bss@bss_remove', 'tester')
|
||||||
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'REMOVEME')
|
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'REMOVEME')
|
||||||
self.plugin.handle_remove(self.mock_connection, mock_event, match)
|
self.plugin.handle_remove(self.mock_connection, mock_event, match)
|
||||||
@ -347,9 +522,9 @@ class IrcPluginTest(TestCase):
|
|||||||
|
|
||||||
self.plugin.seen_hostmasks.remove('bss3!bss@bss')
|
self.plugin.seen_hostmasks.remove('bss3!bss@bss')
|
||||||
|
|
||||||
def test_level_up_games(self):
|
def test_level_up_characters(self):
|
||||||
"""Test the walking of games and characters to level them up."""
|
"""Test the walking of games and characters to level them up."""
|
||||||
game = Game.objects.get(pk=2)
|
game = Game.objects.get(pk=1)
|
||||||
char_1 = Character.objects.register('test_level_1', game, 'test', 'test_1!test@test', 'tester')
|
char_1 = Character.objects.register('test_level_1', game, 'test', 'test_1!test@test', 'tester')
|
||||||
char_2 = Character.objects.register('test_level_2', game, 'test', 'test_2!test@test', 'tester')
|
char_2 = Character.objects.register('test_level_2', game, 'test', 'test_2!test@test', 'tester')
|
||||||
char_2.log_out()
|
char_2.log_out()
|
||||||
@ -359,7 +534,7 @@ class IrcPluginTest(TestCase):
|
|||||||
char_1.save()
|
char_1.save()
|
||||||
char_2.save()
|
char_2.save()
|
||||||
# only one should level up, since char_2 isn't logged in
|
# only one should level up, since char_2 isn't logged in
|
||||||
self.plugin._level_up_games()
|
self.plugin._level_up_characters()
|
||||||
|
|
||||||
char_1 = Character.objects.get(pk=char_1.pk)
|
char_1 = Character.objects.get(pk=char_1.pk)
|
||||||
char_2 = Character.objects.get(pk=char_2.pk)
|
char_2 = Character.objects.get(pk=char_2.pk)
|
||||||
@ -367,7 +542,6 @@ class IrcPluginTest(TestCase):
|
|||||||
assert char_2.level == 0
|
assert char_2.level == 0
|
||||||
assert char_1.next_level != char_2.next_level
|
assert char_1.next_level != char_2.next_level
|
||||||
self.mock_bot.reply.assert_has_calls([
|
self.mock_bot.reply.assert_has_calls([
|
||||||
mock.call(None, "test_level_1, the tester, has attained level 1! Next level at 2024-05-16 00:10:00 UTC.",
|
mock.call(None, "test_level_1, the tester, has attained level 1! Next level at 2024-05-16 13:15:58 UTC.",
|
||||||
explicit_target=game.channel.name),
|
explicit_target=game.channel.name),
|
||||||
])
|
])
|
||||||
assert self.mock_bot.reply.call_count == 2
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user