More combat tests

This commit is contained in:
Griatch 2023-03-06 19:29:43 +01:00
parent b0d6af9cc3
commit 6a7f59eaab
4 changed files with 343 additions and 115 deletions

View file

@ -1,11 +1,9 @@
# Soft Code
Softcode is a very simple programming language that was created for in-game development on TinyMUD derivatives such as MUX, PennMUSH, TinyMUSH, and RhostMUSH. The idea is that by providing a stripped down, minimalistic language for in-game use, you can allow quick and easy building and game development to happen without having to learn C/C++. There is an added benefit of not having to have to hand out shell access to all developers, and permissions can be used to alleviate many security problems.
Softcode is a simple programming language that was created for in-game development on TinyMUD derivatives such as MUX, PennMUSH, TinyMUSH, and RhostMUSH. The idea was that by providing a stripped down, minimalistic language for in-game use, you could allow quick and easy building and game development to happen without builders having to learn the 'hardcode' language for those servers (C/C++). There is an added benefit of not having to have to hand out shell access to all developers. Permissions in softcode can be used to alleviate many security problems.
Writing and installing softcode is done through a MUD client. Thus it is not a formatted language.
Each softcode function is a single line of varying size. Some functions can be a half of a page long
or more which is obviously not very readable nor (easily) maintainable over time.
Writing and installing softcode is done through a MUD client. Thus it is not a formatted language. Each softcode function is a single line of varying size. Some functions can be a half of a page long or more which is obviously not very readable nor (easily) maintainable over time.
## Examples of Softcode
@ -15,27 +13,24 @@ Here is a simple 'Hello World!' command:
@set me=HELLO_WORLD.C:$hello:@pemit %#=Hello World!
```
Pasting this into a MUX/MUSH and typing 'hello' will theoretically yield 'Hello World!', assuming
certain flags are not set on your account object.
Pasting this into a MUD client, sending it to a MUX/MUSH server and typing 'hello' will theoretically yield 'Hello World!', assuming certain flags are not set on your account object.
Setting attributes is done via `@set`. Softcode also allows the use of the ampersand (`&`) symbol.
This shorter version looks like this:
Setting attributes in Softcode is done via `@set`. Softcode also allows the use of the ampersand (`&`) symbol. This shorter version looks like this:
```bash
&HELLO_WORLD.C me=$hello:@pemit %#=Hello World!
```
Perhaps I want to break the Hello World into an attribute which is retrieved when emitting:
We could also read the text from an attribute which is retrieved when emitting:
```bash
&HELLO_VALUE.D me=Hello World
&HELLO_WORLD.C me=$hello:@pemit %#=[v(HELLO_VALUE.D)]
```
The `v()` function returns the `HELLO_VALUE.D` attribute on the object that the command resides
(`me`, which is yourself in this case). This should yield the same output as the first example.
The `v()` function returns the `HELLO_VALUE.D` attribute on the object that the command resides (`me`, which is yourself in this case). This should yield the same output as the first example.
If you are still curious about how Softcode works, take a look at some external resources:
If you are curious about how MUSH/MUX Softcode works, take a look at some external resources:
- https://wiki.tinymux.org/index.php/Softcode
- https://www.duh.com/discordia/mushman/man2x1
@ -44,24 +39,26 @@ If you are still curious about how Softcode works, take a look at some external
Softcode is excellent at what it was intended for: *simple things*. It is a great tool for making an interactive object, a room with ambiance, simple global commands, simple economies and coded systems. However, once you start to try to write something like a complex combat system or a higher end economy, you're likely to find yourself buried under a mountain of functions that span multiple objects across your entire code.
Not to mention, softcode is not an inherently fast language. It is not compiled, it is parsed with each calling of a function. While MUX and MUSH parsers have jumped light years ahead of where they once were they can still stutter under the weight of more complex systems if not designed properly.
Not to mention, softcode is not an inherently fast language. It is not compiled, it is parsed with each calling of a function. While MUX and MUSH parsers have jumped light years ahead of where they once were, they can still stutter under the weight of more complex systems if those are not designed properly.
Also, Softcode is not a standardized language. Different servers each have their own slight variations. Code tools and resources are also limited to the documentation from those servers.
## Changing Times
Now that starting text-based games is easy and an option for even the most technically inarticulate, new projects are a dime a dozen. People are starting new MUDs every day with varying levels of commitment and ability. Because of this shift from fewer, larger, well-staffed games to a bunch of small, one or two developer games, some of the benefit of softcode fades.
Now that starting text-based games is easy and an option for even the most technically inarticulate, new projects are a dime a dozen. People are starting new MUDs every day with varying levels of commitment and ability. Because of this shift from fewer, larger, well-staffed games to a bunch of small, one or two developer games, the benefit of softcode fades.
Softcode is great in that it allows a mid to large sized staff all work on the same game without stepping on one another's toes. As mentioned before, shell access is not necessary to develop a MUX or a MUSH. However, now that we are seeing a lot more small, one or two-man shops, the issue of shell access and stepping on each other's toes is a lot less.
Softcode is great in that it allows a mid to large sized staff all work on the same game without stepping on one another's toes without shell access. However, the rise of modern code collaboration tools (such as private github/gitlab repos) has made it trivial to collaborate on code.
## Our Solution
Evennia shuns in-game softcode for on-disk Python modules. Python is a popular, mature and
professional programming language. You code it using the conveniences of modern text editors.
Evennia developers have access to the entire library of Python modules out there in the wild - not
to mention the vast online help resources available. Python code is not bound to one-line functions
on objects but complex systems may be organized neatly into real source code modules, sub-modules, or even broken out into entire Python packages as desired.
Evennia shuns in-game softcode for on-disk Python modules. Python is a popular, mature and professional programming language. Evennia developers have access to the entire library of Python modules out there in the wild - not to mention the vast online help resources available. Python code is not bound to one-line functions on objects; complex systems may be organized neatly into real source code modules, sub-modules, or even broken out into entire Python packages as desired.
So what is *not* included in Evennia is a MUX/MOO-like online player-coding system. Advanced coding in Evennia is primarily intended to be done outside the game, in full-fledged Python modules. Advanced building is best handled by extending Evennia's command system with your own sophisticated building commands. We feel that with a small development team you are better off using a professional source-control system (svn, git, bazaar, mercurial etc) anyway.
So what is *not* included in Evennia is a MUX/MOO-like online player-coding system (aka Softcode). Advanced coding in Evennia is primarily intended to be done outside the game, in full-fledged Python modules (what MUSH would call 'hardcode'). Advanced building is best handled by extending Evennia's command system with your own sophisticated building commands.
In Evennia you develop your MU like you would any piece of modern software - using your favorite code editor/IDE and online code sharing tools.
## Your Solution
Adding advanced and flexible building commands to your game is easy and will probably be enough to satisfy most creative builders. However, if you really, *really* want to offer online coding, there is of course nothing stopping you from adding that to Evennia, no matter our recommendations. You could even re-implement MUX' softcode in Python should you be very ambitious.
Adding advanced and flexible building commands to your game is easy and will probably be enough to satisfy most creative builders. However, if you really, *really* want to offer online coding, there is of course nothing stopping you from adding that to Evennia, no matter our recommendations. You could even re-implement MUX' softcode in Python should you be very ambitious. The [in-game-python](../Contribs/Contrib-Ingame-Python.md) is an optional pseudo-softcode plugin aimed at developers wanting to script their game from inside it.
In default Evennia, the [Funcparser](Funcparser) system allows for simple remapping of text on-demand without becomeing a full softcode language. The [contribs](Contrib-Overview) has several tools and utililities to start from when adding more complex in-game building.

View file

@ -196,7 +196,7 @@ class CombatAction:
the combatant doing the action and other combatants, respectively.
"""
self.combathandler.msg(self, message, combatant=self.combatant, broadcast=broadcast)
self.combathandler.msg(message, combatant=self.combatant, broadcast=broadcast)
def can_use(self):
"""
@ -250,44 +250,9 @@ class CombatActionAttack(CombatAction):
weapon = attacker.weapon
target = self.target
is_hit, quality, txt = rules.dice.opposed_saving_throw(
attacker,
target,
attack_type=weapon.attack_type,
defense_type=attacker.weapon.defense_type,
advantage=self.has_advantage(attacker, target),
disadvantage=self.has_disadvantage(attacker, target),
)
self.msg(f"$You() $conj(attack) $You({target.key}) with {weapon.key}: {txt}")
if is_hit:
# enemy hit, calculate damage
weapon_dmg_roll = attacker.weapon.damage_roll
dmg = rules.dice.roll(weapon_dmg_roll)
if quality is Ability.CRITICAL_SUCCESS:
dmg += rules.dice.roll(weapon_dmg_roll)
message = (
f" $You() |ycritically|n $conj(hit) $You({target.key}) for |r{dmg}|n damage!"
)
else:
message = f" $You() $conj(hit) $You({target.key}) for |r{dmg}|n damage!"
self.msg(message)
# call hook
target.at_damage(dmg, attacker=attacker)
# note that we mustn't remove anyone from combat yet, because this is
# happening simultaneously. So checking of the final hp
# and rolling of death etc happens in the combathandler at the end of the turn.
else:
# a miss
message = f" $You() $conj(miss) $You({target.key})."
if quality is Ability.CRITICAL_FAILURE:
attacker.weapon.quality -= 1
message += ".. it's a |rcritical miss!|n, damaging the weapon."
self.msg(message)
if weapon.at_pre_use(attacker, target):
weapon.use(attacker, target, advantage=self.has_advantage(attacker, target))
weapon.at_post_use(attacker, target)
class CombatActionStunt(CombatAction):
@ -317,6 +282,7 @@ class CombatActionStunt(CombatAction):
recipient = self.recipient # the one to receive the effect of the stunt
target = self.target # the affected by the stunt (can be the same as recipient/combatant)
is_success = False
txt = ""
if target == self.combatant:
# can always grant dis/advantage against yourself
@ -369,7 +335,7 @@ class CombatActionUseItem(CombatAction):
action_dict = {
"key": "use",
"item": Object
"target": Character/NPC/Object
"target": Character/NPC/Object/None
}
Note:
@ -383,22 +349,14 @@ class CombatActionUseItem(CombatAction):
user = self.combatant
target = self.target
if user == target:
# always manage to use the item on yourself
is_success = True
else:
if item.has_obj_type(ObjType.WEAPON):
# this is something that harms the target. We need to roll defense
is_success, _, txt = rules.dice.opposed_saving_throw(
user,
target,
attack_type=item.attack_type,
defense_type=item.defense_type,
advantage=self.has_advantage(user, target),
disadvantage=self.has_disadvantage(user, target),
)
item.at_use(self.combatant, self.target)
if item.at_pre_use(user, target):
item.use(
user,
target,
advantage=self.has_advantage(user, target),
disadvantage=self.has_disadvantage(user, target),
)
item.at_post_use(user, target)
class CombatActionWield(CombatAction):
@ -514,13 +472,13 @@ class EvAdventureCombatHandler(DefaultScript):
# who is involved in combat, and their action queue,
# as {combatant: [actiondict, actiondict,...]}
combatants = AttributeProperty(defaultdict(deque))
combatants = AttributeProperty(dict)
advantage_matrix = AttributeProperty(defaultdict(dict))
disadvantage_matrix = AttributeProperty(defaultdict(dict))
fleeing_combatants = AttributeProperty(dict)
defeated_combatants = AttributeProperty(dict)
defeated_combatants = AttributeProperty(list)
def msg(self, message, combatant=None, broadcast=True):
"""
@ -568,7 +526,7 @@ class EvAdventureCombatHandler(DefaultScript):
"""
for combatant in combatants:
if combatant not in self.combatants:
self.combatants[combatant] = deque((), self.max_action_queue_size)
self.combatants[combatant] = deque((), maxlen=self.max_action_queue_size)
return True
def remove_combatant(self, combatant):
@ -693,7 +651,8 @@ class EvAdventureCombatHandler(DefaultScript):
# PCs roll on the death table here, NPCs die. Even if PCs survive, they
# are still out of the fight.
combatant.at_defeat()
self.defeated_combatants.append(self.combatant.pop(combatant))
self.combatants.pop(combatant)
self.defeated_combatants.append(combatant)
self.msg("|r$You() $conj(fall) to the ground, defeated.|n", combatant=combatant)
# check if anyone managed to flee
@ -717,12 +676,8 @@ class EvAdventureCombatHandler(DefaultScript):
if not enemies:
# if one way or another, there are no more enemies to fight
still_standing = list_to_string(f"$You({comb.key})" for comb in allies)
knocked_out = list_to_string(
f"$You({comb.key})" for comb in self.defeated_combatants if comb.hp > 0
)
killed = list_to_string(
comb for comb in self.defeated_combatants if comb not in knocked_out
)
knocked_out = list_to_string(comb for comb in self.defeated_combatants if comb.hp > 0)
killed = list_to_string(comb for comb in self.defeated_combatants if comb.hp <= 0)
if still_standing:
txt = [f"The combat is over. {still_standing} are still standing."]

View file

@ -22,6 +22,7 @@ from evennia import AttributeProperty
from evennia.objects.objects import DefaultObject
from evennia.utils.utils import make_iter
from . import rules
from .enums import Ability, ObjType, WieldLocation
from .utils import get_obj_stats
@ -70,6 +71,25 @@ class EvAdventureObject(DefaultObject):
"""
return "No help for this item."
def at_pre_use(self, *args, **kwargs):
"""
Called before use. If returning False, usage should be aborted.
"""
return True
def use(self, *args, **kwargs):
"""
Use this object, whatever that may mean.
"""
raise NotImplementedError
def at_post_use(self, *args, **kwargs):
"""
Called after use happened.
"""
pass
class EvAdventureObjectFiller(EvAdventureObject):
"""
@ -119,17 +139,12 @@ class EvAdventureConsumable(EvAdventureObject):
size = AttributeProperty(0.25)
uses = AttributeProperty(1)
def at_use(self, user, *args, **kwargs):
def use(self, user, *args, **kwargs):
"""
Consume a 'use' of this item. Once it reaches 0 uses, it should normally
not be usable anymore and probably be deleted.
Args:
user (Object): The one using the item.
*args, **kwargs: Extra arguments depending on the usage and item.
Use the consumable.
"""
pass
raise NotImplementedError
def at_post_use(self, user, *args, **kwargs):
"""
@ -162,6 +177,43 @@ class EvAdventureWeapon(EvAdventureObject):
defense_type = AttributeProperty(Ability.ARMOR)
damage_roll = AttributeProperty("1d6")
def use(self, attacker, target, *args, advantage=False, disadvantage=False, **kwargs):
"""When a weapon is used, it attacks an opponent"""
is_hit, quality, txt = rules.dice.opposed_saving_throw(
attacker,
target,
attack_type=self.attack_type,
defense_type=self.defense_type,
advantage=advantage,
disadvantage=disadvantage,
)
self.msg(f"$You() $conj(attack) $You({target.key}) with {self.key}: {txt}")
if is_hit:
# enemy hit, calculate damage
dmg = rules.dice.roll(self.damage_roll)
if quality is Ability.CRITICAL_SUCCESS:
# doble damage roll for critical success
dmg += rules.dice.roll(self.damage_roll)
message = (
f" $You() |ycritically|n $conj(hit) $You({target.key}) for |r{dmg}|n damage!"
)
else:
message = f" $You() $conj(hit) $You({target.key}) for |r{dmg}|n damage!"
self.msg(message)
# call hook
target.at_damage(dmg, attacker=attacker)
else:
# a miss
message = f" $You() $conj(miss) $You({target.key})."
if quality is Ability.CRITICAL_FAILURE:
self.quality -= 1
message += ".. it's a |rcritical miss!|n, damaging the weapon."
self.msg(message)
class EvAdventureThrowable(EvAdventureWeapon, EvAdventureConsumable):
"""
@ -178,7 +230,7 @@ class EvAdventureThrowable(EvAdventureWeapon, EvAdventureConsumable):
damage_roll = AttributeProperty("1d6")
class WeaponEmptyHand:
class WeaponEmptyHand(EvAdventureWeapon):
"""
This is a dummy-class loaded when you wield no weapons. We won't create any db-object for it.
@ -192,9 +244,6 @@ class WeaponEmptyHand:
damage_roll = "1d4"
quality = 100000 # let's assume fists are always available ...
def __repr__(self):
return "<WeaponEmptyHand>"
class EvAdventureRunestone(EvAdventureWeapon, EvAdventureConsumable):
"""

View file

@ -11,7 +11,7 @@ from evennia.utils.test_resources import BaseEvenniaTest
from .. import combat_turnbased as combat
from ..characters import EvAdventureCharacter
from ..enums import WieldLocation
from ..enums import Ability, WieldLocation
from ..npcs import EvAdventureMob
from ..objects import EvAdventureConsumable, EvAdventureRunestone, EvAdventureWeapon
from ..rooms import EvAdventureRoom
@ -53,10 +53,34 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
attributes=(("is_idle", True),),
)
# mock the msg so we can check what they were sent later
self.combatant.msg = Mock()
self.target.msg = Mock()
self.combathandler = combat.get_or_create_combathandler(self.combatant)
# add target to combat
self.combathandler.add_combatants(self.target)
def _get_action(self, action_dict={"key": "nothing"}):
action_class = self.combathandler.action_classes[action_dict["key"]]
return action_class(self.combathandler, self.combatant, action_dict)
def _run_actions(
self, action_dict, action_dict2={"key": "nothing"}, combatant_msg=None, target_msg=None
):
"""
Helper method to run an action and check so combatant saw the expected message.
"""
self.combathandler.queue_action(self.combatant, action_dict)
self.combathandler.queue_action(self.target, action_dict2)
self.combathandler.execute_full_turn()
if combatant_msg is not None:
# this works because we mock combatant.msg in SetUp
self.combatant.msg.assert_called_with(combatant_msg)
if target_msg is not None:
# this works because we mock target.msg in SetUp
self.combatant.msg.assert_called_with(target_msg)
def test_combatanthandler_setup(self):
"""Testing all is set up correctly in the combathandler"""
@ -166,30 +190,233 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
[call(self.combatant), call(self.target)], any_order=True
)
def _get_action(self, action_dict, action_dict2={"key": "nothing"}):
def test_combat_action(self):
"""General tests of action functionality"""
self.combathandler.queue_action(self.combatant, action_dict)
self.combathandler.queue_action(self.target, action_dict2)
combatant = self.combatant
target = self.target
action_cls1 = self.combathandler.action_classes[action_dict["key"]]
action_cls2 = self.combathandler.action_classes[action_dict2["key"]]
action = self._get_action({"key": "nothing"})
return action_cls1, action_cls2
self.assertTrue(action.can_use())
action.give_advantage(combatant, target)
action.give_disadvantage(combatant, target)
self.assertTrue(action.has_advantage(combatant, target))
self.assertTrue(action.has_disadvantage(combatant, target))
action.lose_advantage(combatant, target)
action.lose_disadvantage(combatant, target)
self.assertFalse(action.has_advantage(combatant, target))
self.assertFalse(action.has_disadvantage(combatant, target))
action.flee(combatant)
self.assertIn(combatant, self.combathandler.fleeing_combatants)
action.unflee(combatant)
self.assertNotIn(combatant, self.combathandler.fleeing_combatants)
action.msg(f"$You() attack $You({target.key}).")
combatant.msg.assert_called_with(text=("You attack testmonster.", {}), from_obj=combatant)
def test_action__do_nothing(self):
"""Do nothing"""
actiondict = {"key": "nothing"}
actioncls1, actioncls2 = self._get_action(actiondict, actiondict)
self.assertEqual(actioncls1, actioncls2)
self.combathandler.execute_full_turn()
self._run_actions(actiondict, actiondict)
self.assertEqual(self.combathandler.turn, 1)
# @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
self.combatant.msg.assert_not_called()
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
def test_attack__miss(self, mock_randint):
actiondict = {"key": "attack", "target": self.target}
mock_randint.return_value = 8 # target has default armor 11, so 8+1 str will miss
self._run_actions(actiondict)
self.assertEqual(self.target.hp, 4)
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
def test_attack__success__still_alive(self, mock_randint):
actiondict = {"key": "attack", "target": self.target}
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
# make sure target survives
self.target.hp = 20
self._run_actions(actiondict)
self.assertEqual(self.target.hp, 9)
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
def test_attack__success__kill(self, mock_randint):
actiondict = {"key": "attack", "target": self.target}
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
self._run_actions(actiondict)
self.assertEqual(self.target.hp, -7)
# 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):
action_dict = {
"key": "stunt",
"recipient": self.combatant,
"target": self.target,
"advantage": True,
"stunt_type": Ability.STR,
"defense_type": Ability.DEX,
}
mock_randint.return_value = 8 # fails 8+1 dex vs DEX 11 defence
self._run_actions(action_dict)
self.assertEqual(self.combathandler.advantage_matrix[self.combatant], {})
self.assertEqual(self.combathandler.disadvantage_matrix[self.combatant], {})
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
def test_stunt_advantage__success(self, mock_randint):
action_dict = {
"key": "stunt",
"recipient": self.combatant,
"target": self.target,
"advantage": True,
"stunt_type": Ability.STR,
"defense_type": Ability.DEX,
}
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
self._run_actions(action_dict)
self.assertEqual(
bool(self.combathandler.advantage_matrix[self.combatant][self.target]), True
)
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
def test_stunt_disadvantage__success(self, mock_randint):
action_dict = {
"key": "stunt",
"recipient": self.target,
"target": self.combatant,
"advantage": False,
"stunt_type": Ability.STR,
"defense_type": Ability.DEX,
}
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
self._run_actions(action_dict)
self.assertEqual(
bool(self.combathandler.disadvantage_matrix[self.target][self.combatant]), True
)
def test_use_item(self):
"""
Use up a potion during combat.
"""
item = create.create_object(
EvAdventureConsumable, key="Healing potion", attributes=[("uses", 2)]
)
item.use = Mock()
action_dict = {
"key": "use",
"item": item,
"target": self.target,
}
self.assertEqual(item.uses, 2)
self._run_actions(action_dict)
self.assertEqual(item.uses, 1)
self._run_actions(action_dict)
self.assertEqual(item.pk, None) # deleted, it was used up
def test_swap_wielded_weapon_or_spell(self):
"""
First draw a weapon (from empty fists), then swap that out to another weapon, then
swap to a spell rune.
"""
sword = create.create_object(EvAdventureWeapon, key="sword")
zweihander = create.create_object(
EvAdventureWeapon,
key="zweihander",
attributes=(("inventory_use_slot", WieldLocation.TWO_HANDS),),
)
runestone = create.create_object(EvAdventureRunestone, key="ice rune")
# check hands are empty
self.assertEqual(self.combatant.weapon.key, "Empty Fists")
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], None)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], None)
# swap to sword
actiondict = {"key": "wield", "item": sword}
self._run_actions(actiondict)
self.assertEqual(self.combatant.weapon, sword)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], sword)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], None)
# swap to zweihander (two-handed sword)
actiondict["item"] = zweihander
from evennia import set_trace
set_trace()
self._run_actions(actiondict)
self.assertEqual(self.combatant.weapon, zweihander)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], None)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], zweihander)
# swap to runestone (also using two hands)
actiondict["item"] = runestone
self._run_actions(actiondict)
self.assertEqual(self.combatant.weapon, runestone)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], None)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], runestone)
# swap back to normal one-handed sword
actiondict["item"] = sword
self._run_actions(actiondict)
self.assertEqual(self.combatant.weapon, sword)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], sword)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], None)
# def test_flee__success(self):
# """
# Test fleeing twice, leading to leaving combat.
#
# """
# # first flee records the fleeing state
# self._run_action(combat_turnbased.CombatActionFlee, None)
# self.assertTrue(self.combatant in self.combathandler.fleeing_combatants)
#
# # second flee should remove combatant
# self._run_action(combat_turnbased.CombatActionFlee, None)
# self.assertIsNone(self.combathandler.pk)
#
# @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
# def test_flee__blocked(self, mock_randint):
# """ """
# mock_randint.return_value = 11 # means block will succeed
#
# self._run_action(combat_turnbased.CombatActionFlee, None)
# self.assertTrue(self.combatant in self.combathandler.fleeing_combatants)
#
# # other combatant blocks in the same turn
# self.combathandler.register_action(
# self.combatant, combat_turnbased.CombatActionFlee.key, None
# )
# self.combathandler.register_action(
# self.target, combat_turnbased.CombatActionBlock.key, self.combatant
# )
# self.combathandler._end_turn()
# # the fleeing combatant should remain now
# self.assertTrue(self.combatant not in self.combathandler.fleeing_combatants)
# self.assertTrue(self.combatant in self.combathandler.combatants)
# class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):