From dcd4e0a2de003751e346ae136f7178728a95db82 Mon Sep 17 00:00:00 2001 From: rickard Date: Mon, 29 Jul 2024 21:47:27 +0200 Subject: [PATCH 1/3] spells basics --- tale/base.py | 12 +++++++ tale/cmds/spells.py | 80 ++++++++++++++++++++++++++++++++++++++++++++ tale/items/basic.py | 2 -- tale/magic.py | 27 +++++++++++++++ tale/npc_defs.py | 5 +-- tale/parse_utils.py | 14 ++++++-- tests/test_spells.py | 72 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 tale/cmds/spells.py create mode 100644 tale/magic.py create mode 100644 tests/test_spells.py diff --git a/tale/base.py b/tale/base.py index ca2ac02c..ff508fe8 100644 --- a/tale/base.py +++ b/tale/base.py @@ -50,6 +50,7 @@ from tale.coord import Coord from tale.llm.contexts.CombatContext import CombatContext +from tale.magic import MagicSkill, MagicType from . import lang from . import mud_context @@ -975,8 +976,11 @@ def __init__(self) -> None: self.dexterity = 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.combat_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 def __repr__(self): return "" % self.__dict__ @@ -1025,6 +1029,14 @@ def replenish_combat_points(self, amount: int = None) -> None: if self.combat_points > self.max_combat_points: self.combat_points = self.max_combat_points + def replenish_magic_points(self, amount: int = None) -> None: + if amount: + self.magic_points += amount + else: + self.magic_points = self.max_magic_points + if self.magic_points > self.max_magic_points: + self.magic_points = self.max_magic_points + class Living(MudObject): """ A living entity in the mud world (also known as an NPC). diff --git a/tale/cmds/spells.py b/tale/cmds/spells.py new file mode 100644 index 00000000..d95578e6 --- /dev/null +++ b/tale/cmds/spells.py @@ -0,0 +1,80 @@ + +import random +from tale import base, util, cmds +from tale.cmds import cmd +from tale.errors import ActionRefused, ParseError +from tale.magic import MagicType, MagicSkill +from tale.player import Player + + +@cmd("heal") +def do_heal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """ Heal someone or something """ + + skill = player.stats.magic_skills.get(MagicType.HEAL) # type: MagicSkill + if not skill: + raise ActionRefused("You don't know how to heal") + + num_args = len(parsed.args) + 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: base.Living + if not result or not isinstance(result, base.Living): + raise ActionRefused("Can't heal that") + + level = player.stats.level + if num_args == 2: + level = int(parsed.args[1]) + + if player.stats.magic_points < skill.spell.base_cost * level: + raise ActionRefused("You don't have enough magic points") + + player.stats.magic_points -= skill.spell.base_cost * level + + if random.randint(1, 100) > skill.skill: + player.tell("Your healing spell fizzles out", evoke=True) + return + + result.stats.replenish_hp(5 * level) + + +@cmd("bolt") +def do_bolt(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """ Cast a bolt of energy """ + + skill = player.stats.magic_skills.get(MagicType.BOLT) # type: MagicSkill + if not skill: + raise ActionRefused("You don't know how to cast a bolt") + + num_args = len(parsed.args) + 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: base.Living + if not result or not isinstance(result, base.Living): + raise ActionRefused("Can't attack that") + + level = player.stats.level + if num_args == 2: + level = int(parsed.args[1]) + + if player.stats.magic_points < skill.spell.base_cost * level: + raise ActionRefused("You don't have enough magic points") + + player.stats.magic_points -= skill.spell.base_cost * level + + if random.randint(1, 100) > skill.skill: + player.tell("Your healing spell fizzles out", evoke=True) + return + + result.stats.hp -= random.randrange(1, level) diff --git a/tale/items/basic.py b/tale/items/basic.py index 1f9ce611..5dd34f7b 100644 --- a/tale/items/basic.py +++ b/tale/items/basic.py @@ -220,7 +220,6 @@ def init(self) -> None: def consume(self, actor: 'Living'): if self not in actor.inventory: raise ActionRefused("You don't have that.") - actor.stats.hp += self.affect_thirst actor.tell("You drink the %s." % (self.title), evoke=True, short_len=True) actor.tell_others("{Actor} drinks the %s." % (self.title)) self.destroy(util.Context.from_global()) @@ -243,7 +242,6 @@ def init(self) -> None: def consume(self, actor: 'Living'): if self not in actor.inventory: raise ActionRefused("You don't have that.") - actor.stats.hp += self.affect_fullness actor.tell("You eat the %s." % (self.title), evoke=True, short_len=True) actor.tell_others("{Actor} eats the %s." % (self.title)) self.destroy(util.Context.from_global()) diff --git a/tale/magic.py b/tale/magic.py new file mode 100644 index 00000000..62d2a02b --- /dev/null +++ b/tale/magic.py @@ -0,0 +1,27 @@ +from abc import ABC +from enum import Enum + + +class MagicType(Enum): + HEAL = 1 + + + +class Spell(ABC): + def __init__(self, name: str, base_cost: int, base_value: int = 1, max_level: int = -1): + self.name = name + self.base_value = base_value + self.base_cost = base_cost + self.max_level = max_level + + def do_cast(self, player, target, level): + pass + +spells = { + MagicType.HEAL: Spell('heal', base_cost=2, base_value=5) +} + +class MagicSkill: + def __init__(self, spell: Spell, skill: int = 0): + self.spell = spell + self.skill = skill diff --git a/tale/npc_defs.py b/tale/npc_defs.py index ac69fa5a..a6fc9303 100644 --- a/tale/npc_defs.py +++ b/tale/npc_defs.py @@ -40,12 +40,9 @@ def do_idle_action(self, ctx: Context) -> None: if player_in_location and self.aggressive and not self.attacking: for liv in self.location.livings: if isinstance(liv, Player): - self.do_attack(liv) + self.start_attack(defender=liv) elif player_in_location or self.location.get_wiretap() or self.get_wiretap(): self.idle_action() - - def do_attack(self, target: Living) -> None: - self.start_attack(defender=target) class RoamingMob(StationaryMob): diff --git a/tale/parse_utils.py b/tale/parse_utils.py index 02b7e4ad..1b448e4a 100644 --- a/tale/parse_utils.py +++ b/tale/parse_utils.py @@ -7,6 +7,7 @@ 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.npc_defs import StationaryMob, StationaryNpc from tale.races import BodyType, UnarmedAttack from tale.mob_spawner import MobSpawner @@ -160,7 +161,7 @@ def load_npcs(json_npcs: list, locations = {}) -> dict: npcs[name] = new_npc return npcs -def load_npc(npc: dict, name: str = None, npc_type: str = 'Mob'): +def load_npc(npc: dict, name: str = None, npc_type: str = 'Mob', roaming = False): race = None if npc.get('stats', None): race = npc['stats'].get('race', None) @@ -641,7 +642,8 @@ def save_stats(stats: Stats) -> dict: json_stats['hp'] = stats.hp json_stats['max_hp'] = stats.max_hp json_stats['level'] = stats.level - json_stats['weapon_skills'] = save_weaponskills(stats.weapon_skills) + json_stats['weapon_skills'] = skills_dict_to_json(stats.weapon_skills) + json_stats['magic_skills'] = skills_dict_to_json(stats.magic_skills) json_stats['gender'] = stats.gender = 'n' json_stats['alignment'] = stats.alignment json_stats['weight'] = stats.weight @@ -679,6 +681,12 @@ def load_stats(json_stats: dict) -> Stats: 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'): + 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] return stats def save_items(items: List[Item]) -> dict: @@ -715,7 +723,7 @@ def save_locations(locations: List[Location]) -> dict: json_locations.append(json_location) return json_locations -def save_weaponskills(weaponskills: dict) -> dict: +def skills_dict_to_json(weaponskills: dict) -> dict: json_skills = {} for skill in weaponskills.keys(): json_skills[skill.value] = weaponskills[skill] diff --git a/tests/test_spells.py b/tests/test_spells.py new file mode 100644 index 00000000..1b3661aa --- /dev/null +++ b/tests/test_spells.py @@ -0,0 +1,72 @@ + + +import pytest +from tale import magic +import tale +from tale.base import Location, ParseResult +from tale.cmds import spells +from tale.errors import ActionRefused +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.player import Player +from tale.story import StoryConfig +from tests.supportstuff import FakeDriver, FakeIoUtil + + +class TestSpells: + + 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_heal(self): + self.player.stats.magic_skills[MagicType.HEAL] = MagicSkill(magic.spells[MagicType.HEAL], 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='heal', args=['test']) + result = spells.do_heal(self.player, parse_result, None) + assert self.player.stats.magic_points == 8 + assert npc.stats.hp == 5 + + def test_heal_fail(self): + self.player.stats.magic_skills[MagicType.HEAL] = MagicSkill(magic.spells[MagicType.HEAL], 0) + 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_heal(self.player, parse_result, None) + assert self.player.stats.magic_points == 8 + assert npc.stats.hp == 0 + + def test_heal_refused(self): + parse_result = ParseResult(verb='heal', args=['test']) + with pytest.raises(ActionRefused, match="You don't know how to heal"): + spells.do_heal(self.player, parse_result, None) + + self.player.stats.magic_skills[MagicType.HEAL] = MagicSkill(MagicType.HEAL, 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_heal(self.player, parse_result, None) From d7a868e84f1c2c1322670e5e76c210c1bbd4e52b Mon Sep 17 00:00:00 2001 From: rickard Date: Thu, 1 Aug 2024 18:24:47 +0200 Subject: [PATCH 2/3] more spells --- tale/cmds/spells.py | 69 +++++++++++++++++++++++++++++++++++++++------ tale/magic.py | 13 ++++++++- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/tale/cmds/spells.py b/tale/cmds/spells.py index d95578e6..9ca5688b 100644 --- a/tale/cmds/spells.py +++ b/tale/cmds/spells.py @@ -1,5 +1,6 @@ import random +from typing import Optional from tale import base, util, cmds from tale.cmds import cmd from tale.errors import ActionRefused, ParseError @@ -14,6 +15,9 @@ def do_heal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None skill = player.stats.magic_skills.get(MagicType.HEAL) # type: MagicSkill if not skill: raise ActionRefused("You don't know how to heal") + + if not skill.spell.check_cost(player, level): + raise ActionRefused("You don't have enough magic points") num_args = len(parsed.args) if num_args < 1: @@ -23,16 +27,13 @@ def do_heal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None except ValueError as x: raise ActionRefused(str(x)) - result = player.location.search_living(entity) # type: base.Living + 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") level = player.stats.level if num_args == 2: level = int(parsed.args[1]) - - if player.stats.magic_points < skill.spell.base_cost * level: - raise ActionRefused("You don't have enough magic points") player.stats.magic_points -= skill.spell.base_cost * level @@ -41,6 +42,8 @@ def do_heal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None return result.stats.replenish_hp(5 * level) + player.tell("You heal %s for %d hit points" % (result.name, 5 * level), evoke=True) + player.tell_others("%s heals %s" % (player.name, result.name), evoke=True) @cmd("bolt") @@ -51,6 +54,9 @@ def do_bolt(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None if not skill: raise ActionRefused("You don't know how to cast a bolt") + if not skill.spell.check_cost(player, level): + raise ActionRefused("You don't have enough magic points") + num_args = len(parsed.args) if num_args < 1: raise ParseError("You need to specify who or what to attack") @@ -60,7 +66,7 @@ def do_bolt(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None except ValueError as x: raise ActionRefused(str(x)) - result = player.location.search_living(entity) # type: base.Living + 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") @@ -68,13 +74,60 @@ def do_bolt(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None if num_args == 2: level = int(parsed.args[1]) - if player.stats.magic_points < skill.spell.base_cost * level: + + player.stats.magic_points -= skill.spell.base_cost * level + + if random.randint(1, 100) > skill.skill: + player.tell("Your bolt spell fizzles out", evoke=True) + return + + hp = random.randrange(1, level) + result.stats.hp -= hp + player.tell("Your energy bolt hits %s for %d damage" % (result.name, hp), evoke=True) + player.tell_others("%s's energy bolt hits %s for %d damage" % (player.name, result.name, hp), evoke=True) + +@cmd("drain") +def do_drain(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """ Drain energy from someone or something """ + + skill = player.stats.magic_skills.get(MagicType.DRAIN) + if not skill: + raise ActionRefused("You don't know how to drain") + + if not skill.spell.check_cost(player, level): raise ActionRefused("You don't have enough magic points") + num_args = len(parsed.args) + 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") + + level = player.stats.level + if num_args == 2: + level = int(parsed.args[1]) + + player.stats.magic_points -= skill.spell.base_cost * level if random.randint(1, 100) > skill.skill: - player.tell("Your healing spell fizzles out", evoke=True) + player.tell("Your drain spell fizzles out", evoke=True) return + + points = random.randrange(1, level) + result.stats.combat_points -= points + result.stats.magic_points -= points + + player.stats.magic_points += points + + player.tell("Your drain spell drains %s of %d combat and magic points" % (result.name, points), evoke=True) + player.tell_others("%s casts a drain spell that drains energy from %s" % (player.name, result.name), evoke=True) + - result.stats.hp -= random.randrange(1, level) diff --git a/tale/magic.py b/tale/magic.py index 62d2a02b..a572e14d 100644 --- a/tale/magic.py +++ b/tale/magic.py @@ -1,9 +1,14 @@ from abc import ABC from enum import Enum +from tale.player import Player + class MagicType(Enum): HEAL = 1 + BOLT = 2 + DRAIN = 3 + REJUVENATE = 4 @@ -17,8 +22,14 @@ def __init__(self, name: str, base_cost: int, base_value: int = 1, max_level: in def do_cast(self, player, target, level): pass + def check_cost(self, player: Player, level: int) -> bool: + return player.stats.magic_points >= self.base_cost * level + spells = { - MagicType.HEAL: Spell('heal', base_cost=2, base_value=5) + 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.REJUVENATE: Spell('rejuvenate', base_cost=3, base_value=5) } class MagicSkill: From 58445de4cd383da7aee0ed7ca08151c63088d427 Mon Sep 17 00:00:00 2001 From: rickard Date: Thu, 1 Aug 2024 21:34:33 +0200 Subject: [PATCH 3/3] adding spells to stories and tests --- llm_config.yaml | 2 +- stories/anything/story.py | 5 ++ stories/dungeon/story.py | 5 ++ stories/prancingllama/story.py | 3 + stories/prancingllama/zones/prancingllama.py | 4 +- tale/accounts.py | 3 + tale/cmds/spells.py | 94 +++++++++++--------- tale/driver.py | 5 +- tale/magic.py | 12 +-- tests/test_spells.py | 70 ++++++++++++++- 10 files changed, 141 insertions(+), 62 deletions(-) diff --git a/llm_config.yaml b/llm_config.yaml index 965a7a21..2bff0f1d 100644 --- a/llm_config.yaml +++ b/llm_config.yaml @@ -17,7 +17,7 @@ CHARACTER_TEMPLATE: '{"name":"", "description": "50 words", "appearance": "25 wo FOLLOW_TEMPLATE: '{{"response":"yes or no", "reason":"50 words"}}' ITEM_TYPES: ["Weapon", "Wearable", "Health", "Money", "Trash", "Food", "Drink", "Key"] PRE_PROMPT: 'You are a creative game keeper for a role playing game (RPG). You craft detailed worlds and interesting characters with unique and deep personalities for the player to interact with. Do not acknowledge the task or speak directly to the user, just perform it.' -BASE_PROMPT: '{context}\n[USER_START]Rewrite [{input_text}] in your own words using the information found inside the tags to create a background for your text. Use about {max_words} words.' +BASE_PROMPT: '{context}\n[USER_START] Rewrite [{input_text}] in your own words. The information inside the tags should be used to ensure it fits the story. Use about {max_words} words.' DIALOGUE_PROMPT: '{context}\nThe following is a conversation between {character1} and {character2}; {character2}s sentiment towards {character1}: {sentiment}. Write a single response as {character2} in third person pov, using {character2} description and other information found inside the tags. If {character2} has a quest active, they will discuss it based on its status. Respond in JSON using this template: """{dialogue_template}""". [USER_START]Continue the following conversation as {character2}: {previous_conversation}' COMBAT_PROMPT: '{context}\nThe following is a combat scene between {attackers} and {defenders} in {location}. [USER_START] Describe the following combat result in about 150 words in vivid language, using the characters weapons and their health status: 1.0 is highest, 0.0 is lowest. Combat Result: {input_text}' PRE_JSON_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. Write a response in valid JSON format that appropriately completes the request.' diff --git a/stories/anything/story.py b/stories/anything/story.py index 50075060..9eaaedcd 100644 --- a/stories/anything/story.py +++ b/stories/anything/story.py @@ -8,6 +8,7 @@ from tale import parse_utils from tale.driver import Driver from tale.json_story import JsonStory +from tale.magic import MagicType from tale.main import run_from_cmdline from tale.player import Player, PlayerConnection from tale.charbuilder import PlayerNaming @@ -33,6 +34,10 @@ def init_player(self, player: Player) -> None: 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)) + player.stats.magic_skills[MagicType.HEAL] = 30 + player.stats.magic_skills[MagicType.BOLT] = 30 + player.stats.magic_skills[MagicType.DRAIN] = 30 + player.stats.magic_skills[MagicType.REJUVENATE] = 30 pass def create_account_dialog(self, playerconnection: PlayerConnection, playernaming: PlayerNaming) -> Generator: diff --git a/stories/dungeon/story.py b/stories/dungeon/story.py index 1368b073..aed6e5b7 100644 --- a/stories/dungeon/story.py +++ b/stories/dungeon/story.py @@ -11,6 +11,7 @@ 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.main import run_from_cmdline from tale.npc_defs import RoamingMob from tale.player import Player, PlayerConnection @@ -44,6 +45,10 @@ def init_player(self, player: Player) -> None: 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)) + player.stats.magic_skills[MagicType.HEAL] = 30 + player.stats.magic_skills[MagicType.BOLT] = 30 + player.stats.magic_skills[MagicType.DRAIN] = 30 + player.stats.magic_skills[MagicType.REJUVENATE] = 30 pass def create_account_dialog(self, playerconnection: PlayerConnection, playernaming: PlayerNaming) -> Generator: diff --git a/stories/prancingllama/story.py b/stories/prancingllama/story.py index 3de2bd06..4ee177c0 100644 --- a/stories/prancingllama/story.py +++ b/stories/prancingllama/story.py @@ -4,8 +4,10 @@ import tale from tale.base import Location +from tale.cmds import spells from tale.driver import Driver from tale.llm.llm_ext import DynamicStory +from tale.magic import MagicType from tale.main import run_from_cmdline from tale.player import Player, PlayerConnection from tale.charbuilder import PlayerNaming @@ -55,6 +57,7 @@ def init_player(self, player: Player) -> None: player.stats.set_weapon_skill(weapon_type=WeaponType.ONE_HANDED, value=25) 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 def create_account_dialog(self, playerconnection: PlayerConnection, playernaming: PlayerNaming) -> Generator: """ diff --git a/stories/prancingllama/zones/prancingllama.py b/stories/prancingllama/zones/prancingllama.py index 6d565bf7..ee38363e 100644 --- a/stories/prancingllama/zones/prancingllama.py +++ b/stories/prancingllama/zones/prancingllama.py @@ -1,10 +1,8 @@ import random -from tale.base import Location, Item, Exit, Door, Key, Living, ParseResult, Weapon +from tale.base import Location, Item, Exit, Weapon from tale.errors import StoryCompleted from tale.items.basic import Note -from tale.lang import capital -from tale.player import Player from tale.util import Context, call_periodically from tale.verbdefs import AGGRESSIVE_VERBS from tale.verbdefs import VERBS diff --git a/tale/accounts.py b/tale/accounts.py index f3ecfeb3..c60a229c 100644 --- a/tale/accounts.py +++ b/tale/accounts.py @@ -110,8 +110,11 @@ def _create_database(self) -> None: strength integer NOT NULL, dexterity integer NOT NULL, weapon_skills varchar NOT NULL, + magic_skills varchar NOT NULL, combat_points integer NOT NULL, max_combat_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/cmds/spells.py b/tale/cmds/spells.py index 9ca5688b..03c9fc55 100644 --- a/tale/cmds/spells.py +++ b/tale/cmds/spells.py @@ -2,9 +2,10 @@ import random from typing import Optional from tale import base, util, cmds +from tale import magic from tale.cmds import cmd from tale.errors import ActionRefused, ParseError -from tale.magic import MagicType, MagicSkill +from tale.magic import MagicType, MagicSkill, Spell from tale.player import Player @@ -12,14 +13,20 @@ def do_heal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: """ Heal someone or something """ - skill = player.stats.magic_skills.get(MagicType.HEAL) # type: MagicSkill - if not skill: + 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 - if not skill.spell.check_cost(player, level): + num_args = len(parsed.args) + + level = player.stats.level + if num_args == 2: + level = int(parsed.args[1]) + + if not spell.check_cost(player.stats.magic_points, level): raise ActionRefused("You don't have enough magic points") - num_args = len(parsed.args) if num_args < 1: raise ParseError("You need to specify who or what to heal") try: @@ -31,33 +38,37 @@ def do_heal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None if not result or not isinstance(result, base.Living): raise ActionRefused("Can't heal that") - level = player.stats.level - if num_args == 2: - level = int(parsed.args[1]) - player.stats.magic_points -= skill.spell.base_cost * level + player.stats.magic_points -= spell.base_cost * level - if random.randint(1, 100) > skill.skill: - player.tell("Your healing spell fizzles out", evoke=True) + if random.randint(1, 100) > skillValue: + player.tell("Your healing spell fizzles out", evoke=True, short_len=True) return result.stats.replenish_hp(5 * level) - player.tell("You heal %s for %d hit points" % (result.name, 5 * level), evoke=True) - player.tell_others("%s heals %s" % (player.name, result.name), evoke=True) + 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) @cmd("bolt") def do_bolt(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: """ Cast a bolt of energy """ - skill = player.stats.magic_skills.get(MagicType.BOLT) # type: MagicSkill - if not skill: + skillValue = player.stats.magic_skills.get(MagicType.BOLT, None) + if not skillValue: raise ActionRefused("You don't know how to cast a bolt") - if not skill.spell.check_cost(player, level): + 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]) + + if not spell.check_cost(player.stats.magic_points, level): raise ActionRefused("You don't have enough magic points") - num_args = len(parsed.args) if num_args < 1: raise ParseError("You need to specify who or what to attack") @@ -69,35 +80,37 @@ def do_bolt(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None 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") - - level = player.stats.level - if num_args == 2: - level = int(parsed.args[1]) - - player.stats.magic_points -= skill.spell.base_cost * level + player.stats.magic_points -= spell.base_cost * level - if random.randint(1, 100) > skill.skill: - player.tell("Your bolt spell fizzles out", evoke=True) + if random.randint(1, 100) > skillValue: + player.tell("Your bolt spell fizzles out", evoke=True, short_len=True) return - hp = random.randrange(1, level) + hp = random.randint(1, level) result.stats.hp -= hp - player.tell("Your energy bolt hits %s for %d damage" % (result.name, hp), evoke=True) - player.tell_others("%s's energy bolt hits %s for %d damage" % (player.name, result.name, hp), evoke=True) + 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") def do_drain(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: """ Drain energy from someone or something """ - skill = player.stats.magic_skills.get(MagicType.DRAIN) - if not skill: + skillValue = player.stats.magic_skills.get(MagicType.DRAIN, None) + if not skillValue: raise ActionRefused("You don't know how to drain") - if not skill.spell.check_cost(player, level): + 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]) + + if not spell.check_cost(player.stats.magic_points, level): raise ActionRefused("You don't have enough magic points") - num_args = len(parsed.args) if num_args < 1: raise ParseError("You need to specify who or what to drain") @@ -110,24 +123,19 @@ def do_drain(player: Player, parsed: base.ParseResult, ctx: util.Context) -> Non if not result or not isinstance(result, base.Living): raise ActionRefused("Can't drain that") - level = player.stats.level - if num_args == 2: - level = int(parsed.args[1]) - - - player.stats.magic_points -= skill.spell.base_cost * level + player.stats.magic_points -= spell.base_cost * level - if random.randint(1, 100) > skill.skill: - player.tell("Your drain spell fizzles out", evoke=True) + if random.randint(1, 100) > skillValue: + player.tell("Your drain spell fizzles out", evoke=True, short_len=True) return - points = random.randrange(1, level) + points = random.randint(1, level) result.stats.combat_points -= points result.stats.magic_points -= points player.stats.magic_points += points - player.tell("Your drain spell drains %s of %d combat and magic points" % (result.name, points), evoke=True) - player.tell_others("%s casts a drain spell that drains energy from %s" % (player.name, result.name), evoke=True) + player.tell("You cast a 'drain' spell that drains %s of %d combat and magic points" % (result.name, points), evoke=True) + player.tell_others("%s casts a 'drain' spell that drains energy from %s" % (player.name, result.name), evoke=True) diff --git a/tale/driver.py b/tale/driver.py index 220a3837..9ab3882c 100644 --- a/tale/driver.py +++ b/tale/driver.py @@ -961,8 +961,9 @@ def build_location(self, targetLocation: base.Location, zone: Zone, player: play def do_on_player_death(self, player: player.Player) -> None: pass - @util.call_periodically(10) + @util.call_periodically(20) def replenish(self): for player in self.all_players.values(): player.player.stats.replenish_hp(1) - player.player.stats.replenish_combat_points(1) \ No newline at end of file + player.player.stats.replenish_combat_points(1) + player.player.stats.replenish_magic_points(1) \ No newline at end of file diff --git a/tale/magic.py b/tale/magic.py index a572e14d..33384d41 100644 --- a/tale/magic.py +++ b/tale/magic.py @@ -1,8 +1,6 @@ from abc import ABC from enum import Enum -from tale.player import Player - class MagicType(Enum): HEAL = 1 @@ -19,17 +17,13 @@ def __init__(self, name: str, base_cost: int, base_value: int = 1, max_level: in self.base_cost = base_cost self.max_level = max_level - def do_cast(self, player, target, level): - pass - - def check_cost(self, player: Player, level: int) -> bool: - return player.stats.magic_points >= self.base_cost * level + def check_cost(self, magic_points: int, level: int) -> bool: + return magic_points >= self.base_cost * level 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.REJUVENATE: Spell('rejuvenate', base_cost=3, base_value=5) + MagicType.DRAIN: Spell('drain', base_cost=3, base_value=5) } class MagicSkill: diff --git a/tests/test_spells.py b/tests/test_spells.py index 1b3661aa..46edd113 100644 --- a/tests/test_spells.py +++ b/tests/test_spells.py @@ -15,7 +15,7 @@ from tests.supportstuff import FakeDriver, FakeIoUtil -class TestSpells: +class TestHeal: context = tale.mud_context context.config = StoryConfig() @@ -35,7 +35,7 @@ def setup_method(self): self.location.insert(self.player, actor=None) def test_heal(self): - self.player.stats.magic_skills[MagicType.HEAL] = MagicSkill(magic.spells[MagicType.HEAL], 100) + self.player.stats.magic_skills[MagicType.HEAL] = 100 npc = LivingNpc('test', 'f', age=30) npc.stats.hp = 0 self.player.location.insert(npc, actor=None) @@ -46,7 +46,7 @@ def test_heal(self): assert npc.stats.hp == 5 def test_heal_fail(self): - self.player.stats.magic_skills[MagicType.HEAL] = MagicSkill(magic.spells[MagicType.HEAL], 0) + self.player.stats.magic_skills[MagicType.HEAL] = -1 npc = LivingNpc('test', 'f', age=30) npc.stats.hp = 0 self.player.location.insert(npc, actor=None) @@ -61,7 +61,7 @@ def test_heal_refused(self): with pytest.raises(ActionRefused, match="You don't know how to heal"): spells.do_heal(self.player, parse_result, None) - self.player.stats.magic_skills[MagicType.HEAL] = MagicSkill(MagicType.HEAL, 10) + self.player.stats.magic_skills[MagicType.HEAL] = 10 self.player.stats.magic_points = 0 npc = LivingNpc('test', 'f', age=30) @@ -70,3 +70,65 @@ 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 + 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_bolt(self): + self.player.stats.magic_skills[MagicType.BOLT] = 100 + npc = LivingNpc('test', 'f', age=30) + npc.stats.hp = 5 + self.player.location.insert(npc, actor=None) + self.player.stats.magic_points = 10 + parse_result = ParseResult(verb='bolt', args=['test']) + result = spells.do_bolt(self.player, parse_result, None) + assert self.player.stats.magic_points == 7 + assert npc.stats.hp < 5 + +class TestDrain: + + 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_drain(self): + self.player.stats.magic_skills[MagicType.DRAIN] = 100 + npc = LivingNpc('test', 'f', age=30) + npc.stats.combat_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.magic_points < 5 \ No newline at end of file