Skip to content

Commit

Permalink
generating locations works
Browse files Browse the repository at this point in the history
  • Loading branch information
neph1 committed Aug 26, 2023
1 parent f9c0f64 commit 9eb1ed7
Show file tree
Hide file tree
Showing 22 changed files with 321 additions and 58 deletions.
6 changes: 3 additions & 3 deletions llm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ STREAM: False
STREAM_ENDPOINT: "/api/extra/generate/stream"
DATA_ENDPOINT: "/api/extra/generate/check"
WORD_LIMIT: 200
DEFAULT_BODY: '{"stop_sequence": "", "max_length":500, "max_context_length":4096, "temperature":1.0, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "mirostat":2, "mirostat_tau":5.0, "mirostat_eta":0.1, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}'
DEFAULT_BODY: '{"stop_sequence": "", "max_length":500, "max_context_length":4096, "temperature":1.0, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}'
ANALYSIS_BODY: '{"banned_tokens":"\n\n", "stop_sequence": "\n\n\n", "max_length":500, "max_context_length":4096, "temperature":0.15, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "mirostat":2, "mirostat_tau":5.0, "mirostat_eta":0.1, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}'
MEMORY_SIZE: 512
PRE_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n\n'
Expand All @@ -13,7 +13,7 @@ ACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; The foll
DIALOGUE_PROMPT: '[Story context: {story_context}]; [Location: {location}]; The following is a conversation between {character1} and {character2}; {character1}:{character1_description}; character2:{character2_description}; [Chat history: {previous_conversation}]\n\n [{character2}s sentiment towards {character1}: {sentiment}]. ### Instruction: Write a single response for {character2} in third person pov, using {character2} description.\n\n### Response:\n'
ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n ### Instruction: Decide if an item was explicitly given, taken, dropped or put somewhere in the following text:[Text:{text}]. Insert your thoughts about whether an item was explicitly given, taken, put somewhere or dropped in "my thoughts", and the results in "item", "from" and "to", or make them empty if . Insert {character1}s sentiment towards {character2} in a single word in "sentiment assessment". Fill in the following JSON template: {{ "thoughts":"my thoughts", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON\n\n### Response:\n'
COMBAT_PROMPT: 'Rewrite the following combat between user {attacker}, weapon:{attacker_weapon} and {victim}, weapon:{victim_weapon} into a vivid description in less than 300 words. Location: {location}, {location_description}. ### Instruction: Rewrite the following combat result in about 150 words, using the characters weapons . Combat Result: {attacker_msg} ### Response:\n\n'
CREATE_CHARACTER_PROMPT: '### Instruction: For a low-fantasy roleplaying game, create a diverse character with rich personality that can be interacted with using the story context and keywords. Do not mention height. Story context: {story_context}; keywords: {keywords}. Fill in this JSON template and write nothing else: {{"name":"", "description": "", "appearance": "", "personality": "", "money":(int), "level":"", "gender":"m/f/n", "age":(int), "race":""}} ### Response:\n'
CREATE_LOCATION_PROMPT: '[Story context: {story_context}]; Exit example: {"name" : "connected room", "short_desc":"description visible to use", "long_desc":"detailed description", "enter_msg":"message when using exit"}; Item example: {"name":"", "type":""}, type can be "Weapon", "Armor", "Other" or "Money"; Character example: {"name":"", "type":""}, type can be "friendly", "neutral", "hostile". Existing connected room: {exit_room}. ### Instruction: For a {story_type}, describe the following room: {room_name}. Add a description and include at least one exit leading to an additional room. Fill in this JSON template and do not write anything else: {"name":"", "description": "", "exits": [], "items":[], "npcs":[]}. Make sure the response is valid JSON. ### Response:\n'
CREATE_CHARACTER_PROMPT: '### Instruction: For a low-fantasy roleplaying game, create a diverse character with rich personality that can be interacted with using the story context and keywords. Do not mention height. Story context: {story_context}; keywords: {keywords}. Fill in this JSON template and write nothing else: {{"name":"", "description": "50 words", "appearance": "25 words", "personality": "50 words", "money":(int), "level":"", "gender":"m/f/n", "age":(int), "race":""}} ### Response:\n'
CREATE_LOCATION_PROMPT: '[Story context: {story_context}]; Exit example: {{"name" : "location name", "short_descr":"description visible to user", "enter_msg":"message when using exit"}}; Item example: {{"name":"", "type":""}}, type can be "Weapon", "Armor", "Other" or "Money"; Character example: {{"name":"", "type":""}}, type can be "friendly", "neutral", "hostile". Existing connected location: {exit_location}. ### Instruction: For a {story_type}, describe the following location: {location_name}. Add a description, and one to three additional exits leading to new locations. Fill in this JSON template and do not write anything else: {{"description": "25 words", "exits":[], "items":[], "npcs":[]}}. Write the response in valid JSON. ### Response:\n\n'


16 changes: 14 additions & 2 deletions stories/prancingllama/story.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
from typing import Optional, Generator

import tale
from tale.base import Location
from tale.driver import Driver
from tale.llm_ext import DynamicStory
from tale.main import run_from_cmdline
from tale.player import Player, PlayerConnection
from tale.charbuilder import PlayerNaming
from tale.story import *

class Story(StoryBase):
class Story(DynamicStory):

config = StoryConfig()
config.name = "The Prancing Llama"
Expand All @@ -28,10 +30,14 @@ class Story(StoryBase):
config.startlocation_wizard = "prancingllama.entrance"
config.zones = ["prancingllama"]
config.context = "The Prancing Llama is the final outpost high up in a cold, craggy mountain range. It's frequented by adventurers and those seeking to avoid attention."

config.type = "A low level fantasy adventure with focus of character building and interaction."


def init(self, driver: Driver) -> None:
"""Called by the game driver when it is done with its initial initialization."""
self.driver = driver
self._dynamic_locations = dict() # type: dict(str, [])
self._dynamic_locations["prancingllama"] = []

def init_player(self, player: Player) -> None:
"""
Expand Down Expand Up @@ -75,6 +81,12 @@ def goodbye(self, player: Player) -> None:
player.tell("Goodbye, %s. Please come back again soon." % player.title)
player.tell("\n")

def add_location(self, location: Location, zone: str = '') -> None:
""" Add a location to the story.
If zone is specified, add to that zone, otherwise add to first zone.
"""
self._dynamic_locations["prancingllama"].append(location)


if __name__ == "__main__":
# story is invoked as a script, start it in the Tale Driver.
Expand Down
9 changes: 6 additions & 3 deletions stories/prancingllama/zones/prancingllama.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ def spawn_rat(self, ctx: Context) -> None:
kitchen = Location("Kitchen", "The walls are lined with barrels and smoke is surrounding the steaming pots on the fires.")
hearth = Location("Hearth", "A place for newly arrived visitors to unthaw their frozen limbs.")
entrance = Location("Entrance", "A room full of furry and snow-covered coats. Loud voices and roars of laughter can be heard from the main hall.")
outside = Location("Outside", "A snow-storm is raging across the craggy landscape outside, it's dark, and noone to be seen. It's better to stay indoors")
outside = Location("Outside")
outside.built = False # Specify this to be generated by the LLM
cellar = Cellar("Cellar", "A dark and damp place, with cob-webs in the corners. Filled with barrels and boxes of various kind.")


Expand All @@ -42,7 +43,9 @@ def spawn_rat(self, ctx: Context) -> None:

Exit.connect(main_hall, ["entrance", "south"], "", "The entrance to the building is to the south.", entrance, ["main hall", "north"], "There's a warm glow and loud, noisy conversations coming through a doorway to the north", "")

Exit.connect(entrance, ["outside", "south"], "A biting wind reaches you through the door to the south.", "", outside, ["entrance", "north"], "There's shelter from the cold wind through a door to the north.", "")
entrance.add_exits([Exit(["outside", "south"], short_descr="A biting wind reaches you through the door to the south.", target_location=outside)])

outside.add_exits([Exit(["entrance", "north"], short_descr="The door to the north leads inside The Prancing Llama.", target_location=entrance)])

main_hall.init_inventory([shanda, elid_gald])

Expand Down Expand Up @@ -86,7 +89,7 @@ def _generate_character():
return True
return False

# 10 attempts to generate 2 characters
# 5 attempts to generate 2 characters
generated = 0
for i in range(5):
if _generate_character():
Expand Down
12 changes: 12 additions & 0 deletions stories/test_story/npcs/test_npcs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"name":"Kobbo",
"gender":"m",
"race":"kobold",
"type":"Living",
"title":"Kobbo the King",
"descr":"Kobbo has a grace only royal kobolds posses",
"short_descr":"A kobold",
"location":"Cave.Royal Grotto"
}
]
27 changes: 27 additions & 0 deletions stories/test_story/test_items.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[
{
"name":"Box 1",
"title":"Box 1",
"descr":"A small box with intricate bejewelled patterns",
"short_descr": "A small bejewelled box",
"type":"Boxlike",
"location":"Cave.Cave entrance"
},
{
"name":"Royal Sceptre",
"title":"Royal Sceptre",
"descr":"This stick is Kobbo the Kings royal sceptre. Looks good for walloping someone with",
"short_descr":"A gnarly looking stick",
"type":"Weapon",
"location":"Cave.Royal Grotto"
},
{
"name":"Hoodie",
"title":"Grey Hoodie",
"descr":"A brand new, grey hoodie with a logo of a common brand",
"short_descr": "A grey hoodie",
"type":"Wearable",
"location":"Cave.Cave entrance"
}

]
15 changes: 15 additions & 0 deletions stories/test_story/test_locations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name":"Cave",
"rooms":[
{
"name": "Cave entrance",
"descr": "This cave entrance gives shelter from the wind and rain.",
"exits": [{"name" : "Royal Grotto", "short_desc":"Exit to Grotto", "long_desc":"There's an opening that leads deeper into the cave'", "enter_msg":""}]
},
{
"name": "Royal Grotto",
"descr": "This is Kobbo the Kings throne room. It's a dark, damp place with a log in one end",
"exits": [{"name" : "Cave entrance", "short_desc":"exit to Cave entrance", "long_desc":"", "enter_msg":""}]
}
]
}
33 changes: 33 additions & 0 deletions stories/test_story/test_story_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name":"Test Story Config 1",
"type":"StoryConfig",
"author":"test author",
"author_address":"" ,
"version":"1.10" ,
"requires_tale":"4.0" ,
"supported_modes":["IF"] ,
"player_name":"" ,
"player_gender":"" ,
"player_race":"human" ,
"player_money":0.0 ,
"playable_races":[] ,
"money_type":"NOTHING" ,
"server_tick_method":"COMMAND" ,
"server_tick_time":5.0 ,
"gametime_to_realtime":1 ,
"max_wait_hours":2 ,
"display_gametime":false ,
"display_race":false ,
"epoch":null ,
"startlocation_player":"Cave.Cave entrance" ,
"startlocation_wizard":"Cave.Cave entrance" ,
"savegames_enabled":true ,
"show_exits_in_look":true ,
"license_file":"",
"mud_host":"",
"mud_port":0,
"zones":["test_locations"],
"npcs":"test_npcs",
"items":"test_items",
"server_mode":"IF"
}
15 changes: 15 additions & 0 deletions stories/test_story/zones/test_locations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name":"Cave",
"rooms":[
{
"name": "Cave entrance",
"descr": "This cave entrance gives shelter from the wind and rain.",
"exits": [{"name" : "Royal Grotto", "short_desc":"Exit to Grotto", "long_desc":"There's an opening that leads deeper into the cave'", "enter_msg":""}]
},
{
"name": "Royal Grotto",
"descr": "This is Kobbo the Kings throne room. It's a dark, damp place with a log in one end",
"exits": [{"name" : "Cave entrance", "short_desc":"exit to Cave entrance", "long_desc":"", "enter_msg":""}]
}
]
}
23 changes: 16 additions & 7 deletions tale/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
| |
| +-- Weapon
| +-- Armour
+-- Wearable
+-- Remains
| +-- Container
| +-- Key
|
Expand Down Expand Up @@ -53,10 +55,9 @@
from . import story
from . import verbdefs
from . import combat
from . import player
from .errors import ActionRefused, ParseError, LocationIntegrityError, TaleError, UnknownVerbException, NonSoulVerb
from tale.races import UnarmedAttack
from tale.wearable import Wearable, WearLocation
from . import wearable

__all__ = ["MudObject", "Armour", 'Container', "Door", "Exit", "Item", "Living", "Stats", "Location", "Weapon", "Key", "Soul"]

Expand Down Expand Up @@ -596,6 +597,14 @@ class Armour(Item):
pass


class Wearable(Item):

def __init__(self, name: str, weight, value, ac, wearable_type):
super(Wearable).__init__(name, descr=name, value=value)
self.ac = ac
self.weight = weight
self.type = wearable_type

class Location(MudObject):
"""
A location in the mud world. Livings and Items are in it.
Expand Down Expand Up @@ -966,7 +975,7 @@ def __init__(self, name: str, gender: str, *, race: str="human",
self.following = None # type: Optional[Living]
self.is_pet = False # set this to True if creature is/becomes someone's pet
self.__wielding = None # type: Optional[Weapon]
self.__wearing = dict() # type: Dict[str, Wearable]
self.__wearing = dict() # type: Dict[str, wearable.Wearable]

super().__init__(name, title=title, descr=descr, short_descr=short_descr)

Expand Down Expand Up @@ -1353,9 +1362,9 @@ def start_attack(self, victim: 'Living') -> None:
victim.tell(victim_msg, evoke=True, max_length=False)


if isinstance(self, player.Player):
if isinstance(self, 'tale.player.Player'):
attacker_name += "as 'You'"
if isinstance(victim, player.Player):
if isinstance(victim, 'tale.player.Player'):
victim_name += "as 'You'"

combat_prompt = mud_context.driver.llm_util.combat_prompt.format(attacker=attacker_name,
Expand Down Expand Up @@ -1473,15 +1482,15 @@ def wielding(self, weapon: Optional[Weapon]) -> None:
self.__wielding = weapon
self.stats.wc = weapon.wc if self.__wielding else 0

def set_wearable(self, wearable: Optional[Wearable], location: Optional[WearLocation]) -> None:
def set_wearable(self, wearable: Optional[Wearable], location: Optional[wearable.WearLocation]) -> None:
""" Wear an item if item is not None, else unwear location"""
if wearable:
loc = location if location else wearable.location
self.__wearing[loc] = wearable
elif location:
self.__wearing.pop(location, None)

def get_wearable(self, location: WearLocation) -> Optional[Wearable]:
def get_wearable(self, location: wearable.WearLocation) -> Optional[Wearable]:
"""Return the wearable item at the given location, or None if no item is worn there."""
return self.__wearing.get(location)

Expand Down
14 changes: 12 additions & 2 deletions tale/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import sys
import threading
import time
import typing
from functools import total_ordering
from types import ModuleType
from typing import Sequence, Union, Tuple, Any, Dict, Callable, Iterable, Generator, Set, List, MutableSequence, Optional
Expand All @@ -25,13 +26,12 @@

from . import __version__ as tale_version_str, _check_required_libraries
from . import mud_context, errors, util, cmds, player, pubsub, charbuilder, lang, verbdefs, vfs, base
from .llm_utils import LlmUtil
from .story import TickMethod, GameMode, MoneyType, StoryBase
from .tio import DEFAULT_SCREEN_WIDTH
from .races import playable_races
from .errors import StoryCompleted
from tale.load_character import CharacterLoader, CharacterV2
from tale.llm_ext import LivingNpc
from tale.llm_ext import LivingNpc, DynamicStory
from tale.llm_utils import LlmUtil


Expand Down Expand Up @@ -606,6 +606,16 @@ def _process_player_command(self, cmd: str, conn: player.PlayerConnection) -> No
def go_through_exit(self, player: player.Player, direction: str, evoke: bool=True) -> None:
xt = player.location.exits[direction]
xt.allow_passage(player)
if not xt.target.built:
# generate the location if it's not built yet. retry 5 times.
for i in range(5):
new_locations = self.llm_util.build_location(location=xt.target, exit_location=player.location)
if new_locations:
break
if not new_locations:
raise AssertionError("failed to build location: " + xt.target.name + ". You can try entering again.")
for location in new_locations:
typing.cast(DynamicStory, self.story).add_location(location)
if xt.enter_msg:
player.tell(xt.enter_msg, end=True, evoke=evoke, max_length=True)
player.tell("\n")
Expand Down
1 change: 1 addition & 0 deletions tale/driver_if.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def _login_dialog_if(self, conn: PlayerConnection) -> Generator:
player.tell("\n")
prompt = self.story.welcome(player)

self.llm_util.story_type = self.story.config.type
self.llm_util.story_background = self.story.config.context

if prompt:
Expand Down
23 changes: 21 additions & 2 deletions tale/json_story.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(self, path: str, config: StoryConfig):
for zone in self.config.zones:
locs, exits = parse_utils.load_locations(parse_utils.load_json(self.path +'zones/'+zone + '.json'))
self._locations = locs
self.zones = locs
self._zones = locs
self._npcs = parse_utils.load_npcs(parse_utils.load_json(self.path +'npcs/'+self.config.npcs + '.json'), self._locations)
self._items = parse_utils.load_items(parse_utils.load_json(self.path + self.config.items + '.json'), self._locations)

Expand All @@ -35,8 +35,27 @@ def goodbye(self, player: Player) -> None:
player.tell("Thanks for trying out Tale!")

def get_location(self, zone: str, name: str) -> Location:
return self._locations[zone][name]
""" Find a location by name in a zone."""
return self._zones[zone][name]

def find_location(self, name: str) -> Location:
""" Find a location by name in any zone."""
for zone in self._zones:
for loc in zone.values():
if loc.name == name:
return loc

def add_location(self, location: Location, zone: str = '') -> None:
""" Add a location to the story.
If zone is specified, add to that zone, otherwise add to first zone.
"""
if zone:
self._zones[zone][location.name] = location
return
for zone in self._zones:
self._zones[zone][location.name] = location
break

def get_npc(self, npc: str) -> Living:
return self._npcs[npc]

Expand Down
8 changes: 7 additions & 1 deletion tale/llm_ext.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from tale import mud_context
from tale.base import Living, ParseResult
from tale.base import Living, Location, ParseResult
from tale.errors import TaleError
from tale.player import Player
from tale.story import StoryBase

class LivingNpc(Living):
"""An NPC with extra fields to define personality and help LLM generate dialogue"""
Expand Down Expand Up @@ -110,3 +111,8 @@ def character_card(self) -> str:
description=self.description,
occupation=self.occupation,
items=','.join(items))

class DynamicStory(StoryBase):

def add_location(self, location: Location, zone: str = '') -> None:
pass
Loading

0 comments on commit 9eb1ed7

Please sign in to comment.