Skip to content

Commit f15e00e

Browse files
authored
Merge pull request #121 from neph1/update-v0.41.0
Update v0.41.0
2 parents e7457d2 + bd4e293 commit f15e00e

35 files changed

+1607
-186
lines changed

stories/dungeon/story.py

Lines changed: 19 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from tale import lang
88
from tale.base import Door, Exit, Location
99
from tale.charbuilder import PlayerNaming
10+
from tale.coord import Coord
1011
from tale.driver import Driver
12+
from tale.dungeon.dungeon import Dungeon
1113
from tale.dungeon.dungeon_generator import ItemPopulator, Layout, LayoutGenerator, MobPopulator
1214
from tale.items.basic import Money
1315
from tale.json_story import JsonStory
@@ -31,10 +33,21 @@ def __init__(self, path = '', layout_generator = LayoutGenerator(), mob_populato
3133
self.item_populator = item_populator
3234
self.max_depth = 5
3335
self.depth = 0
36+
self.dungeon = None # Will be created after init
3437

3538

3639
def init(self, driver: Driver) -> None:
3740
self.llm_util = driver.llm_util
41+
# Create the dungeon instance BEFORE calling super().init()
42+
self.dungeon = Dungeon(
43+
name="The Depths",
44+
story=self,
45+
llm_util=self.llm_util,
46+
layout_generator=self.layout_generator,
47+
mob_populator=self.mob_populator,
48+
item_populator=self.item_populator,
49+
max_depth=self.max_depth
50+
)
3851
super(Story, self).init(driver)
3952

4053
def init_player(self, player: Player) -> None:
@@ -104,105 +117,15 @@ def add_zone(self, zone: Zone) -> bool:
104117
return False
105118
if zone.locations != {}:
106119
return True
107-
first_zone = len(self._zones.values()) == 0
108-
zone.size_z = 1
109-
layout = self.layout_generator.generate()
110-
111-
rooms = self._prepare_locations(layout=layout, first_zone=first_zone)
112-
113-
self._describe_rooms(zone=zone, layout=layout, rooms=rooms)
114120

115-
self._connect_locations(layout=layout)
116-
117-
mob_spawners = self.mob_populator.populate(zone=zone, layout=layout, story=self)
118-
for mob_spawner in mob_spawners:
119-
self.world.add_mob_spawner(mob_spawner)
120-
121-
item_spawners = self.item_populator.populate(zone=zone, story=self)
122-
for item_spawner in item_spawners:
123-
self.world.add_item_spawner(item_spawner)
124-
125-
if zone.center.z == self.max_depth:
126-
self._generate_boss(zone=zone)
127-
128-
if not first_zone:
129-
self.layout_generator.spawn_gold(zone=zone)
130-
121+
# Use the dungeon to generate the level
122+
if self.dungeon:
123+
depth = len(self.dungeon.zones)
124+
self.depth = depth
125+
self.dungeon.generate_level(zone, depth=depth)
126+
131127
return True
132-
133-
def _describe_rooms(self, zone: Zone, layout: Layout, rooms: list):
134-
described_rooms = []
135-
sliced_rooms = []
136-
for num in range(0, len(rooms), 10):
137-
sliced_rooms.extend(rooms[num:num+10])
138-
for i in range(3):
139-
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
140-
if described_rooms_slice.valid:
141-
described_rooms.extend(described_rooms_slice.location_descriptions)
142-
sliced_rooms = []
143-
break
144-
if len(rooms) != len(described_rooms):
145-
print(f'Rooms list not same length: {len(rooms)} vs {len(described_rooms)}')
146-
for room in described_rooms:
147-
i = 1
148-
if zone.get_location(room.name):
149-
# ensure unique names
150-
room.name = f'{room.name}({i})'
151-
i += 1
152-
location = Location(name=room.name, descr=room.description)
153-
location.world_location = list(layout.cells.values())[room.index].coord
154-
zone.add_location(location=location)
155-
self.add_location(zone=zone.name, location=location)
156-
return described_rooms
157128

158-
159-
def _prepare_locations(self, layout: Layout, first_zone: bool = False) -> list:
160-
index = 0
161-
rooms = []
162-
for cell in list(layout.cells.values()):
163-
if cell.is_dungeon_entrance:
164-
rooms.append(f'{{"index": {index}, "name": "Entrance to dungeon"}}')
165-
if cell.is_entrance:
166-
rooms.append(f'{{"index": {index}, "name": "Room with pathway leading up to this level."}}')
167-
elif cell.is_exit:
168-
rooms.append(f'{{"index": {index}, "name": "Room with pathway leading down"}}')
169-
elif cell.is_room:
170-
rooms.append(f'{{"index": {index}, "name": "Room"}}')
171-
else:
172-
rooms.append(f'{{"index": {index}, "name": "Hallway", "description": "A hallway"}}')
173-
index += 1
174-
return rooms
175-
176-
def _connect_locations(self, layout: Layout) -> None:
177-
connections = layout.connections
178-
for connection in connections:
179-
cell_location = self.world._grid.get(connection.coord.as_tuple(), None) # type: Location
180-
parent_location = self.world._grid.get(connection.other.as_tuple(), None) # type: Location
181-
if cell_location.exits.get(parent_location.name, None):
182-
continue
183-
elif parent_location.exits.get(cell_location.name, None):
184-
continue
185-
if connection.door:
186-
Door.connect(cell_location, parent_location.name, '', None, parent_location, cell_location.name, '', None, opened=False, locked=connection.locked, key_code=connection.key_code)
187-
else:
188-
Exit.connect(cell_location, parent_location.name, '', None, parent_location, cell_location.name, '', None)
189-
190-
def _generate_boss(self, zone: Zone) -> bool:
191-
character = self.llm_util.generate_character(keywords=['final boss']) # Characterv2
192-
if character:
193-
boss = RoamingMob(character.name,
194-
gender=character.gender,
195-
title=lang.capital(character.name),
196-
descr=character.description,
197-
short_descr=character.appearance,
198-
age=character.age,
199-
personality=character.personality)
200-
boss.aliases = [character.name.split(' ')[0]]
201-
boss.stats.level = self.max_depth
202-
location = random.choice(list(zone.locations.values()))
203-
location.insert(boss, None)
204-
return True
205-
return False
206129

207130
if __name__ == "__main__":
208131
# story is invoked as a script, start it in the Tale Driver.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

stories/dungeon_example/story.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
Example story demonstrating a dungeon entrance in a normal location.
3+
4+
This story shows how dungeons can be integrated into any story,
5+
not just dungeon-specific stories.
6+
"""
7+
8+
import pathlib
9+
import sys
10+
11+
from tale import parse_utils
12+
from tale.base import Location
13+
from tale.coord import Coord
14+
from tale.driver import Driver
15+
from tale.dungeon.DungeonEntrance import DungeonEntrance
16+
from tale.dungeon.dungeon import Dungeon
17+
from tale.dungeon.dungeon_generator import ItemPopulator, LayoutGenerator, MobPopulator
18+
from tale.json_story import JsonStory
19+
from tale.main import run_from_cmdline
20+
from tale.player import Player
21+
from tale.zone import Zone
22+
23+
24+
class Story(JsonStory):
25+
"""Example story with a normal location that has a dungeon entrance."""
26+
27+
driver = None
28+
29+
def __init__(self, path: str = '') -> None:
30+
if not path:
31+
# If no path provided, use the directory containing this file
32+
import os
33+
path = os.path.dirname(os.path.abspath(__file__)) + '/'
34+
config = parse_utils.load_story_config(parse_utils.load_json(path + 'story_config.json'))
35+
super(Story, self).__init__(path, config)
36+
self.dungeon = None
37+
38+
def init(self, driver: Driver) -> None:
39+
"""Initialize the story and create the dungeon."""
40+
super(Story, self).init(driver)
41+
42+
def welcome(self, player: Player) -> str:
43+
"""Welcome text when player enters a new game."""
44+
player.tell("<bright>Welcome to the Town of Mysteries!</>", end=True)
45+
player.tell("\n")
46+
player.tell("You stand in the town square. Locals speak of an ancient crypt "
47+
"beneath the town, filled with treasures and dangers.")
48+
player.tell("\n")
49+
return ""
50+
51+
def welcome_savegame(self, player: Player) -> str:
52+
"""Welcome text when player loads a saved game."""
53+
player.tell("<bright>Welcome back to the Town of Mysteries!</>", end=True)
54+
player.tell("\n")
55+
return ""
56+
57+
def goodbye(self, player: Player) -> None:
58+
"""Goodbye text when player quits the game."""
59+
player.tell("Farewell, brave adventurer. May we meet again!")
60+
player.tell("\n")
61+
62+
63+
if __name__ == "__main__":
64+
# Story is invoked as a script, start it in the Tale Driver.
65+
gamedir = pathlib.Path(__file__).parent
66+
if gamedir.is_dir() or gamedir.is_file():
67+
cmdline_args = sys.argv[1:]
68+
cmdline_args.insert(0, "--game")
69+
cmdline_args.insert(1, str(gamedir))
70+
run_from_cmdline(cmdline_args)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "Town of Mysteries",
3+
"author": "LlamaTale Example",
4+
"author_address": "",
5+
"version": "1.0",
6+
"requires_tale": "4.0",
7+
"supported_modes": ["IF"],
8+
"player_name": "",
9+
"player_gender": "m",
10+
"player_race": "human",
11+
"player_money": 50.0,
12+
"money_type": "MODERN",
13+
"server_tick_method": "TIMER",
14+
"server_tick_time": 1.0,
15+
"gametime_to_realtime": 1,
16+
"max_load_time": 60.0,
17+
"display_gametime": true,
18+
"startlocation_player": "town.Town Square",
19+
"startlocation_wizard": "town.Town Square",
20+
"zones": ["town"],
21+
"server_mode": "IF",
22+
"context": "A peaceful town with an ancient crypt beneath it.",
23+
"type": "fantasy",
24+
"world_mood": 5,
25+
"world_info": {
26+
"races": ["human", "elf"],
27+
"items": ["torch", "Sword"]
28+
}
29+
}

stories/dungeon_example/world.json

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
{
2+
"story": {
3+
"name": "Town of Mysteries"
4+
},
5+
"zones": {
6+
"Town": {
7+
"description": "town",
8+
"level": 1,
9+
"mood": 3,
10+
"races": [],
11+
"items": [],
12+
"size": 5,
13+
"center": [
14+
0,
15+
0,
16+
0
17+
],
18+
"name": "town",
19+
"dungeon_config": {
20+
"name": "Ancient Crypt",
21+
"description": "A dark and ancient crypt beneath the town",
22+
"races": ["bat", "wolf", "skeleton"],
23+
"items": ["torch", "Sword"],
24+
"max_depth": 5
25+
},
26+
"locations": [
27+
{
28+
"name": "Town Square",
29+
"descr": "A bustling town square with a fountain in the center. ",
30+
"short_descr": "",
31+
"exits": [
32+
{
33+
"name": "Ancient Crypt",
34+
"direction": "north",
35+
"short_descr": "An ancient stone archway descends into darkness.",
36+
"long_descr": "The archway is covered in moss and strange runes. A cold wind blows from the depths below.",
37+
"type": "DungeonEntrance",
38+
"dungeon_name": "Ancient Crypt"
39+
}
40+
],
41+
"world_location": [
42+
0,
43+
0,
44+
0
45+
],
46+
"built": false
47+
},
48+
{
49+
"name": "Ancient Crypt",
50+
"exits": [
51+
{
52+
"name": "Town Square",
53+
"direction": "south",
54+
"short_descr": "A stone archway leading back to the town square.",
55+
"long_descr": "The archway is covered in moss and strange runes. Sunlight filters down from above."
56+
}
57+
]
58+
,
59+
"world_location": [
60+
0,
61+
0,
62+
1
63+
],
64+
"built": false
65+
66+
}
67+
]
68+
}
69+
},
70+
"world": {
71+
"npcs": {},
72+
"items": {},
73+
"spawners": [],
74+
"item_spawners": []
75+
},
76+
"catalogue": {
77+
"items": [
78+
{
79+
"name": "torch",
80+
"type": "light",
81+
"value": 5,
82+
"description": "A simple wooden torch"
83+
},
84+
{
85+
"name": "Sword",
86+
"type": "weapon",
87+
"value": 50,
88+
"description": "A steel sword"
89+
}
90+
],
91+
"creatures": [
92+
{
93+
"name": "bat",
94+
"level": 1,
95+
"aggressive": true,
96+
"description": "A small cave bat"
97+
},
98+
{
99+
"name": "wolf",
100+
"level": 2,
101+
"aggressive": true,
102+
"description": "A grey wolf"
103+
},
104+
{
105+
"name": "skeleton",
106+
"level": 3,
107+
"aggressive": true,
108+
"description": "An animated skeleton warrior"
109+
}
110+
]
111+
}
112+
}

tale/driver.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from typing import Sequence, Union, Tuple, Any, Dict, Callable, Iterable, Generator, Set, List, MutableSequence, Optional
2424

2525
import appdirs
26+
from tale.dungeon import DungeonEntrance
2627
from tale.items.basic import Note
2728

2829
from tale.llm import llm_config
@@ -623,6 +624,16 @@ def go_through_exit(self, player: player.Player, direction: str, evoke: bool=Tru
623624
xt = player.location.exits[direction]
624625
xt.allow_passage(player)
625626
target_location = xt.target # type: base.Location
627+
628+
if isinstance(xt, DungeonEntrance.DungeonEntrance):
629+
dungeon_entrance = typing.cast(DungeonEntrance.DungeonEntrance, xt)
630+
if not dungeon_entrance.dungeon:
631+
# Get the dungeon config from the zone containing the entrance
632+
dynamic_story = typing.cast(DynamicStory, self.story)
633+
zone = dynamic_story.find_zone(location=player.location.name)
634+
dungeon_config = zone.dungeon_config if zone else None
635+
dungeon_entrance.build_dungeon(self.story, self.llm_util, dungeon_config)
636+
626637
if not target_location.built:
627638
dynamic_story = typing.cast(DynamicStory, self.story)
628639
zone = dynamic_story.find_zone(location=player.location.name)
@@ -923,7 +934,8 @@ def build_location(self, targetLocation: base.Location, zone: Zone, player: play
923934
zone_info=zone.get_info(),
924935
world_creatures=dynamic_story.catalogue._creatures,
925936
world_items=dynamic_story.catalogue._items,
926-
neighbors=neighbor_locations)
937+
neighbors=neighbor_locations,
938+
zone=zone)
927939
new_locations = result.new_locations
928940
exits = result.exits
929941
npcs = result.npcs

0 commit comments

Comments
 (0)