mirror of
https://github.com/evennia/evennia.git
synced 2026-03-31 21:17:17 +02:00
Made unit tests for evadventure rules
This commit is contained in:
parent
d19eac8ac9
commit
9361dff184
5 changed files with 418 additions and 87 deletions
|
|
@ -27,8 +27,6 @@ from evennia.utils import evmenu, evtable
|
|||
from .enums import Ability
|
||||
from . import rules
|
||||
|
||||
STUNT_DURATION = 2
|
||||
|
||||
|
||||
class CombatFailure(RuntimeError):
|
||||
"""
|
||||
|
|
@ -152,7 +150,8 @@ class CombatActionStunt(CombatAction):
|
|||
|
||||
class CombatActionAttack(CombatAction):
|
||||
"""
|
||||
A regular attack, using a wielded melee weapon.
|
||||
A regular attack, using a wielded weapon. Depending on weapon type, this will be a ranged or
|
||||
melee attack.
|
||||
|
||||
"""
|
||||
key = "attack"
|
||||
|
|
@ -261,14 +260,26 @@ class CombatActionChase(CombatAction):
|
|||
pass # they are getting away!
|
||||
|
||||
|
||||
|
||||
class EvAdventureCombatHandler(DefaultScript):
|
||||
"""
|
||||
This script is created when combat is initialized and stores a queue
|
||||
of all active participants. It's also possible to join (or leave) the fray later.
|
||||
|
||||
"""
|
||||
# these will all be checked if they are available at a given time.
|
||||
all_action_classes = [
|
||||
CombatActionDoNothing,
|
||||
CombatActionChase,
|
||||
CombatActionUseItem,
|
||||
CombatActionStunt,
|
||||
CombatActionAttack
|
||||
]
|
||||
|
||||
# attributes
|
||||
|
||||
# stores all combatants active in the combat
|
||||
combatants = AttributeProperty(list())
|
||||
combatant_actions = AttributeProperty(defaultdict(dict))
|
||||
action_queue = AttributeProperty(dict())
|
||||
|
||||
turn_stats = AttributeProperty(defaultdict(list))
|
||||
|
|
@ -281,15 +292,10 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
|
||||
fleeing_combatants = AttributeProperty(default=list())
|
||||
|
||||
|
||||
# actions that will be performed before a normal action
|
||||
move_actions = ("approach", "withdraw")
|
||||
|
||||
def at_init(self):
|
||||
self.ndb.actions = {
|
||||
"do_nothing": CombatActionDoNothing,
|
||||
}
|
||||
|
||||
|
||||
def _update_turn_stats(self, combatant, message):
|
||||
"""
|
||||
Store combat messages to display at the end of turn.
|
||||
|
|
@ -312,12 +318,16 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
|
||||
1. Do all regular actions
|
||||
2. Remove combatants that disengaged successfully
|
||||
3. Timeout advantages/disadvantages set for longer than STUNT_DURATION
|
||||
3. Timeout advantages/disadvantages
|
||||
|
||||
"""
|
||||
# do all actions
|
||||
for combatant in self.combatants:
|
||||
action, args, kwargs = self.action_queue[combatant]
|
||||
# read the current action type selected by the player
|
||||
action_class, args, kwargs = self.action_queue[combatant]
|
||||
# get the already initialized CombatAction instance (where state can be tracked)
|
||||
action = self.combatant_actions[combatant][action_class]
|
||||
# perform the action on the CombatAction instance
|
||||
action.use(combatant, *args, **kwargs)
|
||||
|
||||
# handle disengaging combatants
|
||||
|
|
@ -364,10 +374,13 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
def add_combatant(self, combatant):
|
||||
if combatant not in self.combatants:
|
||||
self.combatants.append(combatant)
|
||||
for action_class in self.all_action_classes:
|
||||
self.combatant_actions[combatant][action_class] = action_class(self, combatant)
|
||||
|
||||
def remove_combatant(self, combatant):
|
||||
if combatant in self.combatants:
|
||||
self.combatants.remove(combatant)
|
||||
self.combatant_actions[combatant][action_class].pop(None)
|
||||
|
||||
def get_combat_summary(self, combatant):
|
||||
"""
|
||||
|
|
@ -489,7 +502,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
|
||||
Args:
|
||||
combatant (Object): The one performing the action.
|
||||
action (str): An available action, will be prepended with `action_` and
|
||||
action (CombatAction): An available action, will be prepended with `action_` and
|
||||
used to call the relevant handler on this script.
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ class Ability(Enum):
|
|||
LEVEL = "level"
|
||||
XP = "xp"
|
||||
|
||||
CRITICAL_FAILURE = "critical_failure"
|
||||
CRITICAL_SUCCESS = "critical_success"
|
||||
|
||||
class WieldLocation(Enum):
|
||||
"""
|
||||
Wield (or wear) locations.
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ character_generation = {
|
|||
"student",
|
||||
"tracker",
|
||||
],
|
||||
"mifortuntes": [
|
||||
"misfortune": [
|
||||
"abandoned",
|
||||
"addicted",
|
||||
"blackmailed",
|
||||
|
|
@ -238,15 +238,15 @@ character_generation = {
|
|||
('20', "chain"),
|
||||
],
|
||||
"helmets and shields": [
|
||||
('1-13', "no helmet"),
|
||||
('1-13', "no helmet or shield"),
|
||||
('14-16', "helmet"),
|
||||
('17-19', "shield"),
|
||||
('20', "helmet and shield"),
|
||||
],
|
||||
"starting weapon": [ # note: these are all d6 dmg weapons
|
||||
('1-7', "dagger",
|
||||
'8-13', "club",
|
||||
'14-20', "staff"),
|
||||
('1-7', "dagger"),
|
||||
('8-13', "club"),
|
||||
('14-20', "staff"),
|
||||
],
|
||||
"dungeoning gear": [
|
||||
"rope, 50ft",
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ from random import randint
|
|||
from evennia.utils.evform import EvForm
|
||||
from evennia.utils.evtable import EvTable
|
||||
from .enums import Ability
|
||||
from .utils import roll
|
||||
from .random_tables import character_generation as chargen_table
|
||||
from .random_tables import (
|
||||
character_generation as chargen_table,
|
||||
death_and_dismemberment as death_table
|
||||
)
|
||||
|
||||
|
||||
# Basic rolls
|
||||
|
|
@ -96,13 +98,13 @@ class EvAdventureRollEngine:
|
|||
"""
|
||||
if not (advantage or disadvantage) or (advantage and disadvantage):
|
||||
# normal roll
|
||||
return roll("1d20")
|
||||
return self.roll("1d20")
|
||||
elif advantage:
|
||||
return max(roll("1d20"), roll("1d20"))
|
||||
return max(self.roll("1d20"), self.roll("1d20"))
|
||||
else:
|
||||
return min(roll("1d20"), roll("1d20"))
|
||||
return min(self.roll("1d20"), self.roll("1d20"))
|
||||
|
||||
def saving_throw(self, character, bonus_type=Ability.STR,
|
||||
def saving_throw(self, character, bonus_type=Ability.STR, target=15,
|
||||
advantage=False, disadvantage=False, modifier=0):
|
||||
"""
|
||||
A saving throw without a clear enemy to beat. In _Knave_ all unopposed saving
|
||||
|
|
@ -112,9 +114,11 @@ class EvAdventureRollEngine:
|
|||
character (Object): The one attempting to save themselves.
|
||||
bonus_type (enum.Ability): The ability bonus to apply, like strength or
|
||||
charisma.
|
||||
advantage (bool): Roll 2d20 and use the bigger number.
|
||||
disadvantage (bool): Roll 2d20 and use the smaller number.
|
||||
modifier (int): An additional +/- modifier to the roll.
|
||||
target (int, optional): Used for opposed throws (in Knave any regular
|
||||
saving through must always beat 15).
|
||||
advantage (bool, optional): Roll 2d20 and use the bigger number.
|
||||
disadvantage (bool, optional): Roll 2d20 and use the smaller number.
|
||||
modifier (int, optional): An additional +/- modifier to the roll.
|
||||
|
||||
Returns:
|
||||
tuple: (bool, str): If the save was passed or not. The second element is the
|
||||
|
|
@ -127,15 +131,15 @@ class EvAdventureRollEngine:
|
|||
Trying to overcome the effects of poison, roll d20 + Constitution-bonus above 15.
|
||||
|
||||
"""
|
||||
bonus = getattr(character, bonus_type, 1)
|
||||
bonus = getattr(character, bonus_type.value, 1)
|
||||
dice_roll = self.roll_with_advantage_or_disadvantage(advantage, disadvantage)
|
||||
if dice_roll == 1:
|
||||
quality = "critical failure"
|
||||
quality = Ability.CRITICAL_FAILURE
|
||||
elif dice_roll == 20:
|
||||
quality = "critical success"
|
||||
quality = Ability.CRITICAL_SUCCESS
|
||||
else:
|
||||
quality = None
|
||||
return (dice_roll + bonus + modifier) > 15, quality
|
||||
return (dice_roll + bonus + modifier) > target, quality
|
||||
|
||||
def opposed_saving_throw(
|
||||
self, attacker, defender,
|
||||
|
|
@ -162,19 +166,13 @@ class EvAdventureRollEngine:
|
|||
Advantage and disadvantage cancel each other out.
|
||||
|
||||
"""
|
||||
attack_bonus = getattr(attacker, attack_type.value, 1)
|
||||
# defense is always bonus + 10 in Knave
|
||||
defender_defense = getattr(defender, defense_type.value, 1) + 10
|
||||
dice_roll = self.roll_with_advantage_or_disadvantage(advantage, disadvantage)
|
||||
if dice_roll == 1:
|
||||
quality = "critical failure"
|
||||
elif dice_roll == 20:
|
||||
quality = "critical success"
|
||||
else:
|
||||
quality = None
|
||||
return (dice_roll + attack_bonus + modifier) > defender_defense, quality
|
||||
return self.saving_throw(attacker, bonus_type=attack_type,
|
||||
target=defender_defense,
|
||||
advantage=advantage, disadvantage=disadvantage,
|
||||
modifier=modifier)
|
||||
|
||||
def roll_random_table(self, dieroll, table, table_choices):
|
||||
def roll_random_table(self, dieroll, table_choices):
|
||||
"""
|
||||
Make a roll on a random table.
|
||||
|
||||
|
|
@ -196,7 +194,9 @@ class EvAdventureRollEngine:
|
|||
If the roll is outside of the listing, the closest edge value is used.
|
||||
|
||||
"""
|
||||
roll_result = roll(dieroll)
|
||||
roll_result = self.roll(dieroll)
|
||||
if not table_choices:
|
||||
return None
|
||||
|
||||
if isinstance(table_choices[0], (tuple, list)):
|
||||
# tuple with range conditional, like ('1-5', "Blue") or ('10', "Purple")
|
||||
|
|
@ -218,9 +218,9 @@ class EvAdventureRollEngine:
|
|||
# if we have no result, we are outside of the range, we pick the edge values. It is also
|
||||
# possible the range contains 'gaps', but that'd be an error in the random table itself.
|
||||
if roll_result > max_range:
|
||||
return max_range
|
||||
return table_choices[-1][1]
|
||||
else:
|
||||
return min_range
|
||||
return table_choices[0][1]
|
||||
else:
|
||||
# regular list - one line per value.
|
||||
roll_result = max(1, min(len(table_choices), roll_result))
|
||||
|
|
@ -240,7 +240,7 @@ class EvAdventureRollEngine:
|
|||
bool: False if morale roll failed, True otherwise.
|
||||
|
||||
"""
|
||||
return roll('2d6') <= defender.morale
|
||||
return self.roll('2d6') <= defender.morale
|
||||
|
||||
def heal(self, character, amount):
|
||||
"""
|
||||
|
|
@ -254,7 +254,7 @@ class EvAdventureRollEngine:
|
|||
damage = character.hp_max - character.hp
|
||||
character.hp += min(damage, amount)
|
||||
|
||||
def healing_from_rest(self, character):
|
||||
def heal_from_rest(self, character):
|
||||
"""
|
||||
A meal and a full night's rest allow for regaining 1d8 + Const bonus HP.
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ class EvAdventureRollEngine:
|
|||
int: How much HP was healed. This is never more than how damaged we are.
|
||||
|
||||
"""
|
||||
self.heal(character, roll('1d8') + character.constitution)
|
||||
self.heal(character, self.roll('1d8') + character.constitution)
|
||||
|
||||
death_map = {
|
||||
"weakened": "strength",
|
||||
|
|
@ -282,7 +282,7 @@ class EvAdventureRollEngine:
|
|||
|
||||
"""
|
||||
|
||||
result = self.roll_random_table('1d8', 'death_and_dismemberment')
|
||||
result = self.roll_random_table('1d8', death_table)
|
||||
if result == "dead":
|
||||
character.handle_death()
|
||||
else:
|
||||
|
|
@ -298,6 +298,7 @@ class EvAdventureRollEngine:
|
|||
# can't lose more - die
|
||||
character.handle_death()
|
||||
else:
|
||||
# refresh health, but get permanent ability loss
|
||||
new_hp = max(character.hp_max, self.roll("1d4"))
|
||||
setattr(character, abi, current_abi)
|
||||
character.hp = new_hp
|
||||
|
|
@ -326,13 +327,11 @@ class EvAdventureCharacterGeneration:
|
|||
online players can (and usually will) just disconnect and reroll until they get values
|
||||
they are happy with.
|
||||
|
||||
So, in standard Knave, the character's attribute bonus is rolled randomly and will give a
|
||||
In standard Knave, the character's attribute bonus is rolled randomly and will give a
|
||||
value 1-6; and there is no guarantee for 'equal' starting characters. Instead we
|
||||
homogenize the results to a flat +2 bonus and let people redistribute the
|
||||
points afterwards. This also allows us to show off some more advanced concepts in the
|
||||
chargen menu, but you can also easily make it random like in base Knave by using the
|
||||
(currently unused, but included) `roll_attribute_bonus` function above to get the bonus
|
||||
instead of the flat +2.
|
||||
chargen menu.
|
||||
|
||||
In the same way, Knave uses a d8 roll to get the initial hit points. Instead we use a
|
||||
flat max of 8 HP to start, in order to give players a little more survivability.
|
||||
|
|
@ -349,12 +348,12 @@ class EvAdventureCharacterGeneration:
|
|||
"""
|
||||
# for clarity we initialize the engine here rather than use the
|
||||
# global singleton at the end of the module
|
||||
dice = EvAdventureRollEngine()
|
||||
roll_engine = EvAdventureRollEngine()
|
||||
|
||||
# name will likely be modified later
|
||||
self.name = dice.roll_random_table('1d282', chargen_table['name'])
|
||||
self.name = roll_engine.roll_random_table('1d282', chargen_table['name'])
|
||||
|
||||
# base attribute bonuses
|
||||
# base attribute bonuses (flat +1 bonus)
|
||||
self.strength = 2
|
||||
self.dexterity = 2
|
||||
self.constitution = 2
|
||||
|
|
@ -363,17 +362,17 @@ class EvAdventureCharacterGeneration:
|
|||
self.charisma = 2
|
||||
|
||||
# physical attributes (only for rp purposes)
|
||||
self.physique = dice.roll_random_table('1d20', chargen_table['physique'])
|
||||
self.face = dice.roll_random_table('1d20', chargen_table['face'])
|
||||
self.skin = dice.roll_random_table('1d20', chargen_table['skin'])
|
||||
self.hair = dice.roll_random_table('1d20', chargen_table['hair'])
|
||||
self.clothing = dice.roll_random_table('1d20', chargen_table['clothing'])
|
||||
self.speech = dice.roll_random_table('1d20', chargen_table['speech'])
|
||||
self.virtue = dice.roll_random_table('1d20', chargen_table['virtue'])
|
||||
self.vice = dice.roll_random_table('1d20', chargen_table['vice'])
|
||||
self.background = dice.roll_random_table('1d20', chargen_table['background'])
|
||||
self.misfortune = dice.roll_random_table('1d20', chargen_table['misfortune'])
|
||||
self.alignment = dice.roll_random_table('1d20', chargen_table['alignment'])
|
||||
self.physique = roll_engine.roll_random_table('1d20', chargen_table['physique'])
|
||||
self.face = roll_engine.roll_random_table('1d20', chargen_table['face'])
|
||||
self.skin = roll_engine.roll_random_table('1d20', chargen_table['skin'])
|
||||
self.hair = roll_engine.roll_random_table('1d20', chargen_table['hair'])
|
||||
self.clothing = roll_engine.roll_random_table('1d20', chargen_table['clothing'])
|
||||
self.speech = roll_engine.roll_random_table('1d20', chargen_table['speech'])
|
||||
self.virtue = roll_engine.roll_random_table('1d20', chargen_table['virtue'])
|
||||
self.vice = roll_engine.roll_random_table('1d20', chargen_table['vice'])
|
||||
self.background = roll_engine.roll_random_table('1d20', chargen_table['background'])
|
||||
self.misfortune = roll_engine.roll_random_table('1d20', chargen_table['misfortune'])
|
||||
self.alignment = roll_engine.roll_random_table('1d20', chargen_table['alignment'])
|
||||
|
||||
# same for all
|
||||
self.exploration_speed = 120
|
||||
|
|
@ -384,21 +383,22 @@ class EvAdventureCharacterGeneration:
|
|||
self.level = 1
|
||||
|
||||
# random equipment
|
||||
self.armor = dice.roll_random_table('1d20', chargen_table['armor'])
|
||||
self.armor = roll_engine.roll_random_table('1d20', chargen_table['armor'])
|
||||
|
||||
_helmet_and_shield = dice.roll_random_table('1d20', chargen_table["helmets and shields"])
|
||||
_helmet_and_shield = roll_engine.roll_random_table(
|
||||
'1d20', chargen_table["helmets and shields"])
|
||||
self.helmet = "helmet" if "helmet" in _helmet_and_shield else "none"
|
||||
self.shield = "shield" if "shield" in _helmet_and_shield else "none"
|
||||
|
||||
self.weapon = dice.roll_random_table(chargen_table['1d20', "starting_weapon"])
|
||||
self.weapon = roll_engine.roll_random_table('1d20', chargen_table["starting weapon"])
|
||||
|
||||
self.backpack = [
|
||||
"ration",
|
||||
"ration",
|
||||
dice.roll_random_table(chargen_table['1d20', "dungeoning gear"]),
|
||||
dice.roll_random_table(chargen_table['1d20', "dungeoning gear"]),
|
||||
dice.roll_random_table(chargen_table['1d20', "general gear 1"]),
|
||||
dice.roll_random_table(chargen_table['1d20', "general gear 2"]),
|
||||
roll_engine.roll_random_table('1d20', chargen_table["dungeoning gear"]),
|
||||
roll_engine.roll_random_table('1d20', chargen_table["dungeoning gear"]),
|
||||
roll_engine.roll_random_table('1d20', chargen_table["general gear 1"]),
|
||||
roll_engine.roll_random_table('1d20', chargen_table["general gear 2"]),
|
||||
]
|
||||
|
||||
def build_desc(self):
|
||||
|
|
@ -432,18 +432,21 @@ class EvAdventureCharacterGeneration:
|
|||
much input validation here, we do make sure we don't overcharge ourselves though.
|
||||
|
||||
"""
|
||||
# we use getattr() to fetch the Ability of e.g. the .strength property etc
|
||||
source_current_bonus = getattr(self, source_attribute.value, 1)
|
||||
target_current_bonus = getattr(self, target_attribute.value, 1)
|
||||
if source_attribute == target_attribute:
|
||||
return
|
||||
|
||||
if source_current_bonus - value < 1:
|
||||
# we use getattr() to fetch the Ability of e.g. the .strength property etc
|
||||
source_current = getattr(self, source_attribute.value, 1)
|
||||
target_current = getattr(self, target_attribute.value, 1)
|
||||
|
||||
if source_current - value < 1:
|
||||
raise ValueError(f"You can't reduce the {source_attribute} bonus below +1.")
|
||||
if target_current_bonus + value > 6:
|
||||
if target_current + value > 6:
|
||||
raise ValueError(f"You can't increase the {target_attribute} bonus above +6.")
|
||||
|
||||
# all is good, apply the change.
|
||||
setattr(self, source_attribute, source_current_bonus - value)
|
||||
setattr(self, target_attribute, source_current_bonus + value)
|
||||
setattr(self, source_attribute.value, source_current - value)
|
||||
setattr(self, target_attribute.value, target_current + value)
|
||||
|
||||
def apply(self, character):
|
||||
"""
|
||||
|
|
@ -459,9 +462,8 @@ class EvAdventureCharacterGeneration:
|
|||
character.wisdom = self.wisdom
|
||||
character.charisma = self.charisma
|
||||
|
||||
character.armor = self.armor_bonus
|
||||
# character.exploration_speed = self.exploration_speed
|
||||
# character.combat_speed = self.combat_speed
|
||||
character.weapon = self.weapon
|
||||
character.armor = self.armor
|
||||
|
||||
character.hp = self.hp
|
||||
character.level = self.level
|
||||
|
|
@ -532,7 +534,7 @@ class EvAdventureImprovement:
|
|||
will need to be done earlier, when the user selects the ability to increase.
|
||||
|
||||
"""
|
||||
dice = EvAdventureRollEngine()
|
||||
roll_engine = EvAdventureRollEngine()
|
||||
|
||||
character.level += 1
|
||||
for ability in set(abilities[:amount_of_abilities_to_upgrades]):
|
||||
|
|
@ -621,7 +623,7 @@ class EvAdventureCharacterSheet:
|
|||
# singletons
|
||||
|
||||
# access sheet as rules.character_sheet.get(character)
|
||||
character_sheet = CharacterSheet()
|
||||
character_sheet = EvAdventureCharacterSheet()
|
||||
# access rolls e.g. with rules.dice.opposed_saving_throw(...)
|
||||
dice = EvAdventureRollEngine()
|
||||
# access improvement e.g. with rules.improvement.add_xp(character, xp)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,17 @@ Tests for EvAdventure.
|
|||
|
||||
"""
|
||||
|
||||
from parameterized import parameterized
|
||||
from unittest.mock import patch, MagicMock, call
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from .character import EvAdventureCharacter
|
||||
from .characters import EvAdventureCharacter
|
||||
from .objects import EvAdventureObject
|
||||
from . import enums
|
||||
from . import combat_turnbased
|
||||
from . import rules
|
||||
from . import random_tables
|
||||
|
||||
|
||||
class EvAdventureMixin:
|
||||
def setUp(self):
|
||||
|
|
@ -24,3 +31,309 @@ class EvAdventureMixin:
|
|||
class EvAdventureEquipmentTest(EvAdventureMixin, BaseEvenniaTest):
|
||||
pass
|
||||
|
||||
|
||||
class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
||||
"""
|
||||
Test the turn-based combat-handler implementation.
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.combathandler = combat_turnbased.EvAdventureCombatHandler()
|
||||
self.combathandler.add_combatant(self.character)
|
||||
|
||||
def test_remove_combatant(self):
|
||||
self.combathandler.remove_combatant(self.character)
|
||||
|
||||
|
||||
class EvAdventureRollEngineTest(BaseEvenniaTest):
|
||||
"""
|
||||
Test the roll engine in the rules module. This is the core of any RPG.
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.roll_engine = rules.EvAdventureRollEngine()
|
||||
|
||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||
def test_roll(self, mock_randint):
|
||||
mock_randint.return_value = 8
|
||||
self.assertEqual(self.roll_engine.roll("1d6"), 8)
|
||||
mock_randint.assert_called_with(1, 6)
|
||||
|
||||
self.assertEqual(self.roll_engine.roll("2d8"), 2 * 8)
|
||||
mock_randint.assert_called_with(1, 8)
|
||||
|
||||
self.assertEqual(self.roll_engine.roll("4d12"), 4 * 8)
|
||||
mock_randint.assert_called_with(1, 12)
|
||||
|
||||
self.assertEqual(self.roll_engine.roll("8d100"), 8 * 8)
|
||||
mock_randint.assert_called_with(1, 100)
|
||||
|
||||
def test_roll_limits(self):
|
||||
with self.assertRaises(TypeError):
|
||||
self.roll_engine.roll('100d6', max_number=10) # too many die
|
||||
with self.assertRaises(TypeError):
|
||||
self.roll_engine.roll('100') # no d
|
||||
with self.assertRaises(TypeError):
|
||||
self.roll_engine.roll('dummy') # non-numerical
|
||||
with self.assertRaises(TypeError):
|
||||
self.roll_engine.roll('Ad4') # non-numerical
|
||||
with self.assertRaises(TypeError):
|
||||
self.roll_engine.roll('1d10000') # limit is d1000
|
||||
|
||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||
def test_roll_with_advantage_disadvantage(self, mock_randint):
|
||||
mock_randint.return_value = 9
|
||||
|
||||
# no advantage/disadvantage
|
||||
self.assertEqual(self.roll_engine.roll_with_advantage_or_disadvantage(), 9)
|
||||
mock_randint.assert_called_once()
|
||||
mock_randint.reset_mock()
|
||||
|
||||
# cancel each other out
|
||||
self.assertEqual(
|
||||
self.roll_engine.roll_with_advantage_or_disadvantage(
|
||||
disadvantage=True, advantage=True), 9)
|
||||
mock_randint.assert_called_once()
|
||||
mock_randint.reset_mock()
|
||||
|
||||
# run with advantage/disadvantage
|
||||
self.assertEqual(
|
||||
self.roll_engine.roll_with_advantage_or_disadvantage(advantage=True), 9)
|
||||
mock_randint.assert_has_calls([call(1, 20), call(1, 20)])
|
||||
mock_randint.reset_mock()
|
||||
|
||||
self.assertEqual(
|
||||
self.roll_engine.roll_with_advantage_or_disadvantage(disadvantage=True), 9)
|
||||
mock_randint.assert_has_calls([call(1, 20), call(1, 20)])
|
||||
mock_randint.reset_mock()
|
||||
|
||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||
def test_saving_throw(self, mock_randint):
|
||||
mock_randint.return_value = 8
|
||||
|
||||
character = MagicMock()
|
||||
character.strength = 2
|
||||
character.dexterity = 1
|
||||
|
||||
self.assertEqual(
|
||||
self.roll_engine.saving_throw(character, bonus_type=enums.Ability.STR),
|
||||
(False, None))
|
||||
self.assertEqual(
|
||||
self.roll_engine.saving_throw(character, bonus_type=enums.Ability.DEX, modifier=1),
|
||||
(False, None))
|
||||
self.assertEqual(
|
||||
self.roll_engine.saving_throw(
|
||||
character,
|
||||
advantage=True,
|
||||
bonus_type=enums.Ability.DEX, modifier=6),
|
||||
(False, None))
|
||||
self.assertEqual(
|
||||
self.roll_engine.saving_throw(
|
||||
character,
|
||||
disadvantage=True,
|
||||
bonus_type=enums.Ability.DEX, modifier=7),
|
||||
(True, None))
|
||||
|
||||
mock_randint.return_value = 1
|
||||
self.assertEqual(
|
||||
self.roll_engine.saving_throw(
|
||||
character,
|
||||
disadvantage=True,
|
||||
bonus_type=enums.Ability.STR, modifier=2),
|
||||
(False, enums.Ability.CRITICAL_FAILURE))
|
||||
|
||||
mock_randint.return_value = 20
|
||||
self.assertEqual(
|
||||
self.roll_engine.saving_throw(
|
||||
character,
|
||||
disadvantage=True,
|
||||
bonus_type=enums.Ability.STR, modifier=2),
|
||||
(True, enums.Ability.CRITICAL_SUCCESS))
|
||||
|
||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||
def test_opposed_saving_throw(self, mock_randint):
|
||||
mock_randint.return_value = 10
|
||||
|
||||
attacker, defender = MagicMock(), MagicMock()
|
||||
attacker.strength = 1
|
||||
defender.armor = 2
|
||||
|
||||
self.assertEqual(
|
||||
self.roll_engine.opposed_saving_throw(
|
||||
attacker, defender,
|
||||
attack_type=enums.Ability.STR, defense_type=enums.Ability.ARMOR
|
||||
),
|
||||
(False, None)
|
||||
)
|
||||
self.assertEqual(
|
||||
self.roll_engine.opposed_saving_throw(
|
||||
attacker, defender,
|
||||
attack_type=enums.Ability.STR, defense_type=enums.Ability.ARMOR,
|
||||
modifier=2
|
||||
),
|
||||
(True, None)
|
||||
)
|
||||
|
||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||
def test_roll_random_table(self, mock_randint):
|
||||
mock_randint.return_value = 10
|
||||
|
||||
self.assertEqual(
|
||||
self.roll_engine.roll_random_table(
|
||||
"1d20", random_tables.character_generation['physique']),
|
||||
"scrawny"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.roll_engine.roll_random_table(
|
||||
"1d20", random_tables.character_generation['vice']),
|
||||
"irascible"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.roll_engine.roll_random_table(
|
||||
"1d20", random_tables.character_generation['alignment']),
|
||||
"neutrality"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.roll_engine.roll_random_table(
|
||||
"1d20", random_tables.character_generation['helmets and shields']),
|
||||
"no helmet or shield"
|
||||
)
|
||||
# testing faulty rolls outside of the table ranges
|
||||
mock_randint.return_value = 25
|
||||
self.assertEqual(
|
||||
self.roll_engine.roll_random_table(
|
||||
"1d20", random_tables.character_generation['helmets and shields']),
|
||||
"helmet and shield"
|
||||
)
|
||||
mock_randint.return_value = -10
|
||||
self.assertEqual(
|
||||
self.roll_engine.roll_random_table(
|
||||
"1d20", random_tables.character_generation['helmets and shields']),
|
||||
"no helmet or shield"
|
||||
)
|
||||
|
||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||
def test_morale_check(self, mock_randint):
|
||||
defender = MagicMock()
|
||||
defender.morale = 12
|
||||
|
||||
mock_randint.return_value = 7 # 2d6 is rolled, so this will become 14
|
||||
self.assertEqual(self.roll_engine.morale_check(defender), False)
|
||||
|
||||
mock_randint.return_value = 3 # 2d6 is rolled, so this will become 6
|
||||
self.assertEqual(self.roll_engine.morale_check(defender), True)
|
||||
|
||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||
def test_heal_from_rest(self, mock_randint):
|
||||
character = MagicMock()
|
||||
character.hp_max = 8
|
||||
character.hp = 1
|
||||
character.constitution = 1
|
||||
|
||||
mock_randint.return_value = 5
|
||||
self.roll_engine.heal_from_rest(character)
|
||||
self.assertEqual(character.hp, 7) # hp + 1d8 + consititution bonus
|
||||
mock_randint.assert_called_with(1, 8) # 1d8
|
||||
|
||||
self.roll_engine.heal_from_rest(character)
|
||||
self.assertEqual(character.hp, 8) # can't have more than max hp
|
||||
|
||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||
def test_roll_death(self, mock_randint):
|
||||
character = MagicMock()
|
||||
character.strength = 13
|
||||
character.hp = 0
|
||||
character.hp_max = 8
|
||||
|
||||
# death
|
||||
mock_randint.return_value = 1
|
||||
self.roll_engine.roll_death(character)
|
||||
character.handle_death.assert_called()
|
||||
# strength loss
|
||||
mock_randint.return_value = 3
|
||||
self.roll_engine.roll_death(character)
|
||||
self.assertEqual(character.strength, 10)
|
||||
|
||||
|
||||
class EvAdventureCharacterGenerationTest(BaseEvenniaTest):
|
||||
"""
|
||||
Test the Character generator tracing object in the rule engine.
|
||||
|
||||
"""
|
||||
|
||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||
def setUp(self, mock_randint):
|
||||
super().setUp()
|
||||
mock_randint.return_value = 10
|
||||
self.chargen = rules.EvAdventureCharacterGeneration()
|
||||
|
||||
def test_base_chargen(self):
|
||||
self.assertEqual(self.chargen.strength, 2)
|
||||
self.assertEqual(self.chargen.physique, "scrawny")
|
||||
self.assertEqual(self.chargen.skin, "pockmarked")
|
||||
self.assertEqual(self.chargen.hair, "greased")
|
||||
self.assertEqual(self.chargen.clothing, "stained")
|
||||
self.assertEqual(self.chargen.misfortune, "exiled")
|
||||
self.assertEqual(self.chargen.armor, "gambeson")
|
||||
self.assertEqual(self.chargen.shield, "shield")
|
||||
self.assertEqual(self.chargen.backpack, ['ration', 'ration', 'waterskin',
|
||||
'waterskin', 'drill', 'twine'])
|
||||
|
||||
def test_build_desc(self):
|
||||
self.assertEqual(
|
||||
self.chargen.build_desc(),
|
||||
"Herbalist. Wears stained clothes, and has hoarse speech. Has a scrawny physique, "
|
||||
"a broken face, pockmarked skin and greased hair. Is honest, but irascible. "
|
||||
"Has been exiled in the past. Favors neutrality."
|
||||
)
|
||||
|
||||
|
||||
@parameterized.expand([
|
||||
# source, target, value, new_source_val, new_target_val
|
||||
(enums.Ability.CON, enums.Ability.STR, 1, 1, 3),
|
||||
(enums.Ability.INT, enums.Ability.DEX, 1, 1, 3),
|
||||
(enums.Ability.CHA, enums.Ability.CON, 1, 1, 3),
|
||||
(enums.Ability.STR, enums.Ability.WIS, 1, 1, 3),
|
||||
(enums.Ability.WIS, enums.Ability.CHA, 1, 1, 3),
|
||||
(enums.Ability.DEX, enums.Ability.DEX, 1, 2, 2),
|
||||
])
|
||||
def test_adjust_attribute(self, source, target, value, new_source_val, new_target_val):
|
||||
self.chargen.adjust_attribute(source, target, value)
|
||||
self.assertEqual(
|
||||
getattr(self.chargen, source.value), new_source_val, f"{source}->{target}")
|
||||
self.assertEqual(
|
||||
getattr(self.chargen, target.value), new_target_val, f"{source}->{target}")
|
||||
|
||||
def test_adjust_consecutive(self):
|
||||
# gradually shift all to STR (starts at 2)
|
||||
self.chargen.adjust_attribute(enums.Ability.CON, enums.Ability.STR, 1)
|
||||
self.chargen.adjust_attribute(enums.Ability.CHA, enums.Ability.STR, 1)
|
||||
self.chargen.adjust_attribute(enums.Ability.DEX, enums.Ability.STR, 1)
|
||||
self.chargen.adjust_attribute(enums.Ability.WIS, enums.Ability.STR, 1)
|
||||
self.assertEqual(self.chargen.constitution, 1)
|
||||
self.assertEqual(self.chargen.strength, 6)
|
||||
|
||||
# max is 6
|
||||
with self.assertRaises(ValueError):
|
||||
self.chargen.adjust_attribute(enums.Ability.INT, enums.Ability.STR, 1)
|
||||
# minimum is 1
|
||||
with self.assertRaises(ValueError):
|
||||
self.chargen.adjust_attribute(enums.Ability.DEX, enums.Ability.WIS, 1)
|
||||
|
||||
# move all from str to wis
|
||||
self.chargen.adjust_attribute(enums.Ability.STR, enums.Ability.WIS, 5)
|
||||
|
||||
self.assertEqual(self.chargen.strength, 1)
|
||||
self.assertEqual(self.chargen.wisdom, 6)
|
||||
|
||||
def test_apply(self):
|
||||
character = MagicMock()
|
||||
|
||||
self.chargen.apply(character)
|
||||
|
||||
self.assertTrue(character.db.desc.startswith("Herbalist"))
|
||||
self.assertEqual(character.armor, "gambeson")
|
||||
|
||||
character.equipment.store.assert_called()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue