Testing adventure dungeon

This commit is contained in:
Griatch 2022-07-24 08:47:25 +02:00
parent a83d3f7fe4
commit 077a43ef7b
2 changed files with 159 additions and 25 deletions

View file

@ -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
)

View file

@ -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)