Continue making unit tests for combathandler
This commit is contained in:
parent
3f96004e99
commit
b0d6af9cc3
3 changed files with 224 additions and 48 deletions
|
|
@ -492,10 +492,15 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
# how many actions can be queued at a time (per combatant)
|
# how many actions can be queued at a time (per combatant)
|
||||||
max_action_queue_size = 1
|
max_action_queue_size = 1
|
||||||
|
|
||||||
# available actions
|
# available actions in combat
|
||||||
action_classes = {
|
action_classes = {
|
||||||
"nothing": CombatActionDoNothing,
|
"nothing": CombatActionDoNothing,
|
||||||
"attack": CombatActionAttack,
|
"attack": CombatActionAttack,
|
||||||
|
"stunt": CombatActionStunt,
|
||||||
|
"use": CombatActionUseItem,
|
||||||
|
"wield": CombatActionWield,
|
||||||
|
"flee": CombatActionFlee,
|
||||||
|
"hinder": CombatActionHinder,
|
||||||
}
|
}
|
||||||
|
|
||||||
# fallback action if not selecting anything
|
# fallback action if not selecting anything
|
||||||
|
|
@ -509,7 +514,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
|
|
||||||
# who is involved in combat, and their action queue,
|
# who is involved in combat, and their action queue,
|
||||||
# as {combatant: [actiondict, actiondict,...]}
|
# as {combatant: [actiondict, actiondict,...]}
|
||||||
combatants = AttributeProperty(defaultdict(list))
|
combatants = AttributeProperty(defaultdict(deque))
|
||||||
|
|
||||||
advantage_matrix = AttributeProperty(defaultdict(dict))
|
advantage_matrix = AttributeProperty(defaultdict(dict))
|
||||||
disadvantage_matrix = AttributeProperty(defaultdict(dict))
|
disadvantage_matrix = AttributeProperty(defaultdict(dict))
|
||||||
|
|
@ -549,21 +554,22 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
mapping={locobj.key: locobj for locobj in location_objs},
|
mapping={locobj.key: locobj for locobj in location_objs},
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_combatant(self, combatant):
|
def add_combatants(self, *combatants):
|
||||||
"""
|
"""
|
||||||
Add a new combatant to the battle.
|
Add a new combatant to the battle.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
combatant (EvAdventureCharacter, EvAdventureNPC): A combatant to add to
|
*combatants (EvAdventureCharacter, EvAdventureNPC): Any number of combatants to add to
|
||||||
the combat.
|
the combat.
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the combatant was added, False otherwise (that is, they
|
bool: True if the combatant was added, False otherwise (that is, they
|
||||||
were already added from before).
|
were already added from before).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if combatant not in self.combatants:
|
for combatant in combatants:
|
||||||
self.combatants[combatant] = deque((), self.max_action_queue_size)
|
if combatant not in self.combatants:
|
||||||
return True
|
self.combatants[combatant] = deque((), self.max_action_queue_size)
|
||||||
|
return True
|
||||||
|
|
||||||
def remove_combatant(self, combatant):
|
def remove_combatant(self, combatant):
|
||||||
"""
|
"""
|
||||||
|
|
@ -656,14 +662,14 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
queue will be rotated to the left and be `[b, c, a]` (so next time, `b` will be used).
|
queue will be rotated to the left and be `[b, c, a]` (so next time, `b` will be used).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
queue = self.combatants[combatant]
|
action_queue = self.combatants[combatant]
|
||||||
action_dict = queue[0] if queue else COMBAT_ACTION_DICT_DONOTHING
|
action_dict = action_queue[0] if action_queue else COMBAT_ACTION_DICT_DONOTHING
|
||||||
# rotate the queue to the left so that the first element is now the last one
|
# rotate the queue to the left so that the first element is now the last one
|
||||||
queue.rotate(-1)
|
action_queue.rotate(-1)
|
||||||
|
|
||||||
# use the action-dict to select and create an action from an action class
|
# use the action-dict to select and create an action from an action class
|
||||||
action_class = self.action_classes[action_dict["key"]]
|
action_class = self.action_classes[action_dict["key"]]
|
||||||
action = action_class(combatant, action_dict)
|
action = action_class(self, combatant, action_dict)
|
||||||
|
|
||||||
action.execute()
|
action.execute()
|
||||||
|
|
||||||
|
|
@ -674,7 +680,8 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
"""
|
"""
|
||||||
self.turn += 1
|
self.turn += 1
|
||||||
# random turn order
|
# random turn order
|
||||||
combatants = random.shuffle(list(self.combatants.keys()))
|
combatants = list(self.combatants.keys())
|
||||||
|
random.shuffle(combatants) # shuffles in place
|
||||||
|
|
||||||
# do everyone's next queued combat action
|
# do everyone's next queued combat action
|
||||||
for combatant in combatants:
|
for combatant in combatants:
|
||||||
|
|
@ -704,11 +711,11 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
allies, enemies = (), ()
|
allies, enemies = (), ()
|
||||||
else:
|
else:
|
||||||
# grab a random survivor and check of they have any living enemies.
|
# grab a random survivor and check of they have any living enemies.
|
||||||
surviving_combatant = random.choice(list(self.combatant.keys()))
|
surviving_combatant = random.choice(list(self.combatants.keys()))
|
||||||
allies, enemies = self.get_sides(surviving_combatant)
|
allies, enemies = self.get_sides(surviving_combatant)
|
||||||
|
|
||||||
if not enemies:
|
if not enemies:
|
||||||
# one way or another, there are no more enemies to fight
|
# 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)
|
still_standing = list_to_string(f"$You({comb.key})" for comb in allies)
|
||||||
knocked_out = list_to_string(
|
knocked_out = list_to_string(
|
||||||
f"$You({comb.key})" for comb in self.defeated_combatants if comb.hp > 0
|
f"$You({comb.key})" for comb in self.defeated_combatants if comb.hp > 0
|
||||||
|
|
@ -729,6 +736,37 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
self.stop_combat()
|
self.stop_combat()
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_combathandler(combatant, combathandler_name="combathandler", combat_tick=5):
|
||||||
|
"""
|
||||||
|
Joins or continues combat. This is a access function that will either get the
|
||||||
|
combathandler on the current room or create a new one.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
combatant (EvAdventureCharacter, EvAdventureNPC): The one to
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CombatHandler: The new or created combathandler.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
location = combatant.location
|
||||||
|
|
||||||
|
if not location:
|
||||||
|
raise CombatFailure("Cannot start combat without a location.")
|
||||||
|
|
||||||
|
combathandler = location.scripts.get(combathandler_name)
|
||||||
|
if not combathandler:
|
||||||
|
combathandler = create_script(
|
||||||
|
EvAdventureCombatHandler,
|
||||||
|
key=combathandler_name,
|
||||||
|
obj=location,
|
||||||
|
interval=combat_tick,
|
||||||
|
persistent=True,
|
||||||
|
)
|
||||||
|
combathandler.add_combatants(combatant)
|
||||||
|
return combathandler
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# Tick-based fast combat (Diku-style)
|
# Tick-based fast combat (Diku-style)
|
||||||
|
|
@ -770,15 +808,10 @@ class _CmdCombatBase(Command):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def combathandler(self):
|
def combathandler(self):
|
||||||
self.combathandler = self.caller.location.scripts.get(self.combathandler_name)
|
combathandler = getattr(self, "combathandler", None)
|
||||||
if not self.combathandler:
|
if not combathandler:
|
||||||
self.combathandler = create_script(
|
self.combathandler = combathandler = get_or_create_combathandler(self.caller)
|
||||||
EvAdventureCombatHandler,
|
return combathandler
|
||||||
key=combathandler_name,
|
|
||||||
obj=location,
|
|
||||||
interval=self.combat_tick,
|
|
||||||
persistent=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse(self):
|
def parse(self):
|
||||||
super().parse()
|
super().parse()
|
||||||
|
|
@ -847,7 +880,7 @@ class CmdAttack(_CmdCombatBase):
|
||||||
return
|
return
|
||||||
|
|
||||||
# this can be done over and over
|
# this can be done over and over
|
||||||
is_new = self.combathandler.add_combatant(self)
|
is_new = self.combathandler.add_combatants(self)
|
||||||
if is_new:
|
if is_new:
|
||||||
# just joined combat - add the combat cmdset
|
# just joined combat - add the combat cmdset
|
||||||
self.caller.cmdset.add(CombatCmdSet)
|
self.caller.cmdset.add(CombatCmdSet)
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,23 @@ class EvAdventureConsumable(EvAdventureObject):
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureWeapon(EvAdventureObject):
|
||||||
|
"""
|
||||||
|
Base weapon class for all EvAdventure weapons.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
obj_type = ObjType.WEAPON
|
||||||
|
inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND)
|
||||||
|
quality = AttributeProperty(3)
|
||||||
|
|
||||||
|
# what ability used to attack with this weapon
|
||||||
|
attack_type = AttributeProperty(Ability.STR)
|
||||||
|
# what defense stat of the enemy it must defeat
|
||||||
|
defense_type = AttributeProperty(Ability.ARMOR)
|
||||||
|
damage_roll = AttributeProperty("1d6")
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureThrowable(EvAdventureWeapon, EvAdventureConsumable):
|
class EvAdventureThrowable(EvAdventureWeapon, EvAdventureConsumable):
|
||||||
"""
|
"""
|
||||||
Something you can throw at an enemy to harm them once, like a knife or exploding potion/grenade.
|
Something you can throw at an enemy to harm them once, like a knife or exploding potion/grenade.
|
||||||
|
|
@ -179,23 +196,6 @@ class WeaponEmptyHand:
|
||||||
return "<WeaponEmptyHand>"
|
return "<WeaponEmptyHand>"
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureWeapon(EvAdventureObject):
|
|
||||||
"""
|
|
||||||
Base weapon class for all EvAdventure weapons.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
obj_type = ObjType.WEAPON
|
|
||||||
inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND)
|
|
||||||
quality = AttributeProperty(3)
|
|
||||||
|
|
||||||
# what ability used to attack with this weapon
|
|
||||||
attack_type = AttributeProperty(Ability.STR)
|
|
||||||
# what defense stat of the enemy it must defeat
|
|
||||||
defense_type = AttributeProperty(Ability.ARMOR)
|
|
||||||
damage_roll = AttributeProperty("1d6")
|
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureRunestone(EvAdventureWeapon, EvAdventureConsumable):
|
class EvAdventureRunestone(EvAdventureWeapon, EvAdventureConsumable):
|
||||||
"""
|
"""
|
||||||
Base class for magic runestones. In _Knave_, every spell is represented by a rune stone
|
Base class for magic runestones. In _Knave_, every spell is represented by a rune stone
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,22 @@ Test EvAdventure combat.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest.mock import MagicMock, patch
|
from collections import deque
|
||||||
|
from unittest.mock import Mock, call, patch
|
||||||
|
|
||||||
from evennia.utils import create
|
from evennia.utils import create
|
||||||
from evennia.utils.test_resources import BaseEvenniaTest
|
from evennia.utils.test_resources import BaseEvenniaTest
|
||||||
|
|
||||||
from .. import combat_turnbased
|
from .. import combat_turnbased as combat
|
||||||
from ..characters import EvAdventureCharacter
|
from ..characters import EvAdventureCharacter
|
||||||
from ..enums import WieldLocation
|
from ..enums import WieldLocation
|
||||||
from ..npcs import EvAdventureMob
|
from ..npcs import EvAdventureMob
|
||||||
from ..objects import EvAdventureConsumable, EvAdventureRunestone, EvAdventureWeapon
|
from ..objects import EvAdventureConsumable, EvAdventureRunestone, EvAdventureWeapon
|
||||||
|
from ..rooms import EvAdventureRoom
|
||||||
from .mixins import EvAdventureMixin
|
from .mixins import EvAdventureMixin
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
||||||
"""
|
"""
|
||||||
Test methods on the turn-based combat handler
|
Test methods on the turn-based combat handler
|
||||||
|
|
||||||
|
|
@ -31,13 +33,19 @@ class EvAdventureCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
)
|
)
|
||||||
@patch(
|
@patch(
|
||||||
"evennia.contrib.tutorials.evadventure.combat_turnbased.delay",
|
"evennia.contrib.tutorials.evadventure.combat_turnbased.delay",
|
||||||
new=MagicMock(return_value=None),
|
new=Mock(return_value=None),
|
||||||
)
|
)
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
|
self.location = create.create_object(EvAdventureRoom, key="testroom")
|
||||||
|
self.combatant = create.create_object(
|
||||||
|
EvAdventureCharacter, key="testchar", location=self.location
|
||||||
|
)
|
||||||
|
|
||||||
self.location.allow_combat = True
|
self.location.allow_combat = True
|
||||||
self.location.allow_death = True
|
self.location.allow_death = True
|
||||||
self.combatant = self.character
|
|
||||||
self.target = create.create_object(
|
self.target = create.create_object(
|
||||||
EvAdventureMob,
|
EvAdventureMob,
|
||||||
key="testmonster",
|
key="testmonster",
|
||||||
|
|
@ -45,8 +53,143 @@ class EvAdventureCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
attributes=(("is_idle", True),),
|
attributes=(("is_idle", True),),
|
||||||
)
|
)
|
||||||
|
|
||||||
# this already starts turn 1
|
self.combathandler = combat.get_or_create_combathandler(self.combatant)
|
||||||
self.combathandler = combat_turnbased.join_combat(self.combatant, self.target)
|
# add target to combat
|
||||||
|
self.combathandler.add_combatants(self.target)
|
||||||
|
|
||||||
|
def test_combatanthandler_setup(self):
|
||||||
|
"""Testing all is set up correctly in the combathandler"""
|
||||||
|
|
||||||
|
chandler = self.combathandler
|
||||||
|
self.assertEqual(dict(chandler.combatants), {self.combatant: deque(), self.target: deque()})
|
||||||
|
self.assertEqual(
|
||||||
|
dict(chandler.action_classes),
|
||||||
|
{
|
||||||
|
"nothing": combat.CombatActionDoNothing,
|
||||||
|
"attack": combat.CombatActionAttack,
|
||||||
|
"stunt": combat.CombatActionStunt,
|
||||||
|
"use": combat.CombatActionUseItem,
|
||||||
|
"wield": combat.CombatActionWield,
|
||||||
|
"flee": combat.CombatActionFlee,
|
||||||
|
"hinder": combat.CombatActionHinder,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(chandler.flee_timeout, 1)
|
||||||
|
self.assertEqual(dict(chandler.advantage_matrix), {})
|
||||||
|
self.assertEqual(dict(chandler.disadvantage_matrix), {})
|
||||||
|
self.assertEqual(dict(chandler.fleeing_combatants), {})
|
||||||
|
self.assertEqual(dict(chandler.defeated_combatants), {})
|
||||||
|
|
||||||
|
def test_combathandler_msg(self):
|
||||||
|
"""Test sending messages to all in handler"""
|
||||||
|
|
||||||
|
self.location.msg_contents = Mock()
|
||||||
|
|
||||||
|
self.combathandler.msg("test_message")
|
||||||
|
|
||||||
|
self.location.msg_contents.assert_called_with(
|
||||||
|
"test_message",
|
||||||
|
exclude=[],
|
||||||
|
from_obj=None,
|
||||||
|
mapping={"testchar": self.combatant, "testmonster": self.target},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_remove_combatant(self):
|
||||||
|
"""Remove a combatant."""
|
||||||
|
|
||||||
|
self.combathandler.remove_combatant(self.target)
|
||||||
|
|
||||||
|
self.assertEqual(dict(self.combathandler.combatants), {self.combatant: deque()})
|
||||||
|
|
||||||
|
def test_stop_combat(self):
|
||||||
|
"""Stopping combat, making sure combathandler is deleted."""
|
||||||
|
|
||||||
|
self.combathandler.stop_combat()
|
||||||
|
self.assertIsNone(self.combathandler.pk)
|
||||||
|
|
||||||
|
def test_get_sides(self):
|
||||||
|
"""Getting the sides of combat"""
|
||||||
|
|
||||||
|
combatant2 = create.create_object(
|
||||||
|
EvAdventureCharacter, key="testchar2", location=self.location
|
||||||
|
)
|
||||||
|
target2 = create.create_object(
|
||||||
|
EvAdventureMob,
|
||||||
|
key="testmonster2",
|
||||||
|
location=self.location,
|
||||||
|
attributes=(("is_idle", True),),
|
||||||
|
)
|
||||||
|
self.combathandler.add_combatants(combatant2, target2)
|
||||||
|
|
||||||
|
# allies to combatant
|
||||||
|
allies, enemies = self.combathandler.get_sides(self.combatant)
|
||||||
|
self.assertEqual((allies, enemies), ([combatant2], [self.target, target2]))
|
||||||
|
|
||||||
|
# allies to monster
|
||||||
|
allies, enemies = self.combathandler.get_sides(self.target)
|
||||||
|
self.assertEqual((allies, enemies), ([target2], [self.combatant, combatant2]))
|
||||||
|
|
||||||
|
def test_queue_and_execute_action(self):
|
||||||
|
"""Queue actions and execute"""
|
||||||
|
|
||||||
|
donothing = {"key": "nothing"}
|
||||||
|
|
||||||
|
self.combathandler.queue_action(self.combatant, donothing)
|
||||||
|
self.assertEqual(
|
||||||
|
dict(self.combathandler.combatants),
|
||||||
|
{self.combatant: deque([donothing]), self.target: deque()},
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_action = Mock()
|
||||||
|
self.combathandler.action_classes["nothing"] = Mock(return_value=mock_action)
|
||||||
|
|
||||||
|
self.combathandler.execute_next_action(self.combatant)
|
||||||
|
|
||||||
|
self.combathandler.action_classes["nothing"].assert_called_with(
|
||||||
|
self.combathandler, self.combatant, donothing
|
||||||
|
)
|
||||||
|
mock_action.execute.assert_called_once()
|
||||||
|
|
||||||
|
def test_execute_full_turn(self):
|
||||||
|
"""Run a full (passive) turn"""
|
||||||
|
|
||||||
|
donothing = {"key": "nothing"}
|
||||||
|
|
||||||
|
self.combathandler.queue_action(self.combatant, donothing)
|
||||||
|
self.combathandler.queue_action(self.target, donothing)
|
||||||
|
|
||||||
|
self.combathandler.execute_next_action = Mock()
|
||||||
|
|
||||||
|
self.combathandler.execute_full_turn()
|
||||||
|
|
||||||
|
self.combathandler.execute_next_action.assert_has_calls(
|
||||||
|
[call(self.combatant), call(self.target)], any_order=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_action(self, action_dict, action_dict2={"key": "nothing"}):
|
||||||
|
|
||||||
|
self.combathandler.queue_action(self.combatant, action_dict)
|
||||||
|
self.combathandler.queue_action(self.target, action_dict2)
|
||||||
|
|
||||||
|
action_cls1 = self.combathandler.action_classes[action_dict["key"]]
|
||||||
|
action_cls2 = self.combathandler.action_classes[action_dict2["key"]]
|
||||||
|
|
||||||
|
return action_cls1, action_cls2
|
||||||
|
|
||||||
|
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.assertEqual(self.combathandler.turn, 1)
|
||||||
|
|
||||||
|
# @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
|
|
||||||
|
|
||||||
# class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
# class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue