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

Update v0.20.0 #54

Merged
merged 11 commits into from
Jan 1, 2024
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
11 changes: 7 additions & 4 deletions stories/prancingllama/npcs/npcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from tale.llm.LivingNpc import LivingNpc
from tale.player import Player
from tale.quest import Quest, QuestType
from tale.resources_utils import pad_text_for_avatar
from tale.util import call_periodically, Context
from tale import lang
from typing import Optional
Expand Down Expand Up @@ -112,10 +113,12 @@ def init(self) -> None:

@call_periodically(10, 25)
def do_idle_action(self, ctx: Context) -> None:
if random.random() < 0.5:
self.tell_others("{Actor} hisses.", evoke=False, short_len=True)
else:
self.tell_others("{Actor} sniffs around.", evoke=False, short_len=True)
action = "{Actor} " + random.choice(["sniffs", "scratches", "hisses", "looks menacing"])
if self.avatar:
action = pad_text_for_avatar(action, self.name)
self.tell_others(action, evoke=False, short_len=True)




norhardt = Patron("Norhardt", "m", age=56, descr="A grizzled old man, with parchment-like skin and sunken eyes. He\'s wearing ragged clothing and big leather boots. He\'s a formidable presence, commanding yet somber.", personality="An experienced explorer who is obsessed with finding the mythological Yeti which supposedly haunts these mountains. He won\'t stop talking about it.", short_descr="An old grizzled man sitting by the bar.")
Expand Down
Binary file added stories/prancingllama/resources/bar.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added stories/prancingllama/resources/cellar.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added stories/prancingllama/resources/entrance.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added stories/prancingllama/resources/hearth.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added stories/prancingllama/resources/kitchen.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added stories/prancingllama/resources/main_hall.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion stories/prancingllama/story.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Story(DynamicStory):

config = StoryConfig()
config.name = "The Prancing Llama"
config.author = "Rickard Edén, neph1@github.com"
config.author = "Rickard Edén, github.com/neph1/LlamaTale"
config.author_address = "[email protected]"
config.version = tale.__version__
config.supported_modes = {GameMode.IF, GameMode.MUD}
Expand Down
Binary file added stories/teaparty/resources/ace_of_spades.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added stories/teaparty/resources/duchess.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added stories/teaparty/resources/living_room.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added stories/teaparty/resources/mad_hatter.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions stories/teaparty/story_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"context": "The player is having tea in the Mad Hatter's house, as Alice. The guests are all mad in their own way, and making sense of anything is difficult.",
"type": "A whimsical and humoristic tale of tea and madness. Guests are so busy with their own problems that it's difficult to make yourself heard.",
"world_info": "",
"world_mood": 0
}
"world_mood": 0,
"custom_resources" : "True"
}
5 changes: 3 additions & 2 deletions tale/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from textwrap import dedent
from types import ModuleType
from typing import Iterable, Any, Sequence, Optional, Set, Dict, Union, FrozenSet, Tuple, List, Type, no_type_check
from tale import resources_utils

from tale.coord import Coord

Expand Down Expand Up @@ -286,6 +287,7 @@ def __init__(self, name: str, title: str = "", *, descr: str = "", short_descr:
# register all periodical tagged methods
self.story_data = {} # type: Dict[Any, Any] # not used by Tale itself, story can put custom data here. Use builtin types only.
self.visible = True # can this object be seen by others?
self.avatar = resources_utils.check_file_exists_in_resources(self.name.strip().replace(" ", "_").lower())
self.init()
if util.get_periodicals(self):
if mud_context.driver is None:
Expand Down Expand Up @@ -418,7 +420,6 @@ def notify_action(self, parsed: ParseResult, actor: 'Living') -> None:
"""
pass


class Item(MudObject):
"""
Root class of all Items in the mud world. Items are physical objects.
Expand Down Expand Up @@ -1541,7 +1542,7 @@ def wielding(self, weapon: Optional[Weapon]) -> None:
self.tell_others("{Actor} unwields %s." % self.__wielding.title, evoke=True, short_len=True)
self.tell("You unwield %s." % self.__wielding.title)

def set_wearable(self, wearable: Optional[Wearable], wear_location: Optional[wearable.WearLocation]) -> None:
def set_wearable(self, wearable: Optional[Wearable], wear_location: Optional[wearable.WearLocation] = wearable.WearLocation.TORSO) -> None:
""" Wear an item if item is not None, else unwear location"""
if wearable:
loc = wear_location if wear_location else wearable.wear_location
Expand Down
19 changes: 18 additions & 1 deletion tale/cmds/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import gc
import importlib
import inspect
import json
import os
import platform
import sys
Expand Down Expand Up @@ -788,16 +789,32 @@ def do_spawn(player: Player, parsed: base.ParseResult, ctx: util.Context) -> Non
@wizcmd("load_character")
def do_load_character(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
"""Load a companion character from file."""
print('load character ' + str(parsed.args) + str(parsed.unparsed))
if len(parsed.args) != 1:
raise ParseError("You need to specify the path to the character file")
try:
path = str(parsed.args[0])
except ValueError as x:
raise ActionRefused(str(x))
try:
ctx.driver.load_character(player, path)
return ctx.driver.load_character_from_path(player, path)
except FileNotFoundError:
raise ActionRefused("File not found")
return None

@wizcmd("load_character_from_data")
def do_load_character_from_data(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
"""Load a companion character from file."""
try:
unparsed = str(parsed.unparsed)
data = json.loads(unparsed)
except ValueError as x:
raise ActionRefused(str(x))
try:
return ctx.driver.load_character(player, data)
except FileNotFoundError:
raise ActionRefused("Could not load character")
return None

@wizcmd("set_visibility")
def do_set_visible(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
Expand Down
36 changes: 20 additions & 16 deletions tale/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"""

import collections
import copy
import datetime
import heapq
import importlib
Expand All @@ -15,7 +14,6 @@
import pathlib
import pkgutil
import random
import shutil
import sys
import threading
import time
Expand All @@ -36,7 +34,7 @@
from .story import TickMethod, GameMode, MoneyType, StoryBase
from .tio import DEFAULT_SCREEN_WIDTH
from .races import playable_races
from .errors import StoryCompleted, StoryConfigError
from .errors import StoryCompleted
from tale.load_character import CharacterLoader, CharacterV2
from tale.llm.llm_ext import DynamicStory
from tale.llm.llm_utils import LlmUtil
Expand Down Expand Up @@ -623,42 +621,46 @@ 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:
target_location = xt.target # type: base.Location
if not target_location.built:
dynamic_story = typing.cast(DynamicStory, self.story)
zone = dynamic_story.find_zone(location=player.location.name)
# are we close to the edge of a zone? if so we need to build the next zone.
new_zone = self.llm_util.get_neighbor_or_generate_zone(current_zone=zone,
current_location=player.location,
target_location=xt.target)
target_location=target_location)
if zone.name != new_zone.name:
player.tell(f"You're entering {new_zone.name}:{new_zone.description}")

# generate the location if it's not built yet. retry 5 times.
for i in range(5):
result = self.build_location(xt.target, new_zone, player)
result = self.build_location(target_location, new_zone, player)
if result:
break

if not result:
raise errors.ActionRefused("Reached max attempts when building location: " + xt.target.name + ". You can try entering again.")
raise errors.ActionRefused("Reached max attempts when building location: " + target_location.name + ". You can try entering again.")

elif random.random() < 0.2 and isinstance(self.story, DynamicStory):
dynamic_story = typing.cast(DynamicStory, self.story)
zone = dynamic_story.find_zone(location=xt.target.name)
self.llm_util.generate_random_spawn(xt.target, zone.get_info())
zone = dynamic_story.find_zone(location=target_location.name)
self.llm_util.generate_random_spawn(target_location, zone.get_info())
elif isinstance(self.story, DynamicStory):
dynamic_story = typing.cast(DynamicStory, self.story)
zone = dynamic_story.find_zone(location=player.location.name)
new_zone = dynamic_story.find_zone(location=xt.target.name)
new_zone = dynamic_story.find_zone(location=target_location.name)
if zone and zone.name != new_zone.name:
player.tell(f"You're entering {new_zone.name}:{new_zone.description}")

if self.story.config.custom_resources and not target_location.avatar:
result = self.llm_util.generate_image(target_location.name, target_location.description)
if result:
target_location.avatar = target_location.name

if xt.enter_msg:
player.tell(xt.enter_msg, end=True, evoke=False, short_len=True)
player.tell("\n")
player.move(xt.target, direction_names=[xt.name] + list(xt.aliases))
player.move(target_location, direction_names=[xt.name] + list(xt.aliases))
player.look(evoke=evoke)

def lookup_location(self, location_name: str) -> base.Location:
Expand Down Expand Up @@ -836,12 +838,15 @@ def register_periodicals(self, obj: base.MudObject) -> None:
assert len(period) == 3
mud_context.driver.defer(period, func)

def load_character(self, player: player.Player, path: str):
def load_character_from_path(self, player: player.Player, path: str) -> LivingNpc:
"""Loads a character from a json file and inserts it into the player's location."""
character_loader = CharacterLoader()
char_data = character_loader.load_character(path)
if not char_data:
raise errors.TaleError("Character not found.")
self.load_character(player, char_data)

def load_character(self, player: player.Player, char_data: dict) -> LivingNpc:
character = CharacterV2().from_json(char_data)
npc = StationaryNpc(name = character.name.lower(),
gender = character.gender,
Expand All @@ -856,18 +861,17 @@ def load_character(self, player: player.Player, path: str):
wearing = character.wearing.split(',')
for item in wearing:
if item:
wearable = base.Wearable(name=item.lower().trim())
wearable = base.Wearable(name=item.lower().strip())
npc.set_wearable(wearable)

npc.following = player
npc.stats.hp = character.hp
if isinstance(self.story, DynamicStory):
dynamic_story = typing.cast(DynamicStory, self.story)
dynamic_story.world.add_npc(npc)

player.location.insert(npc, None)
player.location.tell("%s arrives." % npc.title)

return npc

@property
def uptime(self) -> Tuple[int, int, int]:
Expand Down
9 changes: 4 additions & 5 deletions tale/llm/LivingNpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def __init__(self, name: str, gender: str, *,
self.goal = None # type: str # a free form string describing the goal of the NPC
self.quest = None # type: Quest # a quest object
self.deferred_actions = set() # type: set[str]
self.avatar = None
self.autonomous = False

def notify_action(self, parsed: ParseResult, actor: Living) -> None:
Expand Down Expand Up @@ -99,9 +98,9 @@ def do_say(self, what_happened: str, actor: Living) -> None:
short_len=short_len)
if response:
if not self.avatar:
result = mud_context.driver.llm_util.generate_avatar(self.name, self.description)
result = mud_context.driver.llm_util.generate_image(self.name, self.description)
if result:
self.avatar = self.name + '.jpg'
self.avatar = self.name
break
if not response:
raise LlmResponseException("Failed to parse dialogue")
Expand Down Expand Up @@ -217,7 +216,7 @@ def autonomous_action(self) -> str:
if action.get('target'):
target = self.location.search_living(action['target'])
if target:
target.tell(text, evoke=False)
#target.tell(text, evoke=False)
target.notify_action(ParseResult(verb='say', unparsed=text, who_list=[target]), actor=self)
defered_actions.append(f'"{text}"')
if not action.get('action', ''):
Expand Down Expand Up @@ -247,7 +246,7 @@ def autonomous_action(self) -> str:


def _defer_result(self, action: str, verb: str="idle-action"):
if mud_context.config.custom_resources:
if mud_context.config.custom_resources and self.avatar:
action = pad_text_for_avatar(text=action, npc_name=self.title)
else:
action = f"{self.title} : {action}"
Expand Down
4 changes: 2 additions & 2 deletions tale/llm/llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def update_memory(self, rolling_prompt: str, response_text: str):
def generate_character(self, story_context: str = '', keywords: list = [], story_type: str = ''):
character = self._character.generate_character(story_context, keywords, story_type)
if not character.avatar and self.__story.config.image_gen:
result = self.generate_avatar(character.name, character.appearance)
result = self.generate_image(character.name, character.appearance)
if result:
character.avatar = character.name + '.jpg'
return character
Expand Down Expand Up @@ -224,7 +224,7 @@ def generate_note_lore(self, zone_info: dict) -> str:
world_info=self.__world_info,
zone_info=zone_info)

def generate_avatar(self, character_name: str, character_appearance: dict = '', save_path: str = "./resources", copy_file: bool = True) -> bool:
def generate_image(self, character_name: str, character_appearance: dict = '', save_path: str = "./resources", copy_file: bool = True) -> bool:
if not self._image_gen:
return False
image_name = character_name.lower().replace(' ', '_')
Expand Down
2 changes: 1 addition & 1 deletion tale/load_character.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def from_json(self, json: dict):
self.appearance = json.get('appearance', description.split(';')[0])
self.personality = json.get('personality', '')
self.occupation = json.get('occupation', '')
self.age = json.get('age', 30)
self.age = int(json.get('age', 30))
self.money = json.get('money', 0)
self.hp = json.get('hp', 10)
self.aliases = json.get('aliases', [])
Expand Down
8 changes: 7 additions & 1 deletion tale/resources_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@

from tale.image_gen.base_gen import ImageGeneratorBase
import os

def pad_text_for_avatar(text: str, npc_name: str) -> str:
"""Pad text for NPC output."""
return npc_name + ' <:> ' + text if npc_name else text

def check_file_exists_in_resources(file_name) -> str:
file_path = os.path.join(os.path.dirname('../../tale/web/resources/'), file_name + '.jpg')
if os.path.exists(file_path):
return file_name
return None
5 changes: 3 additions & 2 deletions tale/tio/if_browser_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from tale.web.web_utils import create_chat_container, dialogue_splitter

from . import iobase
from .. import vfs, lang
from .. import mud_context, vfs, lang
from .styleaware_wrapper import tag_split_re
from .. import __version__ as tale_version_str
from ..driver import Driver
Expand Down Expand Up @@ -346,7 +346,7 @@ def wsgi_handle_eventsource(self, environ: Dict[str, Any], parameters: Dict[str,
html = conn.io.get_html_to_browser()
special = conn.io.get_html_special()
if html or special:
location = conn.player.location
location = conn.player.location # type : Optional[Location]
if conn.io.dont_echo_next_cmd:
special.append("noecho")
npc_names = ''
Expand All @@ -357,6 +357,7 @@ def wsgi_handle_eventsource(self, environ: Dict[str, Any], parameters: Dict[str,
"special": special,
"turns": conn.player.turns,
"location": location.title if location else "???",
"location_image": location.avatar if location and location.avatar else "",
"npcs": npc_names if location else '',
}
result = "event: text\nid: {event_id}\ndata: {data}\n\n"\
Expand Down
Loading