From 8bc5ab214f2e5d87ec3f6fb9bd4523413876c9db Mon Sep 17 00:00:00 2001 From: rickard Date: Thu, 19 Sep 2024 06:02:33 +0200 Subject: [PATCH 1/5] hide and sneak skills --- llm_config.yaml | 2 +- stories/anything/story.py | 4 +- stories/chat_room/story.py | 37 ++++++ stories/combat_sandbox/story.py | 2 +- stories/dungeon/story.py | 4 +- stories/dungeon_test/story.py | 179 +++++++++++++++++++++++++++++ stories/land_story/story.py | 85 ++++++++++++++ stories/prancingllama/npcs/npcs.py | 2 +- stories/prancingllama/story.py | 4 +- tale/accounts.py | 1 + tale/base.py | 17 +-- tale/cmds/normal.py | 58 ++++++++++ tale/cmds/spells.py | 4 +- tale/combat.py | 2 +- tale/items/generic.py | 2 +- tale/parse_utils.py | 19 ++- tale/{ => skills}/magic.py | 0 tale/skills/skills.py | 9 ++ tale/{ => skills}/weapon_type.py | 0 tests/test_combat.py | 2 +- tests/test_generic_items.py | 2 +- tests/test_llm_utils.py | 3 +- tests/test_normal_commands.py | 47 +++++++- tests/test_parse_utils.py | 2 +- tests/test_spells.py | 4 +- 25 files changed, 458 insertions(+), 33 deletions(-) create mode 100644 stories/chat_room/story.py create mode 100644 stories/dungeon_test/story.py create mode 100644 stories/land_story/story.py rename tale/{ => skills}/magic.py (100%) create mode 100644 tale/skills/skills.py rename tale/{ => skills}/weapon_type.py (100%) diff --git a/llm_config.yaml b/llm_config.yaml index 2bff0f1d..39a8486b 100644 --- a/llm_config.yaml +++ b/llm_config.yaml @@ -4,7 +4,7 @@ BACKEND: "kobold_cpp" # valid options: "openai", "llama_cpp", "kobold_cpp". if u MEMORY_SIZE: 512 UNLIMITED_REACTS: False DIALOGUE_TEMPLATE: '{{"response":"may be both dialogue and action.", "sentiment":"sentiment based on response", "give":"if any physical item of {character2}s is given as part of the dialogue. Or nothing."}}' -ACTION_LIST: ['move, say, attack, wear, remove, wield, take, eat, drink, emote'] +ACTION_LIST: ['move, say, attack, wear, remove, wield, take, eat, drink, emote, search'] ACTION_TEMPLATE: '{{"goal": reason for action, "thoughts":thoughts about performing action, 25 words "action":chosen action, "target":character, item or exit or description, "text": if anything is said during the action}}' ITEM_TEMPLATE: '{{"name":"", "type":"", "short_descr":"", "level":int, "value":int}}' CREATURE_TEMPLATE: '{{"name":"", "body":"", "mass":int(kg), "hp":int, "type":"Npc or Mob", "level":int, "aggressive":bool, "unarmed_attack":One of [FISTS, CLAWS, BITE, TAIL, HOOVES, HORN, TUSKS, BEAK, TALON], "short_descr":""}}' diff --git a/stories/anything/story.py b/stories/anything/story.py index 9eaaedcd..eba7ed76 100644 --- a/stories/anything/story.py +++ b/stories/anything/story.py @@ -8,12 +8,12 @@ from tale import parse_utils from tale.driver import Driver from tale.json_story import JsonStory -from tale.magic import MagicType +from tale.skills.magic import MagicType from tale.main import run_from_cmdline from tale.player import Player, PlayerConnection from tale.charbuilder import PlayerNaming from tale.story import * -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType class Story(JsonStory): diff --git a/stories/chat_room/story.py b/stories/chat_room/story.py new file mode 100644 index 00000000..6f08033d --- /dev/null +++ b/stories/chat_room/story.py @@ -0,0 +1,37 @@ +import pathlib +import sys +from typing import Optional, Generator + +import tale +from tale import parse_utils +from tale.base import Location +from tale.driver import Driver +from tale.json_story import JsonStory +from tale.llm.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 * +from tale.skills.weapon_type import WeaponType +from tale.zone import Zone + +class Story(JsonStory): + + driver = None + + def __init__(self) -> None: + super(Story, self).__init__('', parse_utils.load_story_config(parse_utils.load_json('story_config.json'))) + + def init(self, driver: Driver) -> None: + + super(Story, self).init(driver) + + +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) \ No newline at end of file diff --git a/stories/combat_sandbox/story.py b/stories/combat_sandbox/story.py index 9f7eeffb..bb47e466 100644 --- a/stories/combat_sandbox/story.py +++ b/stories/combat_sandbox/story.py @@ -12,7 +12,7 @@ from tale.player import Player, PlayerConnection from tale.charbuilder import PlayerNaming from tale.story import * -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType from tale.zone import Zone class Story(JsonStory): diff --git a/stories/dungeon/story.py b/stories/dungeon/story.py index aed6e5b7..ddcba8c0 100644 --- a/stories/dungeon/story.py +++ b/stories/dungeon/story.py @@ -11,12 +11,12 @@ from tale.dungeon.dungeon_generator import ItemPopulator, Layout, LayoutGenerator, MobPopulator from tale.items.basic import Money from tale.json_story import JsonStory -from tale.magic import MagicType +from tale.skills.magic import MagicType from tale.main import run_from_cmdline from tale.npc_defs import RoamingMob from tale.player import Player, PlayerConnection from tale.story import * -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType from tale.zone import Zone class Story(JsonStory): diff --git a/stories/dungeon_test/story.py b/stories/dungeon_test/story.py new file mode 100644 index 00000000..06b305f5 --- /dev/null +++ b/stories/dungeon_test/story.py @@ -0,0 +1,179 @@ +import pathlib +import random +import sys +from typing import Generator + +from tale import parse_utils +from tale import lang +from tale.base import Door, Exit, Location +from tale.charbuilder import PlayerNaming +from tale.driver import Driver +from tale.dungeon.dungeon_generator import ItemPopulator, Layout, LayoutGenerator, MobPopulator +from tale.items.basic import Money +from tale.json_story import JsonStory +from tale.main import run_from_cmdline +from tale.player import Player, PlayerConnection +from tale.story import * +from tale.skills.weapon_type import WeaponType +from tale.zone import Zone + +class Story(JsonStory): + + driver = None # type: Driver + + def __init__(self, path = '', layout_generator = LayoutGenerator(), mob_populator = MobPopulator(), item_populator = ItemPopulator(), config: StoryConfig = None) -> None: + super(Story, self).__init__(path, config or parse_utils.load_story_config(parse_utils.load_json('story_config.json'))) + self.layout_generator = layout_generator + self.mob_populator = mob_populator + self.item_populator = item_populator + self.max_depth = 5 + self.depth = 0 + + + def init(self, driver: Driver) -> None: + super(Story, self).init(driver) + + def init_player(self, player: Player) -> None: + """ + Called by the game driver when it has created the player object (after successful login). + You can set the hint texts on the player object, or change the state object, etc. + """ + player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED_RANGED, value=random.randint(10, 30)) + player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED_RANGED, value=random.randint(10, 30)) + player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED, value=random.randint(10, 30)) + player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED, value=random.randint(10, 30)) + player.stats.set_weapon_skill(weapon_type=WeaponType.UNARMED, value=random.randint(20, 30)) + pass + + def create_account_dialog(self, playerconnection: PlayerConnection, playernaming: PlayerNaming) -> Generator: + """ + Override to add extra dialog options to the character creation process. + Because there's no actual player yet, you receive PlayerConnection and PlayerNaming arguments. + Write stuff to the user via playerconnection.output(...) + Ask questions using the yield "input", "question?" mechanism. + Return True to declare all is well, and False to abort the player creation process. + """ + ranged = yield "input", ("Do you prefer ranged over close combat? (yes/no)", lang.yesno) + if ranged: + playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED_RANGED, value=random.randint(20, 40)) + playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED_RANGED, value=random.randint(20, 40)) + else: + playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED, value=random.randint(20, 40)) + playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED, value=random.randint(20, 40)) + return True + + def welcome(self, player: Player) -> str: + """welcome text when player enters a new game""" + player.tell("Hello, %s! Welcome to %s." % (player.title, self.config.name), end=True) + player.tell("\n") + player.tell(self.config.context) + player.tell("\n") + return "" + + def welcome_savegame(self, player: Player) -> str: + """welcome text when player enters the game after loading a saved game""" + player.tell("Hello %s, welcome back to %s." % (player.title, self.config.name), end=True) + player.tell("\n") + player.tell(self.driver.resources["messages/welcome.txt"].text) + player.tell("\n") + return "" + + def goodbye(self, player: Player) -> None: + """goodbye text when player quits the game""" + player.tell("Goodbye, %s. Please come back again soon." % player.title) + player.tell("\n") + + def add_zone(self, zone: Zone) -> bool: + if not super(Story, self).add_zone(zone): + 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 not first_zone: + self.layout_generator.spawn_gold(zone=zone) + + 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.driver.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) + + +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) \ No newline at end of file diff --git a/stories/land_story/story.py b/stories/land_story/story.py new file mode 100644 index 00000000..5a79e53f --- /dev/null +++ b/stories/land_story/story.py @@ -0,0 +1,85 @@ +import pathlib +import random +import sys +from typing import Generator + +import tale +from tale import lang +from tale import parse_utils +from tale.driver import Driver +from tale.json_story import JsonStory +from tale.llm.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 * +from tale.skills.weapon_type import WeaponType +from tale.zone import Zone + +class Story(JsonStory): + + def __init__(self) -> None: + super(Story, self).__init__('', parse_utils.load_story_config(parse_utils.load_json('story_config.json'))) + + def init(self, driver: Driver) -> None: + super(Story, self).init(driver) + + def init_player(self, player: Player) -> None: + """ + Called by the game driver when it has created the player object (after successful login). + You can set the hint texts on the player object, or change the state object, etc. + """ + player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED_RANGED, value=random.randint(10, 30)) + player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED_RANGED, value=random.randint(10, 30)) + player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED, value=random.randint(10, 30)) + player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED, value=random.randint(10, 30)) + player.stats.set_weapon_skill(weapon_type=WeaponType.UNARMED, value=random.randint(20, 30)) + pass + + def create_account_dialog(self, playerconnection: PlayerConnection, playernaming: PlayerNaming) -> Generator: + """ + Override to add extra dialog options to the character creation process. + Because there's no actual player yet, you receive PlayerConnection and PlayerNaming arguments. + Write stuff to the user via playerconnection.output(...) + Ask questions using the yield "input", "question?" mechanism. + Return True to declare all is well, and False to abort the player creation process. + """ + ranged = yield "input", ("Do you prefer ranged over close combat? (yes/no)", lang.yesno) + if ranged: + playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED_RANGED, value=random.randint(20, 40)) + playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED_RANGED, value=random.randint(20, 40)) + else: + playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED, value=random.randint(20, 40)) + playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED, value=random.randint(20, 40)) + return True + + def welcome(self, player: Player) -> str: + """welcome text when player enters a new game""" + player.tell("Hello, %s! Welcome to %s." % (player.title, self.config.name), end=True) + player.tell("\n") + player.tell(self.driver.resources["messages/welcome.txt"].text) + player.tell("\n") + return "" + + def welcome_savegame(self, player: Player) -> str: + """welcome text when player enters the game after loading a saved game""" + player.tell("Hello %s, welcome back to %s." % (player.title, self.config.name), end=True) + player.tell("\n") + player.tell(self.driver.resources["messages/welcome.txt"].text) + player.tell("\n") + return "" + + def goodbye(self, player: Player) -> None: + """goodbye text when player quits the game""" + player.tell("Goodbye, %s. Please come back again soon." % player.title) + 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) diff --git a/stories/prancingllama/npcs/npcs.py b/stories/prancingllama/npcs/npcs.py index 6ee8f5f9..e95f5f69 100644 --- a/stories/prancingllama/npcs/npcs.py +++ b/stories/prancingllama/npcs/npcs.py @@ -12,7 +12,7 @@ from tale import lang from typing import Optional -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType class InnKeeper(LivingNpc): diff --git a/stories/prancingllama/story.py b/stories/prancingllama/story.py index 4ee177c0..6f5aa9c5 100644 --- a/stories/prancingllama/story.py +++ b/stories/prancingllama/story.py @@ -7,12 +7,12 @@ from tale.cmds import spells from tale.driver import Driver from tale.llm.llm_ext import DynamicStory -from tale.magic import MagicType +from tale.skills.magic import MagicType from tale.main import run_from_cmdline from tale.player import Player, PlayerConnection from tale.charbuilder import PlayerNaming from tale.story import * -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType from tale.zone import Zone class Story(DynamicStory): diff --git a/tale/accounts.py b/tale/accounts.py index c60a229c..81fd1c6e 100644 --- a/tale/accounts.py +++ b/tale/accounts.py @@ -111,6 +111,7 @@ def _create_database(self) -> None: dexterity integer NOT NULL, weapon_skills varchar NOT NULL, magic_skills varchar NOT NULL, + skils varchar NOT NULL, combat_points integer NOT NULL, max_combat_points integer NOT NULL, magic_points integer NOT NULL, diff --git a/tale/base.py b/tale/base.py index ff508fe8..ce7da7c4 100644 --- a/tale/base.py +++ b/tale/base.py @@ -50,7 +50,7 @@ from tale.coord import Coord from tale.llm.contexts.CombatContext import CombatContext -from tale.magic import MagicSkill, MagicType +from tale.skills.magic import MagicSkill, MagicType from . import lang from . import mud_context @@ -63,7 +63,7 @@ from .errors import ActionRefused, ParseError, LocationIntegrityError, TaleError, UnknownVerbException, NonSoulVerb from tale.races import UnarmedAttack -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType from . import wearable __all__ = ["MudObject", "Armour", 'Container', "Door", "Exit", "Item", "Living", "Stats", "Location", "Weapon", "Key", "Soul"] @@ -289,6 +289,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.hidden = False self.avatar = resources_utils.check_file_exists_in_resources(self.name.strip().replace(" ", "_").lower()) self.init() if util.get_periodicals(self): @@ -774,7 +775,7 @@ def look(self, exclude_living: 'Living'=None, short: bool=False) -> Sequence[str item_names = sorted(item.name for item in self.items) paragraphs.append("You see: " + lang.join(item_names)) if self.livings: - living_names = sorted(living.name for living in self.livings if living != exclude_living and living.visible) + living_names = sorted(living.name for living in self.livings if living != exclude_living and living.visible and not living.hidden) if living_names: paragraphs.append("Present here: " + lang.join(living_names)) return paragraphs @@ -791,8 +792,8 @@ def look(self, exclude_living: 'Living'=None, short: bool=False) -> Sequence[str exit_paragraph.append(exit.short_description) paragraphs.append(" ".join(exit_paragraph)) items_and_livings = [] # type: List[str] - items_with_short_descr = [item for item in self.items if item.short_description and item.visible] - items_without_short_descr = [item for item in self.items if not item.short_description and item.visible] + items_with_short_descr = [item for item in self.items if item.short_description and item.visible and not item.hidden] + items_without_short_descr = [item for item in self.items if not item.short_description and item.visible and not item.hidden] uniq_descriptions = set() if items_with_short_descr: for item in items_with_short_descr: @@ -801,8 +802,8 @@ def look(self, exclude_living: 'Living'=None, short: bool=False) -> Sequence[str if items_without_short_descr: titles = sorted([lang.a(item.title) for item in items_without_short_descr]) items_and_livings.append("You see " + lang.join(titles) + ".") - livings_with_short_descr = [living for living in self.livings if living != exclude_living and living.short_description and living.visible] - livings_without_short_descr = [living for living in self.livings if living != exclude_living and not living.short_description and living.visible] + livings_with_short_descr = [living for living in self.livings if living != exclude_living and living.short_description and living.visible and not living.hidden] + livings_without_short_descr = [living for living in self.livings if living != exclude_living and not living.short_description and living.visible and not living.hidden] if livings_without_short_descr: titles = sorted(living.title for living in livings_without_short_descr) if titles: @@ -977,6 +978,7 @@ def __init__(self) -> None: self.unarmed_attack = Weapon(UnarmedAttack.FISTS.name, weapon_type=WeaponType.UNARMED) self.weapon_skills = {} # type: Dict[WeaponType, int] # weapon type -> skill level self.magic_skills = {} # type: Dict[MagicType, MagicSkill] + self.skills = {} # type: Dict[str, int] # skill name -> skill level self.combat_points = 0 # combat points self.max_combat_points = 5 # max combat points self.max_magic_points = 5 # max magic points @@ -1455,6 +1457,7 @@ def start_attack(self, defender: 'Living', target_body_part: wearable.WearLocati if self.stats.combat_points < 1: self.tell("You are too tired to attack.") return + self.hidden = False self.stats.combat_points -= 1 attacker_name = lang.capital(self.title) victim_name = lang.capital(defender.title) diff --git a/tale/cmds/normal.py b/tale/cmds/normal.py index 36f257cd..d7b91f61 100644 --- a/tale/cmds/normal.py +++ b/tale/cmds/normal.py @@ -12,6 +12,7 @@ from tale.llm.LivingNpc import LivingNpc from tale.llm.llm_ext import DynamicStory +from tale.skills.skills import SkillType from . import abbreviations, cmd, disabled_in_gamemode, disable_notify_action, overrides_soul, no_soul_parse from .. import base @@ -1846,4 +1847,61 @@ def do_unfollow(player: Player, parsed: base.ParseResult, ctx: util.Context) -> result.following = None player.tell("%s stops following you" % result.title) result.tell("You stop following %s" % player.title) + +@cmd("hide") +def do_hide(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """Hide yourself.""" + if player.hidden: + raise ActionRefused("You're already hidden. If you want to reveal yourself, use 'unhide'.") + if len(player.location.livings) > 1: + raise ActionRefused("You can't hide when there are other living entities around.") + + skillValue = player.stats.skills.get(SkillType.HIDE, 0) + if random.randint(1, 100) > skillValue: + player.tell("You fail to hide.") + return + + player.hidden = True + player.tell("You hide yourself.") + player.location.tell("%s hides" % player.title, exclude_living=player) + +@cmd("unhide") +def do_unhide(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """Unhide yourself.""" + if not player.hidden: + raise ActionRefused("You're not hidden.") + + player.hidden = False + player.tell("You reveal yourself") + player.location.tell("%s reveals themselves" % player.title, exclude_living=player) + +@cmd("search_hidden") +def do_search_hidden(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """Search for hidden things.""" + + livings = player.location.livings + + player.location.tell("%s searches for something in the room." % (player.title), exclude_living=player) + + if len(player.location.livings) == 1: + player.tell("You don't find anything.") + return + + skillValue = player.stats.skills.get(SkillType.SEARCH, 0) + + found = False + + for living in livings: + if living != player and living.hidden: + modifier = skillValue - living.stats.skills.get(SkillType.HIDE, 0) + if random.randint(1, 100) < skillValue + modifier: + living.hidden = False + player.tell("You find %s." % living.title) + player.location.tell("%s reveals %s" % (player.title, living.title), exclude_living=player) + found = True + + if not found: + player.tell("You don't find anything.") + + \ No newline at end of file diff --git a/tale/cmds/spells.py b/tale/cmds/spells.py index 03c9fc55..696ae662 100644 --- a/tale/cmds/spells.py +++ b/tale/cmds/spells.py @@ -2,10 +2,10 @@ import random from typing import Optional from tale import base, util, cmds -from tale import magic +from tale.skills import magic from tale.cmds import cmd from tale.errors import ActionRefused, ParseError -from tale.magic import MagicType, MagicSkill, Spell +from tale.skills.magic import MagicType, MagicSkill, Spell from tale.player import Player diff --git a/tale/combat.py b/tale/combat.py index 9234aa03..36ee0cfb 100644 --- a/tale/combat.py +++ b/tale/combat.py @@ -6,7 +6,7 @@ import random from typing import List, Tuple -from tale import weapon_type +from tale.skills import weapon_type import tale.base as base from tale.wearable import WearLocation, body_parts_for_bodytype from tale.wearable import WearLocation diff --git a/tale/items/generic.py b/tale/items/generic.py index 518e62f4..ce47de32 100644 --- a/tale/items/generic.py +++ b/tale/items/generic.py @@ -8,7 +8,7 @@ from tale import parse_utils from tale.base import Item, Weapon from tale.items.basic import Note -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType def load() -> dict: items = dict() diff --git a/tale/parse_utils.py b/tale/parse_utils.py index 1b448e4a..789981db 100644 --- a/tale/parse_utils.py +++ b/tale/parse_utils.py @@ -7,12 +7,12 @@ from tale.item_spawner import ItemSpawner from tale.items.basic import Boxlike, Drink, Food, Health, Money, Note from tale.llm.LivingNpc import LivingNpc -from tale.magic import MagicType +from tale.skills.magic import MagicType from tale.npc_defs import StationaryMob, StationaryNpc from tale.races import BodyType, UnarmedAttack from tale.mob_spawner import MobSpawner from tale.story import GameMode, MoneyType, TickMethod, StoryConfig -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType from tale.wearable import WearLocation import json import re @@ -644,6 +644,7 @@ def save_stats(stats: Stats) -> dict: json_stats['level'] = stats.level json_stats['weapon_skills'] = skills_dict_to_json(stats.weapon_skills) json_stats['magic_skills'] = skills_dict_to_json(stats.magic_skills) + json_stats['skills'] = skills_dict_to_json(stats.skills) json_stats['gender'] = stats.gender = 'n' json_stats['alignment'] = stats.alignment json_stats['weight'] = stats.weight @@ -671,22 +672,28 @@ def load_stats(json_stats: dict) -> Stats: stats.strength = json_stats.get('strength') stats.dexterity = json_stats.get('dexterity') stats.race = json_stats.get('race', 'human') - if json_stats.get('bodytype'): + if json_stats.get('bodytype', None): stats.bodytype = BodyType[json_stats['bodytype'].upper()] - if json_stats.get('unarmed_attack'): + if json_stats.get('unarmed_attack', None): stats.unarmed_attack = Weapon(UnarmedAttack[json_stats['unarmed_attack'].upper()], WeaponType.UNARMED) - if json_stats.get('weapon_skills'): + if json_stats.get('weapon_skills', None): json_skills = json_stats['weapon_skills'] stats.weapon_skills = {} for skill in json_skills.keys(): int_skill = int(skill) stats.weapon_skills[WeaponType(int_skill)] = json_skills[skill] - if json_stats.get('magic_skills'): + if json_stats.get('magic_skills', None): json_skills = json_stats['magic_skills'] stats.magic_skills = {} for skill in json_skills.keys(): int_skill = int(skill) stats.magic_skills[MagicType(int_skill)] = json_skills[skill] + if json_stats.get('skills', None): + json_skills = json_stats['skills'] + stats.skills = {} + for skill in json_skills.keys(): + int_skill = int(skill) + stats.skills[WeaponType(int_skill)] = json return stats def save_items(items: List[Item]) -> dict: diff --git a/tale/magic.py b/tale/skills/magic.py similarity index 100% rename from tale/magic.py rename to tale/skills/magic.py diff --git a/tale/skills/skills.py b/tale/skills/skills.py new file mode 100644 index 00000000..06406843 --- /dev/null +++ b/tale/skills/skills.py @@ -0,0 +1,9 @@ + + +from enum import Enum + + +class SkillType(Enum): + + HIDE = 1 + SEARCH = 2 \ No newline at end of file diff --git a/tale/weapon_type.py b/tale/skills/weapon_type.py similarity index 100% rename from tale/weapon_type.py rename to tale/skills/weapon_type.py diff --git a/tests/test_combat.py b/tests/test_combat.py index b3e1c05c..8660b5e0 100644 --- a/tests/test_combat.py +++ b/tests/test_combat.py @@ -6,7 +6,7 @@ from tale.llm.contexts.CombatContext import CombatContext from tale.player import Player from tale.races import BodyType -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType from tale.wearable import WearLocation from tests.supportstuff import FakeDriver from tale.wearable import WearLocation diff --git a/tests/test_generic_items.py b/tests/test_generic_items.py index fd1e503d..18c2ad97 100644 --- a/tests/test_generic_items.py +++ b/tests/test_generic_items.py @@ -4,7 +4,7 @@ from tale.base import Weapon from tale.items import generic from tale.items.basic import Food, Health, Note -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType class TestGenericItems(): diff --git a/tests/test_llm_utils.py b/tests/test_llm_utils.py index 6eac792b..95468cd4 100644 --- a/tests/test_llm_utils.py +++ b/tests/test_llm_utils.py @@ -8,7 +8,7 @@ from tale.llm.contexts.FollowContext import FollowContext from tale.llm.contexts.WorldGenerationContext import WorldGenerationContext import tale.llm.llm_cache as llm_cache -from tale import mud_context, weapon_type +from tale import mud_context from tale import zone from tale import util from tale.base import Item, Location, Weapon @@ -21,6 +21,7 @@ from tale.npc_defs import StationaryMob from tale.player import Player, PlayerConnection from tale.races import UnarmedAttack +from tale.skills import weapon_type from tale.tio.console_io import ConsoleIo from tale.util import MoneyFormatterFantasy from tale.zone import Zone diff --git a/tests/test_normal_commands.py b/tests/test_normal_commands.py index 8b31e26e..c11736b2 100644 --- a/tests/test_normal_commands.py +++ b/tests/test_normal_commands.py @@ -10,6 +10,7 @@ from tale.llm.llm_ext import DynamicStory from tale.llm.llm_utils import LlmUtil from tale.player import Player +from tale.skills.skills import SkillType from tale.story import StoryConfig from tests.supportstuff import FakeDriver, FakeIoUtil @@ -140,4 +141,48 @@ def test_unfollow(self): location = Location('test_room') location.init_inventory([self.test_player, test_npc]) normal.do_unfollow(self.test_player, ParseResult(verb='unfollow', args=['test_npc']), self.context) - assert not test_npc.following \ No newline at end of file + assert not test_npc.following + + def test_hide(self): + self.test_player.stats.skills[SkillType.HIDE] = 100 + normal.do_hide(self.test_player, ParseResult(verb='hide', args=[]), self.context) + assert self.test_player.hidden + + + with pytest.raises(ActionRefused, match="You're already hidden. If you want to reveal yourself, use 'unhide'"): + normal.do_hide(self.test_player, ParseResult(verb='hide', args=[]), self.context) + + self.test_player.hidden = False + + self.test_player.stats.skills[SkillType.HIDE] = 0 + normal.do_hide(self.test_player, ParseResult(verb='hide', args=[]), self.context) + assert not self.test_player.hidden + + def test_unhide(self): + self.test_player.hidden = True + normal.do_unhide(self.test_player, ParseResult(verb='unhide', args=[]), self.context) + assert not self.test_player.hidden + + with pytest.raises(ActionRefused, match="You're not hidden."): + normal.do_unhide(self.test_player, ParseResult(verb='unhide', args=[]), self.context) + + def test_search_hidden(self): + + test_npc = LivingNpc('test_npc', 'f') + test_npc.hidden = True + test_npc.stats.skills[SkillType.HIDE] = 100 + location = Location('test_room') + location.init_inventory([self.test_player, test_npc]) + + self.test_player.stats.skills[SkillType.SEARCH] = 0 + + normal.do_search_hidden(self.test_player, ParseResult(verb='search', args=[]), self.context) + + assert test_npc.hidden + + self.test_player.stats.skills[SkillType.SEARCH] = 100 + test_npc.stats.skills[SkillType.HIDE] = 0 + + normal.do_search_hidden(self.test_player, ParseResult(verb='search', args=[]), self.context) + + assert not test_npc.hidden \ No newline at end of file diff --git a/tests/test_parse_utils.py b/tests/test_parse_utils.py index c3b967d7..b0d1b680 100644 --- a/tests/test_parse_utils.py +++ b/tests/test_parse_utils.py @@ -10,7 +10,7 @@ from tale.mob_spawner import MobSpawner from tale.races import BodyType from tale.story import GameMode, MoneyType -from tale.weapon_type import WeaponType +from tale.skills.weapon_type import WeaponType from tale.wearable import WearLocation from tale.zone import Zone import tale.parse_utils as parse_utils diff --git a/tests/test_spells.py b/tests/test_spells.py index 46edd113..40a79a2c 100644 --- a/tests/test_spells.py +++ b/tests/test_spells.py @@ -1,7 +1,7 @@ import pytest -from tale import magic +from tale.skills import magic import tale from tale.base import Location, ParseResult from tale.cmds import spells @@ -9,7 +9,7 @@ from tale.llm.LivingNpc import LivingNpc from tale.llm.llm_ext import DynamicStory from tale.llm.llm_utils import LlmUtil -from tale.magic import MagicSkill, MagicType +from tale.skills.magic import MagicSkill, MagicType from tale.player import Player from tale.story import StoryConfig from tests.supportstuff import FakeDriver, FakeIoUtil From c10a41d5bd3cec469056f386eee0b4b482743157 Mon Sep 17 00:00:00 2001 From: rickard Date: Thu, 19 Sep 2024 16:36:00 +0200 Subject: [PATCH 2/5] no replenish when hidden rename combat_points to action_points move hide and search logic to Living --- tale/base.py | 72 +++++++++++++++++--- tale/cmds/normal.py | 41 +---------- tale/cmds/spells.py | 2 +- tale/combat.py | 2 +- tale/driver.py | 2 + tale/proxy_io/proxy_io.py | 125 ++++++++++++++++++++++++++++++++++ tests/test_combat.py | 2 +- tests/test_normal_commands.py | 12 +++- tests/test_spells.py | 4 +- tests/test_stats.py | 6 +- 10 files changed, 212 insertions(+), 56 deletions(-) create mode 100644 tale/proxy_io/proxy_io.py diff --git a/tale/base.py b/tale/base.py index ce7da7c4..6ce7e6ab 100644 --- a/tale/base.py +++ b/tale/base.py @@ -51,6 +51,7 @@ from tale.coord import Coord from tale.llm.contexts.CombatContext import CombatContext from tale.skills.magic import MagicSkill, MagicType +from tale.skills.skills import SkillType from . import lang from . import mud_context @@ -979,7 +980,7 @@ def __init__(self) -> None: self.weapon_skills = {} # type: Dict[WeaponType, int] # weapon type -> skill level self.magic_skills = {} # type: Dict[MagicType, MagicSkill] self.skills = {} # type: Dict[str, int] # skill name -> skill level - self.combat_points = 0 # combat points + self.action_points = 0 # combat points self.max_combat_points = 5 # max combat points self.max_magic_points = 5 # max magic points self.magic_points = 0 # magic points @@ -1025,11 +1026,11 @@ def replenish_hp(self, amount: int = None) -> None: def replenish_combat_points(self, amount: int = None) -> None: if amount: - self.combat_points += amount + self.action_points += amount else: - self.combat_points = self.max_combat_points - if self.combat_points > self.max_combat_points: - self.combat_points = self.max_combat_points + self.action_points = self.max_combat_points + if self.action_points > self.max_combat_points: + self.action_points = self.max_combat_points def replenish_magic_points(self, amount: int = None) -> None: if amount: @@ -1454,11 +1455,14 @@ def locate_item(self, name: str, include_inventory: bool=True, include_location: def start_attack(self, defender: 'Living', target_body_part: wearable.WearLocation= None) -> combat.Combat: """Starts attacking the given living for one round.""" - if self.stats.combat_points < 1: + if self.stats.action_points < 1: self.tell("You are too tired to attack.") return - self.hidden = False - self.stats.combat_points -= 1 + if self.hidden: + self.hidden = False + self.stats.action_points -= 3 + else: + self.stats.action_points -= 1 attacker_name = lang.capital(self.title) victim_name = lang.capital(defender.title) attackers = [self] @@ -1644,6 +1648,58 @@ def do_on_death(self) -> 'Container': self.on_death_callback(remains) self.destroy(util.Context) return remains + + def hide(self, hide: bool = True): + if not hide: + self.hidden = False + self.hidden = False + self.tell("You reveal yourself") + self.location.tell("%s reveals themselves" % self.title, exclude_living=self) + return + + if self.stats.action_points < 1: + raise ActionRefused("You don't have enough action points to hide.") + if len(self.location.livings) > 1: + raise ActionRefused("You can't hide when there are other living entities around.") + + self.stats.action_points -= 1 + + skillValue = self.stats.skills.get(SkillType.HIDE, 0) + if random.randint(1, 100) > skillValue: + self.tell("You fail to hide.") + return + + self.hidden = hide + self.tell("You hide yourself.") + self.location.tell("%s hides" % self.title, exclude_living=self) + + def search_hidden(self): + if self.stats.action_points < 1: + raise ActionRefused("You don't have enough action points to search.") + + livings = self.location.livings + + self.location.tell("%s searches for something in the room." % (self.title), exclude_living=self) + + if len(self.location.livings) == 1: + self.tell("You don't find anything.") + return + + skillValue = self.stats.skills.get(SkillType.SEARCH, 0) + + found = False + + for living in livings: + if living != self and living.hidden: + modifier = skillValue - living.stats.skills.get(SkillType.HIDE, 0) + if random.randint(1, 100) < skillValue + modifier: + living.hidden = False + self.tell("You find %s." % living.title) + self.location.tell("%s reveals %s" % (self.title, living.title), exclude_living=self) + found = True + + if not found: + self.tell("You don't find anything.") class Container(Item): diff --git a/tale/cmds/normal.py b/tale/cmds/normal.py index d7b91f61..4a54c992 100644 --- a/tale/cmds/normal.py +++ b/tale/cmds/normal.py @@ -1853,55 +1853,20 @@ def do_hide(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None """Hide yourself.""" if player.hidden: raise ActionRefused("You're already hidden. If you want to reveal yourself, use 'unhide'.") - if len(player.location.livings) > 1: - raise ActionRefused("You can't hide when there are other living entities around.") - - skillValue = player.stats.skills.get(SkillType.HIDE, 0) - if random.randint(1, 100) > skillValue: - player.tell("You fail to hide.") - return - - player.hidden = True - player.tell("You hide yourself.") - player.location.tell("%s hides" % player.title, exclude_living=player) + player.hide() @cmd("unhide") def do_unhide(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: """Unhide yourself.""" if not player.hidden: raise ActionRefused("You're not hidden.") - - player.hidden = False - player.tell("You reveal yourself") - player.location.tell("%s reveals themselves" % player.title, exclude_living=player) + player.hide(False) @cmd("search_hidden") def do_search_hidden(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: """Search for hidden things.""" - livings = player.location.livings - - player.location.tell("%s searches for something in the room." % (player.title), exclude_living=player) - - if len(player.location.livings) == 1: - player.tell("You don't find anything.") - return - - skillValue = player.stats.skills.get(SkillType.SEARCH, 0) - - found = False - - for living in livings: - if living != player and living.hidden: - modifier = skillValue - living.stats.skills.get(SkillType.HIDE, 0) - if random.randint(1, 100) < skillValue + modifier: - living.hidden = False - player.tell("You find %s." % living.title) - player.location.tell("%s reveals %s" % (player.title, living.title), exclude_living=player) - found = True - - if not found: - player.tell("You don't find anything.") + player.search_hidden() \ No newline at end of file diff --git a/tale/cmds/spells.py b/tale/cmds/spells.py index 696ae662..d17cce87 100644 --- a/tale/cmds/spells.py +++ b/tale/cmds/spells.py @@ -130,7 +130,7 @@ def do_drain(player: Player, parsed: base.ParseResult, ctx: util.Context) -> Non return points = random.randint(1, level) - result.stats.combat_points -= points + result.stats.action_points -= points result.stats.magic_points -= points player.stats.magic_points += points diff --git a/tale/combat.py b/tale/combat.py index 36ee0cfb..03355aee 100644 --- a/tale/combat.py +++ b/tale/combat.py @@ -40,7 +40,7 @@ def _calculate_block_success(self, actor1: 'base.Living', actor2: 'base.Living') if actor2.wielding.type in weapon_type.ranged: # can't block with a ranged weapon return 100 - return random.randrange(0, 100) - actor2.stats.get_weapon_skill(actor2.wielding.type) * (0.8 if actor2.stats.combat_points < 1 else 1) + return random.randrange(0, 100) - actor2.stats.get_weapon_skill(actor2.wielding.type) * (0.8 if actor2.stats.action_points < 1 else 1) def _calculate_weapon_bonus(self, actor: 'base.Living'): weapon = actor.wielding diff --git a/tale/driver.py b/tale/driver.py index 9ab3882c..595a7883 100644 --- a/tale/driver.py +++ b/tale/driver.py @@ -964,6 +964,8 @@ def do_on_player_death(self, player: player.Player) -> None: @util.call_periodically(20) def replenish(self): for player in self.all_players.values(): + if player.hidden: + continue player.player.stats.replenish_hp(1) player.player.stats.replenish_combat_points(1) player.player.stats.replenish_magic_points(1) \ No newline at end of file diff --git a/tale/proxy_io/proxy_io.py b/tale/proxy_io/proxy_io.py new file mode 100644 index 00000000..86e67b1b --- /dev/null +++ b/tale/proxy_io/proxy_io.py @@ -0,0 +1,125 @@ + +from typing import Any, Sequence, Tuple +from tale.player import PlayerConnection +from tale.tio import iobase, styleaware_wrapper +from . import colorama_patched as colorama + + +colorama.init() +assert type(colorama.Style.DIM) is str, "Incompatible colorama library installed. Please upgrade to a more recent version (0.3.6+)" + +style_words = { + "dim": colorama.Style.DIM, + "normal": colorama.Style.NORMAL, + "bright": colorama.Style.BRIGHT, + "ul": colorama.Style.UNDERLINED, + "it": colorama.Style.ITALIC, + "rev": colorama.Style.REVERSEVID, + "/": colorama.Style.RESET_ALL, + "location": colorama.Style.BRIGHT, + "clear": "\033[1;1H\033[2J", # ansi sequence to clear the console screen + "monospaced": "", # we assume the console is already monospaced font + "/monospaced": "" +} +assert len(set(style_words.keys()) ^ iobase.ALL_STYLE_TAGS) == 0, "mismatch in list of style tags" + +class ProxyIo(iobase.IoAdapterBase): + + def __init__(self, player_connection: PlayerConnection) -> None: + super().__init__(player_connection) + + self.stop_main_loop = False + self.waiting_input = '' + + + def singleplayer_mainloop(self, player_connection: PlayerConnection) -> None: + """Main event loop for the console I/O adapter for single player mode""" + while not self.stop_main_loop: + try: + # note that we don't print any prompt ">>", that needs to be done + # by the main thread that handles screen *output* + # (otherwise the prompt will often appear before any regular screen output) + old_player = player_connection.player + # do blocking console input call + cmd = self.waiting_input + if cmd: + self.waiting_input = '' + player_connection.player.store_input_line(cmd) + if old_player is not player_connection.player: + # this situation occurs when a save game has been restored, + # we also have to unblock the old_player + old_player.store_input_line(cmd) + except KeyboardInterrupt: + self.break_pressed() + except EOFError: + pass + + def render_output(self, paragraphs: Sequence[Tuple[str, bool]], **params: Any) -> str: + """ + Render (format) the given paragraphs to a text representation. + It doesn't output anything to the screen yet; it just returns the text string. + Any style-tags are still embedded in the text. + This console-implementation expects 2 extra parameters: "indent" and "width". + """ + if not paragraphs: + return "" + indent = " " * params["indent"] + wrapper = styleaware_wrapper.StyleTagsAwareTextWrapper(width=params["width"], fix_sentence_endings=True, + initial_indent=indent, subsequent_indent=indent) + output = [] + for txt, formatted in paragraphs: + if formatted: + txt = wrapper.fill(txt) + "\n" + else: + # unformatted output, prepend every line with the indent but otherwise leave them alone + txt = indent + ("\n" + indent).join(txt.splitlines()) + "\n" + assert txt.endswith("\n") + output.append(txt) + return self.smartquotes("".join(output)) + + + def output(self, *lines: str) -> None: + super().output(*lines) + for line in lines: + print(self._apply_style(line, self.do_styles)) + + def output_no_newline(self, text: str, new_paragraph = True) -> None: + super().output_no_newline(text, new_paragraph) + print(self._apply_style(text, self.do_styles), end="") + + def write_input_prompt(self) -> None: + """write the input prompt '>>'""" + pass + + def break_pressed(self) -> None: + pass + + def _apply_style(self, line: str, do_styles: bool) -> str: + """Convert style tags to ansi escape sequences suitable for console text output""" + if "<" not in line: + return line + elif style_words and do_styles: + for tag, replacement in style_words.items(): + line = line.replace("<%s>" % tag, replacement) + return line + else: + return iobase.strip_text_styles(line) # type: ignore + + " Discord Client Events " + + class DiscordBot(discord.Client): + + def __init__(self, intents, proxyIo: 'ProxyIo'): + super().__init__(intents=intents) + self.proxyIo = proxyIo + + async def on_ready(self): + print(f'{self.user} has connected to Discord!') + + + async def on_message(self, message: discord.Message): + if message.channel.type == discord.ChannelType.private: + self.proxyIo.waiting_input = message.content + else: + message.author.send('Please send messages in a private channel.') + diff --git a/tests/test_combat.py b/tests/test_combat.py index 8660b5e0..f619cf12 100644 --- a/tests/test_combat.py +++ b/tests/test_combat.py @@ -240,7 +240,7 @@ def test_resolve_attack_group(self): def test_start_attack_no_combat_points(self): attacker = Player(name='att', gender='m') - attacker.stats.combat_points = 0 + attacker.stats.action_points = 0 defender = LivingNpc(name='lucky rat', gender='m', age=2, personality='A squeeky fighter') assert attacker.start_attack(defender) == None diff --git a/tests/test_normal_commands.py b/tests/test_normal_commands.py index c11736b2..63ece16a 100644 --- a/tests/test_normal_commands.py +++ b/tests/test_normal_commands.py @@ -145,16 +145,22 @@ def test_unfollow(self): def test_hide(self): self.test_player.stats.skills[SkillType.HIDE] = 100 + self.test_player.stats.action_points = 1 normal.do_hide(self.test_player, ParseResult(verb='hide', args=[]), self.context) assert self.test_player.hidden - - with pytest.raises(ActionRefused, match="You're already hidden. If you want to reveal yourself, use 'unhide'"): + with pytest.raises(ActionRefused, match="You're already hidden. If you want to reveal yourself, use 'unhide'."): + normal.do_hide(self.test_player, ParseResult(verb='hide', args=[]), self.context) + + self.test_player.hidden = False + + with pytest.raises(ActionRefused, match="You don't have enough action points to hide."): normal.do_hide(self.test_player, ParseResult(verb='hide', args=[]), self.context) self.test_player.hidden = False self.test_player.stats.skills[SkillType.HIDE] = 0 + self.test_player.stats.action_points = 1 normal.do_hide(self.test_player, ParseResult(verb='hide', args=[]), self.context) assert not self.test_player.hidden @@ -175,6 +181,7 @@ def test_search_hidden(self): location.init_inventory([self.test_player, test_npc]) self.test_player.stats.skills[SkillType.SEARCH] = 0 + self.test_player.stats.action_points = 1 normal.do_search_hidden(self.test_player, ParseResult(verb='search', args=[]), self.context) @@ -182,6 +189,7 @@ def test_search_hidden(self): self.test_player.stats.skills[SkillType.SEARCH] = 100 test_npc.stats.skills[SkillType.HIDE] = 0 + self.test_player.stats.action_points = 1 normal.do_search_hidden(self.test_player, ParseResult(verb='search', args=[]), self.context) diff --git a/tests/test_spells.py b/tests/test_spells.py index 40a79a2c..5248f94e 100644 --- a/tests/test_spells.py +++ b/tests/test_spells.py @@ -123,12 +123,12 @@ def setup_method(self): def test_drain(self): self.player.stats.magic_skills[MagicType.DRAIN] = 100 npc = LivingNpc('test', 'f', age=30) - npc.stats.combat_points = 5 + npc.stats.action_points = 5 npc.stats.magic_points = 5 self.player.location.insert(npc, actor=None) self.player.stats.magic_points = 10 parse_result = ParseResult(verb='drain', args=['test']) result = spells.do_drain(self.player, parse_result, None) assert self.player.stats.magic_points > 7 - assert npc.stats.combat_points < 5 + assert npc.stats.action_points < 5 assert npc.stats.magic_points < 5 \ No newline at end of file diff --git a/tests/test_stats.py b/tests/test_stats.py index 5d527d35..0b3943cf 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -21,12 +21,12 @@ def test_replenish_hp(self): def test_replenish_combat_points(self): stats = Stats() stats.max_combat_points = 100 - stats.combat_points = 0 + stats.action_points = 0 stats.replenish_combat_points(10) - assert stats.combat_points == 10 + assert stats.action_points == 10 stats.replenish_combat_points() - assert stats.combat_points == 100 \ No newline at end of file + assert stats.action_points == 100 \ No newline at end of file From 5516c6eadf4946b6c88c3d31cb85f74516c37105 Mon Sep 17 00:00:00 2001 From: rickard Date: Fri, 20 Sep 2024 18:54:03 +0200 Subject: [PATCH 3/5] add spells and refactor spells command --- tale/cmds/spells.py | 204 ++++++++++++++++++++++++++++--------------- tale/skills/magic.py | 7 +- tests/test_spells.py | 163 +++++++++++++++++++++++++++++++++- 3 files changed, 299 insertions(+), 75 deletions(-) diff --git a/tale/cmds/spells.py b/tale/cmds/spells.py index d17cce87..21db1693 100644 --- a/tale/cmds/spells.py +++ b/tale/cmds/spells.py @@ -7,37 +7,21 @@ from tale.errors import ActionRefused, ParseError from tale.skills.magic import MagicType, MagicSkill, Spell from tale.player import Player +from tale.skills.skills import SkillType +not_enough_magic_points = "You don't have enough magic points" -@cmd("heal") +@cmd("cast_heal") def do_heal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: """ Heal someone or something """ - skillValue = player.stats.magic_skills.get(MagicType.HEAL, None) - if not skillValue: - raise ActionRefused("You don't know how to heal") - spell = magic.spells[MagicType.HEAL] # type: Spell - - num_args = len(parsed.args) - - level = player.stats.level - if num_args == 2: - level = int(parsed.args[1]) + skillValue, spell = _check_spell_skill(player, MagicType.HEAL, "You don't know how to heal") + level = _parse_level(player, parsed) if not spell.check_cost(player.stats.magic_points, level): - raise ActionRefused("You don't have enough magic points") - - if num_args < 1: - raise ParseError("You need to specify who or what to heal") - try: - entity = str(parsed.args[0]) - except ValueError as x: - raise ActionRefused(str(x)) - - result = player.location.search_living(entity) # type: Optional[base.Living] - if not result or not isinstance(result, base.Living): - raise ActionRefused("Can't heal that") + raise ActionRefused(not_enough_magic_points) + result = _parse_target(player, parsed) # type: base.Living player.stats.magic_points -= spell.base_cost * level @@ -47,39 +31,20 @@ def do_heal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None result.stats.replenish_hp(5 * level) player.tell("You cast a healing spell that heals %s for %d hit points" % (result.name, 5 * level), evoke=True) - player.tell_others("%s casts a healing spell that heals %s" % (player.name, result.name), evoke=True) + player.tell_others("%s casts a healing spell on %s" % (player.name, result.name), evoke=True) -@cmd("bolt") +@cmd("cast_bolt") def do_bolt(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: """ Cast a bolt of energy """ - skillValue = player.stats.magic_skills.get(MagicType.BOLT, None) - if not skillValue: - raise ActionRefused("You don't know how to cast a bolt") - - spell = magic.spells[MagicType.BOLT] # type: Spell - - num_args = len(parsed.args) - - level = player.stats.level - if num_args == 2: - level = int(parsed.args[1]) + skillValue, spell = _check_spell_skill(player, MagicType.BOLT, "You don't know how to cast a bolt") + level = _parse_level(player, parsed) if not spell.check_cost(player.stats.magic_points, level): - raise ActionRefused("You don't have enough magic points") + raise ActionRefused(not_enough_magic_points) - if num_args < 1: - raise ParseError("You need to specify who or what to attack") - - try: - entity = str(parsed.args[0]) - except ValueError as x: - raise ActionRefused(str(x)) - - result = player.location.search_living(entity) # type: Optional[base.Living] - if not result or not isinstance(result, base.Living): - raise ActionRefused("Can't attack that") + result = _parse_target(player, parsed) # type: base.Living player.stats.magic_points -= spell.base_cost * level @@ -92,36 +57,17 @@ def do_bolt(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None player.tell("You cast an energy bolt that hits %s for %d damage" % (result.name, hp), evoke=True) player.tell_others("%s casts an energy bolt that hits %s for %d damage" % (player.name, result.name, hp), evoke=True) -@cmd("drain") +@cmd("cast_drain") def do_drain(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: """ Drain energy from someone or something """ - skillValue = player.stats.magic_skills.get(MagicType.DRAIN, None) - if not skillValue: - raise ActionRefused("You don't know how to drain") - - spell = magic.spells[MagicType.DRAIN] # type: Spell - - num_args = len(parsed.args) - - level = player.stats.level - if num_args == 2: - level = int(parsed.args[1]) + skillValue, spell = _check_spell_skill(player, MagicType.DRAIN, "You don't know how to drain") + level = _parse_level(player, parsed) if not spell.check_cost(player.stats.magic_points, level): - raise ActionRefused("You don't have enough magic points") + raise ActionRefused(not_enough_magic_points) - if num_args < 1: - raise ParseError("You need to specify who or what to drain") - - try: - entity = str(parsed.args[0]) - except ValueError as x: - raise ActionRefused(str(x)) - - result = player.location.search_living(entity) # type: Optional[base.Living] - if not result or not isinstance(result, base.Living): - raise ActionRefused("Can't drain that") + result = _parse_target(player, parsed) # type: base.Living player.stats.magic_points -= spell.base_cost * level @@ -139,3 +85,117 @@ def do_drain(player: Player, parsed: base.ParseResult, ctx: util.Context) -> Non player.tell_others("%s casts a 'drain' spell that drains energy from %s" % (player.name, result.name), evoke=True) +@cmd("cast_rejuvenate") +def do_rejuvenate(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """ Rejuvenate someone or something """ + + skillValue, spell = _check_spell_skill(player, MagicType.REJUVENATE, "You don't know how to rejuvenate") + level = _parse_level(player, parsed) + + if not spell.check_cost(player.stats.magic_points, level): + raise ActionRefused(not_enough_magic_points) + + result = _parse_target(player, parsed) # type: base.Living + + player.stats.magic_points -= spell.base_cost * level + + if random.randint(1, 100) > skillValue: + player.tell("Your rejuvenate spell fizzles out", evoke=True, short_len=True) + return + + result.stats.replenish_hp(5 * level) + player.tell("You cast a rejuvenate spell that replenishes %s for %d action points" % (result.name, 5 * level), evoke=True) + player.tell_others("%s casts a rejuvenate spell on %s" % (player.name, result.name), evoke=True) + + +@cmd("cast_hide") +def do_hide(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """ Hide from view """ + + skillValue, spell = _check_spell_skill(player, MagicType.HIDE, "You don't know how the 'hide' spell.") + level = _parse_level(player, parsed) + + if not spell.check_cost(player.stats.magic_points, level): + raise ActionRefused(not_enough_magic_points) + + result = _parse_target(player, parsed) # type: base.Living + + player.stats.magic_points -= spell.base_cost * level + + if random.randint(1, 100) > skillValue: + player.tell("Your hide spell fizzles out", evoke=True, short_len=True) + return + + result.hidden = True + player.tell(f"You cast a 'hide' spell and %s disappears from view", evoke=True) + player.tell_others(f"{player.name} casts a 'hide' spell and %s disappears from view", evoke=True) + + +@cmd("cast_reveal") +def do_reveal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """ Reveal hidden things. """ + + skillValue, spell = _check_spell_skill(player, MagicType.REVEAL, "You don't know how the 'reveal' spell.") + level = _parse_level(player, parsed, 0) + + if not spell.check_cost(player.stats.magic_points, level): + raise ActionRefused(not_enough_magic_points) + + player.stats.magic_points -= spell.base_cost * level + + if random.randint(1, 100) > skillValue: + player.tell("Your reveal spell fizzles out", evoke=True, short_len=True) + return + + livings = player.location.livings + + if len(player.location.livings) == 1: + player.tell("Your spell reveals nothing.") + return + + found = False + + for living in livings: + if living != player and living.hidden: + if random.randint(1, 100) < level * 10 - living.stats.skills.get(SkillType.HIDE, 0): + living.hidden = False + player.tell("Your spell reveals %s." % living.title) + player.location.tell("%s's spell reveals %s" % (player.title, living.title), exclude_living=player) + found = True + + if not found: + player.tell("Your spell reveals nothing.") + +def _parse_level(player: Player,parsed: base.ParseResult, arg_pos: int = 1) -> int: + num_args = len(parsed.args) + level = player.stats.level + if num_args > arg_pos: + level = int(parsed.args[arg_pos]) + return level + +def _check_spell_skill(player: Player, spell_type: MagicType, no_skill_message: str) -> int: + skillValue = player.stats.magic_skills.get(spell_type, None) + if not skillValue: + raise ActionRefused(no_skill_message) + + spell = magic.spells[spell_type] # type: Spell + return skillValue, spell + +def _parse_target(player: Player, parsed: base.ParseResult) -> base.Living: + num_args = len(parsed.args) + if num_args < 1: + raise ActionRefused("You need to specify who or what to target") + + try: + entity = str(parsed.args[0]) + except ValueError as x: + raise ActionRefused(str(x)) + + if entity.lower() == "self" or entity.lower() == "me": + return player + + result = player.location.search_living(entity) # type: Optional[base.Living] + if not result or not isinstance(result, base.Living): + raise ActionRefused("Can't target that") + + return result diff --git a/tale/skills/magic.py b/tale/skills/magic.py index 33384d41..b42516ad 100644 --- a/tale/skills/magic.py +++ b/tale/skills/magic.py @@ -7,6 +7,8 @@ class MagicType(Enum): BOLT = 2 DRAIN = 3 REJUVENATE = 4 + HIDE = 5 + REVEAL = 6 @@ -23,7 +25,10 @@ def check_cost(self, magic_points: int, level: int) -> bool: spells = { MagicType.HEAL: Spell('heal', base_cost=2, base_value=5), MagicType.BOLT: Spell('bolt', base_cost=3, base_value=5), - MagicType.DRAIN: Spell('drain', base_cost=3, base_value=5) + MagicType.DRAIN: Spell('drain', base_cost=3, base_value=5), + MagicType.REJUVENATE: Spell('rejuvenate', base_cost=2, base_value=4), + MagicType.HIDE: Spell('hide', base_cost=3), + MagicType.REVEAL: Spell('reveal', base_cost=3) } class MagicSkill: diff --git a/tests/test_spells.py b/tests/test_spells.py index 5248f94e..be159c21 100644 --- a/tests/test_spells.py +++ b/tests/test_spells.py @@ -5,7 +5,7 @@ import tale from tale.base import Location, ParseResult from tale.cmds import spells -from tale.errors import ActionRefused +from tale.errors import ActionRefused, ParseError from tale.llm.LivingNpc import LivingNpc from tale.llm.llm_ext import DynamicStory from tale.llm.llm_utils import LlmUtil @@ -71,6 +71,7 @@ def test_heal_refused(self): with pytest.raises(ActionRefused, match="You don't have enough magic points"): spells.do_heal(self.player, parse_result, None) + class TestBolt: context = tale.mud_context @@ -131,4 +132,162 @@ def test_drain(self): result = spells.do_drain(self.player, parse_result, None) assert self.player.stats.magic_points > 7 assert npc.stats.action_points < 5 - assert npc.stats.magic_points < 5 \ No newline at end of file + assert npc.stats.magic_points < 5 + +class TestRejuvenate: + + context = tale.mud_context + context.config = StoryConfig() + + io_util = FakeIoUtil(response=[]) + io_util.stream = False + llm_util = LlmUtil(io_util) + story = DynamicStory() + llm_util.set_story(story) + + def setup_method(self): + tale.mud_context.driver = FakeDriver() + tale.mud_context.driver.story = DynamicStory() + tale.mud_context.driver.llm_util = self.llm_util + self.player = Player('player', 'f') + self.location = Location('test_location') + self.location.insert(self.player, actor=None) + + def test_rejuvenate(self): + self.player.stats.magic_skills[MagicType.REJUVENATE] = 100 + npc = LivingNpc('test', 'f', age=30) + npc.stats.hp = 0 + self.player.location.insert(npc, actor=None) + self.player.stats.magic_points = 10 + parse_result = ParseResult(verb='rejuvenate', args=['test']) + result = spells.do_rejuvenate(self.player, parse_result, None) + assert self.player.stats.magic_points == 8 + assert npc.stats.hp == 5 + + def test_rejuvenate_fail(self): + self.player.stats.magic_skills[MagicType.REJUVENATE] = -1 + npc = LivingNpc('test', 'f', age=30) + npc.stats.hp = 0 + self.player.location.insert(npc, actor=None) + self.player.stats.magic_points = 10 + parse_result = ParseResult(verb='heal', args=['test']) + result = spells.do_rejuvenate(self.player, parse_result, None) + assert self.player.stats.magic_points == 8 + assert npc.stats.hp == 0 + + def test_rejuvenate_refused(self): + parse_result = ParseResult(verb='heal', args=['test']) + with pytest.raises(ActionRefused, match="You don't know how to rejuvenate"): + spells.do_rejuvenate(self.player, parse_result, None) + + self.player.stats.magic_skills[MagicType.REJUVENATE] = 10 + self.player.stats.magic_points = 0 + + npc = LivingNpc('test', 'f', age=30) + npc.stats.hp = 0 + self.player.location.insert(npc, actor=None) + + with pytest.raises(ActionRefused, match="You don't have enough magic points"): + spells.do_rejuvenate(self.player, parse_result, None) + +class TestHide: + + context = tale.mud_context + context.config = StoryConfig() + + io_util = FakeIoUtil(response=[]) + io_util.stream = False + llm_util = LlmUtil(io_util) + story = DynamicStory() + llm_util.set_story(story) + + def setup_method(self): + tale.mud_context.driver = FakeDriver() + tale.mud_context.driver.story = DynamicStory() + tale.mud_context.driver.llm_util = self.llm_util + self.player = Player('player', 'f') + self.location = Location('test_location') + self.location.insert(self.player, actor=None) + + def test_hide(self): + self.player.stats.magic_skills[MagicType.HIDE] = 100 + self.player.stats.magic_points = 10 + parse_result = ParseResult(verb='hide', args=['self']) + result = spells.do_hide(self.player, parse_result, None) + assert self.player.stats.magic_points == 7 + assert self.player.hidden == True + + def test_hide_fail(self): + self.player.stats.magic_skills[MagicType.HIDE] = -1 + self.player.stats.magic_points = 10 + parse_result = ParseResult(verb='hide', args=['self']) + result = spells.do_hide(self.player, parse_result, None) + assert self.player.stats.magic_points == 7 + + def test_hide_refused(self): + parse_result = ParseResult(verb='hide', args=[]) + with pytest.raises(ActionRefused, match="You don't know how the 'hide' spell."): + spells.do_hide(self.player, parse_result, None) + + self.player.stats.magic_skills[MagicType.HIDE] = 10 + self.player.stats.magic_points = 0 + + with pytest.raises(ActionRefused, match="You don't have enough magic points"): + spells.do_hide(self.player, parse_result, None) + + self.player.stats.magic_points = 10 + + with pytest.raises(ActionRefused, match="You need to specify who or what to target"): + spells.do_hide(self.player, parse_result, None) + +class TestReveal: + + context = tale.mud_context + context.config = StoryConfig() + + io_util = FakeIoUtil(response=[]) + io_util.stream = False + llm_util = LlmUtil(io_util) + story = DynamicStory() + llm_util.set_story(story) + + def setup_method(self): + tale.mud_context.driver = FakeDriver() + tale.mud_context.driver.story = DynamicStory() + tale.mud_context.driver.llm_util = self.llm_util + self.player = Player('player', 'f') + self.location = Location('test_location') + self.location.insert(self.player, actor=None) + + def test_reveal(self): + self.player.stats.magic_skills[MagicType.REVEAL] = 100 + npc = LivingNpc('test', 'f', age=30) + npc.hidden = True + self.player.location.insert(npc, actor=None) + self.player.stats.magic_points = 1000 + parse_result = ParseResult(verb='reveal', args=['100']) + result = spells.do_reveal(self.player, parse_result, None) + assert self.player.stats.magic_points == 700 + assert npc.hidden == False + + def test_reveal_fail(self): + self.player.stats.magic_skills[MagicType.REVEAL] = -1 + npc = LivingNpc('test', 'f', age=30) + npc.hidden = True + self.player.location.insert(npc, actor=None) + self.player.stats.magic_points = 10 + parse_result = ParseResult(verb='reveal', args=[]) + result = spells.do_reveal(self.player, parse_result, None) + assert self.player.stats.magic_points == 7 + assert npc.hidden == True + + def test_reveal_refused(self): + parse_result = ParseResult(verb='reveal', args=[]) + with pytest.raises(ActionRefused, match="You don't know how the 'reveal' spell."): + spells.do_reveal(self.player, parse_result, None) + + self.player.stats.magic_skills[MagicType.REVEAL] = 10 + self.player.stats.magic_points = 0 + + with pytest.raises(ActionRefused, match="You don't have enough magic points"): + spells.do_reveal(self.player, parse_result, None) \ No newline at end of file From a1486a6b3a14527b4c7be3344971c7bc6843ca31 Mon Sep 17 00:00:00 2001 From: rickard Date: Fri, 20 Sep 2024 21:28:14 +0200 Subject: [PATCH 4/5] add pick_lock skill and command --- tale/accounts.py | 8 ++- tale/base.py | 83 +++++++++++++++++++--- tale/cmds/normal.py | 16 +++++ tale/cmds/spells.py | 4 +- tale/proxy_io/proxy_io.py | 125 ---------------------------------- tale/skills/skills.py | 3 +- tests/test_normal_commands.py | 36 +++++++++- tests/test_player.py | 2 + tests/test_spells.py | 4 +- tests/test_stats.py | 2 +- 10 files changed, 137 insertions(+), 146 deletions(-) delete mode 100644 tale/proxy_io/proxy_io.py diff --git a/tale/accounts.py b/tale/accounts.py index 81fd1c6e..422b034e 100644 --- a/tale/accounts.py +++ b/tale/accounts.py @@ -109,11 +109,13 @@ def _create_database(self) -> None: alignment integer NOT NULL, strength integer NOT NULL, dexterity integer NOT NULL, + perception integer NOT NULL, + intelligence integer NOT NULL, weapon_skills varchar NOT NULL, magic_skills varchar NOT NULL, - skils varchar NOT NULL, - combat_points integer NOT NULL, - max_combat_points integer NOT NULL, + skills varchar NOT NULL, + action_points integer NOT NULL, + max_action_points integer NOT NULL, magic_points integer NOT NULL, max_magic_points integer NOT NULL, FOREIGN KEY(account) REFERENCES Account(id) diff --git a/tale/base.py b/tale/base.py index 6ce7e6ab..2850eabf 100644 --- a/tale/base.py +++ b/tale/base.py @@ -976,12 +976,14 @@ def __init__(self) -> None: self.race = "" # the name of the race of this creature self.strength = 3 self.dexterity = 3 + self.perception = 3 + self.intelligence = 3 self.unarmed_attack = Weapon(UnarmedAttack.FISTS.name, weapon_type=WeaponType.UNARMED) self.weapon_skills = {} # type: Dict[WeaponType, int] # weapon type -> skill level self.magic_skills = {} # type: Dict[MagicType, MagicSkill] self.skills = {} # type: Dict[str, int] # skill name -> skill level self.action_points = 0 # combat points - self.max_combat_points = 5 # max combat points + self.max_action_points = 5 # max combat points self.max_magic_points = 5 # max magic points self.magic_points = 0 # magic points @@ -1001,7 +1003,7 @@ def from_race(cls: type, race: builtins.str, gender: builtins.str='n') -> 'Stats def set_stats_from_race(self) -> None: # the stats that are static are always initialized from the races table # we look it up via the name, not needed to store the actual Race object here - r = races.races[self.race] + r = races.races[self.race] # type: Race self.bodytype = r.body self.language = r.language self.weight = r.mass @@ -1028,9 +1030,9 @@ def replenish_combat_points(self, amount: int = None) -> None: if amount: self.action_points += amount else: - self.action_points = self.max_combat_points - if self.action_points > self.max_combat_points: - self.action_points = self.max_combat_points + self.action_points = self.max_action_points + if self.action_points > self.max_action_points: + self.action_points = self.max_action_points def replenish_magic_points(self, amount: int = None) -> None: if amount: @@ -1650,6 +1652,8 @@ def do_on_death(self) -> 'Container': return remains def hide(self, hide: bool = True): + """ Hide or reveal the living entity. """ + if not hide: self.hidden = False self.hidden = False @@ -1673,19 +1677,29 @@ def hide(self, hide: bool = True): self.tell("You hide yourself.") self.location.tell("%s hides" % self.title, exclude_living=self) - def search_hidden(self): + def search_hidden(self, silent: bool = False): + """ Search for hidden entities in the room. + For automatic searches (like when entering a room), + no fail messages will show, and no action points will be used.""" + if self.stats.action_points < 1: raise ActionRefused("You don't have enough action points to search.") livings = self.location.livings - self.location.tell("%s searches for something in the room." % (self.title), exclude_living=self) + if not silent: + self.stats.action_points -= 1 + self.location.tell("%s searches for something in the room." % (self.title), exclude_living=self) if len(self.location.livings) == 1: - self.tell("You don't find anything.") + if not silent: + self.tell("You don't find anything.") return - skillValue = self.stats.skills.get(SkillType.SEARCH, 0) + if silent: + skillValue = self.stats.perception * 5 + else: + skillValue = self.stats.skills.get(SkillType.SEARCH, 0) found = False @@ -1698,9 +1712,31 @@ def search_hidden(self): self.location.tell("%s reveals %s" % (self.title, living.title), exclude_living=self) found = True - if not found: + if not found and not silent: self.tell("You don't find anything.") + def pick_lock(self, door: 'Door'): + """ Pick a lock on an exit. """ + if not door.locked: + raise ActionRefused("The door is not locked.") + + if self.stats.action_points < 1: + raise ActionRefused("You don't have enough action points to pick the lock.") + + self.stats.action_points -= 1 + + skillValue = self.stats.skills.get(SkillType.PICK_LOCK, 0) + + if random.randint(1, 100) > skillValue: + self.tell("You fail to pick the lock.") + return + + door.unlock() + + self.tell("You successfully pick the lock.", evoke=True) + + if not self.hidden: + self.location.tell("%s picks the lock on the door." % self.title, exclude_living=self) class Container(Item): """ @@ -2044,6 +2080,33 @@ def insert(self, item: Union[Living, Item], actor: Optional[Living]) -> None: else: raise ActionRefused("You could try to lock the door with it instead.") raise ActionRefused("The %s doesn't fit." % item.title) + + def pick_lock(self, actor: Living) -> None: + if not self.locked: + raise ActionRefused("The door is not locked.") + + if actor.stats.action_points < 1: + raise ActionRefused("You don't have enough action points to pick the lock.") + + actor.stats.action_points -= 1 + + skillValue = actor.stats.skills.get(SkillType.PICK_LOCK, 0) + + if random.randint(1, 100) > skillValue: + actor.tell("You fail to pick the lock.") + return + + self.locked = False + self.opened = True + + actor.tell("You successfully pick the %s." % (self.name), evoke=True, short_len=True) + if not actor.hidden: + actor.location.tell("%s picks the lock on the door." % actor.title, exclude_living=actor) + actor.tell_others("{Actor} picks the %s, and opens it." % (self.name), evoke=True, short_len=True) + if self.linked_door: + self.linked_door.locked = False + self.linked_door.opened = True + self.target.tell("The %s is unlocked and opened from the other side." % self.linked_door.name, evoke=False, short_len=True) class Key(Item): diff --git a/tale/cmds/normal.py b/tale/cmds/normal.py index 4a54c992..8105bbcc 100644 --- a/tale/cmds/normal.py +++ b/tale/cmds/normal.py @@ -1868,5 +1868,21 @@ def do_search_hidden(player: Player, parsed: base.ParseResult, ctx: util.Context player.search_hidden() +@cmd("pick_lock") +def do_pick_lock(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """Pick a lock on a door.""" + if len(parsed.args) < 1: + raise ParseError("You need to specify the door to pick") + try: + exit = str(parsed.args[0]) + except ValueError as x: + raise ActionRefused(str(x)) + if exit in player.location.exits: + door = player.location.exits[exit] + + if not isinstance(door, base.Door) or not door.locked: + raise ActionRefused("You can't pick that") + + door.pick_lock(player) \ No newline at end of file diff --git a/tale/cmds/spells.py b/tale/cmds/spells.py index 21db1693..6c2d520c 100644 --- a/tale/cmds/spells.py +++ b/tale/cmds/spells.py @@ -112,7 +112,7 @@ def do_rejuvenate(player: Player, parsed: base.ParseResult, ctx: util.Context) - def do_hide(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: """ Hide from view """ - skillValue, spell = _check_spell_skill(player, MagicType.HIDE, "You don't know how the 'hide' spell.") + skillValue, spell = _check_spell_skill(player, MagicType.HIDE, "You don't know the 'hide' spell.") level = _parse_level(player, parsed) if not spell.check_cost(player.stats.magic_points, level): @@ -135,7 +135,7 @@ def do_hide(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None def do_reveal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: """ Reveal hidden things. """ - skillValue, spell = _check_spell_skill(player, MagicType.REVEAL, "You don't know how the 'reveal' spell.") + skillValue, spell = _check_spell_skill(player, MagicType.REVEAL, "You don't know the 'reveal' spell.") level = _parse_level(player, parsed, 0) if not spell.check_cost(player.stats.magic_points, level): diff --git a/tale/proxy_io/proxy_io.py b/tale/proxy_io/proxy_io.py deleted file mode 100644 index 86e67b1b..00000000 --- a/tale/proxy_io/proxy_io.py +++ /dev/null @@ -1,125 +0,0 @@ - -from typing import Any, Sequence, Tuple -from tale.player import PlayerConnection -from tale.tio import iobase, styleaware_wrapper -from . import colorama_patched as colorama - - -colorama.init() -assert type(colorama.Style.DIM) is str, "Incompatible colorama library installed. Please upgrade to a more recent version (0.3.6+)" - -style_words = { - "dim": colorama.Style.DIM, - "normal": colorama.Style.NORMAL, - "bright": colorama.Style.BRIGHT, - "ul": colorama.Style.UNDERLINED, - "it": colorama.Style.ITALIC, - "rev": colorama.Style.REVERSEVID, - "/": colorama.Style.RESET_ALL, - "location": colorama.Style.BRIGHT, - "clear": "\033[1;1H\033[2J", # ansi sequence to clear the console screen - "monospaced": "", # we assume the console is already monospaced font - "/monospaced": "" -} -assert len(set(style_words.keys()) ^ iobase.ALL_STYLE_TAGS) == 0, "mismatch in list of style tags" - -class ProxyIo(iobase.IoAdapterBase): - - def __init__(self, player_connection: PlayerConnection) -> None: - super().__init__(player_connection) - - self.stop_main_loop = False - self.waiting_input = '' - - - def singleplayer_mainloop(self, player_connection: PlayerConnection) -> None: - """Main event loop for the console I/O adapter for single player mode""" - while not self.stop_main_loop: - try: - # note that we don't print any prompt ">>", that needs to be done - # by the main thread that handles screen *output* - # (otherwise the prompt will often appear before any regular screen output) - old_player = player_connection.player - # do blocking console input call - cmd = self.waiting_input - if cmd: - self.waiting_input = '' - player_connection.player.store_input_line(cmd) - if old_player is not player_connection.player: - # this situation occurs when a save game has been restored, - # we also have to unblock the old_player - old_player.store_input_line(cmd) - except KeyboardInterrupt: - self.break_pressed() - except EOFError: - pass - - def render_output(self, paragraphs: Sequence[Tuple[str, bool]], **params: Any) -> str: - """ - Render (format) the given paragraphs to a text representation. - It doesn't output anything to the screen yet; it just returns the text string. - Any style-tags are still embedded in the text. - This console-implementation expects 2 extra parameters: "indent" and "width". - """ - if not paragraphs: - return "" - indent = " " * params["indent"] - wrapper = styleaware_wrapper.StyleTagsAwareTextWrapper(width=params["width"], fix_sentence_endings=True, - initial_indent=indent, subsequent_indent=indent) - output = [] - for txt, formatted in paragraphs: - if formatted: - txt = wrapper.fill(txt) + "\n" - else: - # unformatted output, prepend every line with the indent but otherwise leave them alone - txt = indent + ("\n" + indent).join(txt.splitlines()) + "\n" - assert txt.endswith("\n") - output.append(txt) - return self.smartquotes("".join(output)) - - - def output(self, *lines: str) -> None: - super().output(*lines) - for line in lines: - print(self._apply_style(line, self.do_styles)) - - def output_no_newline(self, text: str, new_paragraph = True) -> None: - super().output_no_newline(text, new_paragraph) - print(self._apply_style(text, self.do_styles), end="") - - def write_input_prompt(self) -> None: - """write the input prompt '>>'""" - pass - - def break_pressed(self) -> None: - pass - - def _apply_style(self, line: str, do_styles: bool) -> str: - """Convert style tags to ansi escape sequences suitable for console text output""" - if "<" not in line: - return line - elif style_words and do_styles: - for tag, replacement in style_words.items(): - line = line.replace("<%s>" % tag, replacement) - return line - else: - return iobase.strip_text_styles(line) # type: ignore - - " Discord Client Events " - - class DiscordBot(discord.Client): - - def __init__(self, intents, proxyIo: 'ProxyIo'): - super().__init__(intents=intents) - self.proxyIo = proxyIo - - async def on_ready(self): - print(f'{self.user} has connected to Discord!') - - - async def on_message(self, message: discord.Message): - if message.channel.type == discord.ChannelType.private: - self.proxyIo.waiting_input = message.content - else: - message.author.send('Please send messages in a private channel.') - diff --git a/tale/skills/skills.py b/tale/skills/skills.py index 06406843..b8240ff2 100644 --- a/tale/skills/skills.py +++ b/tale/skills/skills.py @@ -6,4 +6,5 @@ class SkillType(Enum): HIDE = 1 - SEARCH = 2 \ No newline at end of file + SEARCH = 2 + PICK_LOCK = 3 \ No newline at end of file diff --git a/tests/test_normal_commands.py b/tests/test_normal_commands.py index 63ece16a..1126c2fa 100644 --- a/tests/test_normal_commands.py +++ b/tests/test_normal_commands.py @@ -3,7 +3,7 @@ import pytest import tale from tale import wearable -from tale.base import Item, Location, ParseResult, Weapon, Wearable +from tale.base import Door, Item, Location, ParseResult, Weapon, Wearable from tale.cmds import normal from tale.errors import ActionRefused, ParseError from tale.llm.LivingNpc import LivingNpc @@ -193,4 +193,36 @@ def test_search_hidden(self): normal.do_search_hidden(self.test_player, ParseResult(verb='search', args=[]), self.context) - assert not test_npc.hidden \ No newline at end of file + assert not test_npc.hidden + + def test_pick_lock(self): + self.test_player.stats.skills[SkillType.PICK_LOCK] = 100 + self.test_player.stats.action_points = 1 + + hall = Location("hall") + door = Door("north", hall, "a locked door", locked=True, opened=False) + hall.add_exits([door]) + hall.insert(self.test_player, actor=None) + + parse_result = ParseResult(verb='pick_lock', args=['north']) + normal.do_pick_lock(self.test_player, parse_result, self.context) + assert not door.locked + + # Test failure + + door.locked = True + + self.test_player.stats.skills[SkillType.PICK_LOCK] = 0 + self.test_player.stats.action_points = 1 + + normal.do_pick_lock(self.test_player, parse_result, self.context) + + assert door.locked + + # Test no action points + + self.test_player.stats.skills[SkillType.PICK_LOCK] = 100 + self.test_player.stats.action_points = 0 + + with pytest.raises(ActionRefused, match="You don't have enough action points to pick the lock."): + normal.do_pick_lock(self.test_player, parse_result, self.context) \ No newline at end of file diff --git a/tests/test_player.py b/tests/test_player.py index f8a2b228..0638edf6 100644 --- a/tests/test_player.py +++ b/tests/test_player.py @@ -811,6 +811,8 @@ def test_dbcreate(self): self.assertEqual(races.BodySize.HUMAN_SIZED, account.stats.size) self.assertEqual("Edhellen", account.stats.language) self.assertEqual({}, account.stats.weapon_skills) + self.assertEqual({}, account.stats.magic_skills) + self.assertEqual({}, account.stats.skills) finally: dbfile.unlink() diff --git a/tests/test_spells.py b/tests/test_spells.py index be159c21..e1fdb9b4 100644 --- a/tests/test_spells.py +++ b/tests/test_spells.py @@ -226,7 +226,7 @@ def test_hide_fail(self): def test_hide_refused(self): parse_result = ParseResult(verb='hide', args=[]) - with pytest.raises(ActionRefused, match="You don't know how the 'hide' spell."): + with pytest.raises(ActionRefused, match="You don't know the 'hide' spell."): spells.do_hide(self.player, parse_result, None) self.player.stats.magic_skills[MagicType.HIDE] = 10 @@ -283,7 +283,7 @@ def test_reveal_fail(self): def test_reveal_refused(self): parse_result = ParseResult(verb='reveal', args=[]) - with pytest.raises(ActionRefused, match="You don't know how the 'reveal' spell."): + with pytest.raises(ActionRefused, match="You don't know the 'reveal' spell."): spells.do_reveal(self.player, parse_result, None) self.player.stats.magic_skills[MagicType.REVEAL] = 10 diff --git a/tests/test_stats.py b/tests/test_stats.py index 0b3943cf..0debf1eb 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -20,7 +20,7 @@ def test_replenish_hp(self): def test_replenish_combat_points(self): stats = Stats() - stats.max_combat_points = 100 + stats.max_action_points = 100 stats.action_points = 0 stats.replenish_combat_points(10) From 67383466a73f0b044b43cfbb4ecd5d060fe37762 Mon Sep 17 00:00:00 2001 From: rickard Date: Sat, 21 Sep 2024 10:12:29 +0200 Subject: [PATCH 5/5] add npc commands --- llm_config.yaml | 2 +- stories/anything/story.py | 10 +++++++++ stories/dungeon/story.py | 10 +++++++++ stories/prancingllama/story.py | 4 ++++ tale/base.py | 6 +++--- tale/cmds/__init__.py | 1 + tale/driver.py | 11 +++++----- tale/llm/LivingNpc.py | 38 ++++++++++++++++++++-------------- tests/test_llm_ext.py | 35 +++++++++++++++++++++++++++++++ 9 files changed, 92 insertions(+), 25 deletions(-) diff --git a/llm_config.yaml b/llm_config.yaml index 39a8486b..8c4fec01 100644 --- a/llm_config.yaml +++ b/llm_config.yaml @@ -4,7 +4,7 @@ BACKEND: "kobold_cpp" # valid options: "openai", "llama_cpp", "kobold_cpp". if u MEMORY_SIZE: 512 UNLIMITED_REACTS: False DIALOGUE_TEMPLATE: '{{"response":"may be both dialogue and action.", "sentiment":"sentiment based on response", "give":"if any physical item of {character2}s is given as part of the dialogue. Or nothing."}}' -ACTION_LIST: ['move, say, attack, wear, remove, wield, take, eat, drink, emote, search'] +ACTION_LIST: ['move, say, attack, wear, remove, wield, take, eat, drink, emote, search, hide, unhide, pick_lock'] ACTION_TEMPLATE: '{{"goal": reason for action, "thoughts":thoughts about performing action, 25 words "action":chosen action, "target":character, item or exit or description, "text": if anything is said during the action}}' ITEM_TEMPLATE: '{{"name":"", "type":"", "short_descr":"", "level":int, "value":int}}' CREATURE_TEMPLATE: '{{"name":"", "body":"", "mass":int(kg), "hp":int, "type":"Npc or Mob", "level":int, "aggressive":bool, "unarmed_attack":One of [FISTS, CLAWS, BITE, TAIL, HOOVES, HORN, TUSKS, BEAK, TALON], "short_descr":""}}' diff --git a/stories/anything/story.py b/stories/anything/story.py index eba7ed76..944790d8 100644 --- a/stories/anything/story.py +++ b/stories/anything/story.py @@ -12,6 +12,7 @@ from tale.main import run_from_cmdline from tale.player import Player, PlayerConnection from tale.charbuilder import PlayerNaming +from tale.skills.skills import SkillType from tale.story import * from tale.skills.weapon_type import WeaponType @@ -55,6 +56,15 @@ def create_account_dialog(self, playerconnection: PlayerConnection, playernaming else: playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED, value=random.randint(20, 40)) playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED, value=random.randint(20, 40)) + stealth = yield "input", ("Are you sneaky? (yes/no)", lang.yesno) + if stealth: + playerconnection.player.stats.skills[SkillType.HIDE] = random.randint(30, 50) + playerconnection.player.stats.skills[SkillType.PICK_LOCK] = random.randint(30, 50) + else: + playerconnection.player.stats.skills[SkillType.HIDE] = random.randint(10, 30) + playerconnection.player.stats.skills[SkillType.PICK_LOCK] = random.randint(10, 30) + playerconnection.player.stats.skills[SkillType.SEARCH] = random.randint(20, 40) + return True def welcome(self, player: Player) -> str: diff --git a/stories/dungeon/story.py b/stories/dungeon/story.py index ddcba8c0..b09c0f11 100644 --- a/stories/dungeon/story.py +++ b/stories/dungeon/story.py @@ -15,6 +15,7 @@ from tale.main import run_from_cmdline from tale.npc_defs import RoamingMob from tale.player import Player, PlayerConnection +from tale.skills.skills import SkillType from tale.story import * from tale.skills.weapon_type import WeaponType from tale.zone import Zone @@ -66,6 +67,15 @@ def create_account_dialog(self, playerconnection: PlayerConnection, playernaming else: playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED, value=random.randint(20, 40)) playerconnection.player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED, value=random.randint(20, 40)) + stealth = yield "input", ("Are you sneaky? (yes/no)", lang.yesno) + if stealth: + playerconnection.player.stats.skills[SkillType.HIDE] = random.randint(30, 50) + playerconnection.player.stats.skills[SkillType.PICK_LOCK] = random.randint(30, 50) + else: + playerconnection.player.stats.skills[SkillType.HIDE] = random.randint(10, 30) + playerconnection.player.stats.skills[SkillType.PICK_LOCK] = random.randint(10, 30) + playerconnection.player.stats.skills[SkillType.SEARCH] = random.randint(20, 40) + return True def welcome(self, player: Player) -> str: diff --git a/stories/prancingllama/story.py b/stories/prancingllama/story.py index 6f5aa9c5..84dc0947 100644 --- a/stories/prancingllama/story.py +++ b/stories/prancingllama/story.py @@ -11,6 +11,7 @@ from tale.main import run_from_cmdline from tale.player import Player, PlayerConnection from tale.charbuilder import PlayerNaming +from tale.skills.skills import SkillType from tale.story import * from tale.skills.weapon_type import WeaponType from tale.zone import Zone @@ -58,6 +59,9 @@ def init_player(self, player: Player) -> None: player.stats.set_weapon_skill(weapon_type=WeaponType.TWO_HANDED, value=15) player.stats.set_weapon_skill(weapon_type=WeaponType.UNARMED, value=35) player.stats.magic_skills[MagicType.HEAL] = 50 + player.stats.skills[SkillType.HIDE] = 25 + player.stats.skills[SkillType.SEARCH] = 25 + player.stats.skills[SkillType.PICK_LOCK] = 25 def create_account_dialog(self, playerconnection: PlayerConnection, playernaming: PlayerNaming) -> Generator: """ diff --git a/tale/base.py b/tale/base.py index 2850eabf..571c0036 100644 --- a/tale/base.py +++ b/tale/base.py @@ -309,7 +309,7 @@ def init(self) -> None: @property def title(self) -> str: - return self._title + return (self._title + '[hidden]') if self.hidden else self._title @title.setter def title(self, value: str) -> None: @@ -1384,7 +1384,7 @@ def display_direction(directions: Sequence[str]) -> str: if direction in {"left", "right"}: return "to the " + direction return "" - + silent = silent or (self.hidden or not self.visible) actor = actor or self original_location = None if self.location: @@ -1689,7 +1689,7 @@ def search_hidden(self, silent: bool = False): if not silent: self.stats.action_points -= 1 - self.location.tell("%s searches for something in the room." % (self.title), exclude_living=self) + self.location.tell("%s searches for something." % (self.title), exclude_living=self) if len(self.location.livings) == 1: if not silent: diff --git a/tale/cmds/__init__.py b/tale/cmds/__init__.py index e7d476d0..34108823 100644 --- a/tale/cmds/__init__.py +++ b/tale/cmds/__init__.py @@ -54,6 +54,7 @@ def all_registered_commands() -> Iterable[Tuple[str, Callable, str]]: """ from . import wizard from . import normal + from . import spells for command, func in _all_wizard_commands.items(): yield command, func, "wizard" for command, func in _all_commands.items(): diff --git a/tale/driver.py b/tale/driver.py index 595a7883..fae3c301 100644 --- a/tale/driver.py +++ b/tale/driver.py @@ -867,6 +867,7 @@ def load_character(self, player: player.Player, char_data: dict) -> LivingNpc: if character.follower: npc.following = player npc.stats.hp = character.hp + npc.stats.action_points = 999 if isinstance(self.story, DynamicStory): dynamic_story = typing.cast(DynamicStory, self.story) dynamic_story.world.add_npc(npc) @@ -963,9 +964,9 @@ def do_on_player_death(self, player: player.Player) -> None: @util.call_periodically(20) def replenish(self): - for player in self.all_players.values(): - if player.hidden: + for player_connection in self.all_players.values(): + if player_connection.player.hidden: continue - player.player.stats.replenish_hp(1) - player.player.stats.replenish_combat_points(1) - player.player.stats.replenish_magic_points(1) \ No newline at end of file + player_connection.player.stats.replenish_hp(1) + player_connection.player.stats.replenish_combat_points(1) + player_connection.player.stats.replenish_magic_points(1) \ No newline at end of file diff --git a/tale/llm/LivingNpc.py b/tale/llm/LivingNpc.py index 983832ab..33ed431c 100644 --- a/tale/llm/LivingNpc.py +++ b/tale/llm/LivingNpc.py @@ -114,18 +114,15 @@ def do_say(self, what_happened: str, actor: Living) -> None: item = None sentiment = None for i in range(3): - if self.autonomous: - response = self.autonomous_action() - else: - response, item, sentiment = mud_context.driver.llm_util.generate_dialogue( - conversation=llm_cache.get_events(self._observed_events), - character_card = self.character_card, - character_name = self.title, - target = actor.title, - target_description = actor.short_description, - sentiment = self.sentiments.get(actor.title, ''), - location_description=self.location.look(exclude_living=self), - short_len=short_len) + response, item, sentiment = mud_context.driver.llm_util.generate_dialogue( + conversation=llm_cache.get_events(self._observed_events), + character_card = self.character_card, + character_name = self.title, + target = actor.title, + target_description = actor.short_description, + sentiment = self.sentiments.get(actor.title, ''), + location_description=self.location.look(exclude_living=self), + short_len=short_len) if response: if not self.avatar: result = mud_context.driver.llm_util.generate_image(self.name, self.description) @@ -275,8 +272,8 @@ def _parse_action(self, action: ActionResponse): elif action.action == 'give' and action.item and action.target: result = ItemHandlingResult(item=action.item, to=action.target, from_=self.title) self.handle_item_result(result, actor=self) - elif action.action == 'take' and action.item: - item = self.search_item(action.item, include_location=True, include_inventory=False) # Type: Item + elif action.action == 'take' and (action.item or action.target): + item = self.search_item(action.item or action.target, include_location=True, include_inventory=False) # Type: Item if item: item.move(target=self, actor=self) defered_actions.append(f"{self.title} takes {item.title}") @@ -285,11 +282,20 @@ def _parse_action(self, action: ActionResponse): if target: self.start_attack(target) defered_actions.append(f"{self.title} attacks {target.title}") - elif action.action == 'wear' and action.item: - item = self.search_item(action.item, include_location=True, include_inventory=False) + elif action.action == 'wear' and (action.item or action.target): + item = self.search_item(action.item or action.target, include_location=True, include_inventory=False) if item: self.set_wearable(item) defered_actions.append(f"{self.title} wears {item.title}") + elif action.action == 'hide': + self.hide() + #defered_actions.append(f"{self.title} hides.") + elif action.action == 'unhide': + self.hide(False) + #defered_actions.append(f"{self.title} reveals themselves.") + elif action.action == 'search': + self.search_hidden() + #defered_actions.append(f"{self.title} searches for something.") return defered_actions def _defer_result(self, action: str, verb: str="idle-action"): diff --git a/tests/test_llm_ext.py b/tests/test_llm_ext.py index ea896fc5..25952ed0 100644 --- a/tests/test_llm_ext.py +++ b/tests/test_llm_ext.py @@ -14,6 +14,7 @@ from tale.llm.llm_io import IoUtil from tale.llm.llm_utils import LlmUtil from tale.player import Player +from tale.skills.skills import SkillType from tale.wearable import WearLocation from tale.zone import Zone from tests.supportstuff import FakeDriver, MsgTraceNPC @@ -286,7 +287,41 @@ def test_say(self): json={'results':[{'text':'{"response":"Fine."}'}]}, status=200) actions = self.npc.autonomous_action() assert(actions == '') # TODO: receives no message due to change of how targeted actions are handled + + @responses.activate + def test_hide(self): + self.npc.stats.skills[SkillType.HIDE] = 100 + self.npc.stats.action_points = 1 + self.npc.location.remove(self.npc2, self.npc2) + self.npc.location.remove(self.msg_trace_npc, self.msg_trace_npc) + responses.add(responses.POST, self.dummy_backend_config['URL'] + self.dummy_backend_config['ENDPOINT'], + json={'results':[{'text':'{"action":"hide", "text":"this looks dangerous!"}'}]}, status=200) + actions = self.npc.autonomous_action() + assert(actions == '"this looks dangerous!"') + assert(self.npc.hidden) + + @responses.activate + def test_unhide(self): + self.npc.hidden = True + self.npc.stats.action_points = 1 + responses.add(responses.POST, self.dummy_backend_config['URL'] + self.dummy_backend_config['ENDPOINT'], + json={'results':[{'text':'{"action":"unhide"}'}]}, status=200) + actions = self.npc.autonomous_action() + assert ['test reveals themselves'] == self.msg_trace_npc.messages + assert not self.npc.hidden + @responses.activate + def test_search(self): + self.npc.stats.skills[SkillType.SEARCH] = 100 + self.npc.stats.action_points = 1 + self.npc2.hidden = True + self.npc2.stats.skills[SkillType.HIDE] = 0 + responses.add(responses.POST, self.dummy_backend_config['URL'] + self.dummy_backend_config['ENDPOINT'], + json={'results':[{'text':'{"action":"search"}'}]}, status=200) + actions = self.npc.autonomous_action() + assert(actions == '') + assert not self.npc2.hidden + assert ["test searches for something.", "test reveals actor"] == self.msg_trace_npc.messages class TestDynamicStory():