From 04a95297b50ecbc4bc40bc7936ff02c94ea4cbce Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 18 Dec 2021 23:42:14 +0100 Subject: [PATCH] First version of moved contrib tests --- .../base_systems/building_menu/tests.py | 177 + .../base_systems/color_markups/tests.py | 66 + .../base_systems/custom_gametime/tests.py | 54 + .../contrib/base_systems/email_login/tests.py | 36 + .../base_systems/ingame_python/tests.py | 4 +- .../contrib/base_systems/menu_login/tests.py | 12 + .../base_systems/mux_comms_cmds/tests.py | 86 + .../contrib/base_systems/unixcommand/tests.py | 50 + evennia/contrib/game_systems/barter/tests.py | 147 + .../contrib/game_systems/clothing/tests.py | 133 + .../contrib/game_systems/cooldowns/tests.py | 147 + .../contrib/game_systems/gendersub/tests.py | 28 + evennia/contrib/game_systems/mail/tests.py | 47 + .../contrib/game_systems/multidescer/tests.py | 40 + evennia/contrib/game_systems/puzzles/tests.py | 975 +++++ .../contrib/game_systems/turnbattle/tests.py | 603 +++ evennia/contrib/grid/extended_room/tests.py | 103 + evennia/contrib/grid/mapbuilder/tests.py | 33 + evennia/contrib/grid/simpledoor/tests.py | 29 + evennia/contrib/grid/slow_exit/tests.py | 27 + evennia/contrib/grid/wilderness/tests.py | 137 + evennia/contrib/rpg/dice/tests.py | 23 + evennia/contrib/rpg/health_bar/tests.py | 46 + evennia/contrib/rpg/rpsystem/tests.py | 321 ++ evennia/contrib/tests.py | 3585 ----------------- .../tutorials/{ => bodyfunctions}/tests.py | 6 +- evennia/contrib/tutorials/mirror/README.md | 17 + evennia/contrib/tutorials/mirror/__init__.py | 6 + .../contrib/tutorials/talking_npc/tests.py | 14 + .../contrib/tutorials/tutorial_world/tests.py | 192 + evennia/contrib/utils/auditing/README.md | 71 + evennia/contrib/utils/auditing/__init__.py | 0 evennia/contrib/utils/auditing/outputs.py | 60 + evennia/contrib/utils/auditing/server.py | 249 ++ evennia/contrib/utils/auditing/tests.py | 113 + .../utils/random_string_generator/tests.py | 25 + evennia/contrib/utils/tree_select/tests.py | 62 + 37 files changed, 4135 insertions(+), 3589 deletions(-) create mode 100644 evennia/contrib/base_systems/building_menu/tests.py create mode 100644 evennia/contrib/base_systems/color_markups/tests.py create mode 100644 evennia/contrib/base_systems/custom_gametime/tests.py create mode 100644 evennia/contrib/base_systems/email_login/tests.py create mode 100644 evennia/contrib/base_systems/menu_login/tests.py create mode 100644 evennia/contrib/base_systems/mux_comms_cmds/tests.py create mode 100644 evennia/contrib/base_systems/unixcommand/tests.py create mode 100644 evennia/contrib/game_systems/barter/tests.py create mode 100644 evennia/contrib/game_systems/clothing/tests.py create mode 100644 evennia/contrib/game_systems/cooldowns/tests.py create mode 100644 evennia/contrib/game_systems/gendersub/tests.py create mode 100644 evennia/contrib/game_systems/mail/tests.py create mode 100644 evennia/contrib/game_systems/multidescer/tests.py create mode 100644 evennia/contrib/game_systems/puzzles/tests.py create mode 100644 evennia/contrib/game_systems/turnbattle/tests.py create mode 100644 evennia/contrib/grid/extended_room/tests.py create mode 100644 evennia/contrib/grid/mapbuilder/tests.py create mode 100644 evennia/contrib/grid/simpledoor/tests.py create mode 100644 evennia/contrib/grid/slow_exit/tests.py create mode 100644 evennia/contrib/grid/wilderness/tests.py create mode 100644 evennia/contrib/rpg/dice/tests.py create mode 100644 evennia/contrib/rpg/health_bar/tests.py create mode 100644 evennia/contrib/rpg/rpsystem/tests.py delete mode 100644 evennia/contrib/tests.py rename evennia/contrib/tutorials/{ => bodyfunctions}/tests.py (98%) create mode 100644 evennia/contrib/tutorials/mirror/README.md create mode 100644 evennia/contrib/tutorials/mirror/__init__.py create mode 100644 evennia/contrib/tutorials/talking_npc/tests.py create mode 100644 evennia/contrib/tutorials/tutorial_world/tests.py create mode 100644 evennia/contrib/utils/auditing/README.md create mode 100644 evennia/contrib/utils/auditing/__init__.py create mode 100644 evennia/contrib/utils/auditing/outputs.py create mode 100644 evennia/contrib/utils/auditing/server.py create mode 100644 evennia/contrib/utils/auditing/tests.py create mode 100644 evennia/contrib/utils/random_string_generator/tests.py create mode 100644 evennia/contrib/utils/tree_select/tests.py diff --git a/evennia/contrib/base_systems/building_menu/tests.py b/evennia/contrib/base_systems/building_menu/tests.py new file mode 100644 index 0000000000..63bf9ab6f2 --- /dev/null +++ b/evennia/contrib/base_systems/building_menu/tests.py @@ -0,0 +1,177 @@ +""" +Building menu tests. + +""" + +from evennia.commands.default.tests import CommandTest +from . building_menu import BuildingMenu, CmdNoMatch + + +class Submenu(BuildingMenu): + def init(self, exit): + self.add_choice("title", key="t", attr="key") + + +class TestBuildingMenu(CommandTest): + def setUp(self): + super(TestBuildingMenu, self).setUp() + self.menu = BuildingMenu(caller=self.char1, obj=self.room1, title="test") + self.menu.add_choice("title", key="t", attr="key") + + def test_quit(self): + """Try to quit the building menu.""" + self.assertFalse(self.char1.cmdset.has("building_menu")) + self.menu.open() + self.assertTrue(self.char1.cmdset.has("building_menu")) + self.call(CmdNoMatch(building_menu=self.menu), "q") + # char1 tries to quit the editor + self.assertFalse(self.char1.cmdset.has("building_menu")) + + def test_setattr(self): + """Test the simple setattr provided by building menus.""" + self.menu.open() + self.call(CmdNoMatch(building_menu=self.menu), "t") + self.assertIsNotNone(self.menu.current_choice) + self.call(CmdNoMatch(building_menu=self.menu), "some new title") + self.call(CmdNoMatch(building_menu=self.menu), "@") + self.assertIsNone(self.menu.current_choice) + self.assertEqual(self.room1.key, "some new title") + self.call(CmdNoMatch(building_menu=self.menu), "q") + + def test_add_choice_without_key(self): + """Try to add choices without keys.""" + choices = [] + for i in range(20): + choices.append(self.menu.add_choice("choice", attr="test")) + self.menu._add_keys_choice() + keys = [ + "c", + "h", + "o", + "i", + "e", + "ch", + "ho", + "oi", + "ic", + "ce", + "cho", + "hoi", + "oic", + "ice", + "choi", + "hoic", + "oice", + "choic", + "hoice", + "choice", + ] + for i in range(20): + self.assertEqual(choices[i].key, keys[i]) + + # Adding another key of the same title would break, no more available shortcut + self.menu.add_choice("choice", attr="test") + with self.assertRaises(ValueError): + self.menu._add_keys_choice() + + def test_callbacks(self): + """Test callbacks in menus.""" + self.room1.key = "room1" + + def on_enter(caller, menu): + caller.msg("on_enter:{}".format(menu.title)) + + def on_nomatch(caller, string, choice): + caller.msg("on_nomatch:{},{}".format(string, choice.key)) + + def on_leave(caller, obj): + caller.msg("on_leave:{}".format(obj.key)) + + self.menu.add_choice( + "test", key="e", on_enter=on_enter, on_nomatch=on_nomatch, on_leave=on_leave + ) + self.call(CmdNoMatch(building_menu=self.menu), "e", "on_enter:test") + self.call(CmdNoMatch(building_menu=self.menu), "ok", "on_nomatch:ok,e") + self.call(CmdNoMatch(building_menu=self.menu), "@", "on_leave:room1") + self.call(CmdNoMatch(building_menu=self.menu), "q") + + def test_multi_level(self): + """Test multi-level choices.""" + # Creaste three succeeding menu (t2 is contained in t1, t3 is contained in t2) + def on_nomatch_t1(caller, menu): + menu.move("whatever") # this will be valid since after t1 is a joker + + def on_nomatch_t2(caller, menu): + menu.move("t3") # this time the key matters + + t1 = self.menu.add_choice("what", key="t1", on_nomatch=on_nomatch_t1) + t2 = self.menu.add_choice("and", key="t1.*", on_nomatch=on_nomatch_t2) + t3 = self.menu.add_choice("why", key="t1.*.t3") + self.menu.open() + + # Move into t1 + self.assertIn(t1, self.menu.relevant_choices) + self.assertNotIn(t2, self.menu.relevant_choices) + self.assertNotIn(t3, self.menu.relevant_choices) + self.assertIsNone(self.menu.current_choice) + self.call(CmdNoMatch(building_menu=self.menu), "t1") + self.assertEqual(self.menu.current_choice, t1) + self.assertNotIn(t1, self.menu.relevant_choices) + self.assertIn(t2, self.menu.relevant_choices) + self.assertNotIn(t3, self.menu.relevant_choices) + + # Move into t2 + self.call(CmdNoMatch(building_menu=self.menu), "t2") + self.assertEqual(self.menu.current_choice, t2) + self.assertNotIn(t1, self.menu.relevant_choices) + self.assertNotIn(t2, self.menu.relevant_choices) + self.assertIn(t3, self.menu.relevant_choices) + + # Move into t3 + self.call(CmdNoMatch(building_menu=self.menu), "t3") + self.assertEqual(self.menu.current_choice, t3) + self.assertNotIn(t1, self.menu.relevant_choices) + self.assertNotIn(t2, self.menu.relevant_choices) + self.assertNotIn(t3, self.menu.relevant_choices) + + # Move back to t2 + self.call(CmdNoMatch(building_menu=self.menu), "@") + self.assertEqual(self.menu.current_choice, t2) + self.assertNotIn(t1, self.menu.relevant_choices) + self.assertNotIn(t2, self.menu.relevant_choices) + self.assertIn(t3, self.menu.relevant_choices) + + # Move back into t1 + self.call(CmdNoMatch(building_menu=self.menu), "@") + self.assertEqual(self.menu.current_choice, t1) + self.assertNotIn(t1, self.menu.relevant_choices) + self.assertIn(t2, self.menu.relevant_choices) + self.assertNotIn(t3, self.menu.relevant_choices) + + # Moves back to the main menu + self.call(CmdNoMatch(building_menu=self.menu), "@") + self.assertIn(t1, self.menu.relevant_choices) + self.assertNotIn(t2, self.menu.relevant_choices) + self.assertNotIn(t3, self.menu.relevant_choices) + self.assertIsNone(self.menu.current_choice) + self.call(CmdNoMatch(building_menu=self.menu), "q") + + def test_submenu(self): + """Test to add sub-menus.""" + + def open_exit(menu): + menu.open_submenu("evennia.contrib.tests.Submenu", self.exit) + return False + + self.menu.add_choice("exit", key="x", on_enter=open_exit) + self.menu.open() + self.call(CmdNoMatch(building_menu=self.menu), "x") + self.menu = self.char1.ndb._building_menu + self.call(CmdNoMatch(building_menu=self.menu), "t") + self.call(CmdNoMatch(building_menu=self.menu), "in") + self.call(CmdNoMatch(building_menu=self.menu), "@") + self.call(CmdNoMatch(building_menu=self.menu), "@") + self.menu = self.char1.ndb._building_menu + self.assertEqual(self.char1.ndb._building_menu.obj, self.room1) + self.call(CmdNoMatch(building_menu=self.menu), "q") + self.assertEqual(self.exit.key, "in") diff --git a/evennia/contrib/base_systems/color_markups/tests.py b/evennia/contrib/base_systems/color_markups/tests.py new file mode 100644 index 0000000000..9dddc346be --- /dev/null +++ b/evennia/contrib/base_systems/color_markups/tests.py @@ -0,0 +1,66 @@ +""" +Test Color markup. + +""" + +import re +from evennia.utils.test_resources import EvenniaTest +from . import color_markups + + +class TestColorMarkup(EvenniaTest): + """ + Note: Normally this would be tested by importing the ansi parser and run + the mappings through it. This is not possible since the ansi module creates + its mapping at the module/class level; since the ansi module is used by so + many other modules it appears that trying to overload + settings to test it causes issues with unrelated tests. + """ + + def test_curly_markup(self): + ansi_map = color_markups.CURLY_COLOR_ANSI_EXTRA_MAP + self.assertIsNotNone(re.match(re.escape(ansi_map[7][0]), "{r")) + self.assertIsNotNone(re.match(re.escape(ansi_map[-1][0]), "{[X")) + xterm_fg = color_markups.CURLY_COLOR_XTERM256_EXTRA_FG + self.assertIsNotNone(re.match(xterm_fg[0], "{001")) + self.assertIsNotNone(re.match(xterm_fg[0], "{123")) + self.assertIsNotNone(re.match(xterm_fg[0], "{455")) + xterm_bg = color_markups.CURLY_COLOR_XTERM256_EXTRA_BG + self.assertIsNotNone(re.match(xterm_bg[0], "{[001")) + self.assertIsNotNone(re.match(xterm_bg[0], "{[123")) + self.assertIsNotNone(re.match(xterm_bg[0], "{[455")) + xterm_gfg = color_markups.CURLY_COLOR_XTERM256_EXTRA_GFG + self.assertIsNotNone(re.match(xterm_gfg[0], "{=h")) + self.assertIsNotNone(re.match(xterm_gfg[0], "{=e")) + self.assertIsNotNone(re.match(xterm_gfg[0], "{=w")) + xterm_gbg = color_markups.CURLY_COLOR_XTERM256_EXTRA_GBG + self.assertIsNotNone(re.match(xterm_gbg[0], "{[=a")) + self.assertIsNotNone(re.match(xterm_gbg[0], "{[=k")) + self.assertIsNotNone(re.match(xterm_gbg[0], "{[=z")) + bright_map = color_markups.CURLY_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP + self.assertEqual(bright_map[0][1], "{[500") + self.assertEqual(bright_map[-1][1], "{[222") + + def test_mux_markup(self): + ansi_map = color_markups.MUX_COLOR_ANSI_EXTRA_MAP + self.assertIsNotNone(re.match(re.escape(ansi_map[10][0]), "%cr")) + self.assertIsNotNone(re.match(re.escape(ansi_map[-1][0]), "%cX")) + xterm_fg = color_markups.MUX_COLOR_XTERM256_EXTRA_FG + self.assertIsNotNone(re.match(xterm_fg[0], "%c001")) + self.assertIsNotNone(re.match(xterm_fg[0], "%c123")) + self.assertIsNotNone(re.match(xterm_fg[0], "%c455")) + xterm_bg = color_markups.MUX_COLOR_XTERM256_EXTRA_BG + self.assertIsNotNone(re.match(xterm_bg[0], "%c[001")) + self.assertIsNotNone(re.match(xterm_bg[0], "%c[123")) + self.assertIsNotNone(re.match(xterm_bg[0], "%c[455")) + xterm_gfg = color_markups.MUX_COLOR_XTERM256_EXTRA_GFG + self.assertIsNotNone(re.match(xterm_gfg[0], "%c=h")) + self.assertIsNotNone(re.match(xterm_gfg[0], "%c=e")) + self.assertIsNotNone(re.match(xterm_gfg[0], "%c=w")) + xterm_gbg = color_markups.MUX_COLOR_XTERM256_EXTRA_GBG + self.assertIsNotNone(re.match(xterm_gbg[0], "%c[=a")) + self.assertIsNotNone(re.match(xterm_gbg[0], "%c[=k")) + self.assertIsNotNone(re.match(xterm_gbg[0], "%c[=z")) + bright_map = color_markups.MUX_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP + self.assertEqual(bright_map[0][1], "%c[500") + self.assertEqual(bright_map[-1][1], "%c[222") diff --git a/evennia/contrib/base_systems/custom_gametime/tests.py b/evennia/contrib/base_systems/custom_gametime/tests.py new file mode 100644 index 0000000000..0f69ce7e91 --- /dev/null +++ b/evennia/contrib/base_systems/custom_gametime/tests.py @@ -0,0 +1,54 @@ +""" +Testing custom game time + +""" + +# Testing custom_gametime +from mock import Mock, patch +from evennia.utils.test_resources import EvenniaTest +from . import custom_gametime + + +def _testcallback(): + pass + + +@patch("evennia.utils.gametime.gametime", new=Mock(return_value=2975000898.46)) +class TestCustomGameTime(EvenniaTest): + def tearDown(self): + if hasattr(self, "timescript"): + self.timescript.stop() + + def test_time_to_tuple(self): + self.assertEqual(custom_gametime.time_to_tuple(10000, 34, 2, 4, 6, 1), (294, 2, 0, 0, 0, 0)) + self.assertEqual(custom_gametime.time_to_tuple(10000, 3, 3, 4), (3333, 0, 0, 1)) + self.assertEqual(custom_gametime.time_to_tuple(100000, 239, 24, 3), (418, 4, 0, 2)) + + def test_gametime_to_realtime(self): + self.assertEqual(custom_gametime.gametime_to_realtime(days=2, mins=4), 86520.0) + self.assertEqual( + custom_gametime.gametime_to_realtime(format=True, days=2), (0, 0, 0, 1, 0, 0, 0) + ) + + def test_realtime_to_gametime(self): + self.assertEqual(custom_gametime.realtime_to_gametime(days=3, mins=34), 349680.0) + self.assertEqual( + custom_gametime.realtime_to_gametime(days=3, mins=34, format=True), + (0, 0, 0, 4, 1, 8, 0), + ) + self.assertEqual( + custom_gametime.realtime_to_gametime(format=True, days=3, mins=4), (0, 0, 0, 4, 0, 8, 0) + ) + + def test_custom_gametime(self): + self.assertEqual(custom_gametime.custom_gametime(), (102, 5, 2, 6, 21, 8, 18)) + self.assertEqual(custom_gametime.custom_gametime(absolute=True), (102, 5, 2, 6, 21, 8, 18)) + + def test_real_seconds_until(self): + self.assertEqual( + custom_gametime.real_seconds_until(year=2300, month=12, day=7), 31911667199.77 + ) + + def test_schedule(self): + self.timescript = custom_gametime.schedule(_testcallback, repeat=True, min=5, sec=0) + self.assertEqual(self.timescript.interval, 1700.7699999809265) diff --git a/evennia/contrib/base_systems/email_login/tests.py b/evennia/contrib/base_systems/email_login/tests.py new file mode 100644 index 0000000000..bd0625adba --- /dev/null +++ b/evennia/contrib/base_systems/email_login/tests.py @@ -0,0 +1,36 @@ +""" +Test email login. + +""" + +from evennia.commands.default.tests import CommandTest +from . import email_login + + +class TestEmailLogin(CommandTest): + def test_connect(self): + self.call( + email_login.CmdUnconnectedConnect(), + "mytest@test.com test", + "The email 'mytest@test.com' does not match any accounts.", + ) + self.call( + email_login.CmdUnconnectedCreate(), + '"mytest" mytest@test.com test11111', + "A new account 'mytest' was created. Welcome!", + ) + self.call( + email_login.CmdUnconnectedConnect(), + "mytest@test.com test11111", + "", + caller=self.account.sessions.get()[0], + ) + + def test_quit(self): + self.call(email_login.CmdUnconnectedQuit(), "", "", caller=self.account.sessions.get()[0]) + + def test_unconnectedlook(self): + self.call(email_login.CmdUnconnectedLook(), "", "==========") + + def test_unconnectedhelp(self): + self.call(email_login.CmdUnconnectedHelp(), "", "You are not yet logged into the game.") diff --git a/evennia/contrib/base_systems/ingame_python/tests.py b/evennia/contrib/base_systems/ingame_python/tests.py index 9531b230c4..c321f2da3f 100644 --- a/evennia/contrib/base_systems/ingame_python/tests.py +++ b/evennia/contrib/base_systems/ingame_python/tests.py @@ -12,8 +12,8 @@ from evennia.objects.objects import ExitCommand from evennia.utils import ansi, utils from evennia.utils.create import create_object, create_script from evennia.utils.test_resources import EvenniaTest -from evennia.contrib.base_systems.ingame_python.commands import CmdCallback -from evennia.contrib.base_systems.ingame_python.callbackhandler import CallbackHandler +from .commands import CmdCallback +from .callbackhandler import CallbackHandler # Force settings settings.EVENTS_CALENDAR = "standard" diff --git a/evennia/contrib/base_systems/menu_login/tests.py b/evennia/contrib/base_systems/menu_login/tests.py new file mode 100644 index 0000000000..679f20c976 --- /dev/null +++ b/evennia/contrib/base_systems/menu_login/tests.py @@ -0,0 +1,12 @@ +""" +Test menu_login + +""" + +from evennia.commands.default.tests import CommandTest +from . import menu_login + + +class TestMenuLogin(CommandTest): + def test_cmdunloggedlook(self): + self.call(menu_login.CmdUnloggedinLook(), "", "======") diff --git a/evennia/contrib/base_systems/mux_comms_cmds/tests.py b/evennia/contrib/base_systems/mux_comms_cmds/tests.py new file mode 100644 index 0000000000..98e0f7e78e --- /dev/null +++ b/evennia/contrib/base_systems/mux_comms_cmds/tests.py @@ -0,0 +1,86 @@ +""" +Legacy Mux comms tests (extracted from 0.9.5) + +""" + +from evennia.commands.default.tests import CommandTest +from . import mux_comms_cmds as comms + + +class TestLegacyMuxComms(CommandTest): + """ + Test the legacy comms contrib. + """ + + def setUp(self): + super(CommandTest, self).setUp() + self.call( + comms.CmdChannelCreate(), + "testchan;test=Test Channel", + "Created channel testchan and connected to it.", + receiver=self.account, + ) + + def test_toggle_com(self): + self.call( + comms.CmdAddCom(), + "tc = testchan", + "You are already connected to channel testchan.| You can now", + receiver=self.account, + ) + self.call( + comms.CmdDelCom(), + "tc", + "Any alias 'tc' for channel testchan was cleared.", + receiver=self.account, + ) + + def test_all_com(self): + self.call( + comms.CmdAllCom(), + "", + "Available channels:", + receiver=self.account, + ) + + def test_clock(self): + self.call( + comms.CmdClock(), + "testchan=send:all()", + "Lock(s) applied. Current locks on testchan:", + receiver=self.account, + ) + + def test_cdesc(self): + self.call( + comms.CmdCdesc(), + "testchan = Test Channel", + "Description of channel 'testchan' set to 'Test Channel'.", + receiver=self.account, + ) + + def test_cwho(self): + self.call( + comms.CmdCWho(), + "testchan", + "Channel subscriptions\ntestchan:\n TestAccount", + receiver=self.account, + ) + + def test_cboot(self): + # No one else connected to boot + self.call( + comms.CmdCBoot(), + "", + "Usage: cboot[/quiet] = [:reason]", + receiver=self.account, + ) + + def test_cdestroy(self): + self.call( + comms.CmdCdestroy(), + "testchan", + "[testchan] TestAccount: testchan is being destroyed. Make sure to change your aliases." + "|Channel 'testchan' was destroyed.", + receiver=self.account, + ) diff --git a/evennia/contrib/base_systems/unixcommand/tests.py b/evennia/contrib/base_systems/unixcommand/tests.py new file mode 100644 index 0000000000..6fb19077f4 --- /dev/null +++ b/evennia/contrib/base_systems/unixcommand/tests.py @@ -0,0 +1,50 @@ +""" +Test of the Unixcommand. + +""" + +from evennia.commands.default.tests import CommandTest +from .unixcommand import UnixCommand + + +class CmdDummy(UnixCommand): + + """A dummy UnixCommand.""" + + key = "dummy" + + def init_parser(self): + """Fill out options.""" + self.parser.add_argument("nb1", type=int, help="the first number") + self.parser.add_argument("nb2", type=int, help="the second number") + self.parser.add_argument("-v", "--verbose", action="store_true") + + def func(self): + nb1 = self.opts.nb1 + nb2 = self.opts.nb2 + result = nb1 * nb2 + verbose = self.opts.verbose + if verbose: + self.msg("{} times {} is {}".format(nb1, nb2, result)) + else: + self.msg("{} * {} = {}".format(nb1, nb2, result)) + + +class TestUnixCommand(CommandTest): + def test_success(self): + """See the command parsing succeed.""" + self.call(CmdDummy(), "5 10", "5 * 10 = 50") + self.call(CmdDummy(), "5 10 -v", "5 times 10 is 50") + + def test_failure(self): + """If not provided with the right info, should fail.""" + ret = self.call(CmdDummy(), "5") + lines = ret.splitlines() + self.assertTrue(any(lin.startswith("usage:") for lin in lines)) + self.assertTrue(any(lin.startswith("dummy: error:") for lin in lines)) + + # If we specify an incorrect number as parameter + ret = self.call(CmdDummy(), "five ten") + lines = ret.splitlines() + self.assertTrue(any(lin.startswith("usage:") for lin in lines)) + self.assertTrue(any(lin.startswith("dummy: error:") for lin in lines)) diff --git a/evennia/contrib/game_systems/barter/tests.py b/evennia/contrib/game_systems/barter/tests.py new file mode 100644 index 0000000000..1ef36812cf --- /dev/null +++ b/evennia/contrib/game_systems/barter/tests.py @@ -0,0 +1,147 @@ +""" +Test the contrib barter system +""" + +from mock import Mock +from evennia.commands.default.tests import CommandTest +from evennia.utils.create import create_object +from . import barter + + +class TestBarter(CommandTest): + def setUp(self): + super().setUp() + self.tradeitem1 = create_object(key="TradeItem1", location=self.char1) + self.tradeitem2 = create_object(key="TradeItem2", location=self.char1) + self.tradeitem3 = create_object(key="TradeItem3", location=self.char2) + + def test_tradehandler_base(self): + self.char1.msg = Mock() + self.char2.msg = Mock() + # test all methods of the tradehandler + handler = barter.TradeHandler(self.char1, self.char2) + self.assertEqual(handler.part_a, self.char1) + self.assertEqual(handler.part_b, self.char2) + handler.msg_other(self.char1, "Want to trade?") + handler.msg_other(self.char2, "Yes!") + handler.msg_other(None, "Talking to myself...") + self.assertEqual(self.char2.msg.mock_calls[0][1][0], "Want to trade?") + self.assertEqual(self.char1.msg.mock_calls[0][1][0], "Yes!") + self.assertEqual(self.char1.msg.mock_calls[1][1][0], "Talking to myself...") + self.assertEqual(handler.get_other(self.char1), self.char2) + handler.finish(force=True) + + def test_tradehandler_joins(self): + handler = barter.TradeHandler(self.char1, self.char2) + self.assertTrue(handler.join(self.char2)) + self.assertTrue(handler.unjoin(self.char2)) + self.assertFalse(handler.join(self.char1)) + self.assertFalse(handler.unjoin(self.char1)) + handler.finish(force=True) + + def test_tradehandler_offers(self): + handler = barter.TradeHandler(self.char1, self.char2) + handler.join(self.char2) + handler.offer(self.char1, self.tradeitem1, self.tradeitem2) + self.assertEqual(handler.part_a_offers, [self.tradeitem1, self.tradeitem2]) + self.assertFalse(handler.part_a_accepted) + self.assertFalse(handler.part_b_accepted) + handler.offer(self.char2, self.tradeitem3) + self.assertEqual(handler.list(), ([self.tradeitem1, self.tradeitem2], [self.tradeitem3])) + self.assertEqual(handler.search("TradeItem2"), self.tradeitem2) + self.assertEqual(handler.search("TradeItem3"), self.tradeitem3) + self.assertEqual(handler.search("nonexisting"), None) + self.assertFalse(handler.finish()) # should fail since offer not yet accepted + handler.accept(self.char1) + handler.decline(self.char1) + handler.accept(self.char2) + handler.accept(self.char1) # should trigger handler.finish() automatically + self.assertEqual(self.tradeitem1.location, self.char2) + self.assertEqual(self.tradeitem2.location, self.char2) + self.assertEqual(self.tradeitem3.location, self.char1) + + def test_cmdtrade(self): + self.call( + barter.CmdTrade(), + "Char2 : Hey wanna trade?", + 'You say, "Hey wanna trade?"', + caller=self.char1, + ) + self.call(barter.CmdTrade(), "Char decline : Nope!", 'You say, "Nope!"', caller=self.char2) + self.call( + barter.CmdTrade(), + "Char2 : Hey wanna trade?", + 'You say, "Hey wanna trade?"', + caller=self.char1, + ) + self.call(barter.CmdTrade(), "Char accept : Sure!", 'You say, "Sure!"', caller=self.char2) + self.call( + barter.CmdOffer(), + "TradeItem3", + "Your trade action: You offer TradeItem3", + caller=self.char2, + ) + self.call( + barter.CmdOffer(), + "TradeItem1 : Here's my offer.", + 'You say, "Here\'s my offer."\n [You offer TradeItem1]', + ) + self.call( + barter.CmdAccept(), + "", + "Your trade action: You accept the offer. Char2 must now also accept", + ) + self.call( + barter.CmdDecline(), + "", + "Your trade action: You change your mind, declining the current offer.", + ) + self.call( + barter.CmdAccept(), + ": Sounds good.", + 'You say, "Sounds good."\n' " [You accept the offer. Char must now also accept.", + caller=self.char2, + ) + self.call( + barter.CmdDecline(), + ":No way!", + 'You say, "No way!"\n [You change your mind, declining the current offer.]', + caller=self.char2, + ) + self.call( + barter.CmdOffer(), + "TradeItem1, TradeItem2 : My final offer!", + 'You say, "My final offer!"\n [You offer TradeItem1 and TradeItem2]', + ) + self.call( + barter.CmdAccept(), + "", + "Your trade action: You accept the offer. Char2 must now also accept.", + caller=self.char1, + ) + self.call(barter.CmdStatus(), "", "Offered by Char:", caller=self.char2) + self.tradeitem1.db.desc = "A great offer." + self.call(barter.CmdEvaluate(), "TradeItem1", "A great offer.") + self.call( + barter.CmdAccept(), + ":Ok then.", + 'You say, "Ok then."\n [You accept the deal.', + caller=self.char2, + ) + self.assertEqual(self.tradeitem1.location, self.char2) + self.assertEqual(self.tradeitem2.location, self.char2) + self.assertEqual(self.tradeitem3.location, self.char1) + + def test_cmdtradehelp(self): + self.call( + barter.CmdTrade(), + "Char2 : Hey wanna trade?", + 'You say, "Hey wanna trade?"', + caller=self.char1, + ) + self.call(barter.CmdTradeHelp(), "", "Trading commands\n", caller=self.char1) + self.call( + barter.CmdFinish(), + ": Ending.", + 'You say, "Ending."\n [You aborted trade. No deal was made.]', + ) diff --git a/evennia/contrib/game_systems/clothing/tests.py b/evennia/contrib/game_systems/clothing/tests.py new file mode 100644 index 0000000000..04a4c12a35 --- /dev/null +++ b/evennia/contrib/game_systems/clothing/tests.py @@ -0,0 +1,133 @@ +""" +Testing clothing contrib + +""" + +from evennia.commands.default.tests import CommandTest +from evennia.utils.create import create_object +from evennia.objects.objects import DefaultRoom +from evennia.utils.test_resources import EvenniaTest +from . import clothing + + +class TestClothingCmd(CommandTest): + def test_clothingcommands(self): + wearer = create_object(clothing.ClothedCharacter, key="Wearer") + friend = create_object(clothing.ClothedCharacter, key="Friend") + room = create_object(DefaultRoom, key="room") + wearer.location = room + friend.location = room + # Make a test hat + test_hat = create_object(clothing.Clothing, key="test hat") + test_hat.db.clothing_type = "hat" + test_hat.location = wearer + # Make a test scarf + test_scarf = create_object(clothing.Clothing, key="test scarf") + test_scarf.db.clothing_type = "accessory" + test_scarf.location = wearer + # Test wear command + self.call(clothing.CmdWear(), "", "Usage: wear [wear style]", caller=wearer) + self.call(clothing.CmdWear(), "hat", "Wearer puts on test hat.", caller=wearer) + self.call( + clothing.CmdWear(), + "scarf stylishly", + "Wearer wears test scarf stylishly.", + caller=wearer, + ) + # Test cover command. + self.call( + clothing.CmdCover(), + "", + "Usage: cover [with] ", + caller=wearer, + ) + self.call( + clothing.CmdCover(), + "hat with scarf", + "Wearer covers test hat with test scarf.", + caller=wearer, + ) + # Test remove command. + self.call(clothing.CmdRemove(), "", "Could not find ''.", caller=wearer) + self.call( + clothing.CmdRemove(), "hat", "You have to take off test scarf first.", caller=wearer + ) + self.call( + clothing.CmdRemove(), + "scarf", + "Wearer removes test scarf, revealing test hat.", + caller=wearer, + ) + # Test uncover command. + test_scarf.wear(wearer, True) + test_hat.db.covered_by = test_scarf + self.call(clothing.CmdUncover(), "", "Usage: uncover ", caller=wearer) + self.call(clothing.CmdUncover(), "hat", "Wearer uncovers test hat.", caller=wearer) + # Test drop command. + test_hat.db.covered_by = test_scarf + self.call(clothing.CmdDrop(), "", "Drop what?", caller=wearer) + self.call( + clothing.CmdDrop(), + "hat", + "You can't drop that because it's covered by test scarf.", + caller=wearer, + ) + self.call(clothing.CmdDrop(), "scarf", "You drop test scarf.", caller=wearer) + # Test give command. + self.call( + clothing.CmdGive(), "", "Usage: give = ", caller=wearer + ) + self.call( + clothing.CmdGive(), + "hat = Friend", + "Wearer removes test hat.|You give test hat to Friend.", + caller=wearer, + ) + # Test inventory command. + self.call( + clothing.CmdInventory(), "", "You are not carrying or wearing anything.", caller=wearer + ) + + +class TestClothingFunc(EvenniaTest): + def test_clothingfunctions(self): + wearer = create_object(clothing.ClothedCharacter, key="Wearer") + room = create_object(DefaultRoom, key="room") + wearer.location = room + # Make a test hat + test_hat = create_object(clothing.Clothing, key="test hat") + test_hat.db.clothing_type = "hat" + test_hat.location = wearer + # Make a test shirt + test_shirt = create_object(clothing.Clothing, key="test shirt") + test_shirt.db.clothing_type = "top" + test_shirt.location = wearer + # Make a test pants + test_pants = create_object(clothing.Clothing, key="test pants") + test_pants.db.clothing_type = "bottom" + test_pants.location = wearer + + test_hat.wear(wearer, "on the head") + self.assertEqual(test_hat.db.worn, "on the head") + + test_hat.remove(wearer) + self.assertEqual(test_hat.db.worn, False) + + test_hat.worn = True + test_hat.at_get(wearer) + self.assertEqual(test_hat.db.worn, False) + + clothes_list = [test_shirt, test_hat, test_pants] + self.assertEqual( + clothing.order_clothes_list(clothes_list), [test_hat, test_shirt, test_pants] + ) + + test_hat.wear(wearer, True) + test_pants.wear(wearer, True) + self.assertEqual(clothing.get_worn_clothes(wearer), [test_hat, test_pants]) + + self.assertEqual( + clothing.clothing_type_count(clothes_list), {"hat": 1, "top": 1, "bottom": 1} + ) + + self.assertEqual(clothing.single_type_count(clothes_list, "hat"), 1) diff --git a/evennia/contrib/game_systems/cooldowns/tests.py b/evennia/contrib/game_systems/cooldowns/tests.py new file mode 100644 index 0000000000..ab701959a9 --- /dev/null +++ b/evennia/contrib/game_systems/cooldowns/tests.py @@ -0,0 +1,147 @@ +""" +Cooldowns tests. + +""" + +from mock import patch +from evennia.utils.test_resources import EvenniaTest +from . import cooldowns + + +@patch("evennia.contrib.cooldowns.time.time", return_value=0.0) +class TestCooldowns(EvenniaTest): + def setUp(self): + super().setUp() + self.handler = cooldowns.CooldownHandler(self.char1) + + def test_empty(self, mock_time): + self.assertEqual(self.handler.all, []) + self.assertTrue(self.handler.ready("a", "b", "c")) + self.assertEqual(self.handler.time_left("a", "b", "c"), 0) + + def test_add(self, mock_time): + self.assertEqual(self.handler.add, self.handler.set) + self.handler.add("a", 10) + self.assertFalse(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 10) + mock_time.return_value = 9.0 + self.assertFalse(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 1) + mock_time.return_value = 10.0 + self.assertTrue(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 0) + + def test_add_float(self, mock_time): + self.assertEqual(self.handler.time_left("a"), 0) + self.assertEqual(self.handler.time_left("a", use_int=False), 0) + self.assertEqual(self.handler.time_left("a", use_int=True), 0) + self.handler.add("a", 5.5) + self.assertEqual(self.handler.time_left("a"), 5.5) + self.assertEqual(self.handler.time_left("a", use_int=False), 5.5) + self.assertEqual(self.handler.time_left("a", use_int=True), 6) + + def test_add_multi(self, mock_time): + self.handler.add("a", 10) + self.handler.add("b", 5) + self.handler.add("c", 3) + self.assertFalse(self.handler.ready("a", "b", "c")) + self.assertEqual(self.handler.time_left("a", "b", "c"), 10) + self.assertEqual(self.handler.time_left("a", "b"), 10) + self.assertEqual(self.handler.time_left("a", "c"), 10) + self.assertEqual(self.handler.time_left("b", "c"), 5) + self.assertEqual(self.handler.time_left("c", "c"), 3) + + def test_add_none(self, mock_time): + self.handler.add("a", None) + self.assertTrue(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 0) + + def test_add_negative(self, mock_time): + self.handler.add("a", -5) + self.assertTrue(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 0) + + def test_add_overwrite(self, mock_time): + self.handler.add("a", 5) + self.handler.add("a", 10) + self.handler.add("a", 3) + self.assertFalse(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 3) + + def test_extend(self, mock_time): + self.assertEqual(self.handler.extend("a", 10), 10) + self.assertFalse(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 10) + self.assertEqual(self.handler.extend("a", 10), 20) + self.assertFalse(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 20) + + def test_extend_none(self, mock_time): + self.assertEqual(self.handler.extend("a", None), 0) + self.assertTrue(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 0) + self.handler.add("a", 10) + self.assertEqual(self.handler.extend("a", None), 10) + self.assertEqual(self.handler.time_left("a"), 10) + + def test_extend_negative(self, mock_time): + self.assertEqual(self.handler.extend("a", -5), 0) + self.assertTrue(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 0) + self.handler.add("a", 10) + self.assertEqual(self.handler.extend("a", -5), 5) + self.assertEqual(self.handler.time_left("a"), 5) + + def test_extend_float(self, mock_time): + self.assertEqual(self.handler.extend("a", -5.5), 0) + self.assertTrue(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 0.0) + self.assertEqual(self.handler.time_left("a", use_int=False), 0.0) + self.assertEqual(self.handler.time_left("a", use_int=True), 0) + self.handler.add("a", 10.5) + self.assertEqual(self.handler.extend("a", -5.25), 5.25) + self.assertEqual(self.handler.time_left("a"), 5.25) + self.assertEqual(self.handler.time_left("a", use_int=False), 5.25) + self.assertEqual(self.handler.time_left("a", use_int=True), 6) + + def test_reset_non_existent(self, mock_time): + self.handler.reset("a") + self.assertTrue(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 0) + + def test_reset(self, mock_time): + self.handler.set("a", 10) + self.handler.reset("a") + self.assertTrue(self.handler.ready("a")) + self.assertEqual(self.handler.time_left("a"), 0) + + def test_clear(self, mock_time): + self.handler.add("a", 10) + self.handler.add("b", 10) + self.handler.add("c", 10) + self.handler.clear() + self.assertTrue(self.handler.ready("a", "b", "c")) + self.assertEqual(self.handler.time_left("a", "b", "c"), 0) + + def test_cleanup(self, mock_time): + self.handler.add("a", 10) + self.handler.add("b", 5) + self.handler.add("c", 5) + self.handler.add("d", 3.5) + mock_time.return_value = 6.0 + self.handler.cleanup() + self.assertEqual(self.handler.time_left("b", "c", "d"), 0) + self.assertEqual(self.handler.time_left("a"), 4) + self.assertEqual(list(self.handler.data.keys()), ["a"]) + + def test_cleanup_doesnt_delete_anything(self, mock_time): + self.handler.add("a", 10) + self.handler.add("b", 5) + self.handler.add("c", 5) + self.handler.add("d", 3.5) + mock_time.return_value = 1.0 + self.handler.cleanup() + self.assertEqual(self.handler.time_left("d"), 2.5) + self.assertEqual(self.handler.time_left("b", "c"), 4) + self.assertEqual(self.handler.time_left("a"), 9) + self.assertEqual(list(self.handler.data.keys()), ["a", "b", "c", "d"]) diff --git a/evennia/contrib/game_systems/gendersub/tests.py b/evennia/contrib/game_systems/gendersub/tests.py new file mode 100644 index 0000000000..f25b54abf7 --- /dev/null +++ b/evennia/contrib/game_systems/gendersub/tests.py @@ -0,0 +1,28 @@ +""" +Test gendersub contrib. + +""" + + +from evennia.commands.default.tests import CommandTest +from evennia.utils.create import create_object +from mock import patch +from . import gendersub + + +class TestGenderSub(CommandTest): + def test_setgender(self): + self.call(gendersub.SetGender(), "male", "Your gender was set to male.") + self.call(gendersub.SetGender(), "ambiguous", "Your gender was set to ambiguous.") + self.call(gendersub.SetGender(), "Foo", "Usage: @gender") + + def test_gendercharacter(self): + char = create_object(gendersub.GenderCharacter, key="Gendered", location=self.room1) + txt = "Test |p gender" + self.assertEqual( + gendersub._RE_GENDER_PRONOUN.sub(char._get_pronoun, txt), "Test their gender" + ) + with patch("evennia.contrib.gendersub.DefaultCharacter.msg") as mock_msg: + char.db.gender = "female" + char.msg("Test |p gender") + mock_msg.assert_called_with("Test her gender", from_obj=None, session=None) diff --git a/evennia/contrib/game_systems/mail/tests.py b/evennia/contrib/game_systems/mail/tests.py new file mode 100644 index 0000000000..56dc4c7f2f --- /dev/null +++ b/evennia/contrib/game_systems/mail/tests.py @@ -0,0 +1,47 @@ +""" +Test mail contrib + +""" + +from evennia.commands.default.tests import CommandTest +from . import mail + + +class TestMail(CommandTest): + def test_mail(self): + self.call(mail.CmdMail(), "2", "'2' is not a valid mail id.", caller=self.account) + self.call(mail.CmdMail(), "test", "'test' is not a valid mail id.", caller=self.account) + self.call(mail.CmdMail(), "", "There are no messages in your inbox.", caller=self.account) + self.call( + mail.CmdMailCharacter(), + "Char=Message 1", + "You have received a new @mail from Char|You sent your message.", + caller=self.char1, + ) + self.call( + mail.CmdMailCharacter(), "Char=Message 2", "You sent your message.", caller=self.char2 + ) + self.call( + mail.CmdMail(), + "TestAccount2=Message 2", + "You have received a new @mail from TestAccount2", + caller=self.account2, + ) + self.call( + mail.CmdMail(), "TestAccount=Message 1", "You sent your message.", caller=self.account2 + ) + self.call( + mail.CmdMail(), "TestAccount=Message 2", "You sent your message.", caller=self.account2 + ) + self.call(mail.CmdMail(), "", "| ID From Subject", caller=self.account) + self.call(mail.CmdMail(), "2", "From: TestAccount2", caller=self.account) + self.call( + mail.CmdMail(), + "/forward TestAccount2 = 1/Forward message", + "You sent your message.|Message forwarded.", + caller=self.account, + ) + self.call( + mail.CmdMail(), "/reply 2=Reply Message2", "You sent your message.", caller=self.account + ) + self.call(mail.CmdMail(), "/delete 2", "Message 2 deleted", caller=self.account) diff --git a/evennia/contrib/game_systems/multidescer/tests.py b/evennia/contrib/game_systems/multidescer/tests.py new file mode 100644 index 0000000000..574375430e --- /dev/null +++ b/evennia/contrib/game_systems/multidescer/tests.py @@ -0,0 +1,40 @@ +""" +Test multidescer contrib. + +""" + +from evennia.commands.default.tests import CommandTest +from . import multidescer + + +class TestMultidescer(CommandTest): + def test_cmdmultidesc(self): + self.call(multidescer.CmdMultiDesc(), "/list", "Stored descs:\ncaller:") + self.call( + multidescer.CmdMultiDesc(), "test = Desc 1", "Stored description 'test': \"Desc 1\"" + ) + self.call( + multidescer.CmdMultiDesc(), "test2 = Desc 2", "Stored description 'test2': \"Desc 2\"" + ) + self.call( + multidescer.CmdMultiDesc(), "/swap test-test2", "Swapped descs 'test' and 'test2'." + ) + self.call( + multidescer.CmdMultiDesc(), + "test3 = Desc 3init", + "Stored description 'test3': \"Desc 3init\"", + ) + self.call( + multidescer.CmdMultiDesc(), + "/list", + "Stored descs:\ntest3: Desc 3init\ntest: Desc 1\ntest2: Desc 2\ncaller:", + ) + self.call( + multidescer.CmdMultiDesc(), "test3 = Desc 3", "Stored description 'test3': \"Desc 3\"" + ) + self.call( + multidescer.CmdMultiDesc(), + "/set test1 + test2 + + test3", + "test1 Desc 2 Desc 3\n\n" "The above was set as the current description.", + ) + self.assertEqual(self.char1.db.desc, "test1 Desc 2 Desc 3") diff --git a/evennia/contrib/game_systems/puzzles/tests.py b/evennia/contrib/game_systems/puzzles/tests.py new file mode 100644 index 0000000000..94f91baf3d --- /dev/null +++ b/evennia/contrib/game_systems/puzzles/tests.py @@ -0,0 +1,975 @@ +""" +Testing puzzles. + +""" + +# Test of the Puzzles module + +import re +import itertools +from mock import Mock +from evennia.utils import search +from evennia.commands.default.tests import CommandTest +from evennia.utils.create import create_object +from . import puzzles + + +class TestPuzzles(CommandTest): + def setUp(self): + super(TestPuzzles, self).setUp() + self.steel = create_object(self.object_typeclass, key="steel", location=self.char1.location) + self.flint = create_object(self.object_typeclass, key="flint", location=self.char1.location) + self.fire = create_object(self.object_typeclass, key="fire", location=self.char1.location) + self.steel.tags.add("tag-steel") + self.steel.tags.add("tag-steel", category="tagcat") + self.flint.tags.add("tag-flint") + self.flint.tags.add("tag-flint", category="tagcat") + self.fire.tags.add("tag-fire") + self.fire.tags.add("tag-fire", category="tagcat") + + def _assert_msg_matched(self, msg, regexs, re_flags=0): + matches = [] + for regex in regexs: + m = re.search(regex, msg, re_flags) + self.assertIsNotNone(m, "%r didn't match %r" % (regex, msg)) + matches.append(m) + return matches + + def _assert_recipe(self, name, parts, results, and_destroy_it=True, expected_count=1): + def _keys(items): + return [item["key"] for item in items] + + recipes = search.search_script_tag("", category=puzzles._PUZZLES_TAG_CATEGORY) + self.assertEqual(expected_count, len(recipes)) + self.assertEqual(name, recipes[expected_count - 1].db.puzzle_name) + self.assertEqual(parts, _keys(recipes[expected_count - 1].db.parts)) + self.assertEqual(results, _keys(recipes[expected_count - 1].db.results)) + self.assertEqual( + puzzles._PUZZLES_TAG_RECIPE, + recipes[expected_count - 1].tags.get(category=puzzles._PUZZLES_TAG_CATEGORY), + ) + recipe_dbref = recipes[expected_count - 1].dbref + if and_destroy_it: + recipes[expected_count - 1].delete() + return recipe_dbref if not and_destroy_it else None + + def _assert_no_recipes(self): + self.assertEqual( + 0, len(search.search_script_tag("", category=puzzles._PUZZLES_TAG_CATEGORY)) + ) + + # good recipes + def _good_recipe(self, name, parts, results, and_destroy_it=True, expected_count=1): + regexs = [] + for p in parts: + regexs.append(r"^Part %s\(#\d+\)$" % (p)) + for r in results: + regexs.append(r"^Result %s\(#\d+\)$" % (r)) + regexs.append(r"^Puzzle '%s' %s\(#\d+\) has been created successfully.$" % (name, name)) + lhs = [name] + parts + cmdstr = ",".join(lhs) + "=" + ",".join(results) + msg = self.call(puzzles.CmdCreatePuzzleRecipe(), cmdstr, caller=self.char1) + recipe_dbref = self._assert_recipe(name, parts, results, and_destroy_it, expected_count) + self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) + return recipe_dbref + + def _check_room_contents(self, expected, check_test_tags=False): + by_obj_key = lambda o: o.key + room1_contents = sorted(self.room1.contents, key=by_obj_key) + for key, grp in itertools.groupby(room1_contents, by_obj_key): + if key in expected: + grp = list(grp) + self.assertEqual( + expected[key], + len(grp), + "Expected %d but got %d for %s" % (expected[key], len(grp), key), + ) + if check_test_tags: + for gi in grp: + tags = gi.tags.all(return_key_and_category=True) + self.assertIn(("tag-" + gi.key, "tagcat"), tags) + + def _arm(self, recipe_dbref, name, parts): + regexs = [ + r"^Puzzle Recipe %s\(#\d+\) '%s' found.$" % (name, name), + r"^Spawning %d parts ...$" % (len(parts)), + ] + for p in parts: + regexs.append(r"^Part %s\(#\d+\) spawned .*$" % (p)) + regexs.append(r"^Puzzle armed successfully.$") + msg = self.call(puzzles.CmdArmPuzzle(), recipe_dbref, caller=self.char1) + self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) + + def test_cmdset_puzzle(self): + self.char1.cmdset.add("evennia.contrib.puzzles.PuzzleSystemCmdSet") + # FIXME: testing nothing, this is just to bump up coverage + + def test_cmd_puzzle(self): + self._assert_no_recipes() + + # bad syntax + def _bad_syntax(cmdstr): + self.call( + puzzles.CmdCreatePuzzleRecipe(), + cmdstr, + "Usage: @puzzle name, = ", + caller=self.char1, + ) + + _bad_syntax("") + _bad_syntax("=") + _bad_syntax("nothing =") + _bad_syntax("= nothing") + _bad_syntax("nothing") + _bad_syntax(",nothing") + _bad_syntax("name, nothing") + _bad_syntax("name, nothing =") + + self._assert_no_recipes() + + self._good_recipe("makefire", ["steel", "flint"], ["fire", "steel", "flint"]) + self._good_recipe("hot steels", ["steel", "fire"], ["steel", "fire"]) + self._good_recipe( + "furnace", + ["steel", "steel", "fire"], + ["steel", "steel", "fire", "fire", "fire", "fire"], + ) + + # bad recipes + def _bad_recipe(name, parts, results, fail_regex): + cmdstr = ",".join([name] + parts) + "=" + ",".join(results) + msg = self.call(puzzles.CmdCreatePuzzleRecipe(), cmdstr, caller=self.char1) + self._assert_no_recipes() + self.assertIsNotNone(re.match(fail_regex, msg), msg) + + _bad_recipe("name", ["nothing"], ["neither"], r"Could not find 'nothing'.") + _bad_recipe("name", ["steel"], ["nothing"], r"Could not find 'nothing'.") + _bad_recipe("", ["steel", "fire"], ["steel", "fire"], r"^Invalid puzzle name ''.") + self.steel.location = self.char1 + _bad_recipe("name", ["steel"], ["fire"], r"^Invalid location for steel$") + _bad_recipe("name", ["flint"], ["steel"], r"^Invalid location for steel$") + _bad_recipe("name", ["self"], ["fire"], r"^Invalid typeclass for Char$") + _bad_recipe("name", ["here"], ["fire"], r"^Invalid typeclass for Room$") + + self._assert_no_recipes() + + def test_cmd_armpuzzle(self): + # bad arms + self.call( + puzzles.CmdArmPuzzle(), + "1", + "A puzzle recipe's #dbref must be specified", + caller=self.char1, + ) + self.call(puzzles.CmdArmPuzzle(), "#1", "Invalid puzzle '#1'", caller=self.char1) + + recipe_dbref = self._good_recipe( + "makefire", ["steel", "flint"], ["fire", "steel", "flint"], and_destroy_it=False + ) + + # delete proto parts and proto result + self.steel.delete() + self.flint.delete() + self.fire.delete() + + # good arm + self._arm(recipe_dbref, "makefire", ["steel", "flint"]) + self._check_room_contents({"steel": 1, "flint": 1}, check_test_tags=True) + + def _use(self, cmdstr, expmsg): + msg = self.call(puzzles.CmdUsePuzzleParts(), cmdstr, expmsg, caller=self.char1) + return msg + + def test_cmd_use(self): + + self._use("", "Use what?") + self._use("something", "There is no something around.") + self._use("steel", "You have no idea how this can be used") + self._use("steel flint", "There is no steel flint around.") + self._use("steel, flint", "You have no idea how these can be used") + + recipe_dbref = self._good_recipe( + "makefire", ["steel", "flint"], ["fire"], and_destroy_it=False + ) + recipe2_dbref = self._good_recipe( + "makefire2", ["steel", "flint"], ["fire"], and_destroy_it=False, expected_count=2 + ) + + # although there is steel and flint + # those aren't valid puzzle parts because + # the puzzle hasn't been armed + self._use("steel", "You have no idea how this can be used") + self._use("steel, flint", "You have no idea how these can be used") + self._arm(recipe_dbref, "makefire", ["steel", "flint"]) + self._check_room_contents({"steel": 2, "flint": 2}, check_test_tags=True) + + # there are duplicated objects now + self._use("steel", "Which steel. There are many") + self._use("flint", "Which flint. There are many") + + # delete proto parts and proto results + self.steel.delete() + self.flint.delete() + self.fire.delete() + + # solve puzzle + self._use("steel, flint", "You are a Genius") + self.assertEqual( + 1, + len( + list( + filter( + lambda o: o.key == "fire" + and ("makefire", puzzles._PUZZLES_TAG_CATEGORY) + in o.tags.all(return_key_and_category=True) + and (puzzles._PUZZLES_TAG_MEMBER, puzzles._PUZZLES_TAG_CATEGORY) + in o.tags.all(return_key_and_category=True), + self.room1.contents, + ) + ) + ), + ) + self._check_room_contents({"steel": 0, "flint": 0, "fire": 1}, check_test_tags=True) + + # trying again will fail as it was resolved already + # and the parts were destroyed + self._use("steel, flint", "There is no steel around") + self._use("flint, steel", "There is no flint around") + + # arm same puzzle twice so there are duplicated parts + self._arm(recipe_dbref, "makefire", ["steel", "flint"]) + self._arm(recipe_dbref, "makefire", ["steel", "flint"]) + self._check_room_contents({"steel": 2, "flint": 2, "fire": 1}, check_test_tags=True) + + # try solving with multiple parts but incomplete set + self._use( + "steel-1, steel-2", "You try to utilize these but nothing happens ... something amiss?" + ) + + # arm the other puzzle. Their parts are identical + self._arm(recipe2_dbref, "makefire2", ["steel", "flint"]) + self._check_room_contents({"steel": 3, "flint": 3, "fire": 1}, check_test_tags=True) + + # solve with multiple parts for + # multiple puzzles. Both can be solved but + # only one is. + self._use( + "steel-1, flint-2, steel-3, flint-3", + "Your gears start turning and 2 different ideas come to your mind ... ", + ) + self._check_room_contents({"steel": 2, "flint": 2, "fire": 2}, check_test_tags=True) + + self.room1.msg_contents = Mock() + + # solve all + self._use("steel-1, flint-1", "You are a Genius") + self.room1.msg_contents.assert_called_once_with( + "|cChar|n performs some kind of tribal dance and |yfire|n seems to appear from thin air", + exclude=(self.char1,), + ) + self._use("steel, flint", "You are a Genius") + self._check_room_contents({"steel": 0, "flint": 0, "fire": 4}, check_test_tags=True) + + def test_puzzleedit(self): + recipe_dbref = self._good_recipe( + "makefire", ["steel", "flint"], ["fire"], and_destroy_it=False + ) + + def _puzzleedit(swt, dbref, args, expmsg): + if (swt is None) and (dbref is None) and (args is None): + cmdstr = "" + else: + cmdstr = "%s %s%s" % (swt, dbref, args) + self.call(puzzles.CmdEditPuzzle(), cmdstr, expmsg, caller=self.char1) + + # delete proto parts and proto results + self.steel.delete() + self.flint.delete() + self.fire.delete() + + sid = self.script.id + # bad syntax + _puzzleedit( + None, None, None, "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit" + ) + _puzzleedit("", "1", "", "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") + _puzzleedit("", "", "", "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") + _puzzleedit( + "", + recipe_dbref, + "dummy", + "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit", + ) + _puzzleedit("", self.script.dbref, "", "Script(#{}) is not a puzzle".format(sid)) + + # edit use_success_message and use_success_location_message + _puzzleedit( + "", + recipe_dbref, + "/use_success_message = Yes!", + "makefire(%s) use_success_message = Yes!" % recipe_dbref, + ) + _puzzleedit( + "", + recipe_dbref, + "/use_success_location_message = {result_names} Yeah baby! {caller}", + "makefire(%s) use_success_location_message = {result_names} Yeah baby! {caller}" + % recipe_dbref, + ) + + self._arm(recipe_dbref, "makefire", ["steel", "flint"]) + self.room1.msg_contents = Mock() + self._use("steel, flint", "Yes!") + self.room1.msg_contents.assert_called_once_with( + "fire Yeah baby! Char", exclude=(self.char1,) + ) + self.room1.msg_contents.reset_mock() + + # edit mask: exclude location and desc during matching + _puzzleedit( + "", + recipe_dbref, + "/mask = location,desc", + "makefire(%s) mask = ('location', 'desc')" % recipe_dbref, + ) + + self._arm(recipe_dbref, "makefire", ["steel", "flint"]) + # change location and desc + self.char1.search("steel").db.desc = "A solid bar of steel" + self.char1.search("steel").location = self.char1 + self.char1.search("flint").db.desc = "A flint steel" + self.char1.search("flint").location = self.char1 + self._use("steel, flint", "Yes!") + self.room1.msg_contents.assert_called_once_with( + "fire Yeah baby! Char", exclude=(self.char1,) + ) + + # delete + _puzzleedit("/delete", recipe_dbref, "", "makefire(%s) was deleted" % recipe_dbref) + self._assert_no_recipes() + + def test_puzzleedit_add_remove_parts_results(self): + recipe_dbref = self._good_recipe( + "makefire", ["steel", "flint"], ["fire"], and_destroy_it=False + ) + + def _puzzleedit(swt, dbref, rhslist, expmsg): + cmdstr = "%s %s = %s" % (swt, dbref, ", ".join(rhslist)) + self.call(puzzles.CmdEditPuzzle(), cmdstr, expmsg, caller=self.char1) + + red_steel = create_object( + self.object_typeclass, key="red steel", location=self.char1.location + ) + smoke = create_object(self.object_typeclass, key="smoke", location=self.char1.location) + + _puzzleedit("/addresult", recipe_dbref, ["smoke"], "smoke were added to results") + _puzzleedit( + "/addpart", recipe_dbref, ["red steel", "steel"], "red steel, steel were added to parts" + ) + + # create a box so we can put all objects in + # so that they can't be found during puzzle resolution + self.box = create_object(self.object_typeclass, key="box", location=self.char1.location) + + def _box_all(): + for o in self.room1.contents: + if o not in [self.char1, self.char2, self.exit, self.obj1, self.obj2, self.box]: + o.location = self.box + + _box_all() + + self._arm(recipe_dbref, "makefire", ["steel", "flint", "red steel", "steel"]) + self._check_room_contents({"steel": 2, "red steel": 1, "flint": 1}) + self._use( + "steel-1, flint", "You try to utilize these but nothing happens ... something amiss?" + ) + self._use("steel-1, flint, red steel, steel-3", "You are a Genius") + self._check_room_contents({"smoke": 1, "fire": 1}) + _box_all() + + self.fire.location = self.room1 + self.steel.location = self.room1 + + _puzzleedit("/delresult", recipe_dbref, ["fire"], "fire were removed from results") + _puzzleedit( + "/delpart", recipe_dbref, ["steel", "steel"], "steel, steel were removed from parts" + ) + + _box_all() + + self._arm(recipe_dbref, "makefire", ["flint", "red steel"]) + self._check_room_contents({"red steel": 1, "flint": 1}) + self._use("red steel, flint", "You are a Genius") + self._check_room_contents({"smoke": 1, "fire": 0}) + + def test_lspuzzlerecipes_lsarmedpuzzles(self): + msg = self.call(puzzles.CmdListPuzzleRecipes(), "", caller=self.char1) + self._assert_msg_matched( + msg, [r"^-+$", r"^Found 0 puzzle\(s\)\.$", r"-+$"], re.MULTILINE | re.DOTALL + ) + + recipe_dbref = self._good_recipe( + "makefire", ["steel", "flint"], ["fire"], and_destroy_it=False + ) + + msg = self.call(puzzles.CmdListPuzzleRecipes(), "", caller=self.char1) + self._assert_msg_matched( + msg, + [ + r"^-+$", + r"^Puzzle 'makefire'.*$", + r"^Success Caller message:$", + r"^Success Location message:$", + r"^Mask:$", + r"^Parts$", + r"^.*key: steel$", + r"^.*key: flint$", + r"^Results$", + r"^.*key: fire$", + r"^.*key: steel$", + r"^.*key: flint$", + r"^-+$", + r"^Found 1 puzzle\(s\)\.$", + r"^-+$", + ], + re.MULTILINE | re.DOTALL, + ) + + msg = self.call(puzzles.CmdListArmedPuzzles(), "", caller=self.char1) + self._assert_msg_matched( + msg, + [r"^-+$", r"^-+$", r"^Found 0 armed puzzle\(s\)\.$", r"^-+$"], + re.MULTILINE | re.DOTALL, + ) + + self._arm(recipe_dbref, "makefire", ["steel", "flint"]) + + msg = self.call(puzzles.CmdListArmedPuzzles(), "", caller=self.char1) + self._assert_msg_matched( + msg, + [ + r"^-+$", + r"^Puzzle name: makefire$", + r"^.*steel.* at \s+ Room.*$", + r"^.*flint.* at \s+ Room.*$", + r"^Found 1 armed puzzle\(s\)\.$", + r"^-+$", + ], + re.MULTILINE | re.DOTALL, + ) + + def test_e2e(self): + def _destroy_objs_in_room(keys): + for obj in self.room1.contents: + if obj.key in keys: + obj.delete() + + # parts don't survive resolution + # but produce a large result set + tree = create_object(self.object_typeclass, key="tree", location=self.char1.location) + axe = create_object(self.object_typeclass, key="axe", location=self.char1.location) + sweat = create_object(self.object_typeclass, key="sweat", location=self.char1.location) + dull_axe = create_object( + self.object_typeclass, key="dull axe", location=self.char1.location + ) + timber = create_object(self.object_typeclass, key="timber", location=self.char1.location) + log = create_object(self.object_typeclass, key="log", location=self.char1.location) + parts = ["tree", "axe"] + results = (["sweat"] * 10) + ["dull axe"] + (["timber"] * 20) + (["log"] * 50) + recipe_dbref = self._good_recipe("lumberjack", parts, results, and_destroy_it=False) + + _destroy_objs_in_room(set(parts + results)) + + sps = sorted(parts) + expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} + expected.update({r: 0 for r in set(results)}) + + self._arm(recipe_dbref, "lumberjack", parts) + self._check_room_contents(expected) + + self._use(",".join(parts), "You are a Genius") + srs = sorted(set(results)) + expected = {(key, len(list(grp))) for key, grp in itertools.groupby(srs)} + expected.update({p: 0 for p in set(parts)}) + self._check_room_contents(expected) + + # parts also appear in results + # causing a new puzzle to be armed 'automatically' + # i.e. the puzzle is self-sustaining + hole = create_object(self.object_typeclass, key="hole", location=self.char1.location) + shovel = create_object(self.object_typeclass, key="shovel", location=self.char1.location) + dirt = create_object(self.object_typeclass, key="dirt", location=self.char1.location) + + parts = ["shovel", "hole"] + results = ["dirt", "hole", "shovel"] + recipe_dbref = self._good_recipe( + "digger", parts, results, and_destroy_it=False, expected_count=2 + ) + + _destroy_objs_in_room(set(parts + results)) + + nresolutions = 0 + + sps = sorted(set(parts)) + expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} + expected.update({"dirt": nresolutions}) + + self._arm(recipe_dbref, "digger", parts) + self._check_room_contents(expected) + + for i in range(10): + self._use(",".join(parts), "You are a Genius") + nresolutions += 1 + expected.update({"dirt": nresolutions}) + self._check_room_contents(expected) + + # Uppercase puzzle name + balloon = create_object(self.object_typeclass, key="Balloon", location=self.char1.location) + parts = ["Balloon"] + results = ["Balloon"] + recipe_dbref = self._good_recipe( + "boom!!!", parts, results, and_destroy_it=False, expected_count=3 + ) + + _destroy_objs_in_room(set(parts + results)) + + sps = sorted(parts) + expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} + + self._arm(recipe_dbref, "boom!!!", parts) + self._check_room_contents(expected) + + self._use(",".join(parts), "You are a Genius") + srs = sorted(set(results)) + expected = {(key, len(list(grp))) for key, grp in itertools.groupby(srs)} + self._check_room_contents(expected) + + def test_e2e_accumulative(self): + flashlight = create_object( + self.object_typeclass, key="flashlight", location=self.char1.location + ) + flashlight_w_1 = create_object( + self.object_typeclass, key="flashlight-w-1", location=self.char1.location + ) + flashlight_w_2 = create_object( + self.object_typeclass, key="flashlight-w-2", location=self.char1.location + ) + flashlight_w_3 = create_object( + self.object_typeclass, key="flashlight-w-3", location=self.char1.location + ) + battery = create_object(self.object_typeclass, key="battery", location=self.char1.location) + + battery.tags.add("flashlight-1", category=puzzles._PUZZLES_TAG_CATEGORY) + battery.tags.add("flashlight-2", category=puzzles._PUZZLES_TAG_CATEGORY) + battery.tags.add("flashlight-3", category=puzzles._PUZZLES_TAG_CATEGORY) + + # TODO: instead of tagging each flashlight, + # arm and resolve each puzzle in order so they all + # are tagged correctly + # it will be necessary to add/remove parts/results because + # each battery is supposed to be consumed during resolution + # as the new flashlight has one more battery than before + flashlight_w_1.tags.add("flashlight-2", category=puzzles._PUZZLES_TAG_CATEGORY) + flashlight_w_2.tags.add("flashlight-3", category=puzzles._PUZZLES_TAG_CATEGORY) + + recipe_fl1_dbref = self._good_recipe( + "flashlight-1", + ["flashlight", "battery"], + ["flashlight-w-1"], + and_destroy_it=False, + expected_count=1, + ) + recipe_fl2_dbref = self._good_recipe( + "flashlight-2", + ["flashlight-w-1", "battery"], + ["flashlight-w-2"], + and_destroy_it=False, + expected_count=2, + ) + recipe_fl3_dbref = self._good_recipe( + "flashlight-3", + ["flashlight-w-2", "battery"], + ["flashlight-w-3"], + and_destroy_it=False, + expected_count=3, + ) + + # delete protoparts + for obj in [battery, flashlight, flashlight_w_1, flashlight_w_2, flashlight_w_3]: + obj.delete() + + def _group_parts(parts, excluding=set()): + group = dict() + dbrefs = dict() + for o in self.room1.contents: + if o.key in parts and o.dbref not in excluding: + if o.key not in group: + group[o.key] = [] + group[o.key].append(o.dbref) + dbrefs[o.dbref] = o + return group, dbrefs + + # arm each puzzle and group its parts + self._arm(recipe_fl1_dbref, "flashlight-1", ["battery", "flashlight"]) + fl1_parts, fl1_dbrefs = _group_parts(["battery", "flashlight"]) + self._arm(recipe_fl2_dbref, "flashlight-2", ["battery", "flashlight-w-1"]) + fl2_parts, fl2_dbrefs = _group_parts( + ["battery", "flashlight-w-1"], excluding=list(fl1_dbrefs.keys()) + ) + self._arm(recipe_fl3_dbref, "flashlight-3", ["battery", "flashlight-w-2"]) + fl3_parts, fl3_dbrefs = _group_parts( + ["battery", "flashlight-w-2"], + excluding=set(list(fl1_dbrefs.keys()) + list(fl2_dbrefs.keys())), + ) + + self._check_room_contents( + { + "battery": 3, + "flashlight": 1, + "flashlight-w-1": 1, + "flashlight-w-2": 1, + "flashlight-w-3": 0, + } + ) + + # all batteries have identical protodefs + battery_1 = fl1_dbrefs[fl1_parts["battery"][0]] + battery_2 = fl2_dbrefs[fl2_parts["battery"][0]] + battery_3 = fl3_dbrefs[fl3_parts["battery"][0]] + protodef_battery_1 = puzzles.proto_def(battery_1, with_tags=False) + del protodef_battery_1["prototype_key"] + protodef_battery_2 = puzzles.proto_def(battery_2, with_tags=False) + del protodef_battery_2["prototype_key"] + protodef_battery_3 = puzzles.proto_def(battery_3, with_tags=False) + del protodef_battery_3["prototype_key"] + assert protodef_battery_1 == protodef_battery_2 == protodef_battery_3 + + # each battery can be used in every other puzzle + + b1_parts_dict, b1_puzzlenames, b1_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( + [battery_1] + ) + _puzzles = puzzles._puzzles_by_names(b1_puzzlenames.keys()) + assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( + [p.db.puzzle_name for p in _puzzles] + ) + matched_puzzles = puzzles._matching_puzzles(_puzzles, b1_puzzlenames, b1_protodefs) + assert 0 == len(matched_puzzles) + + b2_parts_dict, b2_puzzlenames, b2_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( + [battery_2] + ) + _puzzles = puzzles._puzzles_by_names(b2_puzzlenames.keys()) + assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( + [p.db.puzzle_name for p in _puzzles] + ) + matched_puzzles = puzzles._matching_puzzles(_puzzles, b2_puzzlenames, b2_protodefs) + assert 0 == len(matched_puzzles) + b3_parts_dict, b3_puzzlenames, b3_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( + [battery_3] + ) + _puzzles = puzzles._puzzles_by_names(b3_puzzlenames.keys()) + assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( + [p.db.puzzle_name for p in _puzzles] + ) + matched_puzzles = puzzles._matching_puzzles(_puzzles, b3_puzzlenames, b3_protodefs) + assert 0 == len(matched_puzzles) + + assert battery_1 == list(b1_parts_dict.values())[0] + assert battery_2 == list(b2_parts_dict.values())[0] + assert battery_3 == list(b3_parts_dict.values())[0] + assert b1_puzzlenames.keys() == b2_puzzlenames.keys() == b3_puzzlenames.keys() + for puzzle_name in ["flashlight-1", "flashlight-2", "flashlight-3"]: + assert puzzle_name in b1_puzzlenames + assert puzzle_name in b2_puzzlenames + assert puzzle_name in b3_puzzlenames + assert ( + list(b1_protodefs.values())[0] + == list(b2_protodefs.values())[0] + == list(b3_protodefs.values())[0] + == protodef_battery_1 + == protodef_battery_2 + == protodef_battery_3 + ) + + # all flashlights have similar protodefs except their key + flashlight_1 = fl1_dbrefs[fl1_parts["flashlight"][0]] + flashlight_2 = fl2_dbrefs[fl2_parts["flashlight-w-1"][0]] + flashlight_3 = fl3_dbrefs[fl3_parts["flashlight-w-2"][0]] + protodef_flashlight_1 = puzzles.proto_def(flashlight_1, with_tags=False) + del protodef_flashlight_1["prototype_key"] + assert protodef_flashlight_1["key"] == "flashlight" + del protodef_flashlight_1["key"] + protodef_flashlight_2 = puzzles.proto_def(flashlight_2, with_tags=False) + del protodef_flashlight_2["prototype_key"] + assert protodef_flashlight_2["key"] == "flashlight-w-1" + del protodef_flashlight_2["key"] + protodef_flashlight_3 = puzzles.proto_def(flashlight_3, with_tags=False) + del protodef_flashlight_3["prototype_key"] + assert protodef_flashlight_3["key"] == "flashlight-w-2" + del protodef_flashlight_3["key"] + assert protodef_flashlight_1 == protodef_flashlight_2 == protodef_flashlight_3 + + # each flashlight can only be used in its own puzzle + + f1_parts_dict, f1_puzzlenames, f1_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( + [flashlight_1] + ) + _puzzles = puzzles._puzzles_by_names(f1_puzzlenames.keys()) + assert set(["flashlight-1"]) == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, f1_puzzlenames, f1_protodefs) + assert 0 == len(matched_puzzles) + f2_parts_dict, f2_puzzlenames, f2_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( + [flashlight_2] + ) + _puzzles = puzzles._puzzles_by_names(f2_puzzlenames.keys()) + assert set(["flashlight-2"]) == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, f2_puzzlenames, f2_protodefs) + assert 0 == len(matched_puzzles) + f3_parts_dict, f3_puzzlenames, f3_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( + [flashlight_3] + ) + _puzzles = puzzles._puzzles_by_names(f3_puzzlenames.keys()) + assert set(["flashlight-3"]) == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, f3_puzzlenames, f3_protodefs) + assert 0 == len(matched_puzzles) + + assert flashlight_1 == list(f1_parts_dict.values())[0] + assert flashlight_2 == list(f2_parts_dict.values())[0] + assert flashlight_3 == list(f3_parts_dict.values())[0] + for puzzle_name in set( + list(f1_puzzlenames.keys()) + list(f2_puzzlenames.keys()) + list(f3_puzzlenames.keys()) + ): + assert puzzle_name in ["flashlight-1", "flashlight-2", "flashlight-3", "puzzle_member"] + protodef_flashlight_1["key"] = "flashlight" + assert list(f1_protodefs.values())[0] == protodef_flashlight_1 + protodef_flashlight_2["key"] = "flashlight-w-1" + assert list(f2_protodefs.values())[0] == protodef_flashlight_2 + protodef_flashlight_3["key"] = "flashlight-w-2" + assert list(f3_protodefs.values())[0] == protodef_flashlight_3 + + # each battery can be matched with every other flashlight + # to potentially resolve each puzzle + for batt in [battery_1, battery_2, battery_3]: + parts_dict, puzzlenames, protodefs = puzzles._lookups_parts_puzzlenames_protodefs( + [batt, flashlight_1] + ) + assert set([batt.dbref, flashlight_1.dbref]) == set(puzzlenames["flashlight-1"]) + assert set([batt.dbref]) == set(puzzlenames["flashlight-2"]) + assert set([batt.dbref]) == set(puzzlenames["flashlight-3"]) + _puzzles = puzzles._puzzles_by_names(puzzlenames.keys()) + assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( + [p.db.puzzle_name for p in _puzzles] + ) + matched_puzzles = puzzles._matching_puzzles(_puzzles, puzzlenames, protodefs) + assert 1 == len(matched_puzzles) + parts_dict, puzzlenames, protodefs = puzzles._lookups_parts_puzzlenames_protodefs( + [batt, flashlight_2] + ) + assert set([batt.dbref]) == set(puzzlenames["flashlight-1"]) + assert set([batt.dbref, flashlight_2.dbref]) == set(puzzlenames["flashlight-2"]) + assert set([batt.dbref]) == set(puzzlenames["flashlight-3"]) + _puzzles = puzzles._puzzles_by_names(puzzlenames.keys()) + assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( + [p.db.puzzle_name for p in _puzzles] + ) + matched_puzzles = puzzles._matching_puzzles(_puzzles, puzzlenames, protodefs) + assert 1 == len(matched_puzzles) + parts_dict, puzzlenames, protodefs = puzzles._lookups_parts_puzzlenames_protodefs( + [batt, flashlight_3] + ) + assert set([batt.dbref]) == set(puzzlenames["flashlight-1"]) + assert set([batt.dbref]) == set(puzzlenames["flashlight-2"]) + assert set([batt.dbref, flashlight_3.dbref]) == set(puzzlenames["flashlight-3"]) + _puzzles = puzzles._puzzles_by_names(puzzlenames.keys()) + assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( + [p.db.puzzle_name for p in _puzzles] + ) + matched_puzzles = puzzles._matching_puzzles(_puzzles, puzzlenames, protodefs) + assert 1 == len(matched_puzzles) + + # delete all parts + for part in ( + list(fl1_dbrefs.values()) + list(fl2_dbrefs.values()) + list(fl3_dbrefs.values()) + ): + part.delete() + + self._check_room_contents( + { + "battery": 0, + "flashlight": 0, + "flashlight-w-1": 0, + "flashlight-w-2": 0, + "flashlight-w-3": 0, + } + ) + + # arm first puzzle 3 times and group its parts so we can solve + # all puzzles with the parts from the 1st armed + for i in range(3): + self._arm(recipe_fl1_dbref, "flashlight-1", ["battery", "flashlight"]) + fl1_parts, fl1_dbrefs = _group_parts(["battery", "flashlight"]) + + # delete the 2 extra flashlights so we can start solving + for flashlight_dbref in fl1_parts["flashlight"][1:]: + fl1_dbrefs[flashlight_dbref].delete() + + self._check_room_contents( + { + "battery": 3, + "flashlight": 1, + "flashlight-w-1": 0, + "flashlight-w-2": 0, + "flashlight-w-3": 0, + } + ) + + self._use("battery-1, flashlight", "You are a Genius") + self._check_room_contents( + { + "battery": 2, + "flashlight": 0, + "flashlight-w-1": 1, + "flashlight-w-2": 0, + "flashlight-w-3": 0, + } + ) + + self._use("battery-1, flashlight-w-1", "You are a Genius") + self._check_room_contents( + { + "battery": 1, + "flashlight": 0, + "flashlight-w-1": 0, + "flashlight-w-2": 1, + "flashlight-w-3": 0, + } + ) + + self._use("battery, flashlight-w-2", "You are a Genius") + self._check_room_contents( + { + "battery": 0, + "flashlight": 0, + "flashlight-w-1": 0, + "flashlight-w-2": 0, + "flashlight-w-3": 1, + } + ) + + def test_e2e_interchangeable_parts_and_results(self): + # Parts and Results can be used in multiple puzzles + egg = create_object(self.object_typeclass, key="egg", location=self.char1.location) + flour = create_object(self.object_typeclass, key="flour", location=self.char1.location) + boiling_water = create_object( + self.object_typeclass, key="boiling water", location=self.char1.location + ) + boiled_egg = create_object( + self.object_typeclass, key="boiled egg", location=self.char1.location + ) + dough = create_object(self.object_typeclass, key="dough", location=self.char1.location) + pasta = create_object(self.object_typeclass, key="pasta", location=self.char1.location) + + # Three recipes: + # 1. breakfast: egg + boiling water = boiled egg & boiling water + # 2. dough: egg + flour = dough + # 3. entree: dough + boiling water = pasta & boiling water + # tag interchangeable parts according to their puzzles' name + egg.tags.add("breakfast", category=puzzles._PUZZLES_TAG_CATEGORY) + egg.tags.add("dough", category=puzzles._PUZZLES_TAG_CATEGORY) + dough.tags.add("entree", category=puzzles._PUZZLES_TAG_CATEGORY) + boiling_water.tags.add("breakfast", category=puzzles._PUZZLES_TAG_CATEGORY) + boiling_water.tags.add("entree", category=puzzles._PUZZLES_TAG_CATEGORY) + + # create recipes + recipe1_dbref = self._good_recipe( + "breakfast", + ["egg", "boiling water"], + ["boiled egg", "boiling water"], + and_destroy_it=False, + ) + recipe2_dbref = self._good_recipe( + "dough", ["egg", "flour"], ["dough"], and_destroy_it=False, expected_count=2 + ) + recipe3_dbref = self._good_recipe( + "entree", + ["dough", "boiling water"], + ["pasta", "boiling water"], + and_destroy_it=False, + expected_count=3, + ) + + # delete protoparts + for obj in [egg, flour, boiling_water, boiled_egg, dough, pasta]: + obj.delete() + + # arm each puzzle and group its parts + def _group_parts(parts, excluding=set()): + group = dict() + dbrefs = dict() + for o in self.room1.contents: + if o.key in parts and o.dbref not in excluding: + if o.key not in group: + group[o.key] = [] + group[o.key].append(o.dbref) + dbrefs[o.dbref] = o + return group, dbrefs + + self._arm(recipe1_dbref, "breakfast", ["egg", "boiling water"]) + breakfast_parts, breakfast_dbrefs = _group_parts(["egg", "boiling water"]) + self._arm(recipe2_dbref, "dough", ["egg", "flour"]) + dough_parts, dough_dbrefs = _group_parts( + ["egg", "flour"], excluding=list(breakfast_dbrefs.keys()) + ) + self._arm(recipe3_dbref, "entree", ["dough", "boiling water"]) + entree_parts, entree_dbrefs = _group_parts( + ["dough", "boiling water"], + excluding=set(list(breakfast_dbrefs.keys()) + list(dough_dbrefs.keys())), + ) + + # create a box so we can put all objects in + # so that they can't be found during puzzle resolution + self.box = create_object(self.object_typeclass, key="box", location=self.char1.location) + + def _box_all(): + # print "boxing all\n", "-"*20 + for o in self.room1.contents: + if o not in [self.char1, self.char2, self.exit, self.obj1, self.obj2, self.box]: + o.location = self.box + # print o.key, o.dbref, "boxed" + else: + # print "skipped", o.key, o.dbref + pass + + def _unbox(dbrefs): + # print "unboxing", dbrefs, "\n", "-"*20 + for o in self.box.contents: + if o.dbref in dbrefs: + o.location = self.room1 + # print "unboxed", o.key, o.dbref + + # solve dough puzzle using breakfast's egg + # and dough's flour. A new dough will be created + _box_all() + _unbox(breakfast_parts.pop("egg") + dough_parts.pop("flour")) + self._use("egg, flour", "You are a Genius") + + # solve entree puzzle with newly created dough + # and breakfast's boiling water. A new + # boiling water and pasta will be created + _unbox(breakfast_parts.pop("boiling water")) + self._use("boiling water, dough", "You are a Genius") + + # solve breakfast puzzle with dough's egg + # and newly created boiling water. A new + # boiling water and boiled egg will be created + _unbox(dough_parts.pop("egg")) + self._use("boiling water, egg", "You are a Genius") + + # solve entree puzzle using entree's dough + # and newly created boiling water. A new + # boiling water and pasta will be created + _unbox(entree_parts.pop("dough")) + self._use("boiling water, dough", "You are a Genius") + + self._check_room_contents({"boiling water": 1, "pasta": 2, "boiled egg": 1}) diff --git a/evennia/contrib/game_systems/turnbattle/tests.py b/evennia/contrib/game_systems/turnbattle/tests.py new file mode 100644 index 0000000000..8f11c74b5b --- /dev/null +++ b/evennia/contrib/game_systems/turnbattle/tests.py @@ -0,0 +1,603 @@ +""" +Turnbattle tests. + +""" + +from mock import patch, MagicMock +from evennia.commands.default.tests import CommandTest +from evennia.utils.create import create_object +from evennia.utils.test_resources import EvenniaTest +from evennia.objects.objects import DefaultRoom +from . import tb_basic, tb_equip, tb_range, tb_items, tb_magic + + +class TestTurnBattleBasicCmd(CommandTest): + + # Test basic combat commands + def test_turnbattlecmd(self): + self.call(tb_basic.CmdFight(), "", "You can't start a fight if you've been defeated!") + self.call(tb_basic.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_basic.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_basic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.") + + +class TestTurnBattleEquipCmd(CommandTest): + def setUp(self): + super(TestTurnBattleEquipCmd, self).setUp() + self.testweapon = create_object(tb_equip.TBEWeapon, key="test weapon") + self.testarmor = create_object(tb_equip.TBEArmor, key="test armor") + self.testweapon.move_to(self.char1) + self.testarmor.move_to(self.char1) + + # Test equipment commands + def test_turnbattleequipcmd(self): + # Start with equip module specific commands. + self.call(tb_equip.CmdWield(), "weapon", "Char wields test weapon.") + self.call(tb_equip.CmdUnwield(), "", "Char lowers test weapon.") + self.call(tb_equip.CmdDon(), "armor", "Char dons test armor.") + self.call(tb_equip.CmdDoff(), "", "Char removes test armor.") + # Also test the commands that are the same in the basic module + self.call(tb_equip.CmdFight(), "", "You can't start a fight if you've been defeated!") + self.call(tb_equip.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_equip.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.") + + +class TestTurnBattleRangeCmd(CommandTest): + # Test range commands + def test_turnbattlerangecmd(self): + # Start with range module specific commands. + self.call(tb_range.CmdShoot(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdApproach(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdWithdraw(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdStatus(), "", "HP Remaining: 100 / 100") + # Also test the commands that are the same in the basic module + self.call(tb_range.CmdFight(), "", "There's nobody here to fight!") + self.call(tb_range.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdRest(), "", "Char rests to recover HP.") + + +class TestTurnBattleItemsCmd(CommandTest): + def setUp(self): + super(TestTurnBattleItemsCmd, self).setUp() + self.testitem = create_object(key="test item") + self.testitem.move_to(self.char1) + + # Test item commands + def test_turnbattleitemcmd(self): + self.call(tb_items.CmdUse(), "item", "'Test item' is not a usable item.") + # Also test the commands that are the same in the basic module + self.call(tb_items.CmdFight(), "", "You can't start a fight if you've been defeated!") + self.call(tb_items.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_items.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_items.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_items.CmdRest(), "", "Char rests to recover HP.") + + +class TestTurnBattleMagicCmd(CommandTest): + + # Test magic commands + def test_turnbattlemagiccmd(self): + self.call(tb_magic.CmdStatus(), "", "You have 100 / 100 HP and 20 / 20 MP.") + self.call(tb_magic.CmdLearnSpell(), "test spell", "There is no spell with that name.") + self.call(tb_magic.CmdCast(), "", "Usage: cast = , ") + # Also test the commands that are the same in the basic module + self.call(tb_magic.CmdFight(), "", "There's nobody here to fight!") + self.call(tb_magic.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_magic.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_magic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_magic.CmdRest(), "", "Char rests to recover HP and MP.") + + +class TestTurnBattleBasicFunc(EvenniaTest): + def setUp(self): + super(TestTurnBattleBasicFunc, self).setUp() + self.testroom = create_object(DefaultRoom, key="Test Room") + self.attacker = create_object( + tb_basic.TBBasicCharacter, key="Attacker", location=self.testroom + ) + self.defender = create_object( + tb_basic.TBBasicCharacter, key="Defender", location=self.testroom + ) + self.joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner", location=None) + + def tearDown(self): + super(TestTurnBattleBasicFunc, self).tearDown() + self.turnhandler.stop() + self.testroom.delete() + self.attacker.delete() + self.defender.delete() + self.joiner.delete() + + # Test combat functions + def test_tbbasicfunc(self): + # Initiative roll + initiative = tb_basic.roll_init(self.attacker) + self.assertTrue(initiative >= 0 and initiative <= 1000) + # Attack roll + attack_roll = tb_basic.get_attack(self.attacker, self.defender) + self.assertTrue(attack_roll >= 0 and attack_roll <= 100) + # Defense roll + defense_roll = tb_basic.get_defense(self.attacker, self.defender) + self.assertTrue(defense_roll == 50) + # Damage roll + damage_roll = tb_basic.get_damage(self.attacker, self.defender) + self.assertTrue(damage_roll >= 15 and damage_roll <= 25) + # Apply damage + self.defender.db.hp = 10 + tb_basic.apply_damage(self.defender, 3) + self.assertTrue(self.defender.db.hp == 7) + # Resolve attack + self.defender.db.hp = 40 + tb_basic.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) + self.assertTrue(self.defender.db.hp < 40) + # Combat cleanup + self.attacker.db.Combat_attribute = True + tb_basic.combat_cleanup(self.attacker) + self.assertFalse(self.attacker.db.combat_attribute) + # Is in combat + self.assertFalse(tb_basic.is_in_combat(self.attacker)) + # Set up turn handler script for further tests + self.attacker.location.scripts.add(tb_basic.TBBasicTurnHandler) + self.turnhandler = self.attacker.db.combat_TurnHandler + self.assertTrue(self.attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + self.turnhandler.interval = 10000 + # Force turn order + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + # Test is turn + self.assertTrue(tb_basic.is_turn(self.attacker)) + # Spend actions + self.attacker.db.Combat_ActionsLeft = 1 + tb_basic.spend_action(self.attacker, 1, action_name="Test") + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "Test") + # Initialize for combat + self.attacker.db.Combat_ActionsLeft = 983 + self.turnhandler.initialize_for_combat(self.attacker) + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "null") + # Start turn + self.defender.db.Combat_ActionsLeft = 0 + self.turnhandler.start_turn(self.defender) + self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) + # Next turn + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.next_turn() + self.assertTrue(self.turnhandler.db.turn == 1) + # Turn end check + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.attacker.db.Combat_ActionsLeft = 0 + self.turnhandler.turn_end_check(self.attacker) + self.assertTrue(self.turnhandler.db.turn == 1) + # Join fight + self.joiner.location = self.testroom + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.join_fight(self.joiner) + self.assertTrue(self.turnhandler.db.turn == 1) + self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) + + +class TestTurnBattleEquipFunc(EvenniaTest): + def setUp(self): + super(TestTurnBattleEquipFunc, self).setUp() + self.testroom = create_object(DefaultRoom, key="Test Room") + self.attacker = create_object( + tb_equip.TBEquipCharacter, key="Attacker", location=self.testroom + ) + self.defender = create_object( + tb_equip.TBEquipCharacter, key="Defender", location=self.testroom + ) + self.joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner", location=None) + + def tearDown(self): + super(TestTurnBattleEquipFunc, self).tearDown() + self.turnhandler.stop() + self.testroom.delete() + self.attacker.delete() + self.defender.delete() + self.joiner.delete() + + # Test the combat functions in tb_equip too. They work mostly the same. + def test_tbequipfunc(self): + # Initiative roll + initiative = tb_equip.roll_init(self.attacker) + self.assertTrue(initiative >= 0 and initiative <= 1000) + # Attack roll + attack_roll = tb_equip.get_attack(self.attacker, self.defender) + self.assertTrue(attack_roll >= -50 and attack_roll <= 150) + # Defense roll + defense_roll = tb_equip.get_defense(self.attacker, self.defender) + self.assertTrue(defense_roll == 50) + # Damage roll + damage_roll = tb_equip.get_damage(self.attacker, self.defender) + self.assertTrue(damage_roll >= 0 and damage_roll <= 50) + # Apply damage + self.defender.db.hp = 10 + tb_equip.apply_damage(self.defender, 3) + self.assertTrue(self.defender.db.hp == 7) + # Resolve attack + self.defender.db.hp = 40 + tb_equip.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) + self.assertTrue(self.defender.db.hp < 40) + # Combat cleanup + self.attacker.db.Combat_attribute = True + tb_equip.combat_cleanup(self.attacker) + self.assertFalse(self.attacker.db.combat_attribute) + # Is in combat + self.assertFalse(tb_equip.is_in_combat(self.attacker)) + # Set up turn handler script for further tests + self.attacker.location.scripts.add(tb_equip.TBEquipTurnHandler) + self.turnhandler = self.attacker.db.combat_TurnHandler + self.assertTrue(self.attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + self.turnhandler.interval = 10000 + # Force turn order + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + # Test is turn + self.assertTrue(tb_equip.is_turn(self.attacker)) + # Spend actions + self.attacker.db.Combat_ActionsLeft = 1 + tb_equip.spend_action(self.attacker, 1, action_name="Test") + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "Test") + # Initialize for combat + self.attacker.db.Combat_ActionsLeft = 983 + self.turnhandler.initialize_for_combat(self.attacker) + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "null") + # Start turn + self.defender.db.Combat_ActionsLeft = 0 + self.turnhandler.start_turn(self.defender) + self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) + # Next turn + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.next_turn() + self.assertTrue(self.turnhandler.db.turn == 1) + # Turn end check + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.attacker.db.Combat_ActionsLeft = 0 + self.turnhandler.turn_end_check(self.attacker) + self.assertTrue(self.turnhandler.db.turn == 1) + # Join fight + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.join_fight(self.joiner) + self.assertTrue(self.turnhandler.db.turn == 1) + self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) + + +class TestTurnBattleRangeFunc(EvenniaTest): + def setUp(self): + super(TestTurnBattleRangeFunc, self).setUp() + self.testroom = create_object(DefaultRoom, key="Test Room") + self.attacker = create_object( + tb_range.TBRangeCharacter, key="Attacker", location=self.testroom + ) + self.defender = create_object( + tb_range.TBRangeCharacter, key="Defender", location=self.testroom + ) + self.joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=self.testroom) + + def tearDown(self): + super(TestTurnBattleRangeFunc, self).tearDown() + self.turnhandler.stop() + self.testroom.delete() + self.attacker.delete() + self.defender.delete() + self.joiner.delete() + + # Test combat functions in tb_range too. + def test_tbrangefunc(self): + # Initiative roll + initiative = tb_range.roll_init(self.attacker) + self.assertTrue(initiative >= 0 and initiative <= 1000) + # Attack roll + attack_roll = tb_range.get_attack(self.attacker, self.defender, "test") + self.assertTrue(attack_roll >= 0 and attack_roll <= 100) + # Defense roll + defense_roll = tb_range.get_defense(self.attacker, self.defender, "test") + self.assertTrue(defense_roll == 50) + # Damage roll + damage_roll = tb_range.get_damage(self.attacker, self.defender) + self.assertTrue(damage_roll >= 15 and damage_roll <= 25) + # Apply damage + self.defender.db.hp = 10 + tb_range.apply_damage(self.defender, 3) + self.assertTrue(self.defender.db.hp == 7) + # Resolve attack + self.defender.db.hp = 40 + tb_range.resolve_attack( + self.attacker, self.defender, "test", attack_value=20, defense_value=10 + ) + self.assertTrue(self.defender.db.hp < 40) + # Combat cleanup + self.attacker.db.Combat_attribute = True + tb_range.combat_cleanup(self.attacker) + self.assertFalse(self.attacker.db.combat_attribute) + # Is in combat + self.assertFalse(tb_range.is_in_combat(self.attacker)) + # Set up turn handler script for further tests + self.attacker.location.scripts.add(tb_range.TBRangeTurnHandler) + self.turnhandler = self.attacker.db.combat_TurnHandler + self.assertTrue(self.attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + self.turnhandler.interval = 10000 + # Force turn order + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + # Test is turn + self.assertTrue(tb_range.is_turn(self.attacker)) + # Spend actions + self.attacker.db.Combat_ActionsLeft = 1 + tb_range.spend_action(self.attacker, 1, action_name="Test") + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "Test") + # Initialize for combat + self.attacker.db.Combat_ActionsLeft = 983 + self.turnhandler.initialize_for_combat(self.attacker) + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "null") + # Set up ranges again, since initialize_for_combat clears them + self.attacker.db.combat_range = {} + self.attacker.db.combat_range[self.attacker] = 0 + self.attacker.db.combat_range[self.defender] = 1 + self.defender.db.combat_range = {} + self.defender.db.combat_range[self.defender] = 0 + self.defender.db.combat_range[self.attacker] = 1 + # Start turn + self.defender.db.Combat_ActionsLeft = 0 + self.turnhandler.start_turn(self.defender) + self.assertTrue(self.defender.db.Combat_ActionsLeft == 2) + # Next turn + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.next_turn() + self.assertTrue(self.turnhandler.db.turn == 1) + # Turn end check + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.attacker.db.Combat_ActionsLeft = 0 + self.turnhandler.turn_end_check(self.attacker) + self.assertTrue(self.turnhandler.db.turn == 1) + # Join fight + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.join_fight(self.joiner) + self.assertTrue(self.turnhandler.db.turn == 1) + self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) + # Now, test for approach/withdraw functions + self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 1) + # Approach + tb_range.approach(self.attacker, self.defender) + self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 0) + # Withdraw + tb_range.withdraw(self.attacker, self.defender) + self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 1) + + +class TestTurnBattleItemsFunc(EvenniaTest): + @patch("evennia.contrib.turnbattle.tb_items.tickerhandler", new=MagicMock()) + def setUp(self): + super(TestTurnBattleItemsFunc, self).setUp() + self.testroom = create_object(DefaultRoom, key="Test Room") + self.attacker = create_object( + tb_items.TBItemsCharacter, key="Attacker", location=self.testroom + ) + self.defender = create_object( + tb_items.TBItemsCharacter, key="Defender", location=self.testroom + ) + self.joiner = create_object(tb_items.TBItemsCharacter, key="Joiner", location=self.testroom) + self.user = create_object(tb_items.TBItemsCharacter, key="User", location=self.testroom) + self.test_healpotion = create_object(key="healing potion") + self.test_healpotion.db.item_func = "heal" + self.test_healpotion.db.item_uses = 3 + + def tearDown(self): + super(TestTurnBattleItemsFunc, self).tearDown() + self.turnhandler.stop() + self.testroom.delete() + self.attacker.delete() + self.defender.delete() + self.joiner.delete() + self.user.delete() + + # Test functions in tb_items. + def test_tbitemsfunc(self): + # Initiative roll + initiative = tb_items.roll_init(self.attacker) + self.assertTrue(initiative >= 0 and initiative <= 1000) + # Attack roll + attack_roll = tb_items.get_attack(self.attacker, self.defender) + self.assertTrue(attack_roll >= 0 and attack_roll <= 100) + # Defense roll + defense_roll = tb_items.get_defense(self.attacker, self.defender) + self.assertTrue(defense_roll == 50) + # Damage roll + damage_roll = tb_items.get_damage(self.attacker, self.defender) + self.assertTrue(damage_roll >= 15 and damage_roll <= 25) + # Apply damage + self.defender.db.hp = 10 + tb_items.apply_damage(self.defender, 3) + self.assertTrue(self.defender.db.hp == 7) + # Resolve attack + self.defender.db.hp = 40 + tb_items.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) + self.assertTrue(self.defender.db.hp < 40) + # Combat cleanup + self.attacker.db.Combat_attribute = True + tb_items.combat_cleanup(self.attacker) + self.assertFalse(self.attacker.db.combat_attribute) + # Is in combat + self.assertFalse(tb_items.is_in_combat(self.attacker)) + # Set up turn handler script for further tests + self.attacker.location.scripts.add(tb_items.TBItemsTurnHandler) + self.turnhandler = self.attacker.db.combat_TurnHandler + self.assertTrue(self.attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + self.turnhandler.interval = 10000 + # Force turn order + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + # Test is turn + self.assertTrue(tb_items.is_turn(self.attacker)) + # Spend actions + self.attacker.db.Combat_ActionsLeft = 1 + tb_items.spend_action(self.attacker, 1, action_name="Test") + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "Test") + # Initialize for combat + self.attacker.db.Combat_ActionsLeft = 983 + self.turnhandler.initialize_for_combat(self.attacker) + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "null") + # Start turn + self.defender.db.Combat_ActionsLeft = 0 + self.turnhandler.start_turn(self.defender) + self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) + # Next turn + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.next_turn() + self.assertTrue(self.turnhandler.db.turn == 1) + # Turn end check + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.attacker.db.Combat_ActionsLeft = 0 + self.turnhandler.turn_end_check(self.attacker) + self.assertTrue(self.turnhandler.db.turn == 1) + # Join fight + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.join_fight(self.joiner) + self.assertTrue(self.turnhandler.db.turn == 1) + self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) + # Now time to test item stuff. + # Spend item use + tb_items.spend_item_use(self.test_healpotion, self.user) + self.assertTrue(self.test_healpotion.db.item_uses == 2) + # Use item + self.user.db.hp = 2 + tb_items.use_item(self.user, self.test_healpotion, self.user) + self.assertTrue(self.user.db.hp > 2) + # Add contition + tb_items.add_condition(self.user, self.user, "Test", 5) + self.assertTrue(self.user.db.conditions == {"Test": [5, self.user]}) + # Condition tickdown + tb_items.condition_tickdown(self.user, self.user) + self.assertTrue(self.user.db.conditions == {"Test": [4, self.user]}) + # Test item functions now! + # Item heal + self.user.db.hp = 2 + tb_items.itemfunc_heal(self.test_healpotion, self.user, self.user) + # Item add condition + self.user.db.conditions = {} + tb_items.itemfunc_add_condition(self.test_healpotion, self.user, self.user) + self.assertTrue(self.user.db.conditions == {"Regeneration": [5, self.user]}) + # Item cure condition + self.user.db.conditions = {"Poisoned": [5, self.user]} + tb_items.itemfunc_cure_condition(self.test_healpotion, self.user, self.user) + self.assertTrue(self.user.db.conditions == {}) + + +class TestTurnBattleMagicFunc(EvenniaTest): + def setUp(self): + super(TestTurnBattleMagicFunc, self).setUp() + self.testroom = create_object(DefaultRoom, key="Test Room") + self.attacker = create_object( + tb_magic.TBMagicCharacter, key="Attacker", location=self.testroom + ) + self.defender = create_object( + tb_magic.TBMagicCharacter, key="Defender", location=self.testroom + ) + self.joiner = create_object(tb_magic.TBMagicCharacter, key="Joiner", location=self.testroom) + + def tearDown(self): + super(TestTurnBattleMagicFunc, self).tearDown() + self.turnhandler.stop() + self.testroom.delete() + self.attacker.delete() + self.defender.delete() + self.joiner.delete() + + # Test combat functions in tb_magic. + def test_tbbasicfunc(self): + # Initiative roll + initiative = tb_magic.roll_init(self.attacker) + self.assertTrue(initiative >= 0 and initiative <= 1000) + # Attack roll + attack_roll = tb_magic.get_attack(self.attacker, self.defender) + self.assertTrue(attack_roll >= 0 and attack_roll <= 100) + # Defense roll + defense_roll = tb_magic.get_defense(self.attacker, self.defender) + self.assertTrue(defense_roll == 50) + # Damage roll + damage_roll = tb_magic.get_damage(self.attacker, self.defender) + self.assertTrue(damage_roll >= 15 and damage_roll <= 25) + # Apply damage + self.defender.db.hp = 10 + tb_magic.apply_damage(self.defender, 3) + self.assertTrue(self.defender.db.hp == 7) + # Resolve attack + self.defender.db.hp = 40 + tb_magic.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) + self.assertTrue(self.defender.db.hp < 40) + # Combat cleanup + self.attacker.db.Combat_attribute = True + tb_magic.combat_cleanup(self.attacker) + self.assertFalse(self.attacker.db.combat_attribute) + # Is in combat + self.assertFalse(tb_magic.is_in_combat(self.attacker)) + # Set up turn handler script for further tests + self.attacker.location.scripts.add(tb_magic.TBMagicTurnHandler) + self.turnhandler = self.attacker.db.combat_TurnHandler + self.assertTrue(self.attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + self.turnhandler.interval = 10000 + # Force turn order + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + # Test is turn + self.assertTrue(tb_magic.is_turn(self.attacker)) + # Spend actions + self.attacker.db.Combat_ActionsLeft = 1 + tb_magic.spend_action(self.attacker, 1, action_name="Test") + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "Test") + # Initialize for combat + self.attacker.db.Combat_ActionsLeft = 983 + self.turnhandler.initialize_for_combat(self.attacker) + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "null") + # Start turn + self.defender.db.Combat_ActionsLeft = 0 + self.turnhandler.start_turn(self.defender) + self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) + # Next turn + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.next_turn() + self.assertTrue(self.turnhandler.db.turn == 1) + # Turn end check + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.attacker.db.Combat_ActionsLeft = 0 + self.turnhandler.turn_end_check(self.attacker) + self.assertTrue(self.turnhandler.db.turn == 1) + # Join fight + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.join_fight(self.joiner) + self.assertTrue(self.turnhandler.db.turn == 1) + self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) diff --git a/evennia/contrib/grid/extended_room/tests.py b/evennia/contrib/grid/extended_room/tests.py new file mode 100644 index 0000000000..93311430d4 --- /dev/null +++ b/evennia/contrib/grid/extended_room/tests.py @@ -0,0 +1,103 @@ +""" +Testing of ExtendedRoom contrib + +""" + +import datetime +from mock import patch, Mock +from django.conf import settings +from evennia.commands.default.tests import CommandTest +from evennia.objects.objects import DefaultRoom +from . import extended_room + + +class ForceUTCDatetime(datetime.datetime): + + """Force UTC datetime.""" + + @classmethod + def fromtimestamp(cls, timestamp): + """Force fromtimestamp to run with naive datetimes.""" + return datetime.datetime.utcfromtimestamp(timestamp) + + +@patch("evennia.contrib.extended_room.datetime.datetime", ForceUTCDatetime) +# mock gametime to return April 9, 2064, at 21:06 (spring evening) +@patch("evennia.utils.gametime.gametime", new=Mock(return_value=2975000766)) +class TestExtendedRoom(CommandTest): + room_typeclass = extended_room.ExtendedRoom + DETAIL_DESC = "A test detail." + SPRING_DESC = "A spring description." + OLD_DESC = "Old description." + settings.TIME_ZONE = "UTC" + + def setUp(self): + super().setUp() + self.room1.ndb.last_timeslot = "afternoon" + self.room1.ndb.last_season = "winter" + self.room1.db.details = {"testdetail": self.DETAIL_DESC} + self.room1.db.spring_desc = self.SPRING_DESC + self.room1.db.desc = self.OLD_DESC + + def test_return_appearance(self): + # get the appearance of a non-extended room for contrast purposes + old_desc = DefaultRoom.return_appearance(self.room1, self.char1) + # the new appearance should be the old one, but with the desc switched + self.assertEqual( + old_desc.replace(self.OLD_DESC, self.SPRING_DESC), + self.room1.return_appearance(self.char1), + ) + self.assertEqual("spring", self.room1.ndb.last_season) + self.assertEqual("evening", self.room1.ndb.last_timeslot) + + def test_return_detail(self): + self.assertEqual(self.DETAIL_DESC, self.room1.return_detail("testdetail")) + + def test_cmdextendedlook(self): + rid = self.room1.id + self.call( + extended_room.CmdExtendedRoomLook(), + "here", + "Room(#{})\n{}".format(rid, self.SPRING_DESC), + ) + self.call(extended_room.CmdExtendedRoomLook(), "testdetail", self.DETAIL_DESC) + self.call( + extended_room.CmdExtendedRoomLook(), "nonexistent", "Could not find 'nonexistent'." + ) + + def test_cmdsetdetail(self): + self.call(extended_room.CmdExtendedRoomDetail(), "", "Details on Room") + self.call( + extended_room.CmdExtendedRoomDetail(), + "thingie = newdetail with spaces", + "Detail set 'thingie': 'newdetail with spaces'", + ) + self.call(extended_room.CmdExtendedRoomDetail(), "thingie", "Detail 'thingie' on Room:\n") + self.call( + extended_room.CmdExtendedRoomDetail(), + "/del thingie", + "Detail thingie deleted, if it existed.", + cmdstring="detail", + ) + self.call(extended_room.CmdExtendedRoomDetail(), "thingie", "Detail 'thingie' not found.") + + # Test with aliases + self.call(extended_room.CmdExtendedRoomDetail(), "", "Details on Room") + self.call( + extended_room.CmdExtendedRoomDetail(), + "thingie;other;stuff = newdetail with spaces", + "Detail set 'thingie;other;stuff': 'newdetail with spaces'", + ) + self.call(extended_room.CmdExtendedRoomDetail(), "thingie", "Detail 'thingie' on Room:\n") + self.call(extended_room.CmdExtendedRoomDetail(), "other", "Detail 'other' on Room:\n") + self.call(extended_room.CmdExtendedRoomDetail(), "stuff", "Detail 'stuff' on Room:\n") + self.call( + extended_room.CmdExtendedRoomDetail(), + "/del other;stuff", + "Detail other;stuff deleted, if it existed.", + ) + self.call(extended_room.CmdExtendedRoomDetail(), "other", "Detail 'other' not found.") + self.call(extended_room.CmdExtendedRoomDetail(), "stuff", "Detail 'stuff' not found.") + + def test_cmdgametime(self): + self.call(extended_room.CmdExtendedRoomGameTime(), "", "It's a spring day, in the evening.") diff --git a/evennia/contrib/grid/mapbuilder/tests.py b/evennia/contrib/grid/mapbuilder/tests.py new file mode 100644 index 0000000000..ba890b728a --- /dev/null +++ b/evennia/contrib/grid/mapbuilder/tests.py @@ -0,0 +1,33 @@ +""" +Test map builder. + +""" + +from evennia.commands.default.tests import CommandTest +from . import mapbuilder + + +class TestMapBuilder(CommandTest): + def test_cmdmapbuilder(self): + self.call( + mapbuilder.CmdMapBuilder(), + "evennia.contrib.mapbuilder.EXAMPLE1_MAP evennia.contrib.mapbuilder.EXAMPLE1_LEGEND", + """Creating Map...|≈≈≈≈≈ +≈♣n♣≈ +≈∩▲∩≈ +≈♠n♠≈ +≈≈≈≈≈ +|Creating Landmass...|""", + ) + self.call( + mapbuilder.CmdMapBuilder(), + "evennia.contrib.mapbuilder.EXAMPLE2_MAP evennia.contrib.mapbuilder.EXAMPLE2_LEGEND", + """Creating Map...|≈ ≈ ≈ ≈ ≈ + +≈ ♣-♣-♣ ≈ + ≈ ♣ ♣ ♣ ≈ + ≈ ♣-♣-♣ ≈ + +≈ ≈ ≈ ≈ ≈ +|Creating Landmass...|""", + ) diff --git a/evennia/contrib/grid/simpledoor/tests.py b/evennia/contrib/grid/simpledoor/tests.py new file mode 100644 index 0000000000..c8a6e2e9b3 --- /dev/null +++ b/evennia/contrib/grid/simpledoor/tests.py @@ -0,0 +1,29 @@ +""" +Tests of simpledoor. + +""" + + +from evennia.commands.default.tests import CommandTest +from . import simpledoor + + +class TestSimpleDoor(CommandTest): + def test_cmdopen(self): + self.call( + simpledoor.CmdOpen(), + "newdoor;door:contrib.simpledoor.SimpleDoor,backdoor;door = Room2", + "Created new Exit 'newdoor' from Room to Room2 (aliases: door).|Note: A door-type exit was " + "created - ignored eventual custom return-exit type.|Created new Exit 'newdoor' from Room2 to Room (aliases: door).", + ) + self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You close newdoor.", cmdstring="close") + self.call( + simpledoor.CmdOpenCloseDoor(), + "newdoor", + "newdoor is already closed.", + cmdstring="close", + ) + self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You open newdoor.", cmdstring="open") + self.call( + simpledoor.CmdOpenCloseDoor(), "newdoor", "newdoor is already open.", cmdstring="open" + ) diff --git a/evennia/contrib/grid/slow_exit/tests.py b/evennia/contrib/grid/slow_exit/tests.py new file mode 100644 index 0000000000..dac96b368c --- /dev/null +++ b/evennia/contrib/grid/slow_exit/tests.py @@ -0,0 +1,27 @@ +""" +Slow exit tests. + +""" + +from mock import Mock, patch +from evennia.commands.default.tests import CommandTest +from evennia.utils.create import create_object +from . import slow_exit + +slow_exit.MOVE_DELAY = {"stroll": 0, "walk": 0, "run": 0, "sprint": 0} + + +def _cancellable_mockdelay(time, callback, *args, **kwargs): + callback(*args, **kwargs) + return Mock() + + +class TestSlowExit(CommandTest): + @patch("evennia.utils.delay", _cancellable_mockdelay) + def test_exit(self): + exi = create_object( + slow_exit.SlowExit, key="slowexit", location=self.room1, destination=self.room2 + ) + exi.at_traverse(self.char1, self.room2) + self.call(slow_exit.CmdSetSpeed(), "walk", "You are now walking.") + self.call(slow_exit.CmdStop(), "", "You stop moving.") diff --git a/evennia/contrib/grid/wilderness/tests.py b/evennia/contrib/grid/wilderness/tests.py new file mode 100644 index 0000000000..bfa1cc9839 --- /dev/null +++ b/evennia/contrib/grid/wilderness/tests.py @@ -0,0 +1,137 @@ +""" +Test wilderness + +""" + +from evennia.utils.test_resources import EvenniaTest +from evennia import DefaultCharacter +from evennia.utils.create import create_object +from . import wilderness + + +class TestWilderness(EvenniaTest): + def setUp(self): + super().setUp() + self.char1 = create_object(DefaultCharacter, key="char1") + self.char2 = create_object(DefaultCharacter, key="char2") + + def get_wilderness_script(self, name="default"): + w = wilderness.WildernessScript.objects.get("default") + return w + + def test_create_wilderness_default_name(self): + wilderness.create_wilderness() + w = self.get_wilderness_script() + self.assertIsNotNone(w) + + def test_create_wilderness_custom_name(self): + name = "customname" + wilderness.create_wilderness(name) + w = self.get_wilderness_script(name) + self.assertIsNotNone(w) + + def test_enter_wilderness(self): + wilderness.create_wilderness() + wilderness.enter_wilderness(self.char1) + self.assertIsInstance(self.char1.location, wilderness.WildernessRoom) + w = self.get_wilderness_script() + self.assertEqual(w.db.itemcoordinates[self.char1], (0, 0)) + + def test_enter_wilderness_custom_coordinates(self): + wilderness.create_wilderness() + wilderness.enter_wilderness(self.char1, coordinates=(1, 2)) + self.assertIsInstance(self.char1.location, wilderness.WildernessRoom) + w = self.get_wilderness_script() + self.assertEqual(w.db.itemcoordinates[self.char1], (1, 2)) + + def test_enter_wilderness_custom_name(self): + name = "customnname" + wilderness.create_wilderness(name) + wilderness.enter_wilderness(self.char1, name=name) + self.assertIsInstance(self.char1.location, wilderness.WildernessRoom) + + def test_wilderness_correct_exits(self): + wilderness.create_wilderness() + wilderness.enter_wilderness(self.char1) + + # By default we enter at a corner (0, 0), so only a few exits should + # be visible / traversable + exits = [ + i + for i in self.char1.location.contents + if i.destination and (i.access(self.char1, "view") or i.access(self.char1, "traverse")) + ] + + self.assertEqual(len(exits), 3) + exitsok = ["north", "northeast", "east"] + for each_exit in exitsok: + self.assertTrue(any([e for e in exits if e.key == each_exit])) + + # If we move to another location not on an edge, then all directions + # should be visible / traversable + wilderness.enter_wilderness(self.char1, coordinates=(1, 1)) + exits = [ + i + for i in self.char1.location.contents + if i.destination and (i.access(self.char1, "view") or i.access(self.char1, "traverse")) + ] + self.assertEqual(len(exits), 8) + exitsok = [ + "north", + "northeast", + "east", + "southeast", + "south", + "southwest", + "west", + "northwest", + ] + for each_exit in exitsok: + self.assertTrue(any([e for e in exits if e.key == each_exit])) + + def test_room_creation(self): + # Pretend that both char1 and char2 are connected... + self.char1.sessions.add(1) + self.char2.sessions.add(1) + self.assertTrue(self.char1.has_account) + self.assertTrue(self.char2.has_account) + + wilderness.create_wilderness() + w = self.get_wilderness_script() + + # We should have no unused room after moving the first account in. + self.assertEqual(len(w.db.unused_rooms), 0) + w.move_obj(self.char1, (0, 0)) + self.assertEqual(len(w.db.unused_rooms), 0) + + # And also no unused room after moving the second one in. + w.move_obj(self.char2, (1, 1)) + self.assertEqual(len(w.db.unused_rooms), 0) + + # But if char2 moves into char1's room, we should have one unused room + # Which should be char2's old room that got created. + w.move_obj(self.char2, (0, 0)) + self.assertEqual(len(w.db.unused_rooms), 1) + self.assertEqual(self.char1.location, self.char2.location) + + # And if char2 moves back out, that unused room should be put back to + # use again. + w.move_obj(self.char2, (1, 1)) + self.assertNotEqual(self.char1.location, self.char2.location) + self.assertEqual(len(w.db.unused_rooms), 0) + + def test_get_new_coordinates(self): + loc = (1, 1) + directions = { + "north": (1, 2), + "northeast": (2, 2), + "east": (2, 1), + "southeast": (2, 0), + "south": (1, 0), + "southwest": (0, 0), + "west": (0, 1), + "northwest": (0, 2), + } + for (direction, correct_loc) in directions.items(): # Not compatible with Python 3 + new_loc = wilderness.get_new_coordinates(loc, direction) + self.assertEqual(new_loc, correct_loc, direction) diff --git a/evennia/contrib/rpg/dice/tests.py b/evennia/contrib/rpg/dice/tests.py new file mode 100644 index 0000000000..08ce808b77 --- /dev/null +++ b/evennia/contrib/rpg/dice/tests.py @@ -0,0 +1,23 @@ +""" +Testing of TestDice. + +""" + +from evennia.commands.default.tests import CommandTest +from mock import patch +from . import dice + + +@patch("evennia.contrib.dice.randint", return_value=5) +class TestDice(CommandTest): + def test_roll_dice(self, mocked_randint): + self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4) + self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True) + self.assertEqual(dice.roll_dice(6, 6, conditional=(">", 33)), False) + + def test_cmddice(self, mocked_randint): + self.call( + dice.CmdDice(), "3d6 + 4", "You roll 3d6 + 4.| Roll(s): 5, 5 and 5. Total result is 19." + ) + self.call(dice.CmdDice(), "100000d1000", "The maximum roll allowed is 10000d10000.") + self.call(dice.CmdDice(), "/secret 3d6 + 4", "You roll 3d6 + 4 (secret, not echoed).") diff --git a/evennia/contrib/rpg/health_bar/tests.py b/evennia/contrib/rpg/health_bar/tests.py new file mode 100644 index 0000000000..1212dbc73e --- /dev/null +++ b/evennia/contrib/rpg/health_bar/tests.py @@ -0,0 +1,46 @@ +""" +Test health bar contrib + +""" + +from evennia.utils.test_resources import EvenniaTest +from . import health_bar + + +class TestHealthBar(EvenniaTest): + def test_healthbar(self): + expected_bar_str = "|[R|w|n|[B|w test0 / 200test |n" + self.assertEqual( + health_bar.display_meter( + 0, 200, length=40, pre_text="test", post_text="test", align="center" + ), + expected_bar_str, + ) + expected_bar_str = "|[R|w |n|[B|w test24 / 200test |n" + self.assertEqual( + health_bar.display_meter( + 24, 200, length=40, pre_text="test", post_text="test", align="center" + ), + expected_bar_str, + ) + expected_bar_str = "|[Y|w test100 /|n|[B|w 200test |n" + self.assertEqual( + health_bar.display_meter( + 100, 200, length=40, pre_text="test", post_text="test", align="center" + ), + expected_bar_str, + ) + expected_bar_str = "|[G|w test180 / 200test |n|[B|w |n" + self.assertEqual( + health_bar.display_meter( + 180, 200, length=40, pre_text="test", post_text="test", align="center" + ), + expected_bar_str, + ) + expected_bar_str = "|[G|w test200 / 200test |n|[B|w|n" + self.assertEqual( + health_bar.display_meter( + 200, 200, length=40, pre_text="test", post_text="test", align="center" + ), + expected_bar_str, + ) diff --git a/evennia/contrib/rpg/rpsystem/tests.py b/evennia/contrib/rpg/rpsystem/tests.py new file mode 100644 index 0000000000..fa77ff795a --- /dev/null +++ b/evennia/contrib/rpg/rpsystem/tests.py @@ -0,0 +1,321 @@ +""" +Tests for RP system + +""" +import time +from anything import Anything +from evennia.commands.default.tests import CommandTest +from evennia.utils.test_resources import EvenniaTest +from evennia import create_object + +from . import rpsystem +from . import rplanguage + +mtrans = {"testing": "1", "is": "2", "a": "3", "human": "4"} +atrans = ["An", "automated", "advantageous", "repeatable", "faster"] + +text = ( + "Automated testing is advantageous for a number of reasons: " + "tests may be executed Continuously without the need for human " + "intervention, They are easily repeatable, and often faster." +) + + +class TestLanguage(EvenniaTest): + def setUp(self): + super().setUp() + rplanguage.add_language( + key="testlang", + word_length_variance=1, + noun_prefix="bara", + noun_postfix="'y", + manual_translations=mtrans, + auto_translations=atrans, + force=True, + ) + rplanguage.add_language( + key="binary", + phonemes="oo ii a ck w b d t", + grammar="cvvv cvv cvvcv cvvcvv cvvvc cvvvcvv cvvc", + noun_prefix="beep-", + word_length_variance=4, + ) + + def tearDown(self): + super().tearDown() + rplanguage._LANGUAGE_HANDLER.delete() + rplanguage._LANGUAGE_HANDLER = None + + def test_obfuscate_language(self): + result0 = rplanguage.obfuscate_language(text, level=0.0, language="testlang") + self.assertEqual(result0, text) + result1 = rplanguage.obfuscate_language(text, level=1.0, language="testlang") + result2 = rplanguage.obfuscate_language(text, level=1.0, language="testlang") + result3 = rplanguage.obfuscate_language(text, level=1.0, language="binary") + + self.assertNotEqual(result1, text) + self.assertNotEqual(result3, text) + result1, result2 = result1.split(), result2.split() + self.assertEqual(result1[:4], result2[:4]) + self.assertEqual(result1[1], "1") + self.assertEqual(result1[2], "2") + self.assertEqual(result2[-1], result2[-1]) + + def test_faulty_language(self): + self.assertRaises( + rplanguage.LanguageError, + rplanguage.add_language, + key="binary2", + phonemes="w b d t oe ee, oo e o a wh dw bw", # erroneous comma + grammar="cvvv cvv cvvcv cvvcvvo cvvvc cvvvcvv cvvc c v cc vv ccvvc ccvvccvv ", + vowels="oea", + word_length_variance=4, + ) + + def test_available_languages(self): + self.assertEqual(list(sorted(rplanguage.available_languages())), ["binary", "testlang"]) + + def test_obfuscate_whisper(self): + self.assertEqual(rplanguage.obfuscate_whisper(text, level=0.0), text) + assert rplanguage.obfuscate_whisper(text, level=0.1).startswith( + "-utom-t-d t-sting is -dv-nt-g-ous for - numb-r of r--sons: t-sts m-y b- -x-cut-d Continuously" + ) + assert rplanguage.obfuscate_whisper(text, level=0.5).startswith( + "--------- --s---- -s -----------s f-- - ------ -f ---s--s: --s-s " + ) + self.assertEqual(rplanguage.obfuscate_whisper(text, level=1.0), "...") + + +# Testing of emoting / sdesc / recog system + + +sdesc0 = "A nice sender of emotes" +sdesc1 = "The first receiver of emotes." +sdesc2 = "Another nice colliding sdesc-guy for tests" +recog01 = "Mr Receiver" +recog02 = "Mr Receiver2" +recog10 = "Mr Sender" +emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."' +case_emote = "/me looks at /first, then /FIRST, /First and /Colliding twice." + + +class TestRPSystem(EvenniaTest): + maxDiff = None + + def setUp(self): + super().setUp() + self.room = create_object(rpsystem.ContribRPRoom, key="Location") + self.speaker = create_object(rpsystem.ContribRPCharacter, key="Sender", location=self.room) + self.receiver1 = create_object( + rpsystem.ContribRPCharacter, key="Receiver1", location=self.room + ) + self.receiver2 = create_object( + rpsystem.ContribRPCharacter, key="Receiver2", location=self.room + ) + + def test_ordered_permutation_regex(self): + self.assertEqual( + rpsystem.ordered_permutation_regex(sdesc0), + "/[0-9]*-*A\\ nice\\ sender\\ of\\ emotes(?=\\W|$)+|" + "/[0-9]*-*nice\\ sender\\ of\\ emotes(?=\\W|$)+|" + "/[0-9]*-*A\\ nice\\ sender\\ of(?=\\W|$)+|" + "/[0-9]*-*sender\\ of\\ emotes(?=\\W|$)+|" + "/[0-9]*-*nice\\ sender\\ of(?=\\W|$)+|" + "/[0-9]*-*A\\ nice\\ sender(?=\\W|$)+|" + "/[0-9]*-*nice\\ sender(?=\\W|$)+|" + "/[0-9]*-*of\\ emotes(?=\\W|$)+|" + "/[0-9]*-*sender\\ of(?=\\W|$)+|" + "/[0-9]*-*A\\ nice(?=\\W|$)+|" + "/[0-9]*-*emotes(?=\\W|$)+|" + "/[0-9]*-*sender(?=\\W|$)+|" + "/[0-9]*-*nice(?=\\W|$)+|" + "/[0-9]*-*of(?=\\W|$)+|" + "/[0-9]*-*A(?=\\W|$)+", + ) + + def test_sdesc_handler(self): + self.speaker.sdesc.add(sdesc0) + self.assertEqual(self.speaker.sdesc.get(), sdesc0) + self.speaker.sdesc.add("This is {#324} ignored") + self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored") + self.speaker.sdesc.add("Testing three words") + self.assertEqual( + self.speaker.sdesc.get_regex_tuple()[0].pattern, + "/[0-9]*-*Testing\ three\ words(?=\W|$)+|" + "/[0-9]*-*Testing\ three(?=\W|$)+|" + "/[0-9]*-*three\ words(?=\W|$)+|" + "/[0-9]*-*Testing(?=\W|$)+|" + "/[0-9]*-*three(?=\W|$)+|" + "/[0-9]*-*words(?=\W|$)+", + ) + + def test_recog_handler(self): + self.speaker.sdesc.add(sdesc0) + self.receiver1.sdesc.add(sdesc1) + self.speaker.recog.add(self.receiver1, recog01) + self.speaker.recog.add(self.receiver2, recog02) + self.assertEqual(self.speaker.recog.get(self.receiver1), recog01) + self.assertEqual(self.speaker.recog.get(self.receiver2), recog02) + self.assertEqual( + self.speaker.recog.get_regex_tuple(self.receiver1)[0].pattern, + "/[0-9]*-*Mr\\ Receiver(?=\\W|$)+|/[0-9]*-*Receiver(?=\\W|$)+|/[0-9]*-*Mr(?=\\W|$)+", + ) + self.speaker.recog.remove(self.receiver1) + self.assertEqual(self.speaker.recog.get(self.receiver1), sdesc1) + + self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2}) + + def test_parse_language(self): + self.assertEqual( + rpsystem.parse_language(self.speaker, emote), + ( + "With a flair, /me looks at /first and /colliding sdesc-guy. She says {##0}", + {"##0": (None, '"This is a test."')}, + ), + ) + + def parse_sdescs_and_recogs(self): + speaker = self.speaker + speaker.sdesc.add(sdesc0) + self.receiver1.sdesc.add(sdesc1) + self.receiver2.sdesc.add(sdesc2) + candidates = (self.receiver1, self.receiver2) + result = ( + 'With a flair, {#9} looks at {#10} and {#11}. She says "This is a test."', + { + "#11": "Another nice colliding sdesc-guy for tests", + "#10": "The first receiver of emotes.", + "#9": "A nice sender of emotes", + }, + ) + self.assertEqual( + rpsystem.parse_sdescs_and_recogs(speaker, candidates, emote, case_sensitive=False), + result, + ) + self.speaker.recog.add(self.receiver1, recog01) + self.assertEqual( + rpsystem.parse_sdescs_and_recogs(speaker, candidates, emote, case_sensitive=False), + result, + ) + + def test_send_emote(self): + speaker = self.speaker + receiver1 = self.receiver1 + receiver2 = self.receiver2 + receivers = [speaker, receiver1, receiver2] + speaker.sdesc.add(sdesc0) + receiver1.sdesc.add(sdesc1) + receiver2.sdesc.add(sdesc2) + speaker.msg = lambda text, **kwargs: setattr(self, "out0", text) + receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text) + receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text) + rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False) + self.assertEqual( + self.out0, + "With a flair, |bSender|n looks at |bThe first receiver of emotes.|n " + 'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n', + ) + self.assertEqual( + self.out1, + "With a flair, |bA nice sender of emotes|n looks at |bReceiver1|n and " + '|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n', + ) + self.assertEqual( + self.out2, + "With a flair, |bA nice sender of emotes|n looks at |bThe first " + 'receiver of emotes.|n and |bReceiver2|n. She says |w"This is a test."|n', + ) + + def test_send_case_sensitive_emote(self): + """Test new case-sensitive rp-parsing""" + speaker = self.speaker + receiver1 = self.receiver1 + receiver2 = self.receiver2 + receivers = [speaker, receiver1, receiver2] + speaker.sdesc.add(sdesc0) + receiver1.sdesc.add(sdesc1) + receiver2.sdesc.add(sdesc2) + speaker.msg = lambda text, **kwargs: setattr(self, "out0", text) + receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text) + receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text) + rpsystem.send_emote(speaker, receivers, case_emote) + self.assertEqual( + self.out0, + "|bSender|n looks at |bthe first receiver of emotes.|n, then " + "|bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n and " + "|bAnother nice colliding sdesc-guy for tests|n twice.", + ) + self.assertEqual( + self.out1, + "|bA nice sender of emotes|n looks at |bReceiver1|n, then |bReceiver1|n, " + "|bReceiver1|n and |bAnother nice colliding sdesc-guy for tests|n twice.", + ) + self.assertEqual( + self.out2, + "|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n, " + "then |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of " + "emotes.|n and |bReceiver2|n twice.", + ) + + def test_rpsearch(self): + self.speaker.sdesc.add(sdesc0) + self.receiver1.sdesc.add(sdesc1) + self.receiver2.sdesc.add(sdesc2) + self.speaker.msg = lambda text, **kwargs: setattr(self, "out0", text) + self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1) + self.assertEqual(self.speaker.search("colliding"), self.receiver2) + + def test_regex_tuple_from_key_alias(self): + self.speaker.aliases.add("foo bar") + self.speaker.aliases.add("this thing is a long thing") + t0 = time.time() + result = rpsystem.regex_tuple_from_key_alias(self.speaker) + t1 = time.time() + result = rpsystem.regex_tuple_from_key_alias(self.speaker) + t2 = time.time() + # print(f"t1: {t1 - t0}, t2: {t2 - t1}") + self.assertLess(t2 - t1, 10 ** -4) + self.assertEqual(result, (Anything, self.speaker, self.speaker.key)) + + +class TestRPSystemCommands(CommandTest): + def setUp(self): + super().setUp() + self.char1.swap_typeclass(rpsystem.ContribRPCharacter) + self.char2.swap_typeclass(rpsystem.ContribRPCharacter) + + def test_commands(self): + + self.call( + rpsystem.CmdSdesc(), "Foobar Character", "Char's sdesc was set to 'Foobar Character'." + ) + self.call( + rpsystem.CmdSdesc(), + "BarFoo Character", + "Char2's sdesc was set to 'BarFoo Character'.", + caller=self.char2, + ) + self.call(rpsystem.CmdSay(), "Hello!", 'Char says, "Hello!"') + self.call(rpsystem.CmdEmote(), "/me smiles to /BarFoo.", "Char smiles to BarFoo Character") + self.call( + rpsystem.CmdPose(), + "stands by the bar", + "Pose will read 'Foobar Character stands by the bar.'.", + ) + self.call( + rpsystem.CmdRecog(), + "barfoo as friend", + "Char will now remember BarFoo Character as friend.", + ) + self.call( + rpsystem.CmdRecog(), + "", + "Currently recognized (use 'recog as ' to add new " + "and 'forget ' to remove):\n friend (BarFoo Character)", + ) + self.call( + rpsystem.CmdRecog(), + "friend", + "Char will now know them only as 'BarFoo Character'", + cmdstring="forget", + ) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py deleted file mode 100644 index 561d10831b..0000000000 --- a/evennia/contrib/tests.py +++ /dev/null @@ -1,3585 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Testing suite for contrib folder - -""" - -import time -import datetime -from anything import Anything -from evennia.commands.default.tests import CommandTest -from evennia.utils.test_resources import EvenniaTest, mockdelay, mockdeferLater -from mock import Mock, patch - -# Testing of rplanguage module - -from evennia.contrib import rplanguage - -mtrans = {"testing": "1", "is": "2", "a": "3", "human": "4"} -atrans = ["An", "automated", "advantageous", "repeatable", "faster"] - -text = ( - "Automated testing is advantageous for a number of reasons: " - "tests may be executed Continuously without the need for human " - "intervention, They are easily repeatable, and often faster." -) - - -class TestLanguage(EvenniaTest): - def setUp(self): - super().setUp() - rplanguage.add_language( - key="testlang", - word_length_variance=1, - noun_prefix="bara", - noun_postfix="'y", - manual_translations=mtrans, - auto_translations=atrans, - force=True, - ) - rplanguage.add_language( - key="binary", - phonemes="oo ii a ck w b d t", - grammar="cvvv cvv cvvcv cvvcvv cvvvc cvvvcvv cvvc", - noun_prefix="beep-", - word_length_variance=4, - ) - - def tearDown(self): - super().tearDown() - rplanguage._LANGUAGE_HANDLER.delete() - rplanguage._LANGUAGE_HANDLER = None - - def test_obfuscate_language(self): - result0 = rplanguage.obfuscate_language(text, level=0.0, language="testlang") - self.assertEqual(result0, text) - result1 = rplanguage.obfuscate_language(text, level=1.0, language="testlang") - result2 = rplanguage.obfuscate_language(text, level=1.0, language="testlang") - result3 = rplanguage.obfuscate_language(text, level=1.0, language="binary") - - self.assertNotEqual(result1, text) - self.assertNotEqual(result3, text) - result1, result2 = result1.split(), result2.split() - self.assertEqual(result1[:4], result2[:4]) - self.assertEqual(result1[1], "1") - self.assertEqual(result1[2], "2") - self.assertEqual(result2[-1], result2[-1]) - - def test_faulty_language(self): - self.assertRaises( - rplanguage.LanguageError, - rplanguage.add_language, - key="binary2", - phonemes="w b d t oe ee, oo e o a wh dw bw", # erroneous comma - grammar="cvvv cvv cvvcv cvvcvvo cvvvc cvvvcvv cvvc c v cc vv ccvvc ccvvccvv ", - vowels="oea", - word_length_variance=4, - ) - - def test_available_languages(self): - self.assertEqual(list(sorted(rplanguage.available_languages())), ["binary", "testlang"]) - - def test_obfuscate_whisper(self): - self.assertEqual(rplanguage.obfuscate_whisper(text, level=0.0), text) - assert rplanguage.obfuscate_whisper(text, level=0.1).startswith( - "-utom-t-d t-sting is -dv-nt-g-ous for - numb-r of r--sons: t-sts m-y b- -x-cut-d Continuously" - ) - assert rplanguage.obfuscate_whisper(text, level=0.5).startswith( - "--------- --s---- -s -----------s f-- - ------ -f ---s--s: --s-s " - ) - self.assertEqual(rplanguage.obfuscate_whisper(text, level=1.0), "...") - - -# Testing of emoting / sdesc / recog system - - -from evennia import create_object -from evennia.contrib import rpsystem - -sdesc0 = "A nice sender of emotes" -sdesc1 = "The first receiver of emotes." -sdesc2 = "Another nice colliding sdesc-guy for tests" -recog01 = "Mr Receiver" -recog02 = "Mr Receiver2" -recog10 = "Mr Sender" -emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."' -case_emote = "/me looks at /first, then /FIRST, /First and /Colliding twice." - - -class TestRPSystem(EvenniaTest): - maxDiff = None - - def setUp(self): - super().setUp() - self.room = create_object(rpsystem.ContribRPRoom, key="Location") - self.speaker = create_object(rpsystem.ContribRPCharacter, key="Sender", location=self.room) - self.receiver1 = create_object( - rpsystem.ContribRPCharacter, key="Receiver1", location=self.room - ) - self.receiver2 = create_object( - rpsystem.ContribRPCharacter, key="Receiver2", location=self.room - ) - - def test_ordered_permutation_regex(self): - self.assertEqual( - rpsystem.ordered_permutation_regex(sdesc0), - "/[0-9]*-*A\\ nice\\ sender\\ of\\ emotes(?=\\W|$)+|" - "/[0-9]*-*nice\\ sender\\ of\\ emotes(?=\\W|$)+|" - "/[0-9]*-*A\\ nice\\ sender\\ of(?=\\W|$)+|" - "/[0-9]*-*sender\\ of\\ emotes(?=\\W|$)+|" - "/[0-9]*-*nice\\ sender\\ of(?=\\W|$)+|" - "/[0-9]*-*A\\ nice\\ sender(?=\\W|$)+|" - "/[0-9]*-*nice\\ sender(?=\\W|$)+|" - "/[0-9]*-*of\\ emotes(?=\\W|$)+|" - "/[0-9]*-*sender\\ of(?=\\W|$)+|" - "/[0-9]*-*A\\ nice(?=\\W|$)+|" - "/[0-9]*-*emotes(?=\\W|$)+|" - "/[0-9]*-*sender(?=\\W|$)+|" - "/[0-9]*-*nice(?=\\W|$)+|" - "/[0-9]*-*of(?=\\W|$)+|" - "/[0-9]*-*A(?=\\W|$)+", - ) - - def test_sdesc_handler(self): - self.speaker.sdesc.add(sdesc0) - self.assertEqual(self.speaker.sdesc.get(), sdesc0) - self.speaker.sdesc.add("This is {#324} ignored") - self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored") - self.speaker.sdesc.add("Testing three words") - self.assertEqual( - self.speaker.sdesc.get_regex_tuple()[0].pattern, - "/[0-9]*-*Testing\ three\ words(?=\W|$)+|" - "/[0-9]*-*Testing\ three(?=\W|$)+|" - "/[0-9]*-*three\ words(?=\W|$)+|" - "/[0-9]*-*Testing(?=\W|$)+|" - "/[0-9]*-*three(?=\W|$)+|" - "/[0-9]*-*words(?=\W|$)+", - ) - - def test_recog_handler(self): - self.speaker.sdesc.add(sdesc0) - self.receiver1.sdesc.add(sdesc1) - self.speaker.recog.add(self.receiver1, recog01) - self.speaker.recog.add(self.receiver2, recog02) - self.assertEqual(self.speaker.recog.get(self.receiver1), recog01) - self.assertEqual(self.speaker.recog.get(self.receiver2), recog02) - self.assertEqual( - self.speaker.recog.get_regex_tuple(self.receiver1)[0].pattern, - "/[0-9]*-*Mr\\ Receiver(?=\\W|$)+|/[0-9]*-*Receiver(?=\\W|$)+|/[0-9]*-*Mr(?=\\W|$)+", - ) - self.speaker.recog.remove(self.receiver1) - self.assertEqual(self.speaker.recog.get(self.receiver1), sdesc1) - - self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2}) - - def test_parse_language(self): - self.assertEqual( - rpsystem.parse_language(self.speaker, emote), - ( - "With a flair, /me looks at /first and /colliding sdesc-guy. She says {##0}", - {"##0": (None, '"This is a test."')}, - ), - ) - - def parse_sdescs_and_recogs(self): - speaker = self.speaker - speaker.sdesc.add(sdesc0) - self.receiver1.sdesc.add(sdesc1) - self.receiver2.sdesc.add(sdesc2) - candidates = (self.receiver1, self.receiver2) - result = ( - 'With a flair, {#9} looks at {#10} and {#11}. She says "This is a test."', - { - "#11": "Another nice colliding sdesc-guy for tests", - "#10": "The first receiver of emotes.", - "#9": "A nice sender of emotes", - }, - ) - self.assertEqual( - rpsystem.parse_sdescs_and_recogs(speaker, candidates, emote, case_sensitive=False), - result, - ) - self.speaker.recog.add(self.receiver1, recog01) - self.assertEqual( - rpsystem.parse_sdescs_and_recogs(speaker, candidates, emote, case_sensitive=False), - result, - ) - - def test_send_emote(self): - speaker = self.speaker - receiver1 = self.receiver1 - receiver2 = self.receiver2 - receivers = [speaker, receiver1, receiver2] - speaker.sdesc.add(sdesc0) - receiver1.sdesc.add(sdesc1) - receiver2.sdesc.add(sdesc2) - speaker.msg = lambda text, **kwargs: setattr(self, "out0", text) - receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text) - receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text) - rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False) - self.assertEqual( - self.out0, - "With a flair, |bSender|n looks at |bThe first receiver of emotes.|n " - 'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n', - ) - self.assertEqual( - self.out1, - "With a flair, |bA nice sender of emotes|n looks at |bReceiver1|n and " - '|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n', - ) - self.assertEqual( - self.out2, - "With a flair, |bA nice sender of emotes|n looks at |bThe first " - 'receiver of emotes.|n and |bReceiver2|n. She says |w"This is a test."|n', - ) - - def test_send_case_sensitive_emote(self): - """Test new case-sensitive rp-parsing""" - speaker = self.speaker - receiver1 = self.receiver1 - receiver2 = self.receiver2 - receivers = [speaker, receiver1, receiver2] - speaker.sdesc.add(sdesc0) - receiver1.sdesc.add(sdesc1) - receiver2.sdesc.add(sdesc2) - speaker.msg = lambda text, **kwargs: setattr(self, "out0", text) - receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text) - receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text) - rpsystem.send_emote(speaker, receivers, case_emote) - self.assertEqual( - self.out0, - "|bSender|n looks at |bthe first receiver of emotes.|n, then " - "|bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n and " - "|bAnother nice colliding sdesc-guy for tests|n twice.", - ) - self.assertEqual( - self.out1, - "|bA nice sender of emotes|n looks at |bReceiver1|n, then |bReceiver1|n, " - "|bReceiver1|n and |bAnother nice colliding sdesc-guy for tests|n twice.", - ) - self.assertEqual( - self.out2, - "|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n, " - "then |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of " - "emotes.|n and |bReceiver2|n twice.", - ) - - def test_rpsearch(self): - self.speaker.sdesc.add(sdesc0) - self.receiver1.sdesc.add(sdesc1) - self.receiver2.sdesc.add(sdesc2) - self.speaker.msg = lambda text, **kwargs: setattr(self, "out0", text) - self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1) - self.assertEqual(self.speaker.search("colliding"), self.receiver2) - - def test_regex_tuple_from_key_alias(self): - self.speaker.aliases.add("foo bar") - self.speaker.aliases.add("this thing is a long thing") - t0 = time.time() - result = rpsystem.regex_tuple_from_key_alias(self.speaker) - t1 = time.time() - result = rpsystem.regex_tuple_from_key_alias(self.speaker) - t2 = time.time() - # print(f"t1: {t1 - t0}, t2: {t2 - t1}") - self.assertLess(t2 - t1, 10 ** -4) - self.assertEqual(result, (Anything, self.speaker, self.speaker.key)) - - -class TestRPSystemCommands(CommandTest): - def setUp(self): - super().setUp() - self.char1.swap_typeclass(rpsystem.ContribRPCharacter) - self.char2.swap_typeclass(rpsystem.ContribRPCharacter) - - def test_commands(self): - - self.call( - rpsystem.CmdSdesc(), "Foobar Character", "Char's sdesc was set to 'Foobar Character'." - ) - self.call( - rpsystem.CmdSdesc(), - "BarFoo Character", - "Char2's sdesc was set to 'BarFoo Character'.", - caller=self.char2, - ) - self.call(rpsystem.CmdSay(), "Hello!", 'Char says, "Hello!"') - self.call(rpsystem.CmdEmote(), "/me smiles to /BarFoo.", "Char smiles to BarFoo Character") - self.call( - rpsystem.CmdPose(), - "stands by the bar", - "Pose will read 'Foobar Character stands by the bar.'.", - ) - self.call( - rpsystem.CmdRecog(), - "barfoo as friend", - "Char will now remember BarFoo Character as friend.", - ) - self.call( - rpsystem.CmdRecog(), - "", - "Currently recognized (use 'recog as ' to add new " - "and 'forget ' to remove):\n friend (BarFoo Character)", - ) - self.call( - rpsystem.CmdRecog(), - "friend", - "Char will now know them only as 'BarFoo Character'", - cmdstring="forget", - ) - - -# Testing of ExtendedRoom contrib - -from django.conf import settings -from evennia.contrib import extended_room -from evennia.objects.objects import DefaultRoom - - -class ForceUTCDatetime(datetime.datetime): - - """Force UTC datetime.""" - - @classmethod - def fromtimestamp(cls, timestamp): - """Force fromtimestamp to run with naive datetimes.""" - return datetime.datetime.utcfromtimestamp(timestamp) - - -@patch("evennia.contrib.extended_room.datetime.datetime", ForceUTCDatetime) -# mock gametime to return April 9, 2064, at 21:06 (spring evening) -@patch("evennia.utils.gametime.gametime", new=Mock(return_value=2975000766)) -class TestExtendedRoom(CommandTest): - room_typeclass = extended_room.ExtendedRoom - DETAIL_DESC = "A test detail." - SPRING_DESC = "A spring description." - OLD_DESC = "Old description." - settings.TIME_ZONE = "UTC" - - def setUp(self): - super().setUp() - self.room1.ndb.last_timeslot = "afternoon" - self.room1.ndb.last_season = "winter" - self.room1.db.details = {"testdetail": self.DETAIL_DESC} - self.room1.db.spring_desc = self.SPRING_DESC - self.room1.db.desc = self.OLD_DESC - - def test_return_appearance(self): - # get the appearance of a non-extended room for contrast purposes - old_desc = DefaultRoom.return_appearance(self.room1, self.char1) - # the new appearance should be the old one, but with the desc switched - self.assertEqual( - old_desc.replace(self.OLD_DESC, self.SPRING_DESC), - self.room1.return_appearance(self.char1), - ) - self.assertEqual("spring", self.room1.ndb.last_season) - self.assertEqual("evening", self.room1.ndb.last_timeslot) - - def test_return_detail(self): - self.assertEqual(self.DETAIL_DESC, self.room1.return_detail("testdetail")) - - def test_cmdextendedlook(self): - rid = self.room1.id - self.call( - extended_room.CmdExtendedRoomLook(), - "here", - "Room(#{})\n{}".format(rid, self.SPRING_DESC), - ) - self.call(extended_room.CmdExtendedRoomLook(), "testdetail", self.DETAIL_DESC) - self.call( - extended_room.CmdExtendedRoomLook(), "nonexistent", "Could not find 'nonexistent'." - ) - - def test_cmdsetdetail(self): - self.call(extended_room.CmdExtendedRoomDetail(), "", "Details on Room") - self.call( - extended_room.CmdExtendedRoomDetail(), - "thingie = newdetail with spaces", - "Detail set 'thingie': 'newdetail with spaces'", - ) - self.call(extended_room.CmdExtendedRoomDetail(), "thingie", "Detail 'thingie' on Room:\n") - self.call( - extended_room.CmdExtendedRoomDetail(), - "/del thingie", - "Detail thingie deleted, if it existed.", - cmdstring="detail", - ) - self.call(extended_room.CmdExtendedRoomDetail(), "thingie", "Detail 'thingie' not found.") - - # Test with aliases - self.call(extended_room.CmdExtendedRoomDetail(), "", "Details on Room") - self.call( - extended_room.CmdExtendedRoomDetail(), - "thingie;other;stuff = newdetail with spaces", - "Detail set 'thingie;other;stuff': 'newdetail with spaces'", - ) - self.call(extended_room.CmdExtendedRoomDetail(), "thingie", "Detail 'thingie' on Room:\n") - self.call(extended_room.CmdExtendedRoomDetail(), "other", "Detail 'other' on Room:\n") - self.call(extended_room.CmdExtendedRoomDetail(), "stuff", "Detail 'stuff' on Room:\n") - self.call( - extended_room.CmdExtendedRoomDetail(), - "/del other;stuff", - "Detail other;stuff deleted, if it existed.", - ) - self.call(extended_room.CmdExtendedRoomDetail(), "other", "Detail 'other' not found.") - self.call(extended_room.CmdExtendedRoomDetail(), "stuff", "Detail 'stuff' not found.") - - def test_cmdgametime(self): - self.call(extended_room.CmdExtendedRoomGameTime(), "", "It's a spring day, in the evening.") - - -# Test the contrib barter system - -from evennia.contrib import barter - - -class TestBarter(CommandTest): - def setUp(self): - super().setUp() - self.tradeitem1 = create_object(key="TradeItem1", location=self.char1) - self.tradeitem2 = create_object(key="TradeItem2", location=self.char1) - self.tradeitem3 = create_object(key="TradeItem3", location=self.char2) - - def test_tradehandler_base(self): - self.char1.msg = Mock() - self.char2.msg = Mock() - # test all methods of the tradehandler - handler = barter.TradeHandler(self.char1, self.char2) - self.assertEqual(handler.part_a, self.char1) - self.assertEqual(handler.part_b, self.char2) - handler.msg_other(self.char1, "Want to trade?") - handler.msg_other(self.char2, "Yes!") - handler.msg_other(None, "Talking to myself...") - self.assertEqual(self.char2.msg.mock_calls[0][1][0], "Want to trade?") - self.assertEqual(self.char1.msg.mock_calls[0][1][0], "Yes!") - self.assertEqual(self.char1.msg.mock_calls[1][1][0], "Talking to myself...") - self.assertEqual(handler.get_other(self.char1), self.char2) - handler.finish(force=True) - - def test_tradehandler_joins(self): - handler = barter.TradeHandler(self.char1, self.char2) - self.assertTrue(handler.join(self.char2)) - self.assertTrue(handler.unjoin(self.char2)) - self.assertFalse(handler.join(self.char1)) - self.assertFalse(handler.unjoin(self.char1)) - handler.finish(force=True) - - def test_tradehandler_offers(self): - handler = barter.TradeHandler(self.char1, self.char2) - handler.join(self.char2) - handler.offer(self.char1, self.tradeitem1, self.tradeitem2) - self.assertEqual(handler.part_a_offers, [self.tradeitem1, self.tradeitem2]) - self.assertFalse(handler.part_a_accepted) - self.assertFalse(handler.part_b_accepted) - handler.offer(self.char2, self.tradeitem3) - self.assertEqual(handler.list(), ([self.tradeitem1, self.tradeitem2], [self.tradeitem3])) - self.assertEqual(handler.search("TradeItem2"), self.tradeitem2) - self.assertEqual(handler.search("TradeItem3"), self.tradeitem3) - self.assertEqual(handler.search("nonexisting"), None) - self.assertFalse(handler.finish()) # should fail since offer not yet accepted - handler.accept(self.char1) - handler.decline(self.char1) - handler.accept(self.char2) - handler.accept(self.char1) # should trigger handler.finish() automatically - self.assertEqual(self.tradeitem1.location, self.char2) - self.assertEqual(self.tradeitem2.location, self.char2) - self.assertEqual(self.tradeitem3.location, self.char1) - - def test_cmdtrade(self): - self.call( - barter.CmdTrade(), - "Char2 : Hey wanna trade?", - 'You say, "Hey wanna trade?"', - caller=self.char1, - ) - self.call(barter.CmdTrade(), "Char decline : Nope!", 'You say, "Nope!"', caller=self.char2) - self.call( - barter.CmdTrade(), - "Char2 : Hey wanna trade?", - 'You say, "Hey wanna trade?"', - caller=self.char1, - ) - self.call(barter.CmdTrade(), "Char accept : Sure!", 'You say, "Sure!"', caller=self.char2) - self.call( - barter.CmdOffer(), - "TradeItem3", - "Your trade action: You offer TradeItem3", - caller=self.char2, - ) - self.call( - barter.CmdOffer(), - "TradeItem1 : Here's my offer.", - 'You say, "Here\'s my offer."\n [You offer TradeItem1]', - ) - self.call( - barter.CmdAccept(), - "", - "Your trade action: You accept the offer. Char2 must now also accept", - ) - self.call( - barter.CmdDecline(), - "", - "Your trade action: You change your mind, declining the current offer.", - ) - self.call( - barter.CmdAccept(), - ": Sounds good.", - 'You say, "Sounds good."\n' " [You accept the offer. Char must now also accept.", - caller=self.char2, - ) - self.call( - barter.CmdDecline(), - ":No way!", - 'You say, "No way!"\n [You change your mind, declining the current offer.]', - caller=self.char2, - ) - self.call( - barter.CmdOffer(), - "TradeItem1, TradeItem2 : My final offer!", - 'You say, "My final offer!"\n [You offer TradeItem1 and TradeItem2]', - ) - self.call( - barter.CmdAccept(), - "", - "Your trade action: You accept the offer. Char2 must now also accept.", - caller=self.char1, - ) - self.call(barter.CmdStatus(), "", "Offered by Char:", caller=self.char2) - self.tradeitem1.db.desc = "A great offer." - self.call(barter.CmdEvaluate(), "TradeItem1", "A great offer.") - self.call( - barter.CmdAccept(), - ":Ok then.", - 'You say, "Ok then."\n [You accept the deal.', - caller=self.char2, - ) - self.assertEqual(self.tradeitem1.location, self.char2) - self.assertEqual(self.tradeitem2.location, self.char2) - self.assertEqual(self.tradeitem3.location, self.char1) - - def test_cmdtradehelp(self): - self.call( - barter.CmdTrade(), - "Char2 : Hey wanna trade?", - 'You say, "Hey wanna trade?"', - caller=self.char1, - ) - self.call(barter.CmdTradeHelp(), "", "Trading commands\n", caller=self.char1) - self.call( - barter.CmdFinish(), - ": Ending.", - 'You say, "Ending."\n [You aborted trade. No deal was made.]', - ) - - -# Test wilderness - - -from evennia.contrib import wilderness -from evennia import DefaultCharacter - - -class TestWilderness(EvenniaTest): - def setUp(self): - super().setUp() - self.char1 = create_object(DefaultCharacter, key="char1") - self.char2 = create_object(DefaultCharacter, key="char2") - - def get_wilderness_script(self, name="default"): - w = wilderness.WildernessScript.objects.get("default") - return w - - def test_create_wilderness_default_name(self): - wilderness.create_wilderness() - w = self.get_wilderness_script() - self.assertIsNotNone(w) - - def test_create_wilderness_custom_name(self): - name = "customname" - wilderness.create_wilderness(name) - w = self.get_wilderness_script(name) - self.assertIsNotNone(w) - - def test_enter_wilderness(self): - wilderness.create_wilderness() - wilderness.enter_wilderness(self.char1) - self.assertIsInstance(self.char1.location, wilderness.WildernessRoom) - w = self.get_wilderness_script() - self.assertEqual(w.db.itemcoordinates[self.char1], (0, 0)) - - def test_enter_wilderness_custom_coordinates(self): - wilderness.create_wilderness() - wilderness.enter_wilderness(self.char1, coordinates=(1, 2)) - self.assertIsInstance(self.char1.location, wilderness.WildernessRoom) - w = self.get_wilderness_script() - self.assertEqual(w.db.itemcoordinates[self.char1], (1, 2)) - - def test_enter_wilderness_custom_name(self): - name = "customnname" - wilderness.create_wilderness(name) - wilderness.enter_wilderness(self.char1, name=name) - self.assertIsInstance(self.char1.location, wilderness.WildernessRoom) - - def test_wilderness_correct_exits(self): - wilderness.create_wilderness() - wilderness.enter_wilderness(self.char1) - - # By default we enter at a corner (0, 0), so only a few exits should - # be visible / traversable - exits = [ - i - for i in self.char1.location.contents - if i.destination and (i.access(self.char1, "view") or i.access(self.char1, "traverse")) - ] - - self.assertEqual(len(exits), 3) - exitsok = ["north", "northeast", "east"] - for each_exit in exitsok: - self.assertTrue(any([e for e in exits if e.key == each_exit])) - - # If we move to another location not on an edge, then all directions - # should be visible / traversable - wilderness.enter_wilderness(self.char1, coordinates=(1, 1)) - exits = [ - i - for i in self.char1.location.contents - if i.destination and (i.access(self.char1, "view") or i.access(self.char1, "traverse")) - ] - self.assertEqual(len(exits), 8) - exitsok = [ - "north", - "northeast", - "east", - "southeast", - "south", - "southwest", - "west", - "northwest", - ] - for each_exit in exitsok: - self.assertTrue(any([e for e in exits if e.key == each_exit])) - - def test_room_creation(self): - # Pretend that both char1 and char2 are connected... - self.char1.sessions.add(1) - self.char2.sessions.add(1) - self.assertTrue(self.char1.has_account) - self.assertTrue(self.char2.has_account) - - wilderness.create_wilderness() - w = self.get_wilderness_script() - - # We should have no unused room after moving the first account in. - self.assertEqual(len(w.db.unused_rooms), 0) - w.move_obj(self.char1, (0, 0)) - self.assertEqual(len(w.db.unused_rooms), 0) - - # And also no unused room after moving the second one in. - w.move_obj(self.char2, (1, 1)) - self.assertEqual(len(w.db.unused_rooms), 0) - - # But if char2 moves into char1's room, we should have one unused room - # Which should be char2's old room that got created. - w.move_obj(self.char2, (0, 0)) - self.assertEqual(len(w.db.unused_rooms), 1) - self.assertEqual(self.char1.location, self.char2.location) - - # And if char2 moves back out, that unused room should be put back to - # use again. - w.move_obj(self.char2, (1, 1)) - self.assertNotEqual(self.char1.location, self.char2.location) - self.assertEqual(len(w.db.unused_rooms), 0) - - def test_get_new_coordinates(self): - loc = (1, 1) - directions = { - "north": (1, 2), - "northeast": (2, 2), - "east": (2, 1), - "southeast": (2, 0), - "south": (1, 0), - "southwest": (0, 0), - "west": (0, 1), - "northwest": (0, 2), - } - for (direction, correct_loc) in directions.items(): # Not compatible with Python 3 - new_loc = wilderness.get_new_coordinates(loc, direction) - self.assertEqual(new_loc, correct_loc, direction) - - -# Testing chargen contrib -from evennia.contrib import chargen - - -class TestChargen(CommandTest): - def test_ooclook(self): - self.call( - chargen.CmdOOCLook(), "foo", "You have no characters to look at", caller=self.account - ) - self.call( - chargen.CmdOOCLook(), - "", - "You, TestAccount, are an OOC ghost without form.", - caller=self.account, - ) - - def test_charcreate(self): - self.call( - chargen.CmdOOCCharacterCreate(), - "testchar", - "The character testchar was successfully created!", - caller=self.account, - ) - self.call( - chargen.CmdOOCCharacterCreate(), - "testchar", - "Character testchar already exists.", - caller=self.account, - ) - self.assertTrue(self.account.db._character_dbrefs) - self.call( - chargen.CmdOOCLook(), - "", - "You, TestAccount, are an OOC ghost without form.", - caller=self.account, - ) - self.call(chargen.CmdOOCLook(), "testchar", "testchar(", caller=self.account) - - -# Testing clothing contrib -from evennia.contrib import clothing - - -class TestClothingCmd(CommandTest): - def test_clothingcommands(self): - wearer = create_object(clothing.ClothedCharacter, key="Wearer") - friend = create_object(clothing.ClothedCharacter, key="Friend") - room = create_object(DefaultRoom, key="room") - wearer.location = room - friend.location = room - # Make a test hat - test_hat = create_object(clothing.Clothing, key="test hat") - test_hat.db.clothing_type = "hat" - test_hat.location = wearer - # Make a test scarf - test_scarf = create_object(clothing.Clothing, key="test scarf") - test_scarf.db.clothing_type = "accessory" - test_scarf.location = wearer - # Test wear command - self.call(clothing.CmdWear(), "", "Usage: wear [wear style]", caller=wearer) - self.call(clothing.CmdWear(), "hat", "Wearer puts on test hat.", caller=wearer) - self.call( - clothing.CmdWear(), - "scarf stylishly", - "Wearer wears test scarf stylishly.", - caller=wearer, - ) - # Test cover command. - self.call( - clothing.CmdCover(), - "", - "Usage: cover [with] ", - caller=wearer, - ) - self.call( - clothing.CmdCover(), - "hat with scarf", - "Wearer covers test hat with test scarf.", - caller=wearer, - ) - # Test remove command. - self.call(clothing.CmdRemove(), "", "Could not find ''.", caller=wearer) - self.call( - clothing.CmdRemove(), "hat", "You have to take off test scarf first.", caller=wearer - ) - self.call( - clothing.CmdRemove(), - "scarf", - "Wearer removes test scarf, revealing test hat.", - caller=wearer, - ) - # Test uncover command. - test_scarf.wear(wearer, True) - test_hat.db.covered_by = test_scarf - self.call(clothing.CmdUncover(), "", "Usage: uncover ", caller=wearer) - self.call(clothing.CmdUncover(), "hat", "Wearer uncovers test hat.", caller=wearer) - # Test drop command. - test_hat.db.covered_by = test_scarf - self.call(clothing.CmdDrop(), "", "Drop what?", caller=wearer) - self.call( - clothing.CmdDrop(), - "hat", - "You can't drop that because it's covered by test scarf.", - caller=wearer, - ) - self.call(clothing.CmdDrop(), "scarf", "You drop test scarf.", caller=wearer) - # Test give command. - self.call( - clothing.CmdGive(), "", "Usage: give = ", caller=wearer - ) - self.call( - clothing.CmdGive(), - "hat = Friend", - "Wearer removes test hat.|You give test hat to Friend.", - caller=wearer, - ) - # Test inventory command. - self.call( - clothing.CmdInventory(), "", "You are not carrying or wearing anything.", caller=wearer - ) - - -class TestClothingFunc(EvenniaTest): - def test_clothingfunctions(self): - wearer = create_object(clothing.ClothedCharacter, key="Wearer") - room = create_object(DefaultRoom, key="room") - wearer.location = room - # Make a test hat - test_hat = create_object(clothing.Clothing, key="test hat") - test_hat.db.clothing_type = "hat" - test_hat.location = wearer - # Make a test shirt - test_shirt = create_object(clothing.Clothing, key="test shirt") - test_shirt.db.clothing_type = "top" - test_shirt.location = wearer - # Make a test pants - test_pants = create_object(clothing.Clothing, key="test pants") - test_pants.db.clothing_type = "bottom" - test_pants.location = wearer - - test_hat.wear(wearer, "on the head") - self.assertEqual(test_hat.db.worn, "on the head") - - test_hat.remove(wearer) - self.assertEqual(test_hat.db.worn, False) - - test_hat.worn = True - test_hat.at_get(wearer) - self.assertEqual(test_hat.db.worn, False) - - clothes_list = [test_shirt, test_hat, test_pants] - self.assertEqual( - clothing.order_clothes_list(clothes_list), [test_hat, test_shirt, test_pants] - ) - - test_hat.wear(wearer, True) - test_pants.wear(wearer, True) - self.assertEqual(clothing.get_worn_clothes(wearer), [test_hat, test_pants]) - - self.assertEqual( - clothing.clothing_type_count(clothes_list), {"hat": 1, "top": 1, "bottom": 1} - ) - - self.assertEqual(clothing.single_type_count(clothes_list, "hat"), 1) - - -# Testing custom_gametime -from evennia.contrib import custom_gametime - - -def _testcallback(): - pass - - -@patch("evennia.utils.gametime.gametime", new=Mock(return_value=2975000898.46)) -class TestCustomGameTime(EvenniaTest): - def tearDown(self): - if hasattr(self, "timescript"): - self.timescript.stop() - - def test_time_to_tuple(self): - self.assertEqual(custom_gametime.time_to_tuple(10000, 34, 2, 4, 6, 1), (294, 2, 0, 0, 0, 0)) - self.assertEqual(custom_gametime.time_to_tuple(10000, 3, 3, 4), (3333, 0, 0, 1)) - self.assertEqual(custom_gametime.time_to_tuple(100000, 239, 24, 3), (418, 4, 0, 2)) - - def test_gametime_to_realtime(self): - self.assertEqual(custom_gametime.gametime_to_realtime(days=2, mins=4), 86520.0) - self.assertEqual( - custom_gametime.gametime_to_realtime(format=True, days=2), (0, 0, 0, 1, 0, 0, 0) - ) - - def test_realtime_to_gametime(self): - self.assertEqual(custom_gametime.realtime_to_gametime(days=3, mins=34), 349680.0) - self.assertEqual( - custom_gametime.realtime_to_gametime(days=3, mins=34, format=True), - (0, 0, 0, 4, 1, 8, 0), - ) - self.assertEqual( - custom_gametime.realtime_to_gametime(format=True, days=3, mins=4), (0, 0, 0, 4, 0, 8, 0) - ) - - def test_custom_gametime(self): - self.assertEqual(custom_gametime.custom_gametime(), (102, 5, 2, 6, 21, 8, 18)) - self.assertEqual(custom_gametime.custom_gametime(absolute=True), (102, 5, 2, 6, 21, 8, 18)) - - def test_real_seconds_until(self): - self.assertEqual( - custom_gametime.real_seconds_until(year=2300, month=12, day=7), 31911667199.77 - ) - - def test_schedule(self): - self.timescript = custom_gametime.schedule(_testcallback, repeat=True, min=5, sec=0) - self.assertEqual(self.timescript.interval, 1700.7699999809265) - - -# Test dice module - -from evennia.contrib import dice # noqa - - -@patch("evennia.contrib.dice.randint", return_value=5) -class TestDice(CommandTest): - def test_roll_dice(self, mocked_randint): - self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4) - self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True) - self.assertEqual(dice.roll_dice(6, 6, conditional=(">", 33)), False) - - def test_cmddice(self, mocked_randint): - self.call( - dice.CmdDice(), "3d6 + 4", "You roll 3d6 + 4.| Roll(s): 5, 5 and 5. Total result is 19." - ) - self.call(dice.CmdDice(), "100000d1000", "The maximum roll allowed is 10000d10000.") - self.call(dice.CmdDice(), "/secret 3d6 + 4", "You roll 3d6 + 4 (secret, not echoed).") - - -# Test email-login - - -from evennia.contrib import email_login # noqa - - -class TestEmailLogin(CommandTest): - def test_connect(self): - self.call( - email_login.CmdUnconnectedConnect(), - "mytest@test.com test", - "The email 'mytest@test.com' does not match any accounts.", - ) - self.call( - email_login.CmdUnconnectedCreate(), - '"mytest" mytest@test.com test11111', - "A new account 'mytest' was created. Welcome!", - ) - self.call( - email_login.CmdUnconnectedConnect(), - "mytest@test.com test11111", - "", - caller=self.account.sessions.get()[0], - ) - - def test_quit(self): - self.call(email_login.CmdUnconnectedQuit(), "", "", caller=self.account.sessions.get()[0]) - - def test_unconnectedlook(self): - self.call(email_login.CmdUnconnectedLook(), "", "==========") - - def test_unconnectedhelp(self): - self.call(email_login.CmdUnconnectedHelp(), "", "You are not yet logged into the game.") - - -# test gendersub contrib - - -from evennia.contrib import gendersub - - -class TestGenderSub(CommandTest): - def test_setgender(self): - self.call(gendersub.SetGender(), "male", "Your gender was set to male.") - self.call(gendersub.SetGender(), "ambiguous", "Your gender was set to ambiguous.") - self.call(gendersub.SetGender(), "Foo", "Usage: @gender") - - def test_gendercharacter(self): - char = create_object(gendersub.GenderCharacter, key="Gendered", location=self.room1) - txt = "Test |p gender" - self.assertEqual( - gendersub._RE_GENDER_PRONOUN.sub(char._get_pronoun, txt), "Test their gender" - ) - with patch("evennia.contrib.gendersub.DefaultCharacter.msg") as mock_msg: - char.db.gender = "female" - char.msg("Test |p gender") - mock_msg.assert_called_with("Test her gender", from_obj=None, session=None) - - -# test health bar contrib - -from evennia.contrib import health_bar - - -class TestHealthBar(EvenniaTest): - def test_healthbar(self): - expected_bar_str = "|[R|w|n|[B|w test0 / 200test |n" - self.assertEqual( - health_bar.display_meter( - 0, 200, length=40, pre_text="test", post_text="test", align="center" - ), - expected_bar_str, - ) - expected_bar_str = "|[R|w |n|[B|w test24 / 200test |n" - self.assertEqual( - health_bar.display_meter( - 24, 200, length=40, pre_text="test", post_text="test", align="center" - ), - expected_bar_str, - ) - expected_bar_str = "|[Y|w test100 /|n|[B|w 200test |n" - self.assertEqual( - health_bar.display_meter( - 100, 200, length=40, pre_text="test", post_text="test", align="center" - ), - expected_bar_str, - ) - expected_bar_str = "|[G|w test180 / 200test |n|[B|w |n" - self.assertEqual( - health_bar.display_meter( - 180, 200, length=40, pre_text="test", post_text="test", align="center" - ), - expected_bar_str, - ) - expected_bar_str = "|[G|w test200 / 200test |n|[B|w|n" - self.assertEqual( - health_bar.display_meter( - 200, 200, length=40, pre_text="test", post_text="test", align="center" - ), - expected_bar_str, - ) - - -# test mail contrib - - -from evennia.contrib import mail - - -class TestMail(CommandTest): - def test_mail(self): - self.call(mail.CmdMail(), "2", "'2' is not a valid mail id.", caller=self.account) - self.call(mail.CmdMail(), "test", "'test' is not a valid mail id.", caller=self.account) - self.call(mail.CmdMail(), "", "There are no messages in your inbox.", caller=self.account) - self.call( - mail.CmdMailCharacter(), - "Char=Message 1", - "You have received a new @mail from Char|You sent your message.", - caller=self.char1, - ) - self.call( - mail.CmdMailCharacter(), "Char=Message 2", "You sent your message.", caller=self.char2 - ) - self.call( - mail.CmdMail(), - "TestAccount2=Message 2", - "You have received a new @mail from TestAccount2", - caller=self.account2, - ) - self.call( - mail.CmdMail(), "TestAccount=Message 1", "You sent your message.", caller=self.account2 - ) - self.call( - mail.CmdMail(), "TestAccount=Message 2", "You sent your message.", caller=self.account2 - ) - self.call(mail.CmdMail(), "", "| ID From Subject", caller=self.account) - self.call(mail.CmdMail(), "2", "From: TestAccount2", caller=self.account) - self.call( - mail.CmdMail(), - "/forward TestAccount2 = 1/Forward message", - "You sent your message.|Message forwarded.", - caller=self.account, - ) - self.call( - mail.CmdMail(), "/reply 2=Reply Message2", "You sent your message.", caller=self.account - ) - self.call(mail.CmdMail(), "/delete 2", "Message 2 deleted", caller=self.account) - - -# test map builder contrib - - -from evennia.contrib import mapbuilder - - -class TestMapBuilder(CommandTest): - def test_cmdmapbuilder(self): - self.call( - mapbuilder.CmdMapBuilder(), - "evennia.contrib.mapbuilder.EXAMPLE1_MAP evennia.contrib.mapbuilder.EXAMPLE1_LEGEND", - """Creating Map...|≈≈≈≈≈ -≈♣n♣≈ -≈∩▲∩≈ -≈♠n♠≈ -≈≈≈≈≈ -|Creating Landmass...|""", - ) - self.call( - mapbuilder.CmdMapBuilder(), - "evennia.contrib.mapbuilder.EXAMPLE2_MAP evennia.contrib.mapbuilder.EXAMPLE2_LEGEND", - """Creating Map...|≈ ≈ ≈ ≈ ≈ - -≈ ♣-♣-♣ ≈ - ≈ ♣ ♣ ♣ ≈ - ≈ ♣-♣-♣ ≈ - -≈ ≈ ≈ ≈ ≈ -|Creating Landmass...|""", - ) - - -# test menu_login - -from evennia.contrib import menu_login - - -class TestMenuLogin(CommandTest): - def test_cmdunloggedlook(self): - self.call(menu_login.CmdUnloggedinLook(), "", "======") - - -# test multidescer contrib - -from evennia.contrib import multidescer - - -class TestMultidescer(CommandTest): - def test_cmdmultidesc(self): - self.call(multidescer.CmdMultiDesc(), "/list", "Stored descs:\ncaller:") - self.call( - multidescer.CmdMultiDesc(), "test = Desc 1", "Stored description 'test': \"Desc 1\"" - ) - self.call( - multidescer.CmdMultiDesc(), "test2 = Desc 2", "Stored description 'test2': \"Desc 2\"" - ) - self.call( - multidescer.CmdMultiDesc(), "/swap test-test2", "Swapped descs 'test' and 'test2'." - ) - self.call( - multidescer.CmdMultiDesc(), - "test3 = Desc 3init", - "Stored description 'test3': \"Desc 3init\"", - ) - self.call( - multidescer.CmdMultiDesc(), - "/list", - "Stored descs:\ntest3: Desc 3init\ntest: Desc 1\ntest2: Desc 2\ncaller:", - ) - self.call( - multidescer.CmdMultiDesc(), "test3 = Desc 3", "Stored description 'test3': \"Desc 3\"" - ) - self.call( - multidescer.CmdMultiDesc(), - "/set test1 + test2 + + test3", - "test1 Desc 2 Desc 3\n\n" "The above was set as the current description.", - ) - self.assertEqual(self.char1.db.desc, "test1 Desc 2 Desc 3") - - -# test simpledoor contrib - - -from evennia.contrib import simpledoor - - -class TestSimpleDoor(CommandTest): - def test_cmdopen(self): - self.call( - simpledoor.CmdOpen(), - "newdoor;door:contrib.simpledoor.SimpleDoor,backdoor;door = Room2", - "Created new Exit 'newdoor' from Room to Room2 (aliases: door).|Note: A door-type exit was " - "created - ignored eventual custom return-exit type.|Created new Exit 'newdoor' from Room2 to Room (aliases: door).", - ) - self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You close newdoor.", cmdstring="close") - self.call( - simpledoor.CmdOpenCloseDoor(), - "newdoor", - "newdoor is already closed.", - cmdstring="close", - ) - self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You open newdoor.", cmdstring="open") - self.call( - simpledoor.CmdOpenCloseDoor(), "newdoor", "newdoor is already open.", cmdstring="open" - ) - - -# test slow_exit contrib - - -from evennia.contrib import slow_exit - -slow_exit.MOVE_DELAY = {"stroll": 0, "walk": 0, "run": 0, "sprint": 0} - - -def _cancellable_mockdelay(time, callback, *args, **kwargs): - callback(*args, **kwargs) - return Mock() - - -class TestSlowExit(CommandTest): - @patch("evennia.utils.delay", _cancellable_mockdelay) - def test_exit(self): - exi = create_object( - slow_exit.SlowExit, key="slowexit", location=self.room1, destination=self.room2 - ) - exi.at_traverse(self.char1, self.room2) - self.call(slow_exit.CmdSetSpeed(), "walk", "You are now walking.") - self.call(slow_exit.CmdStop(), "", "You stop moving.") - - -# test talking npc contrib - - -from evennia.contrib import talking_npc - - -class TestTalkingNPC(CommandTest): - def test_talkingnpc(self): - npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1) - self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)") - npc.delete() - - -# tests for the tutorial world - -# test tutorial_world/mob - -from evennia.contrib.tutorial_world import mob - - -class TestTutorialWorldMob(EvenniaTest): - def test_mob(self): - mobobj = create_object(mob.Mob, key="mob") - self.assertEqual(mobobj.db.is_dead, True) - mobobj.set_alive() - self.assertEqual(mobobj.db.is_dead, False) - mobobj.set_dead() - self.assertEqual(mobobj.db.is_dead, True) - mobobj._set_ticker(0, "foo", stop=True) - # TODO should be expanded with further tests of the modes and damage etc. - - -# test tutorial_world/objects - - -from evennia.contrib.tutorial_world import objects as tutobjects -from mock.mock import MagicMock -from twisted.trial.unittest import TestCase as TwistedTestCase - -from twisted.internet.base import DelayedCall - -DelayedCall.debug = True - - -class TestTutorialWorldObjects(TwistedTestCase, CommandTest): - def test_tutorialobj(self): - obj1 = create_object(tutobjects.TutorialObject, key="tutobj") - obj1.reset() - self.assertEqual(obj1.location, obj1.home) - - def test_readable(self): - readable = create_object(tutobjects.TutorialReadable, key="book", location=self.room1) - readable.db.readable_text = "Text to read" - self.call(tutobjects.CmdRead(), "book", "You read book:\n Text to read", obj=readable) - - def test_climbable(self): - climbable = create_object(tutobjects.TutorialClimbable, key="tree", location=self.room1) - self.call( - tutobjects.CmdClimb(), - "tree", - "You climb tree. Having looked around, you climb down again.", - obj=climbable, - ) - self.assertEqual( - self.char1.tags.get("tutorial_climbed_tree", category="tutorial_world"), - "tutorial_climbed_tree", - ) - - def test_obelisk(self): - obelisk = create_object(tutobjects.Obelisk, key="obelisk", location=self.room1) - self.assertEqual(obelisk.return_appearance(self.char1).startswith("|cobelisk("), True) - - @patch("evennia.contrib.tutorial_world.objects.delay", mockdelay) - @patch("evennia.scripts.taskhandler.deferLater", mockdeferLater) - def test_lightsource(self): - light = create_object(tutobjects.LightSource, key="torch", location=self.room1) - self.call( - tutobjects.CmdLight(), - "", - "A torch on the floor flickers and dies.|You light torch.", - obj=light, - ) - self.assertFalse(light.pk) - - @patch("evennia.contrib.tutorial_world.objects.delay", mockdelay) - @patch("evennia.scripts.taskhandler.deferLater", mockdeferLater) - def test_crumblingwall(self): - wall = create_object(tutobjects.CrumblingWall, key="wall", location=self.room1) - wall.db.destination = self.room2.dbref - self.assertFalse(wall.db.button_exposed) - self.assertFalse(wall.db.exit_open) - wall.db.root_pos = {"yellow": 0, "green": 0, "red": 0, "blue": 0} - self.call( - tutobjects.CmdShiftRoot(), - "blue root right", - "You shove the root adorned with small blue flowers to the right.", - obj=wall, - ) - self.call( - tutobjects.CmdShiftRoot(), - "red root left", - "You shift the reddish root to the left.", - obj=wall, - ) - self.call( - tutobjects.CmdShiftRoot(), - "yellow root down", - "You shove the root adorned with small yellow flowers downwards.", - obj=wall, - ) - self.call( - tutobjects.CmdShiftRoot(), - "green root up", - "You shift the weedy green root upwards.|Holding aside the root you think you notice something behind it ...", - obj=wall, - ) - self.call( - tutobjects.CmdPressButton(), - "", - "You move your fingers over the suspicious depression, then gives it a decisive push. First", - obj=wall, - ) - # we patch out the delay, so these are closed immediately - self.assertFalse(wall.db.button_exposed) - self.assertFalse(wall.db.exit_open) - - def test_weapon(self): - weapon = create_object(tutobjects.TutorialWeapon, key="sword", location=self.char1) - self.call( - tutobjects.CmdAttack(), "Char", "You stab with sword.", obj=weapon, cmdstring="stab" - ) - self.call( - tutobjects.CmdAttack(), "Char", "You slash with sword.", obj=weapon, cmdstring="slash" - ) - - def test_weaponrack(self): - rack = create_object(tutobjects.TutorialWeaponRack, key="rack", location=self.room1) - rack.db.available_weapons = ["sword"] - self.call(tutobjects.CmdGetWeapon(), "", "You find Rusty sword.", obj=rack) - - -# test tutorial_world/ -from evennia.contrib.tutorial_world import rooms as tutrooms - - -class TestTutorialWorldRooms(CommandTest): - def test_cmdtutorial(self): - room = create_object(tutrooms.TutorialRoom, key="tutroom") - self.char1.location = room - self.call(tutrooms.CmdTutorial(), "", "Sorry, there is no tutorial help available here.") - self.call( - tutrooms.CmdTutorialSetDetail(), - "detail;foo;foo2 = A detail", - "Detail set: 'detail;foo;foo2': 'A detail'", - obj=room, - ) - self.call(tutrooms.CmdTutorialLook(), "", "tutroom(", obj=room) - self.call(tutrooms.CmdTutorialLook(), "detail", "A detail", obj=room) - self.call(tutrooms.CmdTutorialLook(), "foo", "A detail", obj=room) - room.delete() - - def test_weatherroom(self): - room = create_object(tutrooms.WeatherRoom, key="weatherroom") - room.update_weather() - tutrooms.TICKER_HANDLER.remove( - interval=room.db.interval, callback=room.update_weather, idstring="tutorial" - ) - room.delete() - - def test_introroom(self): - room = create_object(tutrooms.IntroRoom, key="introroom") - room.at_object_receive(self.char1, self.room1) - - def test_bridgeroom(self): - room = create_object(tutrooms.BridgeRoom, key="bridgeroom") - room.update_weather() - self.char1.move_to(room) - self.call( - tutrooms.CmdBridgeHelp(), - "", - "You are trying hard not to fall off the bridge ...", - obj=room, - ) - self.call( - tutrooms.CmdLookBridge(), - "", - "bridgeroom\nYou are standing very close to the the bridge's western foundation.", - obj=room, - ) - room.at_object_leave(self.char1, self.room1) - tutrooms.TICKER_HANDLER.remove( - interval=room.db.interval, callback=room.update_weather, idstring="tutorial" - ) - room.delete() - - def test_darkroom(self): - room = create_object(tutrooms.DarkRoom, key="darkroom") - self.char1.move_to(room) - self.call(tutrooms.CmdDarkHelp(), "", "Can't help you until") - - def test_teleportroom(self): - create_object(tutrooms.TeleportRoom, key="teleportroom") - - def test_outroroom(self): - create_object(tutrooms.OutroRoom, key="outroroom") - - -# test turnbattle -from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range, tb_items, tb_magic - - -class TestTurnBattleBasicCmd(CommandTest): - - # Test basic combat commands - def test_turnbattlecmd(self): - self.call(tb_basic.CmdFight(), "", "You can't start a fight if you've been defeated!") - self.call(tb_basic.CmdAttack(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_basic.CmdPass(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_basic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.") - - -class TestTurnBattleEquipCmd(CommandTest): - def setUp(self): - super(TestTurnBattleEquipCmd, self).setUp() - self.testweapon = create_object(tb_equip.TBEWeapon, key="test weapon") - self.testarmor = create_object(tb_equip.TBEArmor, key="test armor") - self.testweapon.move_to(self.char1) - self.testarmor.move_to(self.char1) - - # Test equipment commands - def test_turnbattleequipcmd(self): - # Start with equip module specific commands. - self.call(tb_equip.CmdWield(), "weapon", "Char wields test weapon.") - self.call(tb_equip.CmdUnwield(), "", "Char lowers test weapon.") - self.call(tb_equip.CmdDon(), "armor", "Char dons test armor.") - self.call(tb_equip.CmdDoff(), "", "Char removes test armor.") - # Also test the commands that are the same in the basic module - self.call(tb_equip.CmdFight(), "", "You can't start a fight if you've been defeated!") - self.call(tb_equip.CmdAttack(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_equip.CmdPass(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.") - - -class TestTurnBattleRangeCmd(CommandTest): - # Test range commands - def test_turnbattlerangecmd(self): - # Start with range module specific commands. - self.call(tb_range.CmdShoot(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_range.CmdApproach(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_range.CmdWithdraw(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_range.CmdStatus(), "", "HP Remaining: 100 / 100") - # Also test the commands that are the same in the basic module - self.call(tb_range.CmdFight(), "", "There's nobody here to fight!") - self.call(tb_range.CmdAttack(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_range.CmdPass(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_range.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_range.CmdRest(), "", "Char rests to recover HP.") - - -class TestTurnBattleItemsCmd(CommandTest): - def setUp(self): - super(TestTurnBattleItemsCmd, self).setUp() - self.testitem = create_object(key="test item") - self.testitem.move_to(self.char1) - - # Test item commands - def test_turnbattleitemcmd(self): - self.call(tb_items.CmdUse(), "item", "'Test item' is not a usable item.") - # Also test the commands that are the same in the basic module - self.call(tb_items.CmdFight(), "", "You can't start a fight if you've been defeated!") - self.call(tb_items.CmdAttack(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_items.CmdPass(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_items.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_items.CmdRest(), "", "Char rests to recover HP.") - - -class TestTurnBattleMagicCmd(CommandTest): - - # Test magic commands - def test_turnbattlemagiccmd(self): - self.call(tb_magic.CmdStatus(), "", "You have 100 / 100 HP and 20 / 20 MP.") - self.call(tb_magic.CmdLearnSpell(), "test spell", "There is no spell with that name.") - self.call(tb_magic.CmdCast(), "", "Usage: cast = , ") - # Also test the commands that are the same in the basic module - self.call(tb_magic.CmdFight(), "", "There's nobody here to fight!") - self.call(tb_magic.CmdAttack(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_magic.CmdPass(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_magic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") - self.call(tb_magic.CmdRest(), "", "Char rests to recover HP and MP.") - - -class TestTurnBattleBasicFunc(EvenniaTest): - def setUp(self): - super(TestTurnBattleBasicFunc, self).setUp() - self.testroom = create_object(DefaultRoom, key="Test Room") - self.attacker = create_object( - tb_basic.TBBasicCharacter, key="Attacker", location=self.testroom - ) - self.defender = create_object( - tb_basic.TBBasicCharacter, key="Defender", location=self.testroom - ) - self.joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner", location=None) - - def tearDown(self): - super(TestTurnBattleBasicFunc, self).tearDown() - self.turnhandler.stop() - self.testroom.delete() - self.attacker.delete() - self.defender.delete() - self.joiner.delete() - - # Test combat functions - def test_tbbasicfunc(self): - # Initiative roll - initiative = tb_basic.roll_init(self.attacker) - self.assertTrue(initiative >= 0 and initiative <= 1000) - # Attack roll - attack_roll = tb_basic.get_attack(self.attacker, self.defender) - self.assertTrue(attack_roll >= 0 and attack_roll <= 100) - # Defense roll - defense_roll = tb_basic.get_defense(self.attacker, self.defender) - self.assertTrue(defense_roll == 50) - # Damage roll - damage_roll = tb_basic.get_damage(self.attacker, self.defender) - self.assertTrue(damage_roll >= 15 and damage_roll <= 25) - # Apply damage - self.defender.db.hp = 10 - tb_basic.apply_damage(self.defender, 3) - self.assertTrue(self.defender.db.hp == 7) - # Resolve attack - self.defender.db.hp = 40 - tb_basic.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) - self.assertTrue(self.defender.db.hp < 40) - # Combat cleanup - self.attacker.db.Combat_attribute = True - tb_basic.combat_cleanup(self.attacker) - self.assertFalse(self.attacker.db.combat_attribute) - # Is in combat - self.assertFalse(tb_basic.is_in_combat(self.attacker)) - # Set up turn handler script for further tests - self.attacker.location.scripts.add(tb_basic.TBBasicTurnHandler) - self.turnhandler = self.attacker.db.combat_TurnHandler - self.assertTrue(self.attacker.db.combat_TurnHandler) - # Set the turn handler's interval very high to keep it from repeating during tests. - self.turnhandler.interval = 10000 - # Force turn order - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - # Test is turn - self.assertTrue(tb_basic.is_turn(self.attacker)) - # Spend actions - self.attacker.db.Combat_ActionsLeft = 1 - tb_basic.spend_action(self.attacker, 1, action_name="Test") - self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(self.attacker.db.Combat_LastAction == "Test") - # Initialize for combat - self.attacker.db.Combat_ActionsLeft = 983 - self.turnhandler.initialize_for_combat(self.attacker) - self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(self.attacker.db.Combat_LastAction == "null") - # Start turn - self.defender.db.Combat_ActionsLeft = 0 - self.turnhandler.start_turn(self.defender) - self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) - # Next turn - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.turnhandler.next_turn() - self.assertTrue(self.turnhandler.db.turn == 1) - # Turn end check - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.attacker.db.Combat_ActionsLeft = 0 - self.turnhandler.turn_end_check(self.attacker) - self.assertTrue(self.turnhandler.db.turn == 1) - # Join fight - self.joiner.location = self.testroom - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.turnhandler.join_fight(self.joiner) - self.assertTrue(self.turnhandler.db.turn == 1) - self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) - - -class TestTurnBattleEquipFunc(EvenniaTest): - def setUp(self): - super(TestTurnBattleEquipFunc, self).setUp() - self.testroom = create_object(DefaultRoom, key="Test Room") - self.attacker = create_object( - tb_equip.TBEquipCharacter, key="Attacker", location=self.testroom - ) - self.defender = create_object( - tb_equip.TBEquipCharacter, key="Defender", location=self.testroom - ) - self.joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner", location=None) - - def tearDown(self): - super(TestTurnBattleEquipFunc, self).tearDown() - self.turnhandler.stop() - self.testroom.delete() - self.attacker.delete() - self.defender.delete() - self.joiner.delete() - - # Test the combat functions in tb_equip too. They work mostly the same. - def test_tbequipfunc(self): - # Initiative roll - initiative = tb_equip.roll_init(self.attacker) - self.assertTrue(initiative >= 0 and initiative <= 1000) - # Attack roll - attack_roll = tb_equip.get_attack(self.attacker, self.defender) - self.assertTrue(attack_roll >= -50 and attack_roll <= 150) - # Defense roll - defense_roll = tb_equip.get_defense(self.attacker, self.defender) - self.assertTrue(defense_roll == 50) - # Damage roll - damage_roll = tb_equip.get_damage(self.attacker, self.defender) - self.assertTrue(damage_roll >= 0 and damage_roll <= 50) - # Apply damage - self.defender.db.hp = 10 - tb_equip.apply_damage(self.defender, 3) - self.assertTrue(self.defender.db.hp == 7) - # Resolve attack - self.defender.db.hp = 40 - tb_equip.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) - self.assertTrue(self.defender.db.hp < 40) - # Combat cleanup - self.attacker.db.Combat_attribute = True - tb_equip.combat_cleanup(self.attacker) - self.assertFalse(self.attacker.db.combat_attribute) - # Is in combat - self.assertFalse(tb_equip.is_in_combat(self.attacker)) - # Set up turn handler script for further tests - self.attacker.location.scripts.add(tb_equip.TBEquipTurnHandler) - self.turnhandler = self.attacker.db.combat_TurnHandler - self.assertTrue(self.attacker.db.combat_TurnHandler) - # Set the turn handler's interval very high to keep it from repeating during tests. - self.turnhandler.interval = 10000 - # Force turn order - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - # Test is turn - self.assertTrue(tb_equip.is_turn(self.attacker)) - # Spend actions - self.attacker.db.Combat_ActionsLeft = 1 - tb_equip.spend_action(self.attacker, 1, action_name="Test") - self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(self.attacker.db.Combat_LastAction == "Test") - # Initialize for combat - self.attacker.db.Combat_ActionsLeft = 983 - self.turnhandler.initialize_for_combat(self.attacker) - self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(self.attacker.db.Combat_LastAction == "null") - # Start turn - self.defender.db.Combat_ActionsLeft = 0 - self.turnhandler.start_turn(self.defender) - self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) - # Next turn - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.turnhandler.next_turn() - self.assertTrue(self.turnhandler.db.turn == 1) - # Turn end check - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.attacker.db.Combat_ActionsLeft = 0 - self.turnhandler.turn_end_check(self.attacker) - self.assertTrue(self.turnhandler.db.turn == 1) - # Join fight - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.turnhandler.join_fight(self.joiner) - self.assertTrue(self.turnhandler.db.turn == 1) - self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) - - -class TestTurnBattleRangeFunc(EvenniaTest): - def setUp(self): - super(TestTurnBattleRangeFunc, self).setUp() - self.testroom = create_object(DefaultRoom, key="Test Room") - self.attacker = create_object( - tb_range.TBRangeCharacter, key="Attacker", location=self.testroom - ) - self.defender = create_object( - tb_range.TBRangeCharacter, key="Defender", location=self.testroom - ) - self.joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=self.testroom) - - def tearDown(self): - super(TestTurnBattleRangeFunc, self).tearDown() - self.turnhandler.stop() - self.testroom.delete() - self.attacker.delete() - self.defender.delete() - self.joiner.delete() - - # Test combat functions in tb_range too. - def test_tbrangefunc(self): - # Initiative roll - initiative = tb_range.roll_init(self.attacker) - self.assertTrue(initiative >= 0 and initiative <= 1000) - # Attack roll - attack_roll = tb_range.get_attack(self.attacker, self.defender, "test") - self.assertTrue(attack_roll >= 0 and attack_roll <= 100) - # Defense roll - defense_roll = tb_range.get_defense(self.attacker, self.defender, "test") - self.assertTrue(defense_roll == 50) - # Damage roll - damage_roll = tb_range.get_damage(self.attacker, self.defender) - self.assertTrue(damage_roll >= 15 and damage_roll <= 25) - # Apply damage - self.defender.db.hp = 10 - tb_range.apply_damage(self.defender, 3) - self.assertTrue(self.defender.db.hp == 7) - # Resolve attack - self.defender.db.hp = 40 - tb_range.resolve_attack( - self.attacker, self.defender, "test", attack_value=20, defense_value=10 - ) - self.assertTrue(self.defender.db.hp < 40) - # Combat cleanup - self.attacker.db.Combat_attribute = True - tb_range.combat_cleanup(self.attacker) - self.assertFalse(self.attacker.db.combat_attribute) - # Is in combat - self.assertFalse(tb_range.is_in_combat(self.attacker)) - # Set up turn handler script for further tests - self.attacker.location.scripts.add(tb_range.TBRangeTurnHandler) - self.turnhandler = self.attacker.db.combat_TurnHandler - self.assertTrue(self.attacker.db.combat_TurnHandler) - # Set the turn handler's interval very high to keep it from repeating during tests. - self.turnhandler.interval = 10000 - # Force turn order - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - # Test is turn - self.assertTrue(tb_range.is_turn(self.attacker)) - # Spend actions - self.attacker.db.Combat_ActionsLeft = 1 - tb_range.spend_action(self.attacker, 1, action_name="Test") - self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(self.attacker.db.Combat_LastAction == "Test") - # Initialize for combat - self.attacker.db.Combat_ActionsLeft = 983 - self.turnhandler.initialize_for_combat(self.attacker) - self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(self.attacker.db.Combat_LastAction == "null") - # Set up ranges again, since initialize_for_combat clears them - self.attacker.db.combat_range = {} - self.attacker.db.combat_range[self.attacker] = 0 - self.attacker.db.combat_range[self.defender] = 1 - self.defender.db.combat_range = {} - self.defender.db.combat_range[self.defender] = 0 - self.defender.db.combat_range[self.attacker] = 1 - # Start turn - self.defender.db.Combat_ActionsLeft = 0 - self.turnhandler.start_turn(self.defender) - self.assertTrue(self.defender.db.Combat_ActionsLeft == 2) - # Next turn - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.turnhandler.next_turn() - self.assertTrue(self.turnhandler.db.turn == 1) - # Turn end check - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.attacker.db.Combat_ActionsLeft = 0 - self.turnhandler.turn_end_check(self.attacker) - self.assertTrue(self.turnhandler.db.turn == 1) - # Join fight - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.turnhandler.join_fight(self.joiner) - self.assertTrue(self.turnhandler.db.turn == 1) - self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) - # Now, test for approach/withdraw functions - self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 1) - # Approach - tb_range.approach(self.attacker, self.defender) - self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 0) - # Withdraw - tb_range.withdraw(self.attacker, self.defender) - self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 1) - - -class TestTurnBattleItemsFunc(EvenniaTest): - @patch("evennia.contrib.turnbattle.tb_items.tickerhandler", new=MagicMock()) - def setUp(self): - super(TestTurnBattleItemsFunc, self).setUp() - self.testroom = create_object(DefaultRoom, key="Test Room") - self.attacker = create_object( - tb_items.TBItemsCharacter, key="Attacker", location=self.testroom - ) - self.defender = create_object( - tb_items.TBItemsCharacter, key="Defender", location=self.testroom - ) - self.joiner = create_object(tb_items.TBItemsCharacter, key="Joiner", location=self.testroom) - self.user = create_object(tb_items.TBItemsCharacter, key="User", location=self.testroom) - self.test_healpotion = create_object(key="healing potion") - self.test_healpotion.db.item_func = "heal" - self.test_healpotion.db.item_uses = 3 - - def tearDown(self): - super(TestTurnBattleItemsFunc, self).tearDown() - self.turnhandler.stop() - self.testroom.delete() - self.attacker.delete() - self.defender.delete() - self.joiner.delete() - self.user.delete() - - # Test functions in tb_items. - def test_tbitemsfunc(self): - # Initiative roll - initiative = tb_items.roll_init(self.attacker) - self.assertTrue(initiative >= 0 and initiative <= 1000) - # Attack roll - attack_roll = tb_items.get_attack(self.attacker, self.defender) - self.assertTrue(attack_roll >= 0 and attack_roll <= 100) - # Defense roll - defense_roll = tb_items.get_defense(self.attacker, self.defender) - self.assertTrue(defense_roll == 50) - # Damage roll - damage_roll = tb_items.get_damage(self.attacker, self.defender) - self.assertTrue(damage_roll >= 15 and damage_roll <= 25) - # Apply damage - self.defender.db.hp = 10 - tb_items.apply_damage(self.defender, 3) - self.assertTrue(self.defender.db.hp == 7) - # Resolve attack - self.defender.db.hp = 40 - tb_items.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) - self.assertTrue(self.defender.db.hp < 40) - # Combat cleanup - self.attacker.db.Combat_attribute = True - tb_items.combat_cleanup(self.attacker) - self.assertFalse(self.attacker.db.combat_attribute) - # Is in combat - self.assertFalse(tb_items.is_in_combat(self.attacker)) - # Set up turn handler script for further tests - self.attacker.location.scripts.add(tb_items.TBItemsTurnHandler) - self.turnhandler = self.attacker.db.combat_TurnHandler - self.assertTrue(self.attacker.db.combat_TurnHandler) - # Set the turn handler's interval very high to keep it from repeating during tests. - self.turnhandler.interval = 10000 - # Force turn order - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - # Test is turn - self.assertTrue(tb_items.is_turn(self.attacker)) - # Spend actions - self.attacker.db.Combat_ActionsLeft = 1 - tb_items.spend_action(self.attacker, 1, action_name="Test") - self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(self.attacker.db.Combat_LastAction == "Test") - # Initialize for combat - self.attacker.db.Combat_ActionsLeft = 983 - self.turnhandler.initialize_for_combat(self.attacker) - self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(self.attacker.db.Combat_LastAction == "null") - # Start turn - self.defender.db.Combat_ActionsLeft = 0 - self.turnhandler.start_turn(self.defender) - self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) - # Next turn - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.turnhandler.next_turn() - self.assertTrue(self.turnhandler.db.turn == 1) - # Turn end check - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.attacker.db.Combat_ActionsLeft = 0 - self.turnhandler.turn_end_check(self.attacker) - self.assertTrue(self.turnhandler.db.turn == 1) - # Join fight - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.turnhandler.join_fight(self.joiner) - self.assertTrue(self.turnhandler.db.turn == 1) - self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) - # Now time to test item stuff. - # Spend item use - tb_items.spend_item_use(self.test_healpotion, self.user) - self.assertTrue(self.test_healpotion.db.item_uses == 2) - # Use item - self.user.db.hp = 2 - tb_items.use_item(self.user, self.test_healpotion, self.user) - self.assertTrue(self.user.db.hp > 2) - # Add contition - tb_items.add_condition(self.user, self.user, "Test", 5) - self.assertTrue(self.user.db.conditions == {"Test": [5, self.user]}) - # Condition tickdown - tb_items.condition_tickdown(self.user, self.user) - self.assertTrue(self.user.db.conditions == {"Test": [4, self.user]}) - # Test item functions now! - # Item heal - self.user.db.hp = 2 - tb_items.itemfunc_heal(self.test_healpotion, self.user, self.user) - # Item add condition - self.user.db.conditions = {} - tb_items.itemfunc_add_condition(self.test_healpotion, self.user, self.user) - self.assertTrue(self.user.db.conditions == {"Regeneration": [5, self.user]}) - # Item cure condition - self.user.db.conditions = {"Poisoned": [5, self.user]} - tb_items.itemfunc_cure_condition(self.test_healpotion, self.user, self.user) - self.assertTrue(self.user.db.conditions == {}) - - -class TestTurnBattleMagicFunc(EvenniaTest): - def setUp(self): - super(TestTurnBattleMagicFunc, self).setUp() - self.testroom = create_object(DefaultRoom, key="Test Room") - self.attacker = create_object( - tb_magic.TBMagicCharacter, key="Attacker", location=self.testroom - ) - self.defender = create_object( - tb_magic.TBMagicCharacter, key="Defender", location=self.testroom - ) - self.joiner = create_object(tb_magic.TBMagicCharacter, key="Joiner", location=self.testroom) - - def tearDown(self): - super(TestTurnBattleMagicFunc, self).tearDown() - self.turnhandler.stop() - self.testroom.delete() - self.attacker.delete() - self.defender.delete() - self.joiner.delete() - - # Test combat functions in tb_magic. - def test_tbbasicfunc(self): - # Initiative roll - initiative = tb_magic.roll_init(self.attacker) - self.assertTrue(initiative >= 0 and initiative <= 1000) - # Attack roll - attack_roll = tb_magic.get_attack(self.attacker, self.defender) - self.assertTrue(attack_roll >= 0 and attack_roll <= 100) - # Defense roll - defense_roll = tb_magic.get_defense(self.attacker, self.defender) - self.assertTrue(defense_roll == 50) - # Damage roll - damage_roll = tb_magic.get_damage(self.attacker, self.defender) - self.assertTrue(damage_roll >= 15 and damage_roll <= 25) - # Apply damage - self.defender.db.hp = 10 - tb_magic.apply_damage(self.defender, 3) - self.assertTrue(self.defender.db.hp == 7) - # Resolve attack - self.defender.db.hp = 40 - tb_magic.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) - self.assertTrue(self.defender.db.hp < 40) - # Combat cleanup - self.attacker.db.Combat_attribute = True - tb_magic.combat_cleanup(self.attacker) - self.assertFalse(self.attacker.db.combat_attribute) - # Is in combat - self.assertFalse(tb_magic.is_in_combat(self.attacker)) - # Set up turn handler script for further tests - self.attacker.location.scripts.add(tb_magic.TBMagicTurnHandler) - self.turnhandler = self.attacker.db.combat_TurnHandler - self.assertTrue(self.attacker.db.combat_TurnHandler) - # Set the turn handler's interval very high to keep it from repeating during tests. - self.turnhandler.interval = 10000 - # Force turn order - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - # Test is turn - self.assertTrue(tb_magic.is_turn(self.attacker)) - # Spend actions - self.attacker.db.Combat_ActionsLeft = 1 - tb_magic.spend_action(self.attacker, 1, action_name="Test") - self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(self.attacker.db.Combat_LastAction == "Test") - # Initialize for combat - self.attacker.db.Combat_ActionsLeft = 983 - self.turnhandler.initialize_for_combat(self.attacker) - self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(self.attacker.db.Combat_LastAction == "null") - # Start turn - self.defender.db.Combat_ActionsLeft = 0 - self.turnhandler.start_turn(self.defender) - self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) - # Next turn - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.turnhandler.next_turn() - self.assertTrue(self.turnhandler.db.turn == 1) - # Turn end check - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.attacker.db.Combat_ActionsLeft = 0 - self.turnhandler.turn_end_check(self.attacker) - self.assertTrue(self.turnhandler.db.turn == 1) - # Join fight - self.turnhandler.db.fighters = [self.attacker, self.defender] - self.turnhandler.db.turn = 0 - self.turnhandler.join_fight(self.joiner) - self.assertTrue(self.turnhandler.db.turn == 1) - self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) - - -# Test tree select - -from evennia.contrib import tree_select - -TREE_MENU_TESTSTR = """Foo -Bar --Baz ---Baz 1 ---Baz 2 --Qux""" - - -class TestTreeSelectFunc(EvenniaTest): - def test_tree_functions(self): - # Dash counter - self.assertTrue(tree_select.dashcount("--test") == 2) - # Is category - self.assertTrue(tree_select.is_category(TREE_MENU_TESTSTR, 1) == True) - # Parse options - self.assertTrue( - tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2) - == [(3, "Baz 1"), (4, "Baz 2")] - ) - # Index to selection - self.assertTrue(tree_select.index_to_selection(TREE_MENU_TESTSTR, 2) == "Baz") - # Go up one category - self.assertTrue(tree_select.go_up_one_category(TREE_MENU_TESTSTR, 4) == 2) - # Option list to menu options - test_optlist = tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2) - optlist_to_menu_expected_result = [ - {"goto": ["menunode_treeselect", {"newindex": 3}], "key": "Baz 1"}, - {"goto": ["menunode_treeselect", {"newindex": 4}], "key": "Baz 2"}, - { - "goto": ["menunode_treeselect", {"newindex": 1}], - "key": ["<< Go Back", "go back", "back"], - "desc": "Return to the previous menu.", - }, - ] - self.assertTrue( - tree_select.optlist_to_menuoptions(TREE_MENU_TESTSTR, test_optlist, 2, True, True) - == optlist_to_menu_expected_result - ) - - -# Test field fill - -from evennia.contrib import fieldfill - -FIELD_TEST_TEMPLATE = [ - {"fieldname": "TextTest", "fieldtype": "text"}, - {"fieldname": "NumberTest", "fieldtype": "number", "blankmsg": "Number here!"}, - {"fieldname": "DefaultText", "fieldtype": "text", "default": "Test"}, - {"fieldname": "DefaultNum", "fieldtype": "number", "default": 3}, -] - -FIELD_TEST_DATA = {"TextTest": None, "NumberTest": None, "DefaultText": "Test", "DefaultNum": 3} - - -class TestFieldFillFunc(EvenniaTest): - def test_field_functions(self): - self.assertTrue(fieldfill.form_template_to_dict(FIELD_TEST_TEMPLATE) == FIELD_TEST_DATA) - - -# Test of the unixcommand module - -from evennia.contrib.unixcommand import UnixCommand - - -class CmdDummy(UnixCommand): - - """A dummy UnixCommand.""" - - key = "dummy" - - def init_parser(self): - """Fill out options.""" - self.parser.add_argument("nb1", type=int, help="the first number") - self.parser.add_argument("nb2", type=int, help="the second number") - self.parser.add_argument("-v", "--verbose", action="store_true") - - def func(self): - nb1 = self.opts.nb1 - nb2 = self.opts.nb2 - result = nb1 * nb2 - verbose = self.opts.verbose - if verbose: - self.msg("{} times {} is {}".format(nb1, nb2, result)) - else: - self.msg("{} * {} = {}".format(nb1, nb2, result)) - - -class TestUnixCommand(CommandTest): - def test_success(self): - """See the command parsing succeed.""" - self.call(CmdDummy(), "5 10", "5 * 10 = 50") - self.call(CmdDummy(), "5 10 -v", "5 times 10 is 50") - - def test_failure(self): - """If not provided with the right info, should fail.""" - ret = self.call(CmdDummy(), "5") - lines = ret.splitlines() - self.assertTrue(any(l.startswith("usage:") for l in lines)) - self.assertTrue(any(l.startswith("dummy: error:") for l in lines)) - - # If we specify an incorrect number as parameter - ret = self.call(CmdDummy(), "five ten") - lines = ret.splitlines() - self.assertTrue(any(l.startswith("usage:") for l in lines)) - self.assertTrue(any(l.startswith("dummy: error:") for l in lines)) - - -import re -from evennia.contrib import color_markups - - -class TestColorMarkup(EvenniaTest): - """ - Note: Normally this would be tested by importing the ansi parser and run - the mappings through it. This is not possible since the ansi module creates - its mapping at the module/class level; since the ansi module is used by so - many other modules it appears that trying to overload - settings to test it causes issues with unrelated tests. - """ - - def test_curly_markup(self): - ansi_map = color_markups.CURLY_COLOR_ANSI_EXTRA_MAP - self.assertIsNotNone(re.match(re.escape(ansi_map[7][0]), "{r")) - self.assertIsNotNone(re.match(re.escape(ansi_map[-1][0]), "{[X")) - xterm_fg = color_markups.CURLY_COLOR_XTERM256_EXTRA_FG - self.assertIsNotNone(re.match(xterm_fg[0], "{001")) - self.assertIsNotNone(re.match(xterm_fg[0], "{123")) - self.assertIsNotNone(re.match(xterm_fg[0], "{455")) - xterm_bg = color_markups.CURLY_COLOR_XTERM256_EXTRA_BG - self.assertIsNotNone(re.match(xterm_bg[0], "{[001")) - self.assertIsNotNone(re.match(xterm_bg[0], "{[123")) - self.assertIsNotNone(re.match(xterm_bg[0], "{[455")) - xterm_gfg = color_markups.CURLY_COLOR_XTERM256_EXTRA_GFG - self.assertIsNotNone(re.match(xterm_gfg[0], "{=h")) - self.assertIsNotNone(re.match(xterm_gfg[0], "{=e")) - self.assertIsNotNone(re.match(xterm_gfg[0], "{=w")) - xterm_gbg = color_markups.CURLY_COLOR_XTERM256_EXTRA_GBG - self.assertIsNotNone(re.match(xterm_gbg[0], "{[=a")) - self.assertIsNotNone(re.match(xterm_gbg[0], "{[=k")) - self.assertIsNotNone(re.match(xterm_gbg[0], "{[=z")) - bright_map = color_markups.CURLY_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP - self.assertEqual(bright_map[0][1], "{[500") - self.assertEqual(bright_map[-1][1], "{[222") - - def test_mux_markup(self): - ansi_map = color_markups.MUX_COLOR_ANSI_EXTRA_MAP - self.assertIsNotNone(re.match(re.escape(ansi_map[10][0]), "%cr")) - self.assertIsNotNone(re.match(re.escape(ansi_map[-1][0]), "%cX")) - xterm_fg = color_markups.MUX_COLOR_XTERM256_EXTRA_FG - self.assertIsNotNone(re.match(xterm_fg[0], "%c001")) - self.assertIsNotNone(re.match(xterm_fg[0], "%c123")) - self.assertIsNotNone(re.match(xterm_fg[0], "%c455")) - xterm_bg = color_markups.MUX_COLOR_XTERM256_EXTRA_BG - self.assertIsNotNone(re.match(xterm_bg[0], "%c[001")) - self.assertIsNotNone(re.match(xterm_bg[0], "%c[123")) - self.assertIsNotNone(re.match(xterm_bg[0], "%c[455")) - xterm_gfg = color_markups.MUX_COLOR_XTERM256_EXTRA_GFG - self.assertIsNotNone(re.match(xterm_gfg[0], "%c=h")) - self.assertIsNotNone(re.match(xterm_gfg[0], "%c=e")) - self.assertIsNotNone(re.match(xterm_gfg[0], "%c=w")) - xterm_gbg = color_markups.MUX_COLOR_XTERM256_EXTRA_GBG - self.assertIsNotNone(re.match(xterm_gbg[0], "%c[=a")) - self.assertIsNotNone(re.match(xterm_gbg[0], "%c[=k")) - self.assertIsNotNone(re.match(xterm_gbg[0], "%c[=z")) - bright_map = color_markups.MUX_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP - self.assertEqual(bright_map[0][1], "%c[500") - self.assertEqual(bright_map[-1][1], "%c[222") - - -from evennia.contrib import random_string_generator - -SIMPLE_GENERATOR = random_string_generator.RandomStringGenerator("simple", "[01]{2}") - - -class TestRandomStringGenerator(EvenniaTest): - def test_generate(self): - """Generate and fail when exhausted.""" - generated = [] - for i in range(4): - generated.append(SIMPLE_GENERATOR.get()) - - generated.sort() - self.assertEqual(generated, ["00", "01", "10", "11"]) - - # At this point, we have generated 4 strings. - # We can't generate one more - with self.assertRaises(random_string_generator.ExhaustedGenerator): - SIMPLE_GENERATOR.get() - - -# Test of the Puzzles module - -import itertools -from evennia.contrib import puzzles -from evennia.utils import search - - -class TestPuzzles(CommandTest): - def setUp(self): - super(TestPuzzles, self).setUp() - self.steel = create_object(self.object_typeclass, key="steel", location=self.char1.location) - self.flint = create_object(self.object_typeclass, key="flint", location=self.char1.location) - self.fire = create_object(self.object_typeclass, key="fire", location=self.char1.location) - self.steel.tags.add("tag-steel") - self.steel.tags.add("tag-steel", category="tagcat") - self.flint.tags.add("tag-flint") - self.flint.tags.add("tag-flint", category="tagcat") - self.fire.tags.add("tag-fire") - self.fire.tags.add("tag-fire", category="tagcat") - - def _assert_msg_matched(self, msg, regexs, re_flags=0): - matches = [] - for regex in regexs: - m = re.search(regex, msg, re_flags) - self.assertIsNotNone(m, "%r didn't match %r" % (regex, msg)) - matches.append(m) - return matches - - def _assert_recipe(self, name, parts, results, and_destroy_it=True, expected_count=1): - def _keys(items): - return [item["key"] for item in items] - - recipes = search.search_script_tag("", category=puzzles._PUZZLES_TAG_CATEGORY) - self.assertEqual(expected_count, len(recipes)) - self.assertEqual(name, recipes[expected_count - 1].db.puzzle_name) - self.assertEqual(parts, _keys(recipes[expected_count - 1].db.parts)) - self.assertEqual(results, _keys(recipes[expected_count - 1].db.results)) - self.assertEqual( - puzzles._PUZZLES_TAG_RECIPE, - recipes[expected_count - 1].tags.get(category=puzzles._PUZZLES_TAG_CATEGORY), - ) - recipe_dbref = recipes[expected_count - 1].dbref - if and_destroy_it: - recipes[expected_count - 1].delete() - return recipe_dbref if not and_destroy_it else None - - def _assert_no_recipes(self): - self.assertEqual( - 0, len(search.search_script_tag("", category=puzzles._PUZZLES_TAG_CATEGORY)) - ) - - # good recipes - def _good_recipe(self, name, parts, results, and_destroy_it=True, expected_count=1): - regexs = [] - for p in parts: - regexs.append(r"^Part %s\(#\d+\)$" % (p)) - for r in results: - regexs.append(r"^Result %s\(#\d+\)$" % (r)) - regexs.append(r"^Puzzle '%s' %s\(#\d+\) has been created successfully.$" % (name, name)) - lhs = [name] + parts - cmdstr = ",".join(lhs) + "=" + ",".join(results) - msg = self.call(puzzles.CmdCreatePuzzleRecipe(), cmdstr, caller=self.char1) - recipe_dbref = self._assert_recipe(name, parts, results, and_destroy_it, expected_count) - self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) - return recipe_dbref - - def _check_room_contents(self, expected, check_test_tags=False): - by_obj_key = lambda o: o.key - room1_contents = sorted(self.room1.contents, key=by_obj_key) - for key, grp in itertools.groupby(room1_contents, by_obj_key): - if key in expected: - grp = list(grp) - self.assertEqual( - expected[key], - len(grp), - "Expected %d but got %d for %s" % (expected[key], len(grp), key), - ) - if check_test_tags: - for gi in grp: - tags = gi.tags.all(return_key_and_category=True) - self.assertIn(("tag-" + gi.key, "tagcat"), tags) - - def _arm(self, recipe_dbref, name, parts): - regexs = [ - r"^Puzzle Recipe %s\(#\d+\) '%s' found.$" % (name, name), - r"^Spawning %d parts ...$" % (len(parts)), - ] - for p in parts: - regexs.append(r"^Part %s\(#\d+\) spawned .*$" % (p)) - regexs.append(r"^Puzzle armed successfully.$") - msg = self.call(puzzles.CmdArmPuzzle(), recipe_dbref, caller=self.char1) - matches = self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) - - def test_cmdset_puzzle(self): - self.char1.cmdset.add("evennia.contrib.puzzles.PuzzleSystemCmdSet") - # FIXME: testing nothing, this is just to bump up coverage - - def test_cmd_puzzle(self): - self._assert_no_recipes() - - # bad syntax - def _bad_syntax(cmdstr): - self.call( - puzzles.CmdCreatePuzzleRecipe(), - cmdstr, - "Usage: @puzzle name, = ", - caller=self.char1, - ) - - _bad_syntax("") - _bad_syntax("=") - _bad_syntax("nothing =") - _bad_syntax("= nothing") - _bad_syntax("nothing") - _bad_syntax(",nothing") - _bad_syntax("name, nothing") - _bad_syntax("name, nothing =") - - self._assert_no_recipes() - - self._good_recipe("makefire", ["steel", "flint"], ["fire", "steel", "flint"]) - self._good_recipe("hot steels", ["steel", "fire"], ["steel", "fire"]) - self._good_recipe( - "furnace", - ["steel", "steel", "fire"], - ["steel", "steel", "fire", "fire", "fire", "fire"], - ) - - # bad recipes - def _bad_recipe(name, parts, results, fail_regex): - cmdstr = ",".join([name] + parts) + "=" + ",".join(results) - msg = self.call(puzzles.CmdCreatePuzzleRecipe(), cmdstr, caller=self.char1) - self._assert_no_recipes() - self.assertIsNotNone(re.match(fail_regex, msg), msg) - - _bad_recipe("name", ["nothing"], ["neither"], r"Could not find 'nothing'.") - _bad_recipe("name", ["steel"], ["nothing"], r"Could not find 'nothing'.") - _bad_recipe("", ["steel", "fire"], ["steel", "fire"], r"^Invalid puzzle name ''.") - self.steel.location = self.char1 - _bad_recipe("name", ["steel"], ["fire"], r"^Invalid location for steel$") - _bad_recipe("name", ["flint"], ["steel"], r"^Invalid location for steel$") - _bad_recipe("name", ["self"], ["fire"], r"^Invalid typeclass for Char$") - _bad_recipe("name", ["here"], ["fire"], r"^Invalid typeclass for Room$") - - self._assert_no_recipes() - - def test_cmd_armpuzzle(self): - # bad arms - self.call( - puzzles.CmdArmPuzzle(), - "1", - "A puzzle recipe's #dbref must be specified", - caller=self.char1, - ) - self.call(puzzles.CmdArmPuzzle(), "#1", "Invalid puzzle '#1'", caller=self.char1) - - recipe_dbref = self._good_recipe( - "makefire", ["steel", "flint"], ["fire", "steel", "flint"], and_destroy_it=False - ) - - # delete proto parts and proto result - self.steel.delete() - self.flint.delete() - self.fire.delete() - - # good arm - self._arm(recipe_dbref, "makefire", ["steel", "flint"]) - self._check_room_contents({"steel": 1, "flint": 1}, check_test_tags=True) - - def _use(self, cmdstr, expmsg): - msg = self.call(puzzles.CmdUsePuzzleParts(), cmdstr, expmsg, caller=self.char1) - return msg - - def test_cmd_use(self): - - self._use("", "Use what?") - self._use("something", "There is no something around.") - self._use("steel", "You have no idea how this can be used") - self._use("steel flint", "There is no steel flint around.") - self._use("steel, flint", "You have no idea how these can be used") - - recipe_dbref = self._good_recipe( - "makefire", ["steel", "flint"], ["fire"], and_destroy_it=False - ) - recipe2_dbref = self._good_recipe( - "makefire2", ["steel", "flint"], ["fire"], and_destroy_it=False, expected_count=2 - ) - - # although there is steel and flint - # those aren't valid puzzle parts because - # the puzzle hasn't been armed - self._use("steel", "You have no idea how this can be used") - self._use("steel, flint", "You have no idea how these can be used") - self._arm(recipe_dbref, "makefire", ["steel", "flint"]) - self._check_room_contents({"steel": 2, "flint": 2}, check_test_tags=True) - - # there are duplicated objects now - self._use("steel", "Which steel. There are many") - self._use("flint", "Which flint. There are many") - - # delete proto parts and proto results - self.steel.delete() - self.flint.delete() - self.fire.delete() - - # solve puzzle - self._use("steel, flint", "You are a Genius") - self.assertEqual( - 1, - len( - list( - filter( - lambda o: o.key == "fire" - and ("makefire", puzzles._PUZZLES_TAG_CATEGORY) - in o.tags.all(return_key_and_category=True) - and (puzzles._PUZZLES_TAG_MEMBER, puzzles._PUZZLES_TAG_CATEGORY) - in o.tags.all(return_key_and_category=True), - self.room1.contents, - ) - ) - ), - ) - self._check_room_contents({"steel": 0, "flint": 0, "fire": 1}, check_test_tags=True) - - # trying again will fail as it was resolved already - # and the parts were destroyed - self._use("steel, flint", "There is no steel around") - self._use("flint, steel", "There is no flint around") - - # arm same puzzle twice so there are duplicated parts - self._arm(recipe_dbref, "makefire", ["steel", "flint"]) - self._arm(recipe_dbref, "makefire", ["steel", "flint"]) - self._check_room_contents({"steel": 2, "flint": 2, "fire": 1}, check_test_tags=True) - - # try solving with multiple parts but incomplete set - self._use( - "steel-1, steel-2", "You try to utilize these but nothing happens ... something amiss?" - ) - - # arm the other puzzle. Their parts are identical - self._arm(recipe2_dbref, "makefire2", ["steel", "flint"]) - self._check_room_contents({"steel": 3, "flint": 3, "fire": 1}, check_test_tags=True) - - # solve with multiple parts for - # multiple puzzles. Both can be solved but - # only one is. - self._use( - "steel-1, flint-2, steel-3, flint-3", - "Your gears start turning and 2 different ideas come to your mind ... ", - ) - self._check_room_contents({"steel": 2, "flint": 2, "fire": 2}, check_test_tags=True) - - self.room1.msg_contents = Mock() - - # solve all - self._use("steel-1, flint-1", "You are a Genius") - self.room1.msg_contents.assert_called_once_with( - "|cChar|n performs some kind of tribal dance and |yfire|n seems to appear from thin air", - exclude=(self.char1,), - ) - self._use("steel, flint", "You are a Genius") - self._check_room_contents({"steel": 0, "flint": 0, "fire": 4}, check_test_tags=True) - - def test_puzzleedit(self): - recipe_dbref = self._good_recipe( - "makefire", ["steel", "flint"], ["fire"], and_destroy_it=False - ) - - def _puzzleedit(swt, dbref, args, expmsg): - if (swt is None) and (dbref is None) and (args is None): - cmdstr = "" - else: - cmdstr = "%s %s%s" % (swt, dbref, args) - self.call(puzzles.CmdEditPuzzle(), cmdstr, expmsg, caller=self.char1) - - # delete proto parts and proto results - self.steel.delete() - self.flint.delete() - self.fire.delete() - - sid = self.script.id - # bad syntax - _puzzleedit( - None, None, None, "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit" - ) - _puzzleedit("", "1", "", "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") - _puzzleedit("", "", "", "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") - _puzzleedit( - "", - recipe_dbref, - "dummy", - "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit", - ) - _puzzleedit("", self.script.dbref, "", "Script(#{}) is not a puzzle".format(sid)) - - # edit use_success_message and use_success_location_message - _puzzleedit( - "", - recipe_dbref, - "/use_success_message = Yes!", - "makefire(%s) use_success_message = Yes!" % recipe_dbref, - ) - _puzzleedit( - "", - recipe_dbref, - "/use_success_location_message = {result_names} Yeah baby! {caller}", - "makefire(%s) use_success_location_message = {result_names} Yeah baby! {caller}" - % recipe_dbref, - ) - - self._arm(recipe_dbref, "makefire", ["steel", "flint"]) - self.room1.msg_contents = Mock() - self._use("steel, flint", "Yes!") - self.room1.msg_contents.assert_called_once_with( - "fire Yeah baby! Char", exclude=(self.char1,) - ) - self.room1.msg_contents.reset_mock() - - # edit mask: exclude location and desc during matching - _puzzleedit( - "", - recipe_dbref, - "/mask = location,desc", - "makefire(%s) mask = ('location', 'desc')" % recipe_dbref, - ) - - self._arm(recipe_dbref, "makefire", ["steel", "flint"]) - # change location and desc - self.char1.search("steel").db.desc = "A solid bar of steel" - self.char1.search("steel").location = self.char1 - self.char1.search("flint").db.desc = "A flint steel" - self.char1.search("flint").location = self.char1 - self._use("steel, flint", "Yes!") - self.room1.msg_contents.assert_called_once_with( - "fire Yeah baby! Char", exclude=(self.char1,) - ) - - # delete - _puzzleedit("/delete", recipe_dbref, "", "makefire(%s) was deleted" % recipe_dbref) - self._assert_no_recipes() - - def test_puzzleedit_add_remove_parts_results(self): - recipe_dbref = self._good_recipe( - "makefire", ["steel", "flint"], ["fire"], and_destroy_it=False - ) - - def _puzzleedit(swt, dbref, rhslist, expmsg): - cmdstr = "%s %s = %s" % (swt, dbref, ", ".join(rhslist)) - self.call(puzzles.CmdEditPuzzle(), cmdstr, expmsg, caller=self.char1) - - red_steel = create_object( - self.object_typeclass, key="red steel", location=self.char1.location - ) - smoke = create_object(self.object_typeclass, key="smoke", location=self.char1.location) - - _puzzleedit("/addresult", recipe_dbref, ["smoke"], "smoke were added to results") - _puzzleedit( - "/addpart", recipe_dbref, ["red steel", "steel"], "red steel, steel were added to parts" - ) - - # create a box so we can put all objects in - # so that they can't be found during puzzle resolution - self.box = create_object(self.object_typeclass, key="box", location=self.char1.location) - - def _box_all(): - for o in self.room1.contents: - if o not in [self.char1, self.char2, self.exit, self.obj1, self.obj2, self.box]: - o.location = self.box - - _box_all() - - self._arm(recipe_dbref, "makefire", ["steel", "flint", "red steel", "steel"]) - self._check_room_contents({"steel": 2, "red steel": 1, "flint": 1}) - self._use( - "steel-1, flint", "You try to utilize these but nothing happens ... something amiss?" - ) - self._use("steel-1, flint, red steel, steel-3", "You are a Genius") - self._check_room_contents({"smoke": 1, "fire": 1}) - _box_all() - - self.fire.location = self.room1 - self.steel.location = self.room1 - - _puzzleedit("/delresult", recipe_dbref, ["fire"], "fire were removed from results") - _puzzleedit( - "/delpart", recipe_dbref, ["steel", "steel"], "steel, steel were removed from parts" - ) - - _box_all() - - self._arm(recipe_dbref, "makefire", ["flint", "red steel"]) - self._check_room_contents({"red steel": 1, "flint": 1}) - self._use("red steel, flint", "You are a Genius") - self._check_room_contents({"smoke": 1, "fire": 0}) - - def test_lspuzzlerecipes_lsarmedpuzzles(self): - msg = self.call(puzzles.CmdListPuzzleRecipes(), "", caller=self.char1) - self._assert_msg_matched( - msg, [r"^-+$", r"^Found 0 puzzle\(s\)\.$", r"-+$"], re.MULTILINE | re.DOTALL - ) - - recipe_dbref = self._good_recipe( - "makefire", ["steel", "flint"], ["fire"], and_destroy_it=False - ) - - msg = self.call(puzzles.CmdListPuzzleRecipes(), "", caller=self.char1) - self._assert_msg_matched( - msg, - [ - r"^-+$", - r"^Puzzle 'makefire'.*$", - r"^Success Caller message:$", - r"^Success Location message:$", - r"^Mask:$", - r"^Parts$", - r"^.*key: steel$", - r"^.*key: flint$", - r"^Results$", - r"^.*key: fire$", - r"^.*key: steel$", - r"^.*key: flint$", - r"^-+$", - r"^Found 1 puzzle\(s\)\.$", - r"^-+$", - ], - re.MULTILINE | re.DOTALL, - ) - - msg = self.call(puzzles.CmdListArmedPuzzles(), "", caller=self.char1) - self._assert_msg_matched( - msg, - [r"^-+$", r"^-+$", r"^Found 0 armed puzzle\(s\)\.$", r"^-+$"], - re.MULTILINE | re.DOTALL, - ) - - self._arm(recipe_dbref, "makefire", ["steel", "flint"]) - - msg = self.call(puzzles.CmdListArmedPuzzles(), "", caller=self.char1) - self._assert_msg_matched( - msg, - [ - r"^-+$", - r"^Puzzle name: makefire$", - r"^.*steel.* at \s+ Room.*$", - r"^.*flint.* at \s+ Room.*$", - r"^Found 1 armed puzzle\(s\)\.$", - r"^-+$", - ], - re.MULTILINE | re.DOTALL, - ) - - def test_e2e(self): - def _destroy_objs_in_room(keys): - for obj in self.room1.contents: - if obj.key in keys: - obj.delete() - - # parts don't survive resolution - # but produce a large result set - tree = create_object(self.object_typeclass, key="tree", location=self.char1.location) - axe = create_object(self.object_typeclass, key="axe", location=self.char1.location) - sweat = create_object(self.object_typeclass, key="sweat", location=self.char1.location) - dull_axe = create_object( - self.object_typeclass, key="dull axe", location=self.char1.location - ) - timber = create_object(self.object_typeclass, key="timber", location=self.char1.location) - log = create_object(self.object_typeclass, key="log", location=self.char1.location) - parts = ["tree", "axe"] - results = (["sweat"] * 10) + ["dull axe"] + (["timber"] * 20) + (["log"] * 50) - recipe_dbref = self._good_recipe("lumberjack", parts, results, and_destroy_it=False) - - _destroy_objs_in_room(set(parts + results)) - - sps = sorted(parts) - expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} - expected.update({r: 0 for r in set(results)}) - - self._arm(recipe_dbref, "lumberjack", parts) - self._check_room_contents(expected) - - self._use(",".join(parts), "You are a Genius") - srs = sorted(set(results)) - expected = {(key, len(list(grp))) for key, grp in itertools.groupby(srs)} - expected.update({p: 0 for p in set(parts)}) - self._check_room_contents(expected) - - # parts also appear in results - # causing a new puzzle to be armed 'automatically' - # i.e. the puzzle is self-sustaining - hole = create_object(self.object_typeclass, key="hole", location=self.char1.location) - shovel = create_object(self.object_typeclass, key="shovel", location=self.char1.location) - dirt = create_object(self.object_typeclass, key="dirt", location=self.char1.location) - - parts = ["shovel", "hole"] - results = ["dirt", "hole", "shovel"] - recipe_dbref = self._good_recipe( - "digger", parts, results, and_destroy_it=False, expected_count=2 - ) - - _destroy_objs_in_room(set(parts + results)) - - nresolutions = 0 - - sps = sorted(set(parts)) - expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} - expected.update({"dirt": nresolutions}) - - self._arm(recipe_dbref, "digger", parts) - self._check_room_contents(expected) - - for i in range(10): - self._use(",".join(parts), "You are a Genius") - nresolutions += 1 - expected.update({"dirt": nresolutions}) - self._check_room_contents(expected) - - # Uppercase puzzle name - balloon = create_object(self.object_typeclass, key="Balloon", location=self.char1.location) - parts = ["Balloon"] - results = ["Balloon"] - recipe_dbref = self._good_recipe( - "boom!!!", parts, results, and_destroy_it=False, expected_count=3 - ) - - _destroy_objs_in_room(set(parts + results)) - - sps = sorted(parts) - expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} - - self._arm(recipe_dbref, "boom!!!", parts) - self._check_room_contents(expected) - - self._use(",".join(parts), "You are a Genius") - srs = sorted(set(results)) - expected = {(key, len(list(grp))) for key, grp in itertools.groupby(srs)} - self._check_room_contents(expected) - - def test_e2e_accumulative(self): - flashlight = create_object( - self.object_typeclass, key="flashlight", location=self.char1.location - ) - flashlight_w_1 = create_object( - self.object_typeclass, key="flashlight-w-1", location=self.char1.location - ) - flashlight_w_2 = create_object( - self.object_typeclass, key="flashlight-w-2", location=self.char1.location - ) - flashlight_w_3 = create_object( - self.object_typeclass, key="flashlight-w-3", location=self.char1.location - ) - battery = create_object(self.object_typeclass, key="battery", location=self.char1.location) - - battery.tags.add("flashlight-1", category=puzzles._PUZZLES_TAG_CATEGORY) - battery.tags.add("flashlight-2", category=puzzles._PUZZLES_TAG_CATEGORY) - battery.tags.add("flashlight-3", category=puzzles._PUZZLES_TAG_CATEGORY) - - # TODO: instead of tagging each flashlight, - # arm and resolve each puzzle in order so they all - # are tagged correctly - # it will be necessary to add/remove parts/results because - # each battery is supposed to be consumed during resolution - # as the new flashlight has one more battery than before - flashlight_w_1.tags.add("flashlight-2", category=puzzles._PUZZLES_TAG_CATEGORY) - flashlight_w_2.tags.add("flashlight-3", category=puzzles._PUZZLES_TAG_CATEGORY) - - recipe_fl1_dbref = self._good_recipe( - "flashlight-1", - ["flashlight", "battery"], - ["flashlight-w-1"], - and_destroy_it=False, - expected_count=1, - ) - recipe_fl2_dbref = self._good_recipe( - "flashlight-2", - ["flashlight-w-1", "battery"], - ["flashlight-w-2"], - and_destroy_it=False, - expected_count=2, - ) - recipe_fl3_dbref = self._good_recipe( - "flashlight-3", - ["flashlight-w-2", "battery"], - ["flashlight-w-3"], - and_destroy_it=False, - expected_count=3, - ) - - # delete protoparts - for obj in [battery, flashlight, flashlight_w_1, flashlight_w_2, flashlight_w_3]: - obj.delete() - - def _group_parts(parts, excluding=set()): - group = dict() - dbrefs = dict() - for o in self.room1.contents: - if o.key in parts and o.dbref not in excluding: - if o.key not in group: - group[o.key] = [] - group[o.key].append(o.dbref) - dbrefs[o.dbref] = o - return group, dbrefs - - # arm each puzzle and group its parts - self._arm(recipe_fl1_dbref, "flashlight-1", ["battery", "flashlight"]) - fl1_parts, fl1_dbrefs = _group_parts(["battery", "flashlight"]) - self._arm(recipe_fl2_dbref, "flashlight-2", ["battery", "flashlight-w-1"]) - fl2_parts, fl2_dbrefs = _group_parts( - ["battery", "flashlight-w-1"], excluding=list(fl1_dbrefs.keys()) - ) - self._arm(recipe_fl3_dbref, "flashlight-3", ["battery", "flashlight-w-2"]) - fl3_parts, fl3_dbrefs = _group_parts( - ["battery", "flashlight-w-2"], - excluding=set(list(fl1_dbrefs.keys()) + list(fl2_dbrefs.keys())), - ) - - self._check_room_contents( - { - "battery": 3, - "flashlight": 1, - "flashlight-w-1": 1, - "flashlight-w-2": 1, - "flashlight-w-3": 0, - } - ) - - # all batteries have identical protodefs - battery_1 = fl1_dbrefs[fl1_parts["battery"][0]] - battery_2 = fl2_dbrefs[fl2_parts["battery"][0]] - battery_3 = fl3_dbrefs[fl3_parts["battery"][0]] - protodef_battery_1 = puzzles.proto_def(battery_1, with_tags=False) - del protodef_battery_1["prototype_key"] - protodef_battery_2 = puzzles.proto_def(battery_2, with_tags=False) - del protodef_battery_2["prototype_key"] - protodef_battery_3 = puzzles.proto_def(battery_3, with_tags=False) - del protodef_battery_3["prototype_key"] - assert protodef_battery_1 == protodef_battery_2 == protodef_battery_3 - - # each battery can be used in every other puzzle - - b1_parts_dict, b1_puzzlenames, b1_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( - [battery_1] - ) - _puzzles = puzzles._puzzles_by_names(b1_puzzlenames.keys()) - assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( - [p.db.puzzle_name for p in _puzzles] - ) - matched_puzzles = puzzles._matching_puzzles(_puzzles, b1_puzzlenames, b1_protodefs) - assert 0 == len(matched_puzzles) - - b2_parts_dict, b2_puzzlenames, b2_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( - [battery_2] - ) - _puzzles = puzzles._puzzles_by_names(b2_puzzlenames.keys()) - assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( - [p.db.puzzle_name for p in _puzzles] - ) - matched_puzzles = puzzles._matching_puzzles(_puzzles, b2_puzzlenames, b2_protodefs) - assert 0 == len(matched_puzzles) - b3_parts_dict, b3_puzzlenames, b3_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( - [battery_3] - ) - _puzzles = puzzles._puzzles_by_names(b3_puzzlenames.keys()) - assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( - [p.db.puzzle_name for p in _puzzles] - ) - matched_puzzles = puzzles._matching_puzzles(_puzzles, b3_puzzlenames, b3_protodefs) - assert 0 == len(matched_puzzles) - - assert battery_1 == list(b1_parts_dict.values())[0] - assert battery_2 == list(b2_parts_dict.values())[0] - assert battery_3 == list(b3_parts_dict.values())[0] - assert b1_puzzlenames.keys() == b2_puzzlenames.keys() == b3_puzzlenames.keys() - for puzzle_name in ["flashlight-1", "flashlight-2", "flashlight-3"]: - assert puzzle_name in b1_puzzlenames - assert puzzle_name in b2_puzzlenames - assert puzzle_name in b3_puzzlenames - assert ( - list(b1_protodefs.values())[0] - == list(b2_protodefs.values())[0] - == list(b3_protodefs.values())[0] - == protodef_battery_1 - == protodef_battery_2 - == protodef_battery_3 - ) - - # all flashlights have similar protodefs except their key - flashlight_1 = fl1_dbrefs[fl1_parts["flashlight"][0]] - flashlight_2 = fl2_dbrefs[fl2_parts["flashlight-w-1"][0]] - flashlight_3 = fl3_dbrefs[fl3_parts["flashlight-w-2"][0]] - protodef_flashlight_1 = puzzles.proto_def(flashlight_1, with_tags=False) - del protodef_flashlight_1["prototype_key"] - assert protodef_flashlight_1["key"] == "flashlight" - del protodef_flashlight_1["key"] - protodef_flashlight_2 = puzzles.proto_def(flashlight_2, with_tags=False) - del protodef_flashlight_2["prototype_key"] - assert protodef_flashlight_2["key"] == "flashlight-w-1" - del protodef_flashlight_2["key"] - protodef_flashlight_3 = puzzles.proto_def(flashlight_3, with_tags=False) - del protodef_flashlight_3["prototype_key"] - assert protodef_flashlight_3["key"] == "flashlight-w-2" - del protodef_flashlight_3["key"] - assert protodef_flashlight_1 == protodef_flashlight_2 == protodef_flashlight_3 - - # each flashlight can only be used in its own puzzle - - f1_parts_dict, f1_puzzlenames, f1_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( - [flashlight_1] - ) - _puzzles = puzzles._puzzles_by_names(f1_puzzlenames.keys()) - assert set(["flashlight-1"]) == set([p.db.puzzle_name for p in _puzzles]) - matched_puzzles = puzzles._matching_puzzles(_puzzles, f1_puzzlenames, f1_protodefs) - assert 0 == len(matched_puzzles) - f2_parts_dict, f2_puzzlenames, f2_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( - [flashlight_2] - ) - _puzzles = puzzles._puzzles_by_names(f2_puzzlenames.keys()) - assert set(["flashlight-2"]) == set([p.db.puzzle_name for p in _puzzles]) - matched_puzzles = puzzles._matching_puzzles(_puzzles, f2_puzzlenames, f2_protodefs) - assert 0 == len(matched_puzzles) - f3_parts_dict, f3_puzzlenames, f3_protodefs = puzzles._lookups_parts_puzzlenames_protodefs( - [flashlight_3] - ) - _puzzles = puzzles._puzzles_by_names(f3_puzzlenames.keys()) - assert set(["flashlight-3"]) == set([p.db.puzzle_name for p in _puzzles]) - matched_puzzles = puzzles._matching_puzzles(_puzzles, f3_puzzlenames, f3_protodefs) - assert 0 == len(matched_puzzles) - - assert flashlight_1 == list(f1_parts_dict.values())[0] - assert flashlight_2 == list(f2_parts_dict.values())[0] - assert flashlight_3 == list(f3_parts_dict.values())[0] - for puzzle_name in set( - list(f1_puzzlenames.keys()) + list(f2_puzzlenames.keys()) + list(f3_puzzlenames.keys()) - ): - assert puzzle_name in ["flashlight-1", "flashlight-2", "flashlight-3", "puzzle_member"] - protodef_flashlight_1["key"] = "flashlight" - assert list(f1_protodefs.values())[0] == protodef_flashlight_1 - protodef_flashlight_2["key"] = "flashlight-w-1" - assert list(f2_protodefs.values())[0] == protodef_flashlight_2 - protodef_flashlight_3["key"] = "flashlight-w-2" - assert list(f3_protodefs.values())[0] == protodef_flashlight_3 - - # each battery can be matched with every other flashlight - # to potentially resolve each puzzle - for batt in [battery_1, battery_2, battery_3]: - parts_dict, puzzlenames, protodefs = puzzles._lookups_parts_puzzlenames_protodefs( - [batt, flashlight_1] - ) - assert set([batt.dbref, flashlight_1.dbref]) == set(puzzlenames["flashlight-1"]) - assert set([batt.dbref]) == set(puzzlenames["flashlight-2"]) - assert set([batt.dbref]) == set(puzzlenames["flashlight-3"]) - _puzzles = puzzles._puzzles_by_names(puzzlenames.keys()) - assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( - [p.db.puzzle_name for p in _puzzles] - ) - matched_puzzles = puzzles._matching_puzzles(_puzzles, puzzlenames, protodefs) - assert 1 == len(matched_puzzles) - parts_dict, puzzlenames, protodefs = puzzles._lookups_parts_puzzlenames_protodefs( - [batt, flashlight_2] - ) - assert set([batt.dbref]) == set(puzzlenames["flashlight-1"]) - assert set([batt.dbref, flashlight_2.dbref]) == set(puzzlenames["flashlight-2"]) - assert set([batt.dbref]) == set(puzzlenames["flashlight-3"]) - _puzzles = puzzles._puzzles_by_names(puzzlenames.keys()) - assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( - [p.db.puzzle_name for p in _puzzles] - ) - matched_puzzles = puzzles._matching_puzzles(_puzzles, puzzlenames, protodefs) - assert 1 == len(matched_puzzles) - parts_dict, puzzlenames, protodefs = puzzles._lookups_parts_puzzlenames_protodefs( - [batt, flashlight_3] - ) - assert set([batt.dbref]) == set(puzzlenames["flashlight-1"]) - assert set([batt.dbref]) == set(puzzlenames["flashlight-2"]) - assert set([batt.dbref, flashlight_3.dbref]) == set(puzzlenames["flashlight-3"]) - _puzzles = puzzles._puzzles_by_names(puzzlenames.keys()) - assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set( - [p.db.puzzle_name for p in _puzzles] - ) - matched_puzzles = puzzles._matching_puzzles(_puzzles, puzzlenames, protodefs) - assert 1 == len(matched_puzzles) - - # delete all parts - for part in ( - list(fl1_dbrefs.values()) + list(fl2_dbrefs.values()) + list(fl3_dbrefs.values()) - ): - part.delete() - - self._check_room_contents( - { - "battery": 0, - "flashlight": 0, - "flashlight-w-1": 0, - "flashlight-w-2": 0, - "flashlight-w-3": 0, - } - ) - - # arm first puzzle 3 times and group its parts so we can solve - # all puzzles with the parts from the 1st armed - for i in range(3): - self._arm(recipe_fl1_dbref, "flashlight-1", ["battery", "flashlight"]) - fl1_parts, fl1_dbrefs = _group_parts(["battery", "flashlight"]) - - # delete the 2 extra flashlights so we can start solving - for flashlight_dbref in fl1_parts["flashlight"][1:]: - fl1_dbrefs[flashlight_dbref].delete() - - self._check_room_contents( - { - "battery": 3, - "flashlight": 1, - "flashlight-w-1": 0, - "flashlight-w-2": 0, - "flashlight-w-3": 0, - } - ) - - self._use("battery-1, flashlight", "You are a Genius") - self._check_room_contents( - { - "battery": 2, - "flashlight": 0, - "flashlight-w-1": 1, - "flashlight-w-2": 0, - "flashlight-w-3": 0, - } - ) - - self._use("battery-1, flashlight-w-1", "You are a Genius") - self._check_room_contents( - { - "battery": 1, - "flashlight": 0, - "flashlight-w-1": 0, - "flashlight-w-2": 1, - "flashlight-w-3": 0, - } - ) - - self._use("battery, flashlight-w-2", "You are a Genius") - self._check_room_contents( - { - "battery": 0, - "flashlight": 0, - "flashlight-w-1": 0, - "flashlight-w-2": 0, - "flashlight-w-3": 1, - } - ) - - def test_e2e_interchangeable_parts_and_results(self): - # Parts and Results can be used in multiple puzzles - egg = create_object(self.object_typeclass, key="egg", location=self.char1.location) - flour = create_object(self.object_typeclass, key="flour", location=self.char1.location) - boiling_water = create_object( - self.object_typeclass, key="boiling water", location=self.char1.location - ) - boiled_egg = create_object( - self.object_typeclass, key="boiled egg", location=self.char1.location - ) - dough = create_object(self.object_typeclass, key="dough", location=self.char1.location) - pasta = create_object(self.object_typeclass, key="pasta", location=self.char1.location) - - # Three recipes: - # 1. breakfast: egg + boiling water = boiled egg & boiling water - # 2. dough: egg + flour = dough - # 3. entree: dough + boiling water = pasta & boiling water - # tag interchangeable parts according to their puzzles' name - egg.tags.add("breakfast", category=puzzles._PUZZLES_TAG_CATEGORY) - egg.tags.add("dough", category=puzzles._PUZZLES_TAG_CATEGORY) - dough.tags.add("entree", category=puzzles._PUZZLES_TAG_CATEGORY) - boiling_water.tags.add("breakfast", category=puzzles._PUZZLES_TAG_CATEGORY) - boiling_water.tags.add("entree", category=puzzles._PUZZLES_TAG_CATEGORY) - - # create recipes - recipe1_dbref = self._good_recipe( - "breakfast", - ["egg", "boiling water"], - ["boiled egg", "boiling water"], - and_destroy_it=False, - ) - recipe2_dbref = self._good_recipe( - "dough", ["egg", "flour"], ["dough"], and_destroy_it=False, expected_count=2 - ) - recipe3_dbref = self._good_recipe( - "entree", - ["dough", "boiling water"], - ["pasta", "boiling water"], - and_destroy_it=False, - expected_count=3, - ) - - # delete protoparts - for obj in [egg, flour, boiling_water, boiled_egg, dough, pasta]: - obj.delete() - - # arm each puzzle and group its parts - def _group_parts(parts, excluding=set()): - group = dict() - dbrefs = dict() - for o in self.room1.contents: - if o.key in parts and o.dbref not in excluding: - if o.key not in group: - group[o.key] = [] - group[o.key].append(o.dbref) - dbrefs[o.dbref] = o - return group, dbrefs - - self._arm(recipe1_dbref, "breakfast", ["egg", "boiling water"]) - breakfast_parts, breakfast_dbrefs = _group_parts(["egg", "boiling water"]) - self._arm(recipe2_dbref, "dough", ["egg", "flour"]) - dough_parts, dough_dbrefs = _group_parts( - ["egg", "flour"], excluding=list(breakfast_dbrefs.keys()) - ) - self._arm(recipe3_dbref, "entree", ["dough", "boiling water"]) - entree_parts, entree_dbrefs = _group_parts( - ["dough", "boiling water"], - excluding=set(list(breakfast_dbrefs.keys()) + list(dough_dbrefs.keys())), - ) - - # create a box so we can put all objects in - # so that they can't be found during puzzle resolution - self.box = create_object(self.object_typeclass, key="box", location=self.char1.location) - - def _box_all(): - # print "boxing all\n", "-"*20 - for o in self.room1.contents: - if o not in [self.char1, self.char2, self.exit, self.obj1, self.obj2, self.box]: - o.location = self.box - # print o.key, o.dbref, "boxed" - else: - # print "skipped", o.key, o.dbref - pass - - def _unbox(dbrefs): - # print "unboxing", dbrefs, "\n", "-"*20 - for o in self.box.contents: - if o.dbref in dbrefs: - o.location = self.room1 - # print "unboxed", o.key, o.dbref - - # solve dough puzzle using breakfast's egg - # and dough's flour. A new dough will be created - _box_all() - _unbox(breakfast_parts.pop("egg") + dough_parts.pop("flour")) - self._use("egg, flour", "You are a Genius") - - # solve entree puzzle with newly created dough - # and breakfast's boiling water. A new - # boiling water and pasta will be created - _unbox(breakfast_parts.pop("boiling water")) - self._use("boiling water, dough", "You are a Genius") - - # solve breakfast puzzle with dough's egg - # and newly created boiling water. A new - # boiling water and boiled egg will be created - _unbox(dough_parts.pop("egg")) - self._use("boiling water, egg", "You are a Genius") - - # solve entree puzzle using entree's dough - # and newly created boiling water. A new - # boiling water and pasta will be created - _unbox(entree_parts.pop("dough")) - self._use("boiling water, dough", "You are a Genius") - - self._check_room_contents({"boiling water": 1, "pasta": 2, "boiled egg": 1}) - - -# Tests for the building_menu contrib -from evennia.contrib.building_menu import BuildingMenu, CmdNoMatch - - -class Submenu(BuildingMenu): - def init(self, exit): - self.add_choice("title", key="t", attr="key") - - -class TestBuildingMenu(CommandTest): - def setUp(self): - super(TestBuildingMenu, self).setUp() - self.menu = BuildingMenu(caller=self.char1, obj=self.room1, title="test") - self.menu.add_choice("title", key="t", attr="key") - - def test_quit(self): - """Try to quit the building menu.""" - self.assertFalse(self.char1.cmdset.has("building_menu")) - self.menu.open() - self.assertTrue(self.char1.cmdset.has("building_menu")) - self.call(CmdNoMatch(building_menu=self.menu), "q") - # char1 tries to quit the editor - self.assertFalse(self.char1.cmdset.has("building_menu")) - - def test_setattr(self): - """Test the simple setattr provided by building menus.""" - key = self.room1.key - self.menu.open() - self.call(CmdNoMatch(building_menu=self.menu), "t") - self.assertIsNotNone(self.menu.current_choice) - self.call(CmdNoMatch(building_menu=self.menu), "some new title") - self.call(CmdNoMatch(building_menu=self.menu), "@") - self.assertIsNone(self.menu.current_choice) - self.assertEqual(self.room1.key, "some new title") - self.call(CmdNoMatch(building_menu=self.menu), "q") - - def test_add_choice_without_key(self): - """Try to add choices without keys.""" - choices = [] - for i in range(20): - choices.append(self.menu.add_choice("choice", attr="test")) - self.menu._add_keys_choice() - keys = [ - "c", - "h", - "o", - "i", - "e", - "ch", - "ho", - "oi", - "ic", - "ce", - "cho", - "hoi", - "oic", - "ice", - "choi", - "hoic", - "oice", - "choic", - "hoice", - "choice", - ] - for i in range(20): - self.assertEqual(choices[i].key, keys[i]) - - # Adding another key of the same title would break, no more available shortcut - self.menu.add_choice("choice", attr="test") - with self.assertRaises(ValueError): - self.menu._add_keys_choice() - - def test_callbacks(self): - """Test callbacks in menus.""" - self.room1.key = "room1" - - def on_enter(caller, menu): - caller.msg("on_enter:{}".format(menu.title)) - - def on_nomatch(caller, string, choice): - caller.msg("on_nomatch:{},{}".format(string, choice.key)) - - def on_leave(caller, obj): - caller.msg("on_leave:{}".format(obj.key)) - - self.menu.add_choice( - "test", key="e", on_enter=on_enter, on_nomatch=on_nomatch, on_leave=on_leave - ) - self.call(CmdNoMatch(building_menu=self.menu), "e", "on_enter:test") - self.call(CmdNoMatch(building_menu=self.menu), "ok", "on_nomatch:ok,e") - self.call(CmdNoMatch(building_menu=self.menu), "@", "on_leave:room1") - self.call(CmdNoMatch(building_menu=self.menu), "q") - - def test_multi_level(self): - """Test multi-level choices.""" - # Creaste three succeeding menu (t2 is contained in t1, t3 is contained in t2) - def on_nomatch_t1(caller, menu): - menu.move("whatever") # this will be valid since after t1 is a joker - - def on_nomatch_t2(caller, menu): - menu.move("t3") # this time the key matters - - t1 = self.menu.add_choice("what", key="t1", on_nomatch=on_nomatch_t1) - t2 = self.menu.add_choice("and", key="t1.*", on_nomatch=on_nomatch_t2) - t3 = self.menu.add_choice("why", key="t1.*.t3") - self.menu.open() - - # Move into t1 - self.assertIn(t1, self.menu.relevant_choices) - self.assertNotIn(t2, self.menu.relevant_choices) - self.assertNotIn(t3, self.menu.relevant_choices) - self.assertIsNone(self.menu.current_choice) - self.call(CmdNoMatch(building_menu=self.menu), "t1") - self.assertEqual(self.menu.current_choice, t1) - self.assertNotIn(t1, self.menu.relevant_choices) - self.assertIn(t2, self.menu.relevant_choices) - self.assertNotIn(t3, self.menu.relevant_choices) - - # Move into t2 - self.call(CmdNoMatch(building_menu=self.menu), "t2") - self.assertEqual(self.menu.current_choice, t2) - self.assertNotIn(t1, self.menu.relevant_choices) - self.assertNotIn(t2, self.menu.relevant_choices) - self.assertIn(t3, self.menu.relevant_choices) - - # Move into t3 - self.call(CmdNoMatch(building_menu=self.menu), "t3") - self.assertEqual(self.menu.current_choice, t3) - self.assertNotIn(t1, self.menu.relevant_choices) - self.assertNotIn(t2, self.menu.relevant_choices) - self.assertNotIn(t3, self.menu.relevant_choices) - - # Move back to t2 - self.call(CmdNoMatch(building_menu=self.menu), "@") - self.assertEqual(self.menu.current_choice, t2) - self.assertNotIn(t1, self.menu.relevant_choices) - self.assertNotIn(t2, self.menu.relevant_choices) - self.assertIn(t3, self.menu.relevant_choices) - - # Move back into t1 - self.call(CmdNoMatch(building_menu=self.menu), "@") - self.assertEqual(self.menu.current_choice, t1) - self.assertNotIn(t1, self.menu.relevant_choices) - self.assertIn(t2, self.menu.relevant_choices) - self.assertNotIn(t3, self.menu.relevant_choices) - - # Moves back to the main menu - self.call(CmdNoMatch(building_menu=self.menu), "@") - self.assertIn(t1, self.menu.relevant_choices) - self.assertNotIn(t2, self.menu.relevant_choices) - self.assertNotIn(t3, self.menu.relevant_choices) - self.assertIsNone(self.menu.current_choice) - self.call(CmdNoMatch(building_menu=self.menu), "q") - - def test_submenu(self): - """Test to add sub-menus.""" - - def open_exit(menu): - menu.open_submenu("evennia.contrib.tests.Submenu", self.exit) - return False - - self.menu.add_choice("exit", key="x", on_enter=open_exit) - self.menu.open() - self.call(CmdNoMatch(building_menu=self.menu), "x") - self.menu = self.char1.ndb._building_menu - self.call(CmdNoMatch(building_menu=self.menu), "t") - self.call(CmdNoMatch(building_menu=self.menu), "in") - self.call(CmdNoMatch(building_menu=self.menu), "@") - self.call(CmdNoMatch(building_menu=self.menu), "@") - self.menu = self.char1.ndb._building_menu - self.assertEqual(self.char1.ndb._building_menu.obj, self.room1) - self.call(CmdNoMatch(building_menu=self.menu), "q") - self.assertEqual(self.exit.key, "in") - - -from evennia.contrib import mux_comms_cmds as comms # noqa - - -class TestLegacyMuxComms(CommandTest): - """ - Test the legacy comms contrib. - """ - - def setUp(self): - super(CommandTest, self).setUp() - self.call( - comms.CmdChannelCreate(), - "testchan;test=Test Channel", - "Created channel testchan and connected to it.", - receiver=self.account, - ) - - def test_toggle_com(self): - self.call( - comms.CmdAddCom(), - "tc = testchan", - "You are already connected to channel testchan.| You can now", - receiver=self.account, - ) - self.call( - comms.CmdDelCom(), - "tc", - "Any alias 'tc' for channel testchan was cleared.", - receiver=self.account, - ) - - def test_all_com(self): - self.call( - comms.CmdAllCom(), - "", - "Available channels:", - receiver=self.account, - ) - - def test_clock(self): - self.call( - comms.CmdClock(), - "testchan=send:all()", - "Lock(s) applied. Current locks on testchan:", - receiver=self.account, - ) - - def test_cdesc(self): - self.call( - comms.CmdCdesc(), - "testchan = Test Channel", - "Description of channel 'testchan' set to 'Test Channel'.", - receiver=self.account, - ) - - def test_cwho(self): - self.call( - comms.CmdCWho(), - "testchan", - "Channel subscriptions\ntestchan:\n TestAccount", - receiver=self.account, - ) - - def test_cboot(self): - # No one else connected to boot - self.call( - comms.CmdCBoot(), - "", - "Usage: cboot[/quiet] = [:reason]", - receiver=self.account, - ) - - def test_cdestroy(self): - self.call( - comms.CmdCdestroy(), - "testchan", - "[testchan] TestAccount: testchan is being destroyed. Make sure to change your aliases." - "|Channel 'testchan' was destroyed.", - receiver=self.account, - ) - - -from evennia.contrib import cooldowns - - -@patch("evennia.contrib.cooldowns.time.time", return_value=0.0) -class TestCooldowns(EvenniaTest): - def setUp(self): - super().setUp() - self.handler = cooldowns.CooldownHandler(self.char1) - - def test_empty(self, mock_time): - self.assertEqual(self.handler.all, []) - self.assertTrue(self.handler.ready("a", "b", "c")) - self.assertEqual(self.handler.time_left("a", "b", "c"), 0) - - def test_add(self, mock_time): - self.assertEqual(self.handler.add, self.handler.set) - self.handler.add("a", 10) - self.assertFalse(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 10) - mock_time.return_value = 9.0 - self.assertFalse(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 1) - mock_time.return_value = 10.0 - self.assertTrue(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 0) - - def test_add_float(self, mock_time): - self.assertEqual(self.handler.time_left("a"), 0) - self.assertEqual(self.handler.time_left("a", use_int=False), 0) - self.assertEqual(self.handler.time_left("a", use_int=True), 0) - self.handler.add("a", 5.5) - self.assertEqual(self.handler.time_left("a"), 5.5) - self.assertEqual(self.handler.time_left("a", use_int=False), 5.5) - self.assertEqual(self.handler.time_left("a", use_int=True), 6) - - def test_add_multi(self, mock_time): - self.handler.add("a", 10) - self.handler.add("b", 5) - self.handler.add("c", 3) - self.assertFalse(self.handler.ready("a", "b", "c")) - self.assertEqual(self.handler.time_left("a", "b", "c"), 10) - self.assertEqual(self.handler.time_left("a", "b"), 10) - self.assertEqual(self.handler.time_left("a", "c"), 10) - self.assertEqual(self.handler.time_left("b", "c"), 5) - self.assertEqual(self.handler.time_left("c", "c"), 3) - - def test_add_none(self, mock_time): - self.handler.add("a", None) - self.assertTrue(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 0) - - def test_add_negative(self, mock_time): - self.handler.add("a", -5) - self.assertTrue(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 0) - - def test_add_overwrite(self, mock_time): - self.handler.add("a", 5) - self.handler.add("a", 10) - self.handler.add("a", 3) - self.assertFalse(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 3) - - def test_extend(self, mock_time): - self.assertEqual(self.handler.extend("a", 10), 10) - self.assertFalse(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 10) - self.assertEqual(self.handler.extend("a", 10), 20) - self.assertFalse(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 20) - - def test_extend_none(self, mock_time): - self.assertEqual(self.handler.extend("a", None), 0) - self.assertTrue(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 0) - self.handler.add("a", 10) - self.assertEqual(self.handler.extend("a", None), 10) - self.assertEqual(self.handler.time_left("a"), 10) - - def test_extend_negative(self, mock_time): - self.assertEqual(self.handler.extend("a", -5), 0) - self.assertTrue(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 0) - self.handler.add("a", 10) - self.assertEqual(self.handler.extend("a", -5), 5) - self.assertEqual(self.handler.time_left("a"), 5) - - def test_extend_float(self, mock_time): - self.assertEqual(self.handler.extend("a", -5.5), 0) - self.assertTrue(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 0.0) - self.assertEqual(self.handler.time_left("a", use_int=False), 0.0) - self.assertEqual(self.handler.time_left("a", use_int=True), 0) - self.handler.add("a", 10.5) - self.assertEqual(self.handler.extend("a", -5.25), 5.25) - self.assertEqual(self.handler.time_left("a"), 5.25) - self.assertEqual(self.handler.time_left("a", use_int=False), 5.25) - self.assertEqual(self.handler.time_left("a", use_int=True), 6) - - def test_reset_non_existent(self, mock_time): - self.handler.reset("a") - self.assertTrue(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 0) - - def test_reset(self, mock_time): - self.handler.set("a", 10) - self.handler.reset("a") - self.assertTrue(self.handler.ready("a")) - self.assertEqual(self.handler.time_left("a"), 0) - - def test_clear(self, mock_time): - self.handler.add("a", 10) - self.handler.add("b", 10) - self.handler.add("c", 10) - self.handler.clear() - self.assertTrue(self.handler.ready("a", "b", "c")) - self.assertEqual(self.handler.time_left("a", "b", "c"), 0) - - def test_cleanup(self, mock_time): - self.handler.add("a", 10) - self.handler.add("b", 5) - self.handler.add("c", 5) - self.handler.add("d", 3.5) - mock_time.return_value = 6.0 - self.handler.cleanup() - self.assertEqual(self.handler.time_left("b", "c", "d"), 0) - self.assertEqual(self.handler.time_left("a"), 4) - self.assertEqual(list(self.handler.data.keys()), ["a"]) - - def test_cleanup_doesnt_delete_anything(self, mock_time): - self.handler.add("a", 10) - self.handler.add("b", 5) - self.handler.add("c", 5) - self.handler.add("d", 3.5) - mock_time.return_value = 1.0 - self.handler.cleanup() - self.assertEqual(self.handler.time_left("d"), 2.5) - self.assertEqual(self.handler.time_left("b", "c"), 4) - self.assertEqual(self.handler.time_left("a"), 9) - self.assertEqual(list(self.handler.data.keys()), ["a", "b", "c", "d"]) diff --git a/evennia/contrib/tutorials/tests.py b/evennia/contrib/tutorials/bodyfunctions/tests.py similarity index 98% rename from evennia/contrib/tutorials/tests.py rename to evennia/contrib/tutorials/bodyfunctions/tests.py index b747e0a55e..d140fea6db 100644 --- a/evennia/contrib/tutorials/tests.py +++ b/evennia/contrib/tutorials/bodyfunctions/tests.py @@ -1,7 +1,9 @@ +""" +Tests for the bodyfunctions. + +""" from mock import Mock, patch - from evennia.utils.test_resources import EvenniaTest - from .bodyfunctions import BodyFunctions diff --git a/evennia/contrib/tutorials/mirror/README.md b/evennia/contrib/tutorials/mirror/README.md new file mode 100644 index 0000000000..7c7147868a --- /dev/null +++ b/evennia/contrib/tutorials/mirror/README.md @@ -0,0 +1,17 @@ +# TutorialMirror + +A simple mirror object to experiment with. + +A simple mirror object that + +- echoes back the description of the object looking at it +- echoes back whatever is being sent to its .msg - to the + sender, if given, otherwise to the location of the mirror. + +## Installation + +Create the mirror with + + create/drop mirror:contrib.tutorials.mirror.TutorialMirror + +Then look at it. diff --git a/evennia/contrib/tutorials/mirror/__init__.py b/evennia/contrib/tutorials/mirror/__init__.py new file mode 100644 index 0000000000..1f8d743d06 --- /dev/null +++ b/evennia/contrib/tutorials/mirror/__init__.py @@ -0,0 +1,6 @@ +""" +Mirror object - Griatch 2015. + +""" + +from . import TutorialMirror # noqa diff --git a/evennia/contrib/tutorials/talking_npc/tests.py b/evennia/contrib/tutorials/talking_npc/tests.py new file mode 100644 index 0000000000..3ec331d008 --- /dev/null +++ b/evennia/contrib/tutorials/talking_npc/tests.py @@ -0,0 +1,14 @@ +""" +Tutorial - talking NPC tests. + +""" +from evennia.commands.default.tests import CommandTest +from evennia.utils.create import create_object +from . import talking_npc + + +class TestTalkingNPC(CommandTest): + def test_talkingnpc(self): + npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1) + self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)") + npc.delete() diff --git a/evennia/contrib/tutorials/tutorial_world/tests.py b/evennia/contrib/tutorials/tutorial_world/tests.py new file mode 100644 index 0000000000..67defee2eb --- /dev/null +++ b/evennia/contrib/tutorials/tutorial_world/tests.py @@ -0,0 +1,192 @@ +""" +Test tutorial_world/mob + +""" + +from mock import patch +from twisted.trial.unittest import TestCase as TwistedTestCase +from twisted.internet.base import DelayedCall +from evennia.commands.default.tests import CommandTest +from evennia.utils.create import create_object +from evennia.contrib.tutorial_world import mob, objects as tutobjects +from evennia.utils.test_resources import EvenniaTest, mockdelay, mockdeferLater +from evennia.contrib.tutorial_world import rooms as tutrooms + + +class TestTutorialWorldMob(EvenniaTest): + def test_mob(self): + mobobj = create_object(mob.Mob, key="mob") + self.assertEqual(mobobj.db.is_dead, True) + mobobj.set_alive() + self.assertEqual(mobobj.db.is_dead, False) + mobobj.set_dead() + self.assertEqual(mobobj.db.is_dead, True) + mobobj._set_ticker(0, "foo", stop=True) + # TODO should be expanded with further tests of the modes and damage etc. + + +# test tutorial_world/objects + + +DelayedCall.debug = True + + +class TestTutorialWorldObjects(TwistedTestCase, CommandTest): + def test_tutorialobj(self): + obj1 = create_object(tutobjects.TutorialObject, key="tutobj") + obj1.reset() + self.assertEqual(obj1.location, obj1.home) + + def test_readable(self): + readable = create_object(tutobjects.TutorialReadable, key="book", location=self.room1) + readable.db.readable_text = "Text to read" + self.call(tutobjects.CmdRead(), "book", "You read book:\n Text to read", obj=readable) + + def test_climbable(self): + climbable = create_object(tutobjects.TutorialClimbable, key="tree", location=self.room1) + self.call( + tutobjects.CmdClimb(), + "tree", + "You climb tree. Having looked around, you climb down again.", + obj=climbable, + ) + self.assertEqual( + self.char1.tags.get("tutorial_climbed_tree", category="tutorial_world"), + "tutorial_climbed_tree", + ) + + def test_obelisk(self): + obelisk = create_object(tutobjects.Obelisk, key="obelisk", location=self.room1) + self.assertEqual(obelisk.return_appearance(self.char1).startswith("|cobelisk("), True) + + @patch("evennia.contrib.tutorial_world.objects.delay", mockdelay) + @patch("evennia.scripts.taskhandler.deferLater", mockdeferLater) + def test_lightsource(self): + light = create_object(tutobjects.LightSource, key="torch", location=self.room1) + self.call( + tutobjects.CmdLight(), + "", + "A torch on the floor flickers and dies.|You light torch.", + obj=light, + ) + self.assertFalse(light.pk) + + @patch("evennia.contrib.tutorial_world.objects.delay", mockdelay) + @patch("evennia.scripts.taskhandler.deferLater", mockdeferLater) + def test_crumblingwall(self): + wall = create_object(tutobjects.CrumblingWall, key="wall", location=self.room1) + wall.db.destination = self.room2.dbref + self.assertFalse(wall.db.button_exposed) + self.assertFalse(wall.db.exit_open) + wall.db.root_pos = {"yellow": 0, "green": 0, "red": 0, "blue": 0} + self.call( + tutobjects.CmdShiftRoot(), + "blue root right", + "You shove the root adorned with small blue flowers to the right.", + obj=wall, + ) + self.call( + tutobjects.CmdShiftRoot(), + "red root left", + "You shift the reddish root to the left.", + obj=wall, + ) + self.call( + tutobjects.CmdShiftRoot(), + "yellow root down", + "You shove the root adorned with small yellow flowers downwards.", + obj=wall, + ) + self.call( + tutobjects.CmdShiftRoot(), + "green root up", + "You shift the weedy green root upwards.|Holding aside the root you " + "think you notice something behind it ...", + obj=wall, + ) + self.call( + tutobjects.CmdPressButton(), + "", + "You move your fingers over the suspicious depression, then gives it a " + "decisive push. First", + obj=wall, + ) + # we patch out the delay, so these are closed immediately + self.assertFalse(wall.db.button_exposed) + self.assertFalse(wall.db.exit_open) + + def test_weapon(self): + weapon = create_object(tutobjects.TutorialWeapon, key="sword", location=self.char1) + self.call( + tutobjects.CmdAttack(), "Char", "You stab with sword.", obj=weapon, cmdstring="stab" + ) + self.call( + tutobjects.CmdAttack(), "Char", "You slash with sword.", obj=weapon, cmdstring="slash" + ) + + def test_weaponrack(self): + rack = create_object(tutobjects.TutorialWeaponRack, key="rack", location=self.room1) + rack.db.available_weapons = ["sword"] + self.call(tutobjects.CmdGetWeapon(), "", "You find Rusty sword.", obj=rack) + + +class TestTutorialWorldRooms(CommandTest): + def test_cmdtutorial(self): + room = create_object(tutrooms.TutorialRoom, key="tutroom") + self.char1.location = room + self.call(tutrooms.CmdTutorial(), "", "Sorry, there is no tutorial help available here.") + self.call( + tutrooms.CmdTutorialSetDetail(), + "detail;foo;foo2 = A detail", + "Detail set: 'detail;foo;foo2': 'A detail'", + obj=room, + ) + self.call(tutrooms.CmdTutorialLook(), "", "tutroom(", obj=room) + self.call(tutrooms.CmdTutorialLook(), "detail", "A detail", obj=room) + self.call(tutrooms.CmdTutorialLook(), "foo", "A detail", obj=room) + room.delete() + + def test_weatherroom(self): + room = create_object(tutrooms.WeatherRoom, key="weatherroom") + room.update_weather() + tutrooms.TICKER_HANDLER.remove( + interval=room.db.interval, callback=room.update_weather, idstring="tutorial" + ) + room.delete() + + def test_introroom(self): + room = create_object(tutrooms.IntroRoom, key="introroom") + room.at_object_receive(self.char1, self.room1) + + def test_bridgeroom(self): + room = create_object(tutrooms.BridgeRoom, key="bridgeroom") + room.update_weather() + self.char1.move_to(room) + self.call( + tutrooms.CmdBridgeHelp(), + "", + "You are trying hard not to fall off the bridge ...", + obj=room, + ) + self.call( + tutrooms.CmdLookBridge(), + "", + "bridgeroom\nYou are standing very close to the the bridge's western foundation.", + obj=room, + ) + room.at_object_leave(self.char1, self.room1) + tutrooms.TICKER_HANDLER.remove( + interval=room.db.interval, callback=room.update_weather, idstring="tutorial" + ) + room.delete() + + def test_darkroom(self): + room = create_object(tutrooms.DarkRoom, key="darkroom") + self.char1.move_to(room) + self.call(tutrooms.CmdDarkHelp(), "", "Can't help you until") + + def test_teleportroom(self): + create_object(tutrooms.TeleportRoom, key="teleportroom") + + def test_outroroom(self): + create_object(tutrooms.OutroRoom, key="outroroom") diff --git a/evennia/contrib/utils/auditing/README.md b/evennia/contrib/utils/auditing/README.md new file mode 100644 index 0000000000..156d790426 --- /dev/null +++ b/evennia/contrib/utils/auditing/README.md @@ -0,0 +1,71 @@ +# Input/Output Auditing + +Contrib - Johnny 2017 + +This is a tap that optionally intercepts all data sent to/from clients and the +server and passes it to a callback of your choosing. + +It is intended for quality assurance, post-incident investigations and debugging +but obviously can be abused. All data is recorded in cleartext. Please +be ethical, and if you are unwilling to properly deal with the implications of +recording user passwords or private communications, please do not enable +this module. + +Some checks have been implemented to protect the privacy of users. + +Files included in this module: + + outputs.py - Example callback methods. This module ships with examples of + callbacks that send data as JSON to a file in your game/server/logs + dir or to your native Linux syslog daemon. You can of course write + your own to do other things like post them to Kafka topics. + + server.py - Extends the Evennia ServerSession object to pipe data to the + callback upon receipt. + + tests.py - Unit tests that check to make sure commands with sensitive + arguments are having their PII scrubbed. + + +## Installation/Configuration: + +Deployment is completed by configuring a few settings in server.conf. This line +is required: + + SERVER_SESSION_CLASS = 'evennia.contrib.security.auditing.server.AuditedServerSession' + +This tells Evennia to use this ServerSession instead of its own. Below are the +other possible options along with the default value that will be used if unset. + + # Where to send logs? Define the path to a module containing your callback + # function. It should take a single dict argument as input + AUDIT_CALLBACK = 'evennia.contrib.security.auditing.outputs.to_file' + + # Log user input? Be ethical about this; it will log all private and + # public communications between players and/or admins (default: False). + AUDIT_IN = False + + # Log server output? This will result in logging of ALL system + # messages and ALL broadcasts to connected players, so on a busy game any + # broadcast to all users will yield a single event for every connected user! + AUDIT_OUT = False + + # The default output is a dict. Do you want to allow key:value pairs with + # null/blank values? If you're just writing to disk, disabling this saves + # some disk space, but whether you *want* sparse values or not is more of a + # consideration if you're shipping logs to a NoSQL/schemaless database. + # (default: False) + AUDIT_ALLOW_SPARSE = False + + # If you write custom commands that handle sensitive data like passwords, + # you must write a regular expression to remove that before writing to log. + # AUDIT_MASKS is a list of dictionaries that define the names of commands + # and the regexes needed to scrub them. + # The system already has defaults to filter out sensitive login/creation + # commands in the default command set. Your list of AUDIT_MASKS will be appended + # to those defaults. + # + # In the regex, the sensitive data itself must be captured in a named group with a + # label of 'secret' (see the Python docs on the `re` module for more info). For + # example: `{'authentication': r"^@auth\s+(?P[\w]+)"}` + AUDIT_MASKS = [] diff --git a/evennia/contrib/utils/auditing/__init__.py b/evennia/contrib/utils/auditing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/evennia/contrib/utils/auditing/outputs.py b/evennia/contrib/utils/auditing/outputs.py new file mode 100644 index 0000000000..d57881521e --- /dev/null +++ b/evennia/contrib/utils/auditing/outputs.py @@ -0,0 +1,60 @@ +""" +Auditable Server Sessions - Example Outputs +Example methods demonstrating output destinations for logs generated by +audited server sessions. + +This is designed to be a single source of events for developers to customize +and add any additional enhancements before events are written out-- i.e. if you +want to keep a running list of what IPs a user logs in from on account/character +objects, or if you want to perform geoip or ASN lookups on IPs before committing, +or tag certain events with the results of a reputational lookup, this should be +the easiest place to do it. Write a method and invoke it via +`settings.AUDIT_CALLBACK` to have log data objects passed to it. + +Evennia contribution - Johnny 2017 +""" +from evennia.utils.logger import log_file +import json +import syslog + + +def to_file(data): + """ + Writes dictionaries of data generated by an AuditedServerSession to files + in JSON format, bucketed by date. + + Uses Evennia's native logger and writes to the default + log directory (~/yourgame/server/logs/ or settings.LOG_DIR) + + Args: + data (dict): Parsed session transmission data. + + """ + # Bucket logs by day and remove objects before serialization + bucket = data.pop("objects")["time"].strftime("%Y-%m-%d") + + # Write it + log_file(json.dumps(data), filename="audit_%s.log" % bucket) + + +def to_syslog(data): + """ + Writes dictionaries of data generated by an AuditedServerSession to syslog. + + Takes advantage of your system's native logger and writes to wherever + you have it configured, which is independent of Evennia. + Linux systems tend to write to /var/log/syslog. + + If you're running rsyslog, you can configure it to dump and/or forward logs + to disk and/or an external data warehouse (recommended-- if your server is + compromised or taken down, losing your logs along with it is no help!). + + Args: + data (dict): Parsed session transmission data. + + """ + # Remove objects before serialization + data.pop("objects") + + # Write it out + syslog.syslog(json.dumps(data)) diff --git a/evennia/contrib/utils/auditing/server.py b/evennia/contrib/utils/auditing/server.py new file mode 100644 index 0000000000..3c653290ed --- /dev/null +++ b/evennia/contrib/utils/auditing/server.py @@ -0,0 +1,249 @@ +""" +Auditable Server Sessions: +Extension of the stock ServerSession that yields objects representing +user inputs and system outputs. + +Evennia contribution - Johnny 2017 +""" +import os +import re +import socket + +from django.utils import timezone +from django.conf import settings as ev_settings +from evennia.utils import utils, logger, mod_import, get_evennia_version +from evennia.server.serversession import ServerSession + +# Attributes governing auditing of commands and where to send log objects +AUDIT_CALLBACK = getattr( + ev_settings, "AUDIT_CALLBACK", "evennia.contrib.security.auditing.outputs.to_file" +) +AUDIT_IN = getattr(ev_settings, "AUDIT_IN", False) +AUDIT_OUT = getattr(ev_settings, "AUDIT_OUT", False) +AUDIT_ALLOW_SPARSE = getattr(ev_settings, "AUDIT_ALLOW_SPARSE", False) +AUDIT_MASKS = [ + {"connect": r"^[@\s]*[connect]{5,8}\s+(\".+?\"|[^\s]+)\s+(?P.+)"}, + {"connect": r"^[@\s]*[connect]{5,8}\s+(?P[\w]+)"}, + {"create": r"^[^@]?[create]{5,6}\s+(\w+|\".+?\")\s+(?P[\w]+)"}, + {"create": r"^[^@]?[create]{5,6}\s+(?P[\w]+)"}, + {"userpassword": r"^[@\s]*[userpassword]{11,14}\s+(\w+|\".+?\")\s+=*\s*(?P[\w]+)"}, + {"userpassword": r"^.*new password set to '(?P[^']+)'\."}, + {"userpassword": r"^.* has changed your password to '(?P[^']+)'\."}, + {"password": r"^[@\s]*[password]{6,9}\s+(?P.*)"}, +] + getattr(ev_settings, "AUDIT_MASKS", []) + + +if AUDIT_CALLBACK: + try: + AUDIT_CALLBACK = getattr( + mod_import(".".join(AUDIT_CALLBACK.split(".")[:-1])), AUDIT_CALLBACK.split(".")[-1] + ) + logger.log_sec("Auditing module online.") + logger.log_sec( + "Audit record User input: {}, output: {}.\n" + "Audit sparse recording: {}, Log callback: {}".format( + AUDIT_IN, AUDIT_OUT, AUDIT_ALLOW_SPARSE, AUDIT_CALLBACK + ) + ) + except Exception as e: + logger.log_err("Failed to activate Auditing module. %s" % e) + + +class AuditedServerSession(ServerSession): + """ + This particular implementation parses all server inputs and/or outputs and + passes a dict containing the parsed metadata to a callback method of your + creation. This is useful for recording player activity where necessary for + security auditing, usage analysis or post-incident forensic discovery. + + *** WARNING *** + All strings are recorded and stored in plaintext. This includes those strings + which might contain sensitive data (create, connect, @password). These commands + have their arguments masked by default, but you must mask or mask any + custom commands of your own that handle sensitive information. + + See README.md for installation/configuration instructions. + """ + + def audit(self, **kwargs): + """ + Extracts messages and system data from a Session object upon message + send or receive. + + Keyword Args: + src (str): Source of data; 'client' or 'server'. Indicates direction. + text (str or list): Client sends messages to server in the form of + lists. Server sends messages to client as string. + + Returns: + log (dict): Dictionary object containing parsed system and user data + related to this message. + + """ + # Get time at start of processing + time_obj = timezone.now() + time_str = str(time_obj) + + session = self + src = kwargs.pop("src", "?") + bytecount = 0 + + # Do not log empty lines + if not kwargs: + return {} + + # Get current session's IP address + client_ip = session.address + + # Capture Account name and dbref together + account = session.get_account() + account_token = "" + if account: + account_token = "%s%s" % (account.key, account.dbref) + + # Capture Character name and dbref together + char = session.get_puppet() + char_token = "" + if char: + char_token = "%s%s" % (char.key, char.dbref) + + # Capture Room name and dbref together + room = None + room_token = "" + if char: + room = char.location + room_token = "%s%s" % (room.key, room.dbref) + + # Try to compile an input/output string + def drill(obj, bucket): + if isinstance(obj, dict): + return bucket + elif utils.is_iter(obj): + for sub_obj in obj: + bucket.extend(drill(sub_obj, [])) + else: + bucket.append(obj) + return bucket + + text = kwargs.pop("text", "") + if utils.is_iter(text): + text = "|".join(drill(text, [])) + + # Mask any PII in message, where possible + bytecount = len(text.encode("utf-8")) + text = self.mask(text) + + # Compile the IP, Account, Character, Room, and the message. + log = { + "time": time_str, + "hostname": socket.getfqdn(), + "application": "%s" % ev_settings.SERVERNAME, + "version": get_evennia_version(), + "pid": os.getpid(), + "direction": "SND" if src == "server" else "RCV", + "protocol": self.protocol_key, + "ip": client_ip, + "session": "session#%s" % self.sessid, + "account": account_token, + "character": char_token, + "room": room_token, + "text": text.strip(), + "bytes": bytecount, + "data": kwargs, + "objects": { + "time": time_obj, + "session": self, + "account": account, + "character": char, + "room": room, + }, + } + + # Remove any keys with blank values + if AUDIT_ALLOW_SPARSE is False: + log["data"] = {k: v for k, v in log["data"].items() if v} + log["objects"] = {k: v for k, v in log["objects"].items() if v} + log = {k: v for k, v in log.items() if v} + + return log + + def mask(self, msg): + """ + Masks potentially sensitive user information within messages before + writing to log. Recording cleartext password attempts is bad policy. + + Args: + msg (str): Raw text string sent from client <-> server + + Returns: + msg (str): Text string with sensitive information masked out. + + """ + # Check to see if the command is embedded within server output + _msg = msg + is_embedded = False + match = re.match(".*Command.*'(.+)'.*is not available.*", msg, flags=re.IGNORECASE) + if match: + msg = match.group(1).replace("\\", "") + submsg = msg + is_embedded = True + + for mask in AUDIT_MASKS: + for command, regex in mask.items(): + try: + match = re.match(regex, msg, flags=re.IGNORECASE) + except Exception as e: + logger.log_err(regex) + logger.log_err(e) + continue + + if match: + term = match.group("secret") + masked = re.sub(term, "*" * len(term.zfill(8)), msg) + + if is_embedded: + msg = re.sub( + submsg, "%s " % (masked, command), _msg, flags=re.IGNORECASE + ) + else: + msg = masked + + return msg + + return _msg + + def data_out(self, **kwargs): + """ + Generic hook for sending data out through the protocol. + + Keyword Args: + kwargs (any): Other data to the protocol. + + """ + if AUDIT_CALLBACK and AUDIT_OUT: + try: + log = self.audit(src="server", **kwargs) + if log: + AUDIT_CALLBACK(log) + except Exception as e: + logger.log_err(e) + + super(AuditedServerSession, self).data_out(**kwargs) + + def data_in(self, **kwargs): + """ + Hook for protocols to send incoming data to the engine. + + Keyword Args: + kwargs (any): Other data from the protocol. + + """ + if AUDIT_CALLBACK and AUDIT_IN: + try: + log = self.audit(src="client", **kwargs) + if log: + AUDIT_CALLBACK(log) + except Exception as e: + logger.log_err(e) + + super(AuditedServerSession, self).data_in(**kwargs) diff --git a/evennia/contrib/utils/auditing/tests.py b/evennia/contrib/utils/auditing/tests.py new file mode 100644 index 0000000000..4b2748e744 --- /dev/null +++ b/evennia/contrib/utils/auditing/tests.py @@ -0,0 +1,113 @@ +""" +Module containing the test cases for the Audit system. +""" + +from anything import Anything +from django.conf import settings +from evennia.utils.test_resources import EvenniaTest +import re + +# Configure session auditing settings - TODO: This is bad practice that leaks over to other tests +settings.AUDIT_CALLBACK = "evennia.security.contrib.auditing.outputs.to_syslog" +settings.AUDIT_IN = True +settings.AUDIT_OUT = True +settings.AUDIT_ALLOW_SPARSE = True + +# Configure settings to use custom session - TODO: This is bad practice, changing global settings +settings.SERVER_SESSION_CLASS = "evennia.contrib.security.auditing.server.AuditedServerSession" + + +class AuditingTest(EvenniaTest): + def test_mask(self): + """ + Make sure the 'mask' function is properly masking potentially sensitive + information from strings. + """ + safe_cmds = ( + "/say hello to my little friend", + "@ccreate channel = for channeling", + "@create/drop some stuff", + "@create rock", + "@create a pretty shirt : evennia.contrib.clothing.Clothing", + "@charcreate johnnyefhiwuhefwhef", + 'Command "@logout" is not available. Maybe you meant "@color" or "@cboot"?', + '/me says, "what is the password?"', + "say the password is plugh", + # Unfortunately given the syntax, there is no way to discern the + # latter of these as sensitive + "@create pretty sunset" "@create johnny password123", + '{"text": "Command \'do stuff\' is not available. Type "help" for help."}', + ) + + for cmd in safe_cmds: + self.assertEqual(self.session.mask(cmd), cmd) + + unsafe_cmds = ( + ( + "something - new password set to 'asdfghjk'.", + "something - new password set to '********'.", + ), + ( + "someone has changed your password to 'something'.", + "someone has changed your password to '*********'.", + ), + ("connect johnny password123", "connect johnny ***********"), + ("concnct johnny password123", "concnct johnny ***********"), + ("concnct johnnypassword123", "concnct *****************"), + ('connect "johnny five" "password 123"', 'connect "johnny five" **************'), + ('connect johnny "password 123"', "connect johnny **************"), + ("create johnny password123", "create johnny ***********"), + ("@password password1234 = password2345", "@password ***************************"), + ("@password password1234 password2345", "@password *************************"), + ("@passwd password1234 = password2345", "@passwd ***************************"), + ("@userpassword johnny = password234", "@userpassword johnny = ***********"), + ("craete johnnypassword123", "craete *****************"), + ( + "Command 'conncect teddy teddy' is not available. Maybe you meant \"@encode\"?", + "Command 'conncect ******** ********' is not available. Maybe you meant \"@encode\"?", + ), + ( + "{'text': u'Command \\'conncect jsis dfiidf\\' is not available. Type \"help\" for help.'}", + "{'text': u'Command \\'conncect jsis ********\\' is not available. Type \"help\" for help.'}", + ), + ) + + for index, (unsafe, safe) in enumerate(unsafe_cmds): + self.assertEqual(re.sub(" ", "", self.session.mask(unsafe)).strip(), safe) + + # Make sure scrubbing is not being abused to evade monitoring + secrets = [ + "say password password password; ive got a secret that i cant explain", + "whisper johnny = password\n let's lynch the landlord", + "say connect johnny password1234|the secret life of arabia", + "@password eval(\"__import__('os').system('clear')\", {'__builtins__':{}})", + ] + for secret in secrets: + self.assertEqual(self.session.mask(secret), secret) + + def test_audit(self): + """ + Make sure the 'audit' function is returning a dictionary based on values + parsed from the Session object. + """ + log = self.session.audit(src="client", text=[["hello"]]) + obj = { + k: v for k, v in log.items() if k in ("direction", "protocol", "application", "text") + } + self.assertEqual( + obj, + { + "direction": "RCV", + "protocol": "telnet", + "application": Anything, # this will change if running tests from the game dir + "text": "hello", + }, + ) + + # Make sure OOB data is being recorded + log = self.session.audit( + src="client", text="connect johnny password123", prompt="hp=20|st=10|ma=15", pane=2 + ) + self.assertEqual(log["text"], "connect johnny ***********") + self.assertEqual(log["data"]["prompt"], "hp=20|st=10|ma=15") + self.assertEqual(log["data"]["pane"], 2) diff --git a/evennia/contrib/utils/random_string_generator/tests.py b/evennia/contrib/utils/random_string_generator/tests.py new file mode 100644 index 0000000000..f8890a16e4 --- /dev/null +++ b/evennia/contrib/utils/random_string_generator/tests.py @@ -0,0 +1,25 @@ +""" +Random string tests. + +""" + +from evennia.utils.test_resources import EvenniaTest +from evennia.contrib import random_string_generator + +SIMPLE_GENERATOR = random_string_generator.RandomStringGenerator("simple", "[01]{2}") + + +class TestRandomStringGenerator(EvenniaTest): + def test_generate(self): + """Generate and fail when exhausted.""" + generated = [] + for i in range(4): + generated.append(SIMPLE_GENERATOR.get()) + + generated.sort() + self.assertEqual(generated, ["00", "01", "10", "11"]) + + # At this point, we have generated 4 strings. + # We can't generate one more + with self.assertRaises(random_string_generator.ExhaustedGenerator): + SIMPLE_GENERATOR.get() diff --git a/evennia/contrib/utils/tree_select/tests.py b/evennia/contrib/utils/tree_select/tests.py new file mode 100644 index 0000000000..cbbba9cd3e --- /dev/null +++ b/evennia/contrib/utils/tree_select/tests.py @@ -0,0 +1,62 @@ +""" +Test tree select + +""" + +from evennia.utils.test_resources import EvenniaTest +from evennia.contrib import tree_select +from evennia.contrib import fieldfill + +TREE_MENU_TESTSTR = """Foo +Bar +-Baz +--Baz 1 +--Baz 2 +-Qux""" + + +class TestTreeSelectFunc(EvenniaTest): + def test_tree_functions(self): + # Dash counter + self.assertTrue(tree_select.dashcount("--test") == 2) + # Is category + self.assertTrue(tree_select.is_category(TREE_MENU_TESTSTR, 1) is True) + # Parse options + self.assertTrue( + tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2) + == [(3, "Baz 1"), (4, "Baz 2")] + ) + # Index to selection + self.assertTrue(tree_select.index_to_selection(TREE_MENU_TESTSTR, 2) == "Baz") + # Go up one category + self.assertTrue(tree_select.go_up_one_category(TREE_MENU_TESTSTR, 4) == 2) + # Option list to menu options + test_optlist = tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2) + optlist_to_menu_expected_result = [ + {"goto": ["menunode_treeselect", {"newindex": 3}], "key": "Baz 1"}, + {"goto": ["menunode_treeselect", {"newindex": 4}], "key": "Baz 2"}, + { + "goto": ["menunode_treeselect", {"newindex": 1}], + "key": ["<< Go Back", "go back", "back"], + "desc": "Return to the previous menu.", + }, + ] + self.assertTrue( + tree_select.optlist_to_menuoptions(TREE_MENU_TESTSTR, test_optlist, 2, True, True) + == optlist_to_menu_expected_result + ) + + +FIELD_TEST_TEMPLATE = [ + {"fieldname": "TextTest", "fieldtype": "text"}, + {"fieldname": "NumberTest", "fieldtype": "number", "blankmsg": "Number here!"}, + {"fieldname": "DefaultText", "fieldtype": "text", "default": "Test"}, + {"fieldname": "DefaultNum", "fieldtype": "number", "default": 3}, +] + +FIELD_TEST_DATA = {"TextTest": None, "NumberTest": None, "DefaultText": "Test", "DefaultNum": 3} + + +class TestFieldFillFunc(EvenniaTest): + def test_field_functions(self): + self.assertTrue(fieldfill.form_template_to_dict(FIELD_TEST_TEMPLATE) == FIELD_TEST_DATA)