Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 19 additions & 96 deletions stories/dungeon/story.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
from tale import lang
from tale.base import Door, Exit, Location
from tale.charbuilder import PlayerNaming
from tale.coord import Coord
from tale.driver import Driver
from tale.dungeon.dungeon import Dungeon
from tale.dungeon.dungeon_generator import ItemPopulator, Layout, LayoutGenerator, MobPopulator
from tale.items.basic import Money
from tale.json_story import JsonStory
Expand All @@ -31,10 +33,21 @@ def __init__(self, path = '', layout_generator = LayoutGenerator(), mob_populato
self.item_populator = item_populator
self.max_depth = 5
self.depth = 0
self.dungeon = None # Will be created after init


def init(self, driver: Driver) -> None:
self.llm_util = driver.llm_util
# Create the dungeon instance BEFORE calling super().init()
self.dungeon = Dungeon(
name="The Depths",
story=self,
llm_util=self.llm_util,
layout_generator=self.layout_generator,
mob_populator=self.mob_populator,
item_populator=self.item_populator,
max_depth=self.max_depth
)
super(Story, self).init(driver)

def init_player(self, player: Player) -> None:
Expand Down Expand Up @@ -104,105 +117,15 @@ def add_zone(self, zone: Zone) -> bool:
return False
if zone.locations != {}:
return True
first_zone = len(self._zones.values()) == 0
zone.size_z = 1
layout = self.layout_generator.generate()

rooms = self._prepare_locations(layout=layout, first_zone=first_zone)

self._describe_rooms(zone=zone, layout=layout, rooms=rooms)

self._connect_locations(layout=layout)

mob_spawners = self.mob_populator.populate(zone=zone, layout=layout, story=self)
for mob_spawner in mob_spawners:
self.world.add_mob_spawner(mob_spawner)

item_spawners = self.item_populator.populate(zone=zone, story=self)
for item_spawner in item_spawners:
self.world.add_item_spawner(item_spawner)

if zone.center.z == self.max_depth:
self._generate_boss(zone=zone)

if not first_zone:
self.layout_generator.spawn_gold(zone=zone)

# Use the dungeon to generate the level
if self.dungeon:
depth = len(self.dungeon.zones)
self.depth = depth
self.dungeon.generate_level(zone, depth=depth)

return True

def _describe_rooms(self, zone: Zone, layout: Layout, rooms: list):
described_rooms = []
sliced_rooms = []
for num in range(0, len(rooms), 10):
sliced_rooms.extend(rooms[num:num+10])
for i in range(3):
described_rooms_slice = self.llm_util.generate_dungeon_locations(zone_info=zone.get_info(), locations=sliced_rooms, depth = self.depth, max_depth=self.max_depth) # type LocationDescriptionResponse
if described_rooms_slice.valid:
described_rooms.extend(described_rooms_slice.location_descriptions)
sliced_rooms = []
break
if len(rooms) != len(described_rooms):
print(f'Rooms list not same length: {len(rooms)} vs {len(described_rooms)}')
for room in described_rooms:
i = 1
if zone.get_location(room.name):
# ensure unique names
room.name = f'{room.name}({i})'
i += 1
location = Location(name=room.name, descr=room.description)
location.world_location = list(layout.cells.values())[room.index].coord
zone.add_location(location=location)
self.add_location(zone=zone.name, location=location)
return described_rooms


def _prepare_locations(self, layout: Layout, first_zone: bool = False) -> list:
index = 0
rooms = []
for cell in list(layout.cells.values()):
if cell.is_dungeon_entrance:
rooms.append(f'{{"index": {index}, "name": "Entrance to dungeon"}}')
if cell.is_entrance:
rooms.append(f'{{"index": {index}, "name": "Room with pathway leading up to this level."}}')
elif cell.is_exit:
rooms.append(f'{{"index": {index}, "name": "Room with pathway leading down"}}')
elif cell.is_room:
rooms.append(f'{{"index": {index}, "name": "Room"}}')
else:
rooms.append(f'{{"index": {index}, "name": "Hallway", "description": "A hallway"}}')
index += 1
return rooms

def _connect_locations(self, layout: Layout) -> None:
connections = layout.connections
for connection in connections:
cell_location = self.world._grid.get(connection.coord.as_tuple(), None) # type: Location
parent_location = self.world._grid.get(connection.other.as_tuple(), None) # type: Location
if cell_location.exits.get(parent_location.name, None):
continue
elif parent_location.exits.get(cell_location.name, None):
continue
if connection.door:
Door.connect(cell_location, parent_location.name, '', None, parent_location, cell_location.name, '', None, opened=False, locked=connection.locked, key_code=connection.key_code)
else:
Exit.connect(cell_location, parent_location.name, '', None, parent_location, cell_location.name, '', None)

def _generate_boss(self, zone: Zone) -> bool:
character = self.llm_util.generate_character(keywords=['final boss']) # Characterv2
if character:
boss = RoamingMob(character.name,
gender=character.gender,
title=lang.capital(character.name),
descr=character.description,
short_descr=character.appearance,
age=character.age,
personality=character.personality)
boss.aliases = [character.name.split(' ')[0]]
boss.stats.level = self.max_depth
location = random.choice(list(zone.locations.values()))
location.insert(boss, None)
return True
return False

if __name__ == "__main__":
# story is invoked as a script, start it in the Tale Driver.
Expand Down
1 change: 1 addition & 0 deletions stories/dungeon_example/llm_cache.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
70 changes: 70 additions & 0 deletions stories/dungeon_example/story.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
Example story demonstrating a dungeon entrance in a normal location.

This story shows how dungeons can be integrated into any story,
not just dungeon-specific stories.
"""

import pathlib
import sys

from tale import parse_utils
from tale.base import Location
from tale.coord import Coord
from tale.driver import Driver
from tale.dungeon.DungeonEntrance import DungeonEntrance
from tale.dungeon.dungeon import Dungeon
from tale.dungeon.dungeon_generator import ItemPopulator, LayoutGenerator, MobPopulator
from tale.json_story import JsonStory
from tale.main import run_from_cmdline
from tale.player import Player
from tale.zone import Zone


class Story(JsonStory):
"""Example story with a normal location that has a dungeon entrance."""

driver = None

def __init__(self, path: str = '') -> None:
if not path:
# If no path provided, use the directory containing this file
import os
path = os.path.dirname(os.path.abspath(__file__)) + '/'
config = parse_utils.load_story_config(parse_utils.load_json(path + 'story_config.json'))
super(Story, self).__init__(path, config)
self.dungeon = None

def init(self, driver: Driver) -> None:
"""Initialize the story and create the dungeon."""
super(Story, self).init(driver)

def welcome(self, player: Player) -> str:
"""Welcome text when player enters a new game."""
player.tell("<bright>Welcome to the Town of Mysteries!</>", end=True)
player.tell("\n")
player.tell("You stand in the town square. Locals speak of an ancient crypt "
"beneath the town, filled with treasures and dangers.")
player.tell("\n")
return ""

def welcome_savegame(self, player: Player) -> str:
"""Welcome text when player loads a saved game."""
player.tell("<bright>Welcome back to the Town of Mysteries!</>", end=True)
player.tell("\n")
return ""

def goodbye(self, player: Player) -> None:
"""Goodbye text when player quits the game."""
player.tell("Farewell, brave adventurer. May we meet again!")
player.tell("\n")


if __name__ == "__main__":
# Story is invoked as a script, start it in the Tale Driver.
gamedir = pathlib.Path(__file__).parent
if gamedir.is_dir() or gamedir.is_file():
cmdline_args = sys.argv[1:]
cmdline_args.insert(0, "--game")
cmdline_args.insert(1, str(gamedir))
run_from_cmdline(cmdline_args)
29 changes: 29 additions & 0 deletions stories/dungeon_example/story_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "Town of Mysteries",
"author": "LlamaTale Example",
"author_address": "",
"version": "1.0",
"requires_tale": "4.0",
"supported_modes": ["IF"],
"player_name": "",
"player_gender": "m",
"player_race": "human",
"player_money": 50.0,
"money_type": "MODERN",
"server_tick_method": "TIMER",
"server_tick_time": 1.0,
"gametime_to_realtime": 1,
"max_load_time": 60.0,
"display_gametime": true,
"startlocation_player": "town.Town Square",
"startlocation_wizard": "town.Town Square",
"zones": ["town"],
"server_mode": "IF",
"context": "A peaceful town with an ancient crypt beneath it.",
"type": "fantasy",
"world_mood": 5,
"world_info": {
"races": ["human", "elf"],
"items": ["torch", "Sword"]
}
}
112 changes: 112 additions & 0 deletions stories/dungeon_example/world.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
{
"story": {
"name": "Town of Mysteries"
},
"zones": {
"Town": {
"description": "town",
"level": 1,
"mood": 3,
"races": [],
"items": [],
"size": 5,
"center": [
0,
0,
0
],
"name": "town",
"dungeon_config": {
"name": "Ancient Crypt",
"description": "A dark and ancient crypt beneath the town",
"races": ["bat", "wolf", "skeleton"],
"items": ["torch", "Sword"],
"max_depth": 5
},
"locations": [
{
"name": "Town Square",
"descr": "A bustling town square with a fountain in the center. ",
"short_descr": "",
"exits": [
{
"name": "Ancient Crypt",
"direction": "north",
"short_descr": "An ancient stone archway descends into darkness.",
"long_descr": "The archway is covered in moss and strange runes. A cold wind blows from the depths below.",
"type": "DungeonEntrance",
"dungeon_name": "Ancient Crypt"
}
],
"world_location": [
0,
0,
0
],
"built": false
},
{
"name": "Ancient Crypt",
"exits": [
{
"name": "Town Square",
"direction": "south",
"short_descr": "A stone archway leading back to the town square.",
"long_descr": "The archway is covered in moss and strange runes. Sunlight filters down from above."
}
]
,
"world_location": [
0,
0,
1
],
"built": false

}
]
}
},
"world": {
"npcs": {},
"items": {},
"spawners": [],
"item_spawners": []
},
"catalogue": {
"items": [
{
"name": "torch",
"type": "light",
"value": 5,
"description": "A simple wooden torch"
},
{
"name": "Sword",
"type": "weapon",
"value": 50,
"description": "A steel sword"
}
],
"creatures": [
{
"name": "bat",
"level": 1,
"aggressive": true,
"description": "A small cave bat"
},
{
"name": "wolf",
"level": 2,
"aggressive": true,
"description": "A grey wolf"
},
{
"name": "skeleton",
"level": 3,
"aggressive": true,
"description": "An animated skeleton warrior"
}
]
}
}
14 changes: 13 additions & 1 deletion tale/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from typing import Sequence, Union, Tuple, Any, Dict, Callable, Iterable, Generator, Set, List, MutableSequence, Optional

import appdirs
from tale.dungeon import DungeonEntrance
from tale.items.basic import Note

from tale.llm import llm_config
Expand Down Expand Up @@ -623,6 +624,16 @@ def go_through_exit(self, player: player.Player, direction: str, evoke: bool=Tru
xt = player.location.exits[direction]
xt.allow_passage(player)
target_location = xt.target # type: base.Location

if isinstance(xt, DungeonEntrance.DungeonEntrance):
dungeon_entrance = typing.cast(DungeonEntrance.DungeonEntrance, xt)
if not dungeon_entrance.dungeon:
# Get the dungeon config from the zone containing the entrance
dynamic_story = typing.cast(DynamicStory, self.story)
zone = dynamic_story.find_zone(location=player.location.name)
dungeon_config = zone.dungeon_config if zone else None
dungeon_entrance.build_dungeon(self.story, self.llm_util, dungeon_config)

if not target_location.built:
dynamic_story = typing.cast(DynamicStory, self.story)
zone = dynamic_story.find_zone(location=player.location.name)
Expand Down Expand Up @@ -923,7 +934,8 @@ def build_location(self, targetLocation: base.Location, zone: Zone, player: play
zone_info=zone.get_info(),
world_creatures=dynamic_story.catalogue._creatures,
world_items=dynamic_story.catalogue._items,
neighbors=neighbor_locations)
neighbors=neighbor_locations,
zone=zone)
new_locations = result.new_locations
exits = result.exits
npcs = result.npcs
Expand Down
Loading