From 73d8f24b7cc1ddc1723fd2d9eb76c06e7721eccd Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 18 Jul 2022 16:58:45 +0200 Subject: [PATCH] Debugging of tutorial --- .../tutorials/evadventure/build_techdemo.py | 4 +- .../tutorials/evadventure/characters.py | 390 +++--------------- .../tutorials/evadventure/combat_turnbased.py | 32 +- evennia/contrib/tutorials/evadventure/npcs.py | 58 ++- .../contrib/tutorials/evadventure/quests.py | 23 +- .../contrib/tutorials/evadventure/rules.py | 6 +- .../evadventure/tests/test_combat.py | 5 +- evennia/utils/utils.py | 62 +-- 8 files changed, 189 insertions(+), 391 deletions(-) diff --git a/evennia/contrib/tutorials/evadventure/build_techdemo.py b/evennia/contrib/tutorials/evadventure/build_techdemo.py index f819baef8c..e6a763e9bf 100644 --- a/evennia/contrib/tutorials/evadventure/build_techdemo.py +++ b/evennia/contrib/tutorials/evadventure/build_techdemo.py @@ -30,7 +30,7 @@ from evennia.contrib.tutorials.evadventure.objects import ( EvAdventureRunestone, EvAdventureWeapon, ) -from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom +from evennia.contrib.tutorials.evadventure.rooms import EvAdventurePvPRoom, EvAdventureRoom # CODE @@ -65,7 +65,7 @@ create_object( # A combat room evtechdemo#01 # with a static enemy -combat_room = create_object(EvAdventureRoom, key="Combat Arena", aliases=("evtechdemo#01",)) +combat_room = create_object(EvAdventurePvPRoom, key="Combat Arena", aliases=("evtechdemo#01",)) # link to/back to hub hub_room = search_object("evtechdemo#00")[0] create_object( diff --git a/evennia/contrib/tutorials/evadventure/characters.py b/evennia/contrib/tutorials/evadventure/characters.py index c527146de8..a63b02cf9e 100644 --- a/evennia/contrib/tutorials/evadventure/characters.py +++ b/evennia/contrib/tutorials/evadventure/characters.py @@ -1,5 +1,5 @@ """ -Base Character and NPCs. +Character class. """ @@ -8,336 +8,8 @@ from evennia.typeclasses.attributes import AttributeProperty from evennia.utils.utils import lazy_property from . import rules -from .enums import Ability, WieldLocation -from .objects import WeaponEmptyHand - - -class EquipmentError(TypeError): - pass - - -class EquipmentHandler: - """ - _Knave_ puts a lot of emphasis on the inventory. You have CON_DEFENSE inventory - slots. Some things, like torches can fit multiple in one slot, other (like - big weapons and armor) use more than one slot. The items carried and wielded has a big impact - on character customization - even magic requires carrying a runestone per spell. - - The inventory also doubles as a measure of negative effects. Getting soaked in mud - or slime could gunk up some of your inventory slots and make the items there unusuable - until you clean them. - - """ - - save_attribute = "inventory_slots" - - def __init__(self, obj): - self.obj = obj - self._load() - - def _load(self): - """ - Load or create a new slot storage. - - """ - self.slots = self.obj.attributes.get( - self.save_attribute, - category="inventory", - default={ - WieldLocation.WEAPON_HAND: None, - WieldLocation.SHIELD_HAND: None, - WieldLocation.TWO_HANDS: None, - WieldLocation.BODY: None, - WieldLocation.HEAD: None, - WieldLocation.BACKPACK: [], - }, - ) - - def _count_slots(self): - """ - Count slot usage. This is fetched from the .size Attribute of the - object. The size can also be partial slots. - - """ - slots = self.slots - wield_usage = sum( - getattr(slotobj, "size", 0) or 0 - for slot, slotobj in slots.items() - if slot is not WieldLocation.BACKPACK - ) - backpack_usage = sum( - getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK] - ) - return wield_usage + backpack_usage - - def _save(self): - """ - Save slot to storage. - - """ - self.obj.attributes.add(self.save_attribute, self.slots, category="inventory") - - @property - def max_slots(self): - """ - The max amount of equipment slots ('carrying capacity') is based on - the constitution defense. - - """ - return getattr(self.obj, Ability.CON.value, 1) + 10 - - def validate_slot_usage(self, obj): - """ - Check if obj can fit in equipment, based on its size. - - Args: - obj (EvAdventureObject): The object to add. - - Raise: - EquipmentError: If there's not enough room. - - """ - size = getattr(obj, "size", 0) - max_slots = self.max_slots - current_slot_usage = self._count_slots() - if current_slot_usage + size > max_slots: - slots_left = max_slots - current_slot_usage - raise EquipmentError( - f"Equipment full ($int2str({slots_left}) slots " - f"remaining, {obj.key} needs $int2str({size}) " - f"$pluralize(slot, {size}))." - ) - return True - - @property - def armor(self): - """ - Armor provided by actually worn equipment/shield. For body armor - this is a base value, like 12, for shield/helmet, it's a bonus, like +1. - We treat values and bonuses equal and just add them up. This value - can thus be 0, the 'unarmored' default should be handled by the calling - method. - - Returns: - int: Armor from equipment. Note that this is the +bonus of Armor, not the - 'defense' (to get that one adds 10). - - """ - slots = self.slots - return sum( - ( - # armor is listed using its defense, so we remove 10 from it - # (11 is base no-armor value in Knave) - getattr(slots[WieldLocation.BODY], "armor", 11) - 10, - # shields and helmets are listed by their bonus to armor - getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0), - getattr(slots[WieldLocation.HEAD], "armor", 0), - ) - ) - - @property - def weapon(self): - """ - Conveniently get the currently active weapon or rune stone. - - Returns: - obj or None: The weapon. None if unarmored. - - """ - # first checks two-handed wield, then one-handed; the two - # should never appear simultaneously anyhow (checked in `use` method). - slots = self.slots - weapon = slots[WieldLocation.TWO_HANDS] - if not weapon: - weapon = slots[WieldLocation.WEAPON_HAND] - if not weapon: - weapon = WeaponEmptyHand() - return weapon - - def display_loadout(self): - """ - Get a visual representation of your current loadout. - - Returns: - str: The current loadout. - - """ - slots = self.slots - weapon_str = "You are fighting with your bare fists" - shield_str = " and have no shield." - armor_str = "You wear no armor" - helmet_str = " and no helmet." - - two_hands = slots[WieldLocation.TWO_HANDS] - if two_hands: - weapon_str = f"You wield {two_hands} with both hands" - shield_str = " (you can't hold a shield at the same time)." - else: - one_hands = slots[WieldLocation.WEAPON_HAND] - if one_hands: - weapon_str = f"You are wielding {one_hands} in one hand." - shield = slots[WieldLocation.SHIELD_HAND] - if shield: - shield_str = f"You have {shield} in your off hand." - - armor = slots[WieldLocation.BODY] - if armor: - armor_str = f"You are wearing {armor}" - - helmet = slots[WieldLocation.BODY] - if helmet: - helmet_str = f" and {helmet} on your head." - - return f"{weapon_str}{shield_str}\n{armor_str}{helmet_str}" - - def use(self, obj): - """ - Make use of item - this makes use of the object's wield slot to decide where - it goes. If it doesn't have any, it goes into backpack. - - Args: - obj (EvAdventureObject): Thing to use. - - Raises: - EquipmentError: If there's no room in inventory. It will contains the details - of the error, suitable to echo to user. - - Notes: - If using an item already in the backpack, it should first be `removed` from the - backpack, before applying here - otherwise, it will be added a second time! - - this will cleanly move any 'colliding' items to the backpack to - make the use possible (such as moving sword + shield to backpack when wielding - a two-handed weapon). If wanting to warn the user about this, it needs to happen - before this call. - - """ - # first check if we have room for this - self.validate_slot_usage(obj) - - slots = self.slots - use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK) - - if use_slot is WieldLocation.TWO_HANDS: - # two-handed weapons can't co-exist with weapon/shield-hand used items - slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None - slots[use_slot] = obj - elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND): - # can't keep a two-handed weapon if adding a one-handede weapon or shield - slots[WieldLocation.TWO_HANDS] = None - slots[use_slot] = obj - elif use_slot is WieldLocation.BACKPACK: - # backpack has multiple slots. - slots[use_slot].append(obj) - else: - # for others (body, head), just replace whatever's there - slots[use_slot] = obj - - # store new state - self._save() - - def add(self, obj): - """ - Put something in the backpack specifically (even if it could be wield/worn). - - """ - # check if we have room - self.validate_slot_usage(obj) - self.slots[WieldLocation.BACKPACK].append(obj) - self._save() - - def can_remove(self, leaving_object): - """ - Called to check if the object can be removed. - - """ - return True # TODO - some things may not be so easy, like mud - - def remove(self, obj_or_slot): - """ - Remove specific object or objects from a slot. - - Args: - obj_or_slot (EvAdventureObject or WieldLocation): The specific object or - location to empty. If this is WieldLocation.BACKPACK, all items - in the backpack will be emptied and returned! - Returns: - list: A list of 0, 1 or more objects emptied from the inventory. - - """ - slots = self.slots - ret = [] - if isinstance(obj_or_slot, WieldLocation): - if obj_or_slot is WieldLocation.BACKPACK: - # empty entire backpack - ret.extend(slots[obj_or_slot]) - slots[obj_or_slot] = [] - else: - ret.append(slots[obj_or_slot]) - slots[obj_or_slot] = None - elif obj_or_slot in self.slots.values(): - # obj in use/wear slot - for slot, objslot in slots.items(): - if objslot is obj_or_slot: - slots[slot] = None - ret.append(objslot) - elif obj_or_slot in slots[WieldLocation.BACKPACK]: - # obj in backpack slot - try: - slots[WieldLocation.BACKPACK].remove(obj_or_slot) - ret.append(obj_or_slot) - except ValueError: - pass - if ret: - self._save() - return ret - - def get_wieldable_objects_from_backpack(self): - """ - Get all wieldable weapons (or spell runes) from backpack. This is useful in order to - have a list to select from when swapping your wielded loadout. - - Returns: - list: A list of objects with a suitable `inventory_use_slot`. We don't check - quality, so this may include broken items (we may want to visually show them - in the list after all). - - """ - return [ - obj - for obj in self.slots[WieldLocation.BACKPACK] - if obj.inventory_use_slot - in (WieldLocation.WEAPON_HAND, WieldLocation.TWO_HANDS, WieldLocation.SHIELD_HAND) - ] - - def get_wearable_objects_from_backpack(self): - """ - Get all wearable items (armor or helmets) from backpack. This is useful in order to - have a list to select from when swapping your worn loadout. - - Returns: - list: A list of objects with a suitable `inventory_use_slot`. We don't check - quality, so this may include broken items (we may want to visually show them - in the list after all). - - """ - return [ - obj - for obj in self.slots[WieldLocation.BACKPACK] - if obj.inventory_use_slot in (WieldLocation.BODY, WieldLocation.HEAD) - ] - - def get_usable_objects_from_backpack(self): - """ - Get all 'usable' items (like potions) from backpack. This is useful for getting a - list to select from. - - Returns: - list: A list of objects that are usable. - - """ - character = self.obj - return [obj for obj in self.slots[WieldLocation.BACKPACK] if obj.at_pre_use(character)] +from .equipment import EquipmentHandler +from .quests import EvAdventureQuestHandler class LivingMixin: @@ -407,6 +79,16 @@ class LivingMixin: """ pass + def at_loot(self, looted): + """ + Called when looting another entity. + + Args: + looted: The thing to loot. + + """ + looted.get_loot() + def get_loot(self, looter): """ Called when being looted (after defeat). @@ -418,8 +100,8 @@ class LivingMixin: max_steal = rules.dice.roll("1d10") owned = self.coin stolen = max(max_steal, owned) - self.coin -= stolen - looter.coin += stolen + self.coins -= stolen + looter.coins += stolen self.location.msg_contents( f"$You(looter) loots $You() for {stolen} coins!", @@ -434,6 +116,9 @@ class LivingMixin: Args: defeated_enemy (Object): The enemy soon to loot. + Returns: + bool: If False, no looting is allowed. + """ pass @@ -481,6 +166,11 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): """Allows to access equipment like char.equipment.worn""" return EquipmentHandler(self) + @lazy_property + def quests(self): + """Access and track quests""" + return EvAdventureQuestHandler(self) + @property def weapon(self): return self.equipment.weapon @@ -544,14 +234,18 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): the death table. """ - rules.dice.roll_death(self) - if self.hp > 0: - # still alive, but lost some stats - self.location.msg_contents( - "|y$You() $conj(stagger) back and fall to the ground - alive, " - "but unable to move.|n", - from_obj=self, - ) + if self.location.allow_death: + rules.dice.roll_death(self) + if self.hp > 0: + # still alive, but lost some stats + self.location.msg_contents( + "|y$You() $conj(stagger) back and fall to the ground - alive, " + "but unable to move.|n", + from_obj=self, + ) + else: + self.location.msg_contents("|y$You() $conj(yield), beaten and out of the fight.|n") + self.hp = self.hp_max def at_death(self): """ @@ -562,3 +256,17 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): "|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n", from_obj=self, ) + + def at_pre_loot(self): + """ + Called before allowing to loot. Return False to block enemy looting. + """ + # don't allow looting in pvp + return not self.location.allow_pvp + + def get_loot(self, looter): + """ + Called when being looted. + + """ + pass diff --git a/evennia/contrib/tutorials/evadventure/combat_turnbased.py b/evennia/contrib/tutorials/evadventure/combat_turnbased.py index e4694d05a1..7788a5ee8d 100644 --- a/evennia/contrib/tutorials/evadventure/combat_turnbased.py +++ b/evennia/contrib/tutorials/evadventure/combat_turnbased.py @@ -111,7 +111,7 @@ from .enums import Ability from .npcs import EvAdventureNPC COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler" -COMBAT_HANDLER_INTERVAL = 60 +COMBAT_HANDLER_INTERVAL = 30 class CombatFailure(RuntimeError): @@ -156,7 +156,7 @@ class CombatAction: self.combatant = combatant self.uses = 0 - def msg(self, message, broadcast=False): + def msg(self, message, broadcast=True): """ Convenience route to the combathandler msg-sender mechanism. @@ -520,9 +520,9 @@ class CombatActionBlock(CombatAction): if is_success: # managed to stop the target from fleeing/disengaging self.combathandler.unflee(fleeing_target) - self.msg("$You() blocks the retreat of $You({fleeing_target.key})") + self.msg(f"$You() $conj(block) the retreat of $You({fleeing_target.key})") else: - self.msg("$You({fleeing_target.key}) dodges away from you $You()!") + self.msg(f"$You({fleeing_target.key}) dodges away from you $You()!") class CombatActionDoNothing(CombatAction): @@ -660,7 +660,7 @@ class EvAdventureCombatHandler(DefaultScript): # start a timer to echo a warning to everyone 15 seconds before end of round if self.interval >= 0: # set -1 for unit tests - warning_time = 15 + warning_time = 10 self._warn_time_task = delay( self.interval - warning_time, self._warn_time, warning_time ) @@ -766,9 +766,9 @@ class EvAdventureCombatHandler(DefaultScript): for ally in allies: for enemy in defeated_enemies: try: - ally.pre_loot(enemy) - enemy.get_loot(ally) - ally.post_loot(enemy) + if ally.pre_loot(enemy): + enemy.get_loot(ally) + ally.post_loot(enemy) except Exception: logger.log_trace() self.stop_combat() @@ -844,7 +844,8 @@ class EvAdventureCombatHandler(DefaultScript): if combatant in self.combatants: self.combatants.remove(combatant) self.combatant_actions.pop(combatant, None) - combatant.ndb._evmenu.close_menu() + if combatant.ndb._evmenu: + combatant.ndb._evmenu.close_menu() del combatant.db.combathandler def start_combat(self): @@ -867,6 +868,7 @@ class EvAdventureCombatHandler(DefaultScript): """ for combatant in self.combatants: self.remove_combatant(combatant) + self.delete() def get_enemy_targets(self, combatant, excluded=None, all_combatants=None): """ @@ -1131,8 +1133,10 @@ def _select_target_helper(caller, raw_string, targets, **kwargs): text = f"Select target for |w{action_key}|n." # make the apply-self option always the first one, give it key 0 - kwargs["action_target"] = caller - options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}] + if caller in targets: + targets.remove(caller) + kwargs["action_target"] = caller + options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}] # filter out ourselves and then make options for everyone else for inum, combatant in enumerate(targets): kwargs["action_target"] = combatant @@ -1385,6 +1389,9 @@ def join_combat(caller, *targets, session=None): if not location: raise CombatFailure("Must have a location to start combat.") + if caller.hp <= 0: + raise CombatFailure("You can't start a fight in your current condition!") + if not getattr(location, "allow_combat", False): raise CombatFailure("This is not the time and place for picking a fight.") @@ -1402,6 +1409,9 @@ def join_combat(caller, *targets, session=None): # it's safe to add a combatant to the same combat more than once combathandler.add_combatant(caller, session=session) for target in targets: + if target.hp <= 0: + caller.msg(f"{target.get_display_name(caller)} is already out of it.") + continue combathandler.add_combatant(target) if created: diff --git a/evennia/contrib/tutorials/evadventure/npcs.py b/evennia/contrib/tutorials/evadventure/npcs.py index a1b25e0d34..12e97d08e1 100644 --- a/evennia/contrib/tutorials/evadventure/npcs.py +++ b/evennia/contrib/tutorials/evadventure/npcs.py @@ -8,8 +8,9 @@ from evennia import DefaultCharacter from evennia.typeclasses.attributes import AttributeProperty from .characters import LivingMixin -from .enums import Ability +from .enums import Ability, WieldLocation from .objects import WeaponEmptyHand +from .rules import dice class EvAdventureNPC(LivingMixin, DefaultCharacter): @@ -114,6 +115,9 @@ class EvAdventureMob(EvAdventureNPC): """ + # chance (%) that this enemy will loot you when defeating you + loot_chance = AttributeProperty(75) + def ai_combat_next_action(self, combathandler): """ Called to get the next action in combat. @@ -150,3 +154,55 @@ class EvAdventureMob(EvAdventureNPC): """ self.at_death() + + def at_loot(self, looted): + """ + Called when mob gets to loot a PC. + + """ + if dice.roll("1d100") > self.loot_chance: + # don't loot + return + + if looted.coins: + # looter prefer coins + loot = dice.roll("1d20") + if looted.coins < loot: + self.location.msg_location( + "$You(looter) loots $You() for all coin!", + from_obj=looted, + mapping={"looter": self}, + ) + else: + self.location.msg_location( + "$You(looter) loots $You() for |y{loot}|n coins!", + from_obj=looted, + mapping={"looter": self}, + ) + elif hasattr(looted, "equipment"): + # go through backpack, first usable, then wieldable, wearable items + # and finally stuff wielded + stealable = looted.equipment.get_usable_objects_from_backpack() + if not stealable: + stealable = looted.equipment.get_wieldable_objects_from_backpack() + if not stealable: + stealable = looted.equipment.get_wearable_objects_from_backpack() + if not stealable: + stealable = [looted.equipment.slots[WieldLocation.SHIELD_HAND]] + if not stealable: + stealable = [looted.equipment.slots[WieldLocation.HEAD]] + if not stealable: + stealable = [looted.equipment.slots[WieldLocation.ARMOR]] + if not stealable: + stealable = [looted.equipment.slots[WieldLocation.WEAPON_HAND]] + if not stealable: + stealable = [looted.equipment.slots[WieldLocation.TWO_HANDS]] + + stolen = looted.equipment.remove(choice(stealable)) + stolen.location = self + + self.location.msg_location( + "$You(looter) steals {stolen.key} from $You()!", + from_obj=looted, + mapping={"looter": self}, + ) diff --git a/evennia/contrib/tutorials/evadventure/quests.py b/evennia/contrib/tutorials/evadventure/quests.py index ec95e8cc4e..d30ce5f153 100644 --- a/evennia/contrib/tutorials/evadventure/quests.py +++ b/evennia/contrib/tutorials/evadventure/quests.py @@ -38,6 +38,7 @@ class EvAdventureQuest: key = "basequest" desc = "This is the base quest. It will just step through its steps immediately." start_step = "start" + end_text = "This quest is completed!" # help entries for quests help_start = "You need to start first" @@ -49,6 +50,7 @@ class EvAdventureQuest: self.questhandler = questhandler self.current_step = start_step + self.completed = False @property def quester(self): @@ -59,17 +61,20 @@ class EvAdventureQuest: Call this to end the quest. """ - self.current_step + self.completed = True - def progress(self): + def progress(self, *args, **kwargs): """ This is called whenever the environment expects a quest may be complete. This will determine which quest-step we are on, run check_, and if it - succeeds, continue with complete_ + succeeds, continue with complete_. + + Args: + *args, **kwargs: Will be passed into the check/complete methods. """ - if getattr(self, f"check_{self.current_step}")(): - getattr(self, f"complete_{self.current_step}")() + if getattr(self, f"check_{self.current_step}")(*args, **kwargs): + getattr(self, f"complete_{self.current_step}")(*args, **kwargs) def help(self): """ @@ -93,7 +98,7 @@ class EvAdventureQuest: # step methods - def check_start(self): + def check_start(self, *args, **kwargs): """ Check if the starting conditions are met. @@ -104,7 +109,7 @@ class EvAdventureQuest: """ return True - def complete_start(self): + def complete_start(self, *args, **kwargs): """ Completed start. This should change `.current_step` to the next step to complete and call `self.progress()` just in case the next step is already completed too. @@ -114,10 +119,10 @@ class EvAdventureQuest: self.current_step = "end" self.progress() - def check_end(self): + def check_end(self, *args, **kwargs): return True - def complete_end(self): + def complete_end(self, *args, **kwargs): self.quester.msg("Quest complete!") self.end_quest() diff --git a/evennia/contrib/tutorials/evadventure/rules.py b/evennia/contrib/tutorials/evadventure/rules.py index f37459cfdd..5a83ce359e 100644 --- a/evennia/contrib/tutorials/evadventure/rules.py +++ b/evennia/contrib/tutorials/evadventure/rules.py @@ -164,7 +164,11 @@ class EvAdventureRollEngine: modtxt = f" + {modifier}" if modifier > 0 else f" - {abs(modifier)}" qualtxt = f" ({quality.value}!)" if quality else "" - txt = f"{rolltxt}={dice_roll} + {bonus_type.value}{bontxt}{modtxt} -> |w{result}{qualtxt}|n" + txt = ( + f"rolled {dice_roll} on {rolltxt} " + f"+ {bonus_type.value}{bontxt}{modtxt} vs " + f"{target} -> |w{result}{qualtxt}|n" + ) return (dice_roll + bonus + modifier) > target, quality, txt diff --git a/evennia/contrib/tutorials/evadventure/tests/test_combat.py b/evennia/contrib/tutorials/evadventure/tests/test_combat.py index 427d237a00..4dcdd3de6b 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_combat.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_combat.py @@ -195,7 +195,8 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest): mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11 self._run_action(combat_turnbased.CombatActionAttack, self.target) self.assertEqual(self.target.hp, -7) - self.assertTrue(self.target not in self.combathandler.combatants) + # after this the combat is over + self.assertIsNone(self.combathandler.pk) @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint") def test_stunt_fail(self, mock_randint): @@ -293,7 +294,7 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest): # second flee should remove combatant self._run_action(combat_turnbased.CombatActionFlee, None) - self.assertTrue(self.combatant not in self.combathandler.combatants) + self.assertIsNone(self.combathandler.pk) @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint") def test_flee__blocked(self, mock_randint): diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index a79b89e89c..25265c0c96 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -6,38 +6,38 @@ They provide some useful string and conversion methods that might be of use when designing your own game. """ -import os import gc -import sys -import types -import math -import threading -import re -import textwrap -import random -import inspect -import traceback import importlib -import importlib.util import importlib.machinery +import importlib.util +import inspect +import math +import os +import random +import re +import sys +import textwrap +import threading +import traceback +import types from ast import literal_eval -from simpleeval import simple_eval -from unicodedata import east_asian_width -from twisted.internet.task import deferLater -from twisted.internet.defer import returnValue # noqa - used as import target -from twisted.internet import threads, reactor +from collections import OrderedDict, defaultdict +from inspect import getmembers, getmodule, getmro, ismodule, trace from os.path import join as osjoin -from inspect import ismodule, trace, getmembers, getmodule, getmro -from collections import defaultdict, OrderedDict +from unicodedata import east_asian_width + +from django.apps import apps from django.conf import settings +from django.core.exceptions import ValidationError as DjangoValidationError +from django.core.validators import validate_email as django_validate_email from django.utils import timezone from django.utils.html import strip_tags from django.utils.translation import gettext as _ -from django.apps import apps -from django.core.validators import validate_email as django_validate_email -from django.core.exceptions import ValidationError as DjangoValidationError - from evennia.utils import logger +from simpleeval import simple_eval +from twisted.internet import reactor, threads +from twisted.internet.defer import returnValue # noqa - used as import target +from twisted.internet.task import deferLater _MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE _EVENNIA_DIR = settings.EVENNIA_DIR @@ -2714,10 +2714,24 @@ def run_in_main_thread(function_or_method, *args, **kwargs): return threads.blockingCallFromThread(reactor, function_or_method, *args, **kwargs) -_INT2STR_MAP_NOUN = {0: "no", 1: "one", 2: "two", 3: "three", 4: "four", 5: "five", 6: "six", - 7: "seven", 8: "eight", 9: "nine", 10: "ten", 11: "eleven", 12: "twelve"} +_INT2STR_MAP_NOUN = { + 0: "no", + 1: "one", + 2: "two", + 3: "three", + 4: "four", + 5: "five", + 6: "six", + 7: "seven", + 8: "eight", + 9: "nine", + 10: "ten", + 11: "eleven", + 12: "twelve", +} _INT2STR_MAP_ADJ = {1: "1st", 2: "2nd", 3: "3rd"} # rest is Xth. + def int2str(self, number, adjective=False): """ Convert a number to an English string for better display; so 1 -> one, 2 -> two etc