Testing adventure dungeon
This commit is contained in:
parent
a83d3f7fe4
commit
077a43ef7b
2 changed files with 159 additions and 25 deletions
|
|
@ -19,8 +19,10 @@ from datetime import datetime
|
||||||
from math import sqrt
|
from math import sqrt
|
||||||
from random import randint, random, shuffle
|
from random import randint, random, shuffle
|
||||||
|
|
||||||
from evennia import AttributeProperty, DefaultExit, DefaultScript
|
from evennia.objects.objects import DefaultExit
|
||||||
from evennia.utils import create
|
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 evennia.utils.utils import inherits_from
|
||||||
|
|
||||||
from .rooms import EvAdventureDungeonRoom
|
from .rooms import EvAdventureDungeonRoom
|
||||||
|
|
@ -81,7 +83,7 @@ class EvAdventureDungeonExit(DefaultExit):
|
||||||
target was not yet created.
|
target was not yet created.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not target_location:
|
if target_location == self.location:
|
||||||
self.destination = target_location = self.dungeon_orchestrator.new_room(self)
|
self.destination = target_location = self.dungeon_orchestrator.new_room(self)
|
||||||
super().at_traverse(traversing_object, target_location, **kwargs)
|
super().at_traverse(traversing_object, target_location, **kwargs)
|
||||||
|
|
||||||
|
|
@ -99,10 +101,10 @@ class EvAdventureDungeonOrchestrator(DefaultScript):
|
||||||
max_new_exits_per_room = 3
|
max_new_exits_per_room = 3
|
||||||
|
|
||||||
rooms = AttributeProperty(list())
|
rooms = AttributeProperty(list())
|
||||||
n_unvisited_exits = AttributeProperty(list())
|
unvisited_exits = AttributeProperty(list())
|
||||||
highest_depth = AttributeProperty(0)
|
highest_depth = AttributeProperty(0)
|
||||||
|
|
||||||
# (x,y): room
|
# (x,y): room coordinates used up by orchestrator
|
||||||
xy_grid = AttributeProperty(dict())
|
xy_grid = AttributeProperty(dict())
|
||||||
|
|
||||||
def register_exit_traversed(self, exit):
|
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.
|
Create outgoing exit from a room. The target room is not yet created.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
out_exit, _ = EvAdventureDungeonExit.create(
|
out_exit = create.create_object(
|
||||||
key=exit_direction, location=location, aliases=_EXIT_ALIASES[exit_direction]
|
EvAdventureDungeonExit,
|
||||||
|
key=exit_direction,
|
||||||
|
location=location,
|
||||||
|
aliases=_EXIT_ALIASES[exit_direction],
|
||||||
)
|
)
|
||||||
self.unvisited_exits.append(out_exit.id)
|
self.unvisited_exits.append(out_exit.id)
|
||||||
|
|
||||||
|
|
@ -130,20 +135,33 @@ class EvAdventureDungeonOrchestrator(DefaultScript):
|
||||||
new_room = create.create_object(
|
new_room = create.create_object(
|
||||||
room_typeclass,
|
room_typeclass,
|
||||||
key="Dungeon room",
|
key="Dungeon room",
|
||||||
tags=((self.key,),),
|
tags=((self.key, "dungeon_room"),),
|
||||||
attributes=(("xy_coord", coords, "dungeon_xygrid"),),
|
attributes=(("xy_coord", coords, "dungeon_xygrid"),),
|
||||||
)
|
)
|
||||||
return new_room
|
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):
|
def new_room(self, from_exit):
|
||||||
"""
|
"""
|
||||||
Create a new Dungeon room leading from the provided 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
|
# figure out coordinate of old room and figure out what coord the
|
||||||
# new one would get
|
# new one would get
|
||||||
source_location = from_exit.location
|
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))
|
dx, dy = _EXIT_GRID_SHIFT.get(from_exit.key, (1, 0))
|
||||||
new_x, new_y = (x + dx, y + dy)
|
new_x, new_y = (x + dx, y + dy)
|
||||||
|
|
||||||
|
|
@ -157,8 +175,9 @@ class EvAdventureDungeonOrchestrator(DefaultScript):
|
||||||
self.xy_grid[(new_x, new_y)] = new_room
|
self.xy_grid[(new_x, new_y)] = new_room
|
||||||
|
|
||||||
# always make a return exit back to where we came from
|
# always make a return exit back to where we came from
|
||||||
back_exit_key = (_EXIT_REVERSE_MAPPING.get(from_exit.key, "back"),)
|
back_exit_key = _EXIT_REVERSE_MAPPING.get(from_exit.key, "back")
|
||||||
EvAdventureDungeonExit(
|
create.create_object(
|
||||||
|
EvAdventureDungeonExit,
|
||||||
key=back_exit_key,
|
key=back_exit_key,
|
||||||
aliases=_EXIT_ALIASES.get(back_exit_key, ()),
|
aliases=_EXIT_ALIASES.get(back_exit_key, ()),
|
||||||
location=new_room,
|
location=new_room,
|
||||||
|
|
@ -168,14 +187,14 @@ class EvAdventureDungeonOrchestrator(DefaultScript):
|
||||||
|
|
||||||
# figure out what other exits should be here, if any
|
# figure out what other exits should be here, if any
|
||||||
n_unexplored = len(self.unvisited_exits)
|
n_unexplored = len(self.unvisited_exits)
|
||||||
if n_unexplored >= self.max_unexplored_exits:
|
|
||||||
# no more exits to open - this is a dead end.
|
if n_unexplored < self.max_unexplored_exits:
|
||||||
return
|
# we have a budget of unexplored exits to open
|
||||||
else:
|
n_exits = min(self.max_new_exits_per_room, self.max_unexplored_exits)
|
||||||
n_exits = randint(1, min(self.max_new_exits_per_room, n_unexplored))
|
if n_exits > 1:
|
||||||
back_exit = from_exit.key
|
n_exits = randint(1, n_exits)
|
||||||
available_directions = [
|
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
|
# randomize order of exits
|
||||||
shuffle(available_directions)
|
shuffle(available_directions)
|
||||||
|
|
@ -184,11 +203,14 @@ class EvAdventureDungeonOrchestrator(DefaultScript):
|
||||||
# get a random direction and check so there isn't a room already
|
# get a random direction and check so there isn't a room already
|
||||||
# created in that direction
|
# created in that direction
|
||||||
direction = available_directions.pop(0)
|
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)
|
target_coord = (new_x + dx, new_y + dy)
|
||||||
if target_coord not in self.xy_grid:
|
if target_coord not in self.xy_grid:
|
||||||
# no room there - make an exit to it
|
# no room there - make an exit to it
|
||||||
self.create_out_exit(new_room, direction)
|
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
|
break
|
||||||
|
|
||||||
self.highest_depth = max(self.highest_depth, depth)
|
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
|
Traversing this exit will either lead to an existing dungeon branch or create
|
||||||
a new one.
|
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)
|
dungeon_orchestrator = AttributeProperty(None, autocreate=False)
|
||||||
|
|
||||||
def reset_exit(self):
|
def reset_exit(self):
|
||||||
|
|
@ -213,18 +241,20 @@ class EvAdventureStartRoomExit(DefaultExit):
|
||||||
Flush the exit, so next traversal creates a new dungeon branch.
|
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):
|
def at_traverse(self, traversing_object, target_location, **kwargs):
|
||||||
"""
|
"""
|
||||||
When traversing create a new orchestrator if one is not already assigned.
|
When traversing create a new orchestrator if one is not already assigned.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if target_location is None or self.dungeon_orchestrator is None:
|
if target_location == self.location or self.dungeon_orchestrator is None:
|
||||||
self.dungeon_orchestrator, _ = EvAdventureDungeonOrchestrator.create(
|
self.dungeon_orchestrator = create.create_script(
|
||||||
f"dungeon_orchestrator_{datetime.utcnow()}",
|
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)
|
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):
|
def at_repeat(self):
|
||||||
"""
|
"""
|
||||||
Called every time the script repeats.
|
Called every time the script repeats.
|
||||||
|
|
@ -259,4 +292,8 @@ class EvAdventureDungeonRoomStart(EvAdventureDungeonRoom):
|
||||||
recycle_time = 5 * 60 # seconds
|
recycle_time = 5 * 60 # seconds
|
||||||
|
|
||||||
def at_object_creation(self):
|
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
|
||||||
|
)
|
||||||
|
|
|
||||||
97
evennia/contrib/tutorials/evadventure/tests/test_dungeon.py
Normal file
97
evennia/contrib/tutorials/evadventure/tests/test_dungeon.py
Normal 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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue