From 077a43ef7b0de8f40551b151a5ff1d657fabc8f4 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 24 Jul 2022 08:47:25 +0200 Subject: [PATCH] Testing adventure dungeon --- .../contrib/tutorials/evadventure/dungeon.py | 87 ++++++++++++----- .../evadventure/tests/test_dungeon.py | 97 +++++++++++++++++++ 2 files changed, 159 insertions(+), 25 deletions(-) create mode 100644 evennia/contrib/tutorials/evadventure/tests/test_dungeon.py diff --git a/evennia/contrib/tutorials/evadventure/dungeon.py b/evennia/contrib/tutorials/evadventure/dungeon.py index d2926e6206..af30168e05 100644 --- a/evennia/contrib/tutorials/evadventure/dungeon.py +++ b/evennia/contrib/tutorials/evadventure/dungeon.py @@ -19,8 +19,10 @@ from datetime import datetime from math import sqrt from random import randint, random, shuffle -from evennia import AttributeProperty, DefaultExit, DefaultScript -from evennia.utils import create +from evennia.objects.objects import DefaultExit +from evennia.scripts.scripts import DefaultScript +from evennia.typeclasses.attributes import AttributeProperty +from evennia.utils import create, search from evennia.utils.utils import inherits_from from .rooms import EvAdventureDungeonRoom @@ -81,7 +83,7 @@ class EvAdventureDungeonExit(DefaultExit): target was not yet created. """ - if not target_location: + if target_location == self.location: self.destination = target_location = self.dungeon_orchestrator.new_room(self) super().at_traverse(traversing_object, target_location, **kwargs) @@ -99,10 +101,10 @@ class EvAdventureDungeonOrchestrator(DefaultScript): max_new_exits_per_room = 3 rooms = AttributeProperty(list()) - n_unvisited_exits = AttributeProperty(list()) + unvisited_exits = AttributeProperty(list()) highest_depth = AttributeProperty(0) - # (x,y): room + # (x,y): room coordinates used up by orchestrator xy_grid = AttributeProperty(dict()) def register_exit_traversed(self, exit): @@ -119,8 +121,11 @@ class EvAdventureDungeonOrchestrator(DefaultScript): Create outgoing exit from a room. The target room is not yet created. """ - out_exit, _ = EvAdventureDungeonExit.create( - key=exit_direction, location=location, aliases=_EXIT_ALIASES[exit_direction] + out_exit = create.create_object( + EvAdventureDungeonExit, + key=exit_direction, + location=location, + aliases=_EXIT_ALIASES[exit_direction], ) self.unvisited_exits.append(out_exit.id) @@ -130,20 +135,33 @@ class EvAdventureDungeonOrchestrator(DefaultScript): new_room = create.create_object( room_typeclass, key="Dungeon room", - tags=((self.key,),), + tags=((self.key, "dungeon_room"),), attributes=(("xy_coord", coords, "dungeon_xygrid"),), ) return new_room + def delete(self): + """ + Clean up the entire dungeon along with the orchestrator. + + """ + rooms = search.search_object_by_tag(self.key, category="dungeon_room") + for room in rooms: + room.delete() + super().delete() + def new_room(self, from_exit): """ Create a new Dungeon room leading from the provided exit. + Args: + from_exit (Exit): The exit leading to this new room. + """ # figure out coordinate of old room and figure out what coord the # new one would get source_location = from_exit.location - x, y = source_location.get("xy_coord", category="dungeon_xygrid", default=(0, 0)) + x, y = source_location.attributes.get("xy_coord", category="dungeon_xygrid", default=(0, 0)) dx, dy = _EXIT_GRID_SHIFT.get(from_exit.key, (1, 0)) new_x, new_y = (x + dx, y + dy) @@ -157,8 +175,9 @@ class EvAdventureDungeonOrchestrator(DefaultScript): self.xy_grid[(new_x, new_y)] = new_room # always make a return exit back to where we came from - back_exit_key = (_EXIT_REVERSE_MAPPING.get(from_exit.key, "back"),) - EvAdventureDungeonExit( + back_exit_key = _EXIT_REVERSE_MAPPING.get(from_exit.key, "back") + create.create_object( + EvAdventureDungeonExit, key=back_exit_key, aliases=_EXIT_ALIASES.get(back_exit_key, ()), location=new_room, @@ -168,14 +187,14 @@ class EvAdventureDungeonOrchestrator(DefaultScript): # figure out what other exits should be here, if any n_unexplored = len(self.unvisited_exits) - if n_unexplored >= self.max_unexplored_exits: - # no more exits to open - this is a dead end. - return - else: - n_exits = randint(1, min(self.max_new_exits_per_room, n_unexplored)) - back_exit = from_exit.key + + if n_unexplored < self.max_unexplored_exits: + # we have a budget of unexplored exits to open + n_exits = min(self.max_new_exits_per_room, self.max_unexplored_exits) + if n_exits > 1: + n_exits = randint(1, n_exits) available_directions = [ - direction for direction in _EXIT_ALIASES if direction != back_exit + direction for direction in _EXIT_ALIASES if direction != back_exit_key ] # randomize order of exits shuffle(available_directions) @@ -184,11 +203,14 @@ class EvAdventureDungeonOrchestrator(DefaultScript): # get a random direction and check so there isn't a room already # created in that direction direction = available_directions.pop(0) - dx, dy = _EXIT_GRID_SHIFT(direction) + dx, dy = _EXIT_GRID_SHIFT[direction] target_coord = (new_x + dx, new_y + dy) if target_coord not in self.xy_grid: # no room there - make an exit to it self.create_out_exit(new_room, direction) + # we create this to avoid other rooms linking here, but don't create the + # room yet + self.xy_grid[target_coord] = None break self.highest_depth = max(self.highest_depth, depth) @@ -204,8 +226,14 @@ class EvAdventureStartRoomExit(DefaultExit): Traversing this exit will either lead to an existing dungeon branch or create a new one. + Since exits need to have a destination, we start out having them loop back to + the same location and change this whenever someone actually traverse them. The + act of passing through creates a room on the other side. + """ + # we store the orchestrator like this since we don't want to actually manipulate it, + # but only use the reference to know when to create a new room dungeon_orchestrator = AttributeProperty(None, autocreate=False) def reset_exit(self): @@ -213,18 +241,20 @@ class EvAdventureStartRoomExit(DefaultExit): Flush the exit, so next traversal creates a new dungeon branch. """ - self.dungeon_orchestrator = self.destination = None + self.dungeon_orchestrator = None + self.destination = self.location def at_traverse(self, traversing_object, target_location, **kwargs): """ When traversing create a new orchestrator if one is not already assigned. """ - if target_location is None or self.dungeon_orchestrator is None: - self.dungeon_orchestrator, _ = EvAdventureDungeonOrchestrator.create( - f"dungeon_orchestrator_{datetime.utcnow()}", + if target_location == self.location or self.dungeon_orchestrator is None: + self.dungeon_orchestrator = create.create_script( + EvAdventureDungeonOrchestrator, + key=f"dungeon_orchestrator_{self.key}_{datetime.utcnow()}", ) - target_location = self.destination = self.dungeon_orchestrator.new_room(self) + self.destination = target_location = self.dungeon_orchestrator.new_room(self) super().at_traverse(traversing_object, target_location, **kwargs) @@ -235,6 +265,9 @@ class EvAdventureStartRoomResetter(DefaultScript): """ + def at_script_creation(self): + self.key = "evadventure_startroom_resetter" + def at_repeat(self): """ Called every time the script repeats. @@ -259,4 +292,8 @@ class EvAdventureDungeonRoomStart(EvAdventureDungeonRoom): recycle_time = 5 * 60 # seconds def at_object_creation(self): - self.scripts.add(EvAdventureStartRoomResetter, interval=self.recycle_time, autostart=True) + # want to set the script interval on creation time, so we use create_script with obj=self + # instead of self.scripts.add() here + create.create_script( + EvAdventureStartRoomResetter, obj=self, interval=self.recycle_time, autostart=True + ) diff --git a/evennia/contrib/tutorials/evadventure/tests/test_dungeon.py b/evennia/contrib/tutorials/evadventure/tests/test_dungeon.py new file mode 100644 index 0000000000..4b04743ad5 --- /dev/null +++ b/evennia/contrib/tutorials/evadventure/tests/test_dungeon.py @@ -0,0 +1,97 @@ +""" +Test Dungeon orchestrator / procedurally generated dungeon rooms. + +""" + +from unittest.mock import MagicMock + +from evennia.utils.create import create_object +from evennia.utils.test_resources import BaseEvenniaTest +from evennia.utils.utils import inherits_from + +from .. import dungeon +from ..rooms import EvAdventureDungeonRoom +from .mixins import EvAdventureMixin + + +class TestDungeon(EvAdventureMixin, BaseEvenniaTest): + """ + Test with a starting room and a character moving through the dungeon, + generating more and more rooms as they go. + + """ + + def setUp(self): + """ + Create a start room with exits leading away from it + + """ + super().setUp() + droomclass = dungeon.EvAdventureDungeonRoomStart + droomclass.recycle_time = 0 # disable the tick + + self.start_room = create_object(droomclass, key="bottom of well") + + self.assertEqual( + self.start_room.scripts.get("evadventure_startroom_resetter")[0].interval, -1 + ) + self.start_north = create_object( + dungeon.EvAdventureStartRoomExit, + key="north", + location=self.start_room, + destination=self.start_room, + ) + self.start_north + self.start_south = create_object( + dungeon.EvAdventureStartRoomExit, + key="south", + location=self.start_room, + destination=self.start_room, + ) + self.character.location = self.start_room + + def _move_character(self, direction): + old_location = self.character.location + for exi in old_location.exits: + if exi.key == direction: + # by setting target to old-location we trigger the + # special behavior of this Exit type + exi.at_traverse(self.character, old_location) + break + return self.character.location + + def test_start_room(self): + """ + Test move through one of the start room exits. + + """ + # begin in start room + self.assertEqual(self.character.location, self.start_room) + + # first go north, this should generate a new room + new_room_north = self._move_character("north") + self.assertNotEqual(self.start_room, new_room_north) + self.assertTrue(inherits_from(new_room_north, EvAdventureDungeonRoom)) + + # check if Orchestrator was created + orchestrator = self.start_north.scripts.get(dungeon.EvAdventureDungeonOrchestrator) + self.assertTrue(bool(orchestrator)) + self.assertTrue(orchestrator.key.startswith("dungeon_orchestrator_north_")) + + def test_different_start_directions(self): + # first go north, this should generate a new room + new_room_north = self._move_character("north") + self.assertNotEqual(self.start_room, new_room_north) + + # back to start room + start_room = self._move_character("south") + self.assertEqual(self.start_room, start_room) + + # next go south, this should generate a new room + new_room_south = self._move_character("south") + self.assertNotEqual(self.start_room, new_room_south) + self.assertNotEqual(new_room_north, new_room_south) + + # back to start room again + start_room = self._move_character("north") + self.assertEqual(self.start_room, start_room)