Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Location generation #10

Merged
merged 3 commits into from
Aug 26, 2023
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
10 changes: 6 additions & 4 deletions tale/llm_config.yaml → llm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ ENDPOINT: "/api/v1/generate"
STREAM: False
STREAM_ENDPOINT: "/api/extra/generate/stream"
DATA_ENDPOINT: "/api/extra/generate/check"
WORD_LIMIT: 300
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}'
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, "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,5 +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. Money as int value. Story context: {story_context}; keywords: {keywords}. Fill in this JSON template and write nothing else: {{"name":"", "description": "", "appearance": "", "personality": "", "money":"", "level":"", "gender":"", "age":"", "race":""}} ### Response:\n'
CREATE_ROOM_PROMPT: 'Story context: {story_context}. Exit template: {"name" : "connected room", "short_desc":"description visible to use", "long_desc":"detailed description", "enter_msg":"message when using exit"}; Item template: {"name":"", "type":""}, type can be "weapon", "armor", "other"; Character template: {"name":"", "type":""}, type can be "friendly", "neutral", "hostile" ### Instruction: Existing room connected to the generated room: {room_desc}. For a {story_type} roleplaying game, create a single new, intriguing, room connected to {room_name}. Include at least one exit leading to an additional room. Add items and characters if if seems fitting. Fill in this JSON template and do not write anything else: {"name":"", "description": "", "exits": [], "items":[], "npcs":[]} ### 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":""}]
}
]
}
32 changes: 29 additions & 3 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,9 +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 . import wearable

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

Expand Down Expand Up @@ -590,10 +592,19 @@ def __init__(self, name: str, wc: int = 0, title: str = "", *, descr: str = "",
class Armour(Item):
"""
An item that can be worn by a Living (i.e. present in an armour itemslot)
Used by Circle Mud. Not related to Wearable at present. Not used by LlamaTale.
"""
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 All @@ -607,6 +618,7 @@ def __init__(self, name: str, descr: str="") -> None:
self.exits = {} # type: Dict[str, Exit] # dictionary of all exits: exit_direction -> Exit object with target & descr
super().__init__(name, descr=descr)
self.name = name # make sure we preserve the case; base object overwrites it in lowercase
self.built = True # has this location been built yet? If not, LLM will describe it.

def __contains__(self, obj: Union['Living', Item]) -> bool:
return obj in self.livings or obj in self.items
Expand Down Expand Up @@ -963,6 +975,8 @@ 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.Wearable]

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

def init_gender(self, gender: str) -> None:
Expand Down Expand Up @@ -1348,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 @@ -1468,6 +1482,18 @@ 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[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: 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)


class Container(Item):
"""
Expand Down
30 changes: 26 additions & 4 deletions tale/cmds/normal.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ..player import Player
from ..story import GameMode
from ..verbdefs import VERBS, ACTION_QUALIFIERS, BODY_PARTS, AGGRESSIVE_VERBS
from tale.wearable import WearLocation


@cmd("inventory")
Expand Down Expand Up @@ -1701,12 +1702,33 @@ def do_wield(player: Player, parsed: base.ParseResult, ctx: util.Context) -> Non
except ValueError as x:
raise ActionRefused(str(x))
result = player.locate_item(weapon, include_location=False)
if result:
player.wielding = result[0]
else:
if not result:
raise ActionRefused("You don't have that weapon")
player.wielding = result[0]

@cmd("unwield")
def do_unwield(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
"""Unwield a weapon."""
player.wielding = None
player.wielding = None

@cmd("wear")
def do_wear(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
"""Wear an item."""
if len(parsed.args) < 1:
raise ParseError("You need to specify the item to wear")
try:
item = str(parsed.args[0])
except ValueError as x:
raise ActionRefused(str(x))
if len(parsed.args) == 2:
try:
parsed_loc = str(parsed.args[1])
location = WearLocation[parsed_loc.upper()]
except ValueError:
raise ActionRefused("Invalid location")


result = player.locate_item(item, include_location=False)
if not result:
raise ActionRefused("You don't have that item")
player.set_wearable(result[0], location=location)
15 changes: 13 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 Expand Up @@ -792,6 +802,7 @@ def load_character(self, player: player.Player, path: str):
short_descr = character.appearance.split('.')[0],
age = character.age,
personality = character.personality,
race = character.race,
occupation = character.occupation)
npc.following = player
npc.stats.hp = character.hp
Expand Down
Loading