Files
dungeon-sheets/tests/test_character.py
T
2021-02-09 22:26:50 -06:00

298 lines
12 KiB
Python
Executable File

#!/usr/bin/env python
from unittest import TestCase
from pathlib import Path
import warnings
from dungeonsheets import race, monsters, exceptions, spells, infusions
from dungeonsheets.character import Character, Wizard, Druid, read_character_file, _resolve_mechanic
from dungeonsheets.weapons import Weapon, Shortsword
from dungeonsheets.armor import Armor, LeatherArmor, Shield
class TestCharacter(TestCase):
"""Tests for the generic character base class."""
def test_constructor(self):
char = Character(name="Inara")
def test_hit_dice(self):
# Test the getter
char = Character()
char.level = 2
char.hit_dice_faces = 10
self.assertEqual(char.hit_dice, '2d10')
def test_max_hp(self):
char = Wizard(level=3, constitution=12)
self.assertEqual(char.hp_max, 17)
def test_set_attrs(self):
char = Character()
char.set_attrs(name='Inara')
self.assertEqual(char.name, 'Inara')
# Check that the weapons get loaded as objects not string
char.set_attrs(weapons=['shortsword'])
self.assertEqual(len(char.weapons), 1)
self.assertTrue(isinstance(char.weapons[0], Shortsword))
# Check that armor and shield gets set_attrs
char.set_attrs(armor='leather armor', shield='shield')
self.assertFalse(isinstance(char.armor, str))
self.assertFalse(isinstance(char.shield, str))
# Check that race gets set to an object
char.set_attrs(race='high elf')
self.assertIsInstance(char.race, race.HighElf)
# Check inspiration works
char.set_attrs(inspiration=True)
self.assertTrue(char.inspiration)
char.set_attrs(inspiration=False)
self.assertFalse(char.inspiration)
def test_homebrew_spells(self):
char = Character()
class MySpell(spells.Spell):
name="my spell!"
char.set_attrs(spells=(MySpell,))
self.assertIsInstance(char.spells[0], spells.Spell)
self.assertEqual(char.spells[0].name, "my spell!")
def test_homebrew_infusions(self):
char = Character(classes="artificer")
class MyInfusion(infusions.Infusion):
name="my infusion!"
# Pass an already created infusion class
char.set_attrs(infusions=(MyInfusion,))
self.assertIsInstance(char.infusions[0], infusions.Infusion)
self.assertEqual(char.infusions[0].name, "my infusion!")
# Pass a previously undefined infusion
char = Character(classes="artificer")
char.set_attrs(infusions=("spam_infusion",))
self.assertIsInstance(char.infusions[0], infusions.Infusion)
self.assertEqual(char.infusions[0].name, "Spam Infusion")
def test_resolve_mechanic(self):
# Test a well defined mechanic
NewSpell = _resolve_mechanic("mage_hand", spells, None)
self.assertTrue(issubclass(NewSpell, spells.Spell))
# Test an unknown mechanic
def new_spell(**params):
return spells.Spell
NewSpell = _resolve_mechanic("hocus_pocus", spells, spells.Spell)
self.assertTrue(issubclass(NewSpell, spells.Spell))
# Test direct resolution of a proper subclass
class MySpell(spells.Spell):
pass
NewSpell = _resolve_mechanic(MySpell, spells, spells.Spell)
def test_wield_weapon(self):
char = Character()
char.strength = 14
char.weapon_proficiencies = [Shortsword]
# Add a weapon
char.wield_weapon('shortsword')
self.assertEqual(len(char.weapons), 1)
sword = char.weapons[0]
self.assertTrue(isinstance(sword, Weapon))
self.assertTrue(isinstance(sword, Shortsword))
self.assertEqual(sword.attack_modifier, 4) # str + prof
self.assertEqual(sword.damage, '1d6+2') # str
# Check if dexterity is used if it's higher (Finesse weapon)
char.weapons = []
char.dexterity = 16
char.wield_weapon('shortsword')
sword = char.weapons[0]
self.assertEqual(sword.attack_modifier, 5) # dex + prof
# Check if race weapon proficiencies are considered
char.weapons = []
char.weapon_proficiencies = []
char.race = race.HighElf()
char.wield_weapon('shortsword')
sword = char.weapons[0]
self.assertEqual(sword.attack_modifier, 5)
def test_str(self):
char = Wizard(name="Inara")
self.assertEqual(str(char), 'Inara')
self.assertEqual(repr(char), '<Wizard: Inara>')
def test_is_proficient(self):
char = Character(classes=['Wizard'])
char.weapon_proficiencies
sword = Shortsword()
# Check for not-proficient weapon
self.assertFalse(char.is_proficient(sword))
# Check if we're proficient in the weapon
char.weapon_proficiencies = [Shortsword]
self.assertTrue(char.is_proficient(sword))
# Now try it with a racial proficiency
char.weapon_proficiencies = tuple()
char.race = race.HighElf()
self.assertTrue(char.is_proficient(sword))
def test_proficiencies_text(self):
char = Character()
char._proficiencies_text = ('hello', 'world')
self.assertIn('hello', char.proficiencies_text.lower())
self.assertIn('world', char.proficiencies_text.lower())
# Check for extra proficiencies
char._proficiencies_text += ("it's", "me")
self.assertIn("it's", char.proficiencies_text.lower())
self.assertIn('me', char.proficiencies_text.lower())
# Check that race proficienceis are included
elf = race.HighElf()
char.race = elf
expected = ("hello", "world", "longswords", "shortswords", "shortbows",
"longbows", "it's", "me")
for e in expected:
self.assertIn(e, char.proficiencies_text.lower())
def test_proficiency_bonus(self):
char = Character()
char.level = 1
self.assertEqual(char.proficiency_bonus, 2)
char.level = 4
self.assertEqual(char.proficiency_bonus, 2)
char.level = 5
self.assertEqual(char.proficiency_bonus, 3)
char.level = 8
self.assertEqual(char.proficiency_bonus, 3)
char.level = 9
self.assertEqual(char.proficiency_bonus, 4)
char.level = 12
self.assertEqual(char.proficiency_bonus, 4)
char.level = 13
self.assertEqual(char.proficiency_bonus, 5)
char.level = 16
self.assertEqual(char.proficiency_bonus, 5)
char.level = 17
self.assertEqual(char.proficiency_bonus, 6)
char.level = 20
self.assertEqual(char.proficiency_bonus, 6)
def test_spell_slots(self):
char = Wizard()
# Wizard level 1
char.level = 1
self.assertEqual(char.spell_slots(spell_level=0), 3)
self.assertEqual(char.spell_slots(spell_level=1), 2)
self.assertEqual(char.spell_slots(spell_level=2), 0)
# Wizard level 2
char.level = 2
self.assertEqual(char.spell_slots(spell_level=0), 3)
self.assertEqual(char.spell_slots(spell_level=1), 3)
self.assertEqual(char.spell_slots(spell_level=2), 0)
def test_equip_armor(self):
char = Character(dexterity=16)
char.wear_armor('leather armor')
self.assertTrue(isinstance(char.armor, Armor))
# Now make sure the armor class is correct
self.assertEqual(char.armor_class, 14)
# Try passing an Armor object directly
char.wear_armor(LeatherArmor())
self.assertEqual(char.armor_class, 14)
# Test equipped armor with max dexterity mod_str
char.armor.dexterity_mod_max = 1
self.assertEqual(char.armor_class, 12)
def test_wield_shield(self):
char = Character(dexterity=16)
char.wield_shield('shield')
self.assertTrue(isinstance(char.shield, Shield), msg=char.shield)
# Now make sure the armor class is correct
self.assertEqual(char.armor_class, 15)
# Try passing an Armor object directly
char.wield_shield(Shield)
self.assertEqual(char.armor_class, 15)
def test_speed(self):
# Check that the speed pulls from the character's race
char = Character(race='lightfoot halfling')
self.assertEqual(char.speed, '25')
# Check that a character with no race defaults to 30 feet
char = Character()
char.race = None
self.assertEqual(char.speed, '30')
class DruidTestCase(TestCase):
def test_learned_spells(self):
"""For a druid, learning spells is not necessary and this field should
be ignored."""
char = Druid()
with warnings.catch_warnings():
warnings.filterwarnings('ignore', message="Druids cannot learn spells")
char.set_attrs(spells=['invisibility'],
spells_prepared=['druidcraft'])
# self.assertEqual(len(char.spells), 1)
self.assertEqual(len(char.spells), 2)
self.assertIn(spells.Druidcraft(), char.spells)
def test_wild_shapes(self):
char = Druid()
# Druid level 2
char.level = 2
# Set reasonable wild shapes
char.wild_shapes = ['Wolf']
self.assertIsInstance(char.wild_shapes[0], monsters.Wolf)
# Check what happens if a non-existent wild_shape is added
with self.assertRaises(exceptions.MonsterError):
char.wild_shapes = ['Wolf', 'Hyperion Loader']
# Check what happens if a valid monster class is directly added
char.wild_shapes = [monsters.Wolf(), ]
self.assertIsInstance(char.wild_shapes[0], monsters.Wolf)
# Check that invalid monsters aren't accepted
char.wild_shapes = ['Wolf', 'giant eagle']
self.assertEqual(len(char.wild_shapes), 1)
self.assertIsInstance(char.wild_shapes[0], monsters.Wolf)
def test_moon_druid_wild_shapes(self):
# Moon druid level 2 gets beasts up to CR 1
char = Druid(level=2, wild_shapes=['Ape'], circle='moon')
self.assertEqual(len(char.wild_shapes), 1)
self.assertIsInstance(char.wild_shapes[0], monsters.Ape)
# Moon druid above level 6 gets beasts up to CR level / 3
char = Druid(level=9, wild_shapes=['ankylosaurus'], circle='moon')
self.assertEqual(len(char.wild_shapes), 1)
self.assertIsInstance(char.wild_shapes[0], monsters.Ankylosaurus)
def test_can_assume_shape(self):
class Beast(monsters.Monster):
description = 'beast'
new_druid = Druid(level=1)
low_druid = Druid(level=2)
mid_druid = Druid(level=4)
high_druid = Druid(level=8)
beast = Beast()
# Check that level 1 druid automatically fails
self.assertFalse(new_druid.can_assume_shape(beast))
# Check if a basic beast can be transformed
self.assertTrue(low_druid.can_assume_shape(beast))
# Check that challenge rating is checked
hard_beast = Beast()
hard_beast.challenge_rating = 1/2
really_hard_beast = Beast()
really_hard_beast.challenge_rating = 1
self.assertFalse(low_druid.can_assume_shape(hard_beast))
self.assertFalse(low_druid.can_assume_shape(really_hard_beast))
self.assertTrue(mid_druid.can_assume_shape(hard_beast))
self.assertFalse(mid_druid.can_assume_shape(really_hard_beast))
self.assertTrue(high_druid.can_assume_shape(hard_beast))
self.assertTrue(high_druid.can_assume_shape(really_hard_beast))
# Check that swim speed is enforced
swim_beast = Beast()
swim_beast.swim_speed = 15
self.assertFalse(low_druid.can_assume_shape(swim_beast))
self.assertTrue(mid_druid.can_assume_shape(swim_beast))
self.assertTrue(high_druid.can_assume_shape(swim_beast))
# Check that fly speed is enforced
fly_beast = Beast()
fly_beast.fly_speed = 15
self.assertFalse(low_druid.can_assume_shape(fly_beast))
self.assertFalse(mid_druid.can_assume_shape(fly_beast))
self.assertTrue(high_druid.can_assume_shape(fly_beast))
# Check that non-beasts are not allowed
not_beast = monsters.Monster()
not_beast.description = "monster"
self.assertFalse(low_druid.can_assume_shape(not_beast))