Continue making unit tests for combathandler

This commit is contained in:
Griatch 2023-03-05 00:32:19 +01:00
parent 3f96004e99
commit b0d6af9cc3
3 changed files with 224 additions and 48 deletions

View file

@ -492,10 +492,15 @@ class EvAdventureCombatHandler(DefaultScript):
# how many actions can be queued at a time (per combatant)
max_action_queue_size = 1
# available actions
# available actions in combat
action_classes = {
"nothing": CombatActionDoNothing,
"attack": CombatActionAttack,
"stunt": CombatActionStunt,
"use": CombatActionUseItem,
"wield": CombatActionWield,
"flee": CombatActionFlee,
"hinder": CombatActionHinder,
}
# fallback action if not selecting anything
@ -509,7 +514,7 @@ class EvAdventureCombatHandler(DefaultScript):
# who is involved in combat, and their action queue,
# as {combatant: [actiondict, actiondict,...]}
combatants = AttributeProperty(defaultdict(list))
combatants = AttributeProperty(defaultdict(deque))
advantage_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},
)
def add_combatant(self, combatant):
def add_combatants(self, *combatants):
"""
Add a new combatant to the battle.
Args:
combatant (EvAdventureCharacter, EvAdventureNPC): A combatant to add to
*combatants (EvAdventureCharacter, EvAdventureNPC): Any number of combatants to add to
the combat.
Returns:
bool: True if the combatant was added, False otherwise (that is, they
were already added from before).
"""
if combatant not in self.combatants:
self.combatants[combatant] = deque((), self.max_action_queue_size)
return True
for combatant in combatants:
if combatant not in self.combatants:
self.combatants[combatant] = deque((), self.max_action_queue_size)
return True
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 = self.combatants[combatant]
action_dict = queue[0] if queue else COMBAT_ACTION_DICT_DONOTHING
action_queue = self.combatants[combatant]
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
queue.rotate(-1)
action_queue.rotate(-1)
# use the action-dict to select and create an action from an action class
action_class = self.action_classes[action_dict["key"]]
action = action_class(combatant, action_dict)
action = action_class(self, combatant, action_dict)
action.execute()
@ -674,7 +680,8 @@ class EvAdventureCombatHandler(DefaultScript):
"""
self.turn += 1
# 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
for combatant in combatants:
@ -704,11 +711,11 @@ class EvAdventureCombatHandler(DefaultScript):
allies, enemies = (), ()
else:
# 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)
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)
knocked_out = list_to_string(
f"$You({comb.key})" for comb in self.defeated_combatants if comb.hp > 0
@ -729,6 +736,37 @@ class EvAdventureCombatHandler(DefaultScript):
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)
@ -770,15 +808,10 @@ class _CmdCombatBase(Command):
@property
def combathandler(self):
self.combathandler = self.caller.location.scripts.get(self.combathandler_name)
if not self.combathandler:
self.combathandler = create_script(
EvAdventureCombatHandler,
key=combathandler_name,
obj=location,
interval=self.combat_tick,
persistent=True,
)
combathandler = getattr(self, "combathandler", None)
if not combathandler:
self.combathandler = combathandler = get_or_create_combathandler(self.caller)
return combathandler
def parse(self):
super().parse()
@ -847,7 +880,7 @@ class CmdAttack(_CmdCombatBase):
return
# this can be done over and over
is_new = self.combathandler.add_combatant(self)
is_new = self.combathandler.add_combatants(self)
if is_new:
# just joined combat - add the combat cmdset
self.caller.cmdset.add(CombatCmdSet)

View file

@ -146,6 +146,23 @@ class EvAdventureConsumable(EvAdventureObject):
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):
"""
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>"
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):
"""
Base class for magic runestones. In _Knave_, every spell is represented by a rune stone

View file

@ -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.test_resources import BaseEvenniaTest
from .. import combat_turnbased
from .. import combat_turnbased as combat
from ..characters import EvAdventureCharacter
from ..enums import WieldLocation
from ..npcs import EvAdventureMob
from ..objects import EvAdventureConsumable, EvAdventureRunestone, EvAdventureWeapon
from ..rooms import EvAdventureRoom
from .mixins import EvAdventureMixin
class EvAdventureCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
class EvAdventureCombatHandlerTest(BaseEvenniaTest):
"""
Test methods on the turn-based combat handler
@ -31,13 +33,19 @@ class EvAdventureCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
)
@patch(
"evennia.contrib.tutorials.evadventure.combat_turnbased.delay",
new=MagicMock(return_value=None),
new=Mock(return_value=None),
)
def setUp(self):
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_death = True
self.combatant = self.character
self.target = create.create_object(
EvAdventureMob,
key="testmonster",
@ -45,8 +53,143 @@ class EvAdventureCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
attributes=(("is_idle", True),),
)
# this already starts turn 1
self.combathandler = combat_turnbased.join_combat(self.combatant, self.target)
self.combathandler = combat.get_or_create_combathandler(self.combatant)
# 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):