Made unit tests for evadventure rules

This commit is contained in:
Griatch 2022-05-26 16:23:11 +02:00
parent d19eac8ac9
commit 9361dff184
5 changed files with 418 additions and 87 deletions

View file

@ -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.
"""

View file

@ -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.

View file

@ -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",

View file

@ -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)

View file

@ -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()