adding tests

This commit is contained in:
evilchili 2024-07-05 14:42:11 -07:00
parent 551140b5bc
commit 4dd72d47d0
4 changed files with 79 additions and 55 deletions

View File

@ -80,9 +80,9 @@ class Ancestry(BaseObject, ModifierMixin):
size: Mapped[str] = mapped_column(nullable=False, default="medium") size: Mapped[str] = mapped_column(nullable=False, default="medium")
speed: Mapped[int] = mapped_column(nullable=False, default=30, info={"min": 0, "max": 99}) speed: Mapped[int] = mapped_column(nullable=False, default=30, info={"min": 0, "max": 99})
_fly_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99}) fly_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
_climb_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99}) climb_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
_swim_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99}) swim_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
_traits = relationship( _traits = relationship(
"AncestryTraitMap", init=False, uselist=True, cascade="all,delete,delete-orphan", lazy="immediate" "AncestryTraitMap", init=False, uselist=True, cascade="all,delete,delete-orphan", lazy="immediate"
@ -92,16 +92,8 @@ class Ancestry(BaseObject, ModifierMixin):
def traits(self): def traits(self):
return [mapping.trait for mapping in self._traits] return [mapping.trait for mapping in self._traits]
@property
def climb_speed(self):
return self._climb_speed or int(self.speed / 2)
@property
def swim_speed(self):
return self._swim_speed or int(self.speed / 2)
def add_trait(self, trait, level=1): def add_trait(self, trait, level=1):
if not self._traits or trait not in self._traits: if trait not in self.traits:
mapping = AncestryTraitMap(ancestry_id=self.id, trait=trait, level=level) mapping = AncestryTraitMap(ancestry_id=self.id, trait=trait, level=level)
if not self._traits: if not self._traits:
self._traits = [mapping] self._traits = [mapping]
@ -110,23 +102,16 @@ class Ancestry(BaseObject, ModifierMixin):
return True return True
return False return False
def __repr__(self):
return self.name
class AncestryTrait(BaseObject, ModifierMixin): class AncestryTrait(BaseObject, ModifierMixin):
""" """
A trait granted to a character via its Ancestry. A trait granted to a character via its Ancestry.
""" """
__tablename__ = "ancestry_trait" __tablename__ = "ancestry_trait"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(collation="NOCASE"), nullable=False, unique=True) name: Mapped[str] = mapped_column(String(collation="NOCASE"), nullable=False, unique=True)
description: Mapped[Text] = mapped_column(Text, default="") description: Mapped[Text] = mapped_column(Text, default="")
def __repr__(self):
return self.name
class CharacterSkillMap(BaseObject): class CharacterSkillMap(BaseObject):
__tablename__ = "character_skill_map" __tablename__ = "character_skill_map"
@ -153,9 +138,6 @@ class CharacterClassMap(BaseObject):
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1) level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
def __repr__(self):
return f"{self.character.name}, {self.character_class.name}, level {self.level}"
class CharacterClassAttributeMap(BaseObject): class CharacterClassAttributeMap(BaseObject):
__tablename__ = "character_class_attribute_map" __tablename__ = "character_class_attribute_map"
@ -212,7 +194,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
nullable=False, default=10, info={"min": 0, "max": 30, "modifiable_class": Stat} nullable=False, default=10, info={"min": 0, "max": 30, "modifiable_class": Stat}
) )
_vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0, "modifiable": True}) vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0, "modifiable": True})
exhaustion: Mapped[int] = mapped_column(nullable=False, default=0, info={"min": 0, "max": 5}) exhaustion: Mapped[int] = mapped_column(nullable=False, default=0, info={"min": 0, "max": 5})
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan") class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
@ -248,11 +230,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
def expertise_bonus(self): def expertise_bonus(self):
return 2 * self.proficiency_bonus return 2 * self.proficiency_bonus
@property
def proficiencies(self):
unified = {}
unified.update(**self._proficiencies)
@property @property
def modifiers(self): def modifiers(self):
unified = {} unified = {}
@ -262,10 +239,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
unified.update(**super().modifiers) unified.update(**super().modifiers)
return unified return unified
@property
def check_modifiers(self):
return [self.check_modifier(skill) for skill in self.skills]
@property @property
def classes(self): def classes(self):
return dict([(mapping.character_class.name, mapping.character_class) for mapping in self.class_map]) return dict([(mapping.character_class.name, mapping.character_class) for mapping in self.class_map])
@ -280,19 +253,22 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
@property @property
def speed(self): def speed(self):
return self._apply_modifiers("speed", self.ancestry.speed) return self._apply_modifiers('speed', self._apply_modifiers("walking_speed", self.ancestry.speed))
@property @property
def climb_speed(self): def climb_speed(self):
return self._apply_modifiers("climb_speed", self.ancestry._climb_speed) return self._apply_modifiers("climb_speed", self.ancestry.climb_speed or int(self.speed / 2))
@property @property
def swim_speed(self): def swim_speed(self):
return self._apply_modifiers("swim_speed", self.ancestry._swim_speed) return self._apply_modifiers("swim_speed", self.ancestry.swim_speed or int(self.speed / 2))
@property @property
def fly_speed(self): def fly_speed(self):
return self._apply_modifiers("fly_speed", self.ancestry._fly_speed) modified = self._apply_modifiers("fly_speed", self.ancestry.fly_speed or 0)
if self.ancestry.fly_speed is None and not modified:
return None
return self._apply_modifiers("speed", modified)
@property @property
def size(self): def size(self):
@ -332,11 +308,13 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
def absorbs(self, damage_type: DamageType): def absorbs(self, damage_type: DamageType):
return self.defense(damage_type) == Defenses.absorbs return self.defense(damage_type) == Defenses.absorbs
def conditions(self): def condition(self, condition):
return [self._apply_modifiers(f"conditions.{name}") for name in Conditions] if not self.immune(condition):
return self._apply_modifiers(condition, False)
return False
def condition(self, condition_name: str): def add_condition(self, condition):
return self._apply_modifiers(f"conditions.{condition_name}", False) self.add_modifier(Modifier(condition, target=condition, new_value=True))
def defense(self, damage_type: DamageType): def defense(self, damage_type: DamageType):
return self._apply_modifiers(damage_type, None) return self._apply_modifiers(damage_type, None)
@ -431,8 +409,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
return True return True
def add_skill(self, skill, proficient=False, expert=False, character_class=None): def add_skill(self, skill, proficient=False, expert=False, character_class=None):
# if not self.id:
# raise Exception("Cannot add a skill before the character has been persisted.")
skillmap = None skillmap = None
exists = False exists = False
if skill in self.skills: if skill in self.skills:
@ -500,14 +476,10 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
self.hit_points = max(0, self.hit_points - (total - self.temp_hit_points)) self.hit_points = max(0, self.hit_points - (total - self.temp_hit_points))
self.temp_hit_points = 0 self.temp_hit_points = 0
return
def spend_hit_die(self, die): def spend_hit_die(self, die):
die.spent = True die.spent = True
def reset_hit_die(self, die):
die.spent = False
def __after_insert__(self, session): def __after_insert__(self, session):
""" """
Called by the session after_flush event listener to add default joins in other tables. Called by the session after_flush event listener to add default joins in other tables.

View File

@ -136,7 +136,9 @@ class ModifierMixin:
Returns True if the modifier was added; False if was already present. Returns True if the modifier was added; False if was already present.
""" """
if modifier.absolute_value is not None and modifier.relative_value is not None and modifier.multiple_value: if modifier.absolute_value is not None and modifier.relative_value is not None and modifier.multiple_value:
raise AttributeError(f"You must provide only one of absolute, relative, and multiple values {modifier}.") raise AttributeError(
f"You must provide only one of absolute, relative, and multiple values {modifier}."
) # pragma: no cover
if [mod for mod in self.modifier_map if mod.modifier == modifier]: if [mod for mod in self.modifier_map if mod.modifier == modifier]:
return False return False
@ -155,7 +157,7 @@ class ModifierMixin:
Returns True if it was removed and False if it wasn't present. Returns True if it was removed and False if it wasn't present.
""" """
if modifier not in self.modifiers[modifier.target]: if modifier not in self.modifiers.get(modifier.target, []):
return False return False
self.modifier_map = [mapping for mapping in self.modifier_map if mapping.modifier != modifier] self.modifier_map = [mapping for mapping in self.modifier_map if mapping.modifier != modifier]
return True return True
@ -175,7 +177,7 @@ class ModifierMixin:
for key in col.info.keys(): for key in col.info.keys():
if key.startswith("modifiable"): if key.startswith("modifiable"):
return col return col
return None return None # pragma: no cover
def _get_modifiable_base(self, attr_name: str) -> object: def _get_modifiable_base(self, attr_name: str) -> object:
""" """
@ -217,7 +219,7 @@ class ModifierMixin:
if modifier.relative_value is not None: if modifier.relative_value is not None:
return base_value + modifier.relative_value return base_value + modifier.relative_value
raise Exception(f"Cannot apply modifier: {modifier = }") raise Exception(f"Cannot apply modifier: {modifier = }") # pragma: no cover
def _apply_modifiers(self, target: str, initial: Any, modifiable_class: type = None) -> Modifiable: def _apply_modifiers(self, target: str, initial: Any, modifiable_class: type = None) -> Modifiable:
""" """
@ -258,7 +260,7 @@ class ModifierMixin:
""" """
col = self._modifiable_column(attr_name) col = self._modifiable_column(attr_name)
if col is not None: if col is not None:
raise AttributeError(f"You cannot modify .{attr_name}. Did you mean ._{attr_name}?") raise AttributeError(f"You cannot modify .{attr_name}. Did you mean ._{attr_name}?") # pragma: no cover
return super().__setattr__(attr_name, value) return super().__setattr__(attr_name, value)
def __getattr__(self, attr_name): def __getattr__(self, attr_name):

View File

@ -21,3 +21,8 @@ def test_dump_load(db, bootstrap):
def test_loader(db, bootstrap): def test_loader(db, bootstrap):
loader.load(db.dump()) loader.load(db.dump())
assert len(db.Ancestry.all()) > 0 assert len(db.Ancestry.all()) > 0
def test_default(db):
loader.load()
assert len(db.Ancestry.all()) > 0

View File

@ -1,7 +1,7 @@
import json import json
from ttfrog.db import schema from ttfrog.db import schema
from ttfrog.db.schema.constants import DamageType, Defenses from ttfrog.db.schema.constants import DamageType, Defenses, Conditions
def test_manage_character(db, bootstrap): def test_manage_character(db, bootstrap):
@ -23,7 +23,9 @@ def test_manage_character(db, bootstrap):
assert char.intelligence == 10 assert char.intelligence == 10
assert char.wisdom == 10 assert char.wisdom == 10
assert char.charisma == 10 assert char.charisma == 10
assert darkvision not in char.traits assert darkvision not in char.traits
assert char.vision_in_darkness == 0
# verify basic skills were added at creation time # verify basic skills were added at creation time
for skill in db.Skill.filter( for skill in db.Skill.filter(
@ -39,6 +41,7 @@ def test_manage_character(db, bootstrap):
assert char.ancestry_id == tiefling.id assert char.ancestry_id == tiefling.id
assert char.ancestry.name == "tiefling" assert char.ancestry.name == "tiefling"
assert darkvision in char.traits assert darkvision in char.traits
assert char.vision_in_darkness == 120
# tiefling ancestry adds INT and CHA modifiers # tiefling ancestry adds INT and CHA modifiers
assert char.intelligence == 11 assert char.intelligence == 11
@ -110,8 +113,22 @@ def test_manage_character(db, bootstrap):
db.add_or_update(char) db.add_or_update(char)
assert char.level == 8 assert char.level == 8
assert char.levels == {"fighter": 7, "rogue": 1} assert char.levels == {"fighter": 7, "rogue": 1}
assert list(char.classes.keys()) == ['fighter', 'rogue']
assert sum([len(dice) for dice in char.hit_dice.values()]) == char.level == 8 assert sum([len(dice) for dice in char.hit_dice.values()]) == char.level == 8
# test conditions
assert not char.condition(Conditions.frightened)
char.add_condition(Conditions.frightened)
assert char.condition(Conditions.frightened)
char.add_modifier(
schema.Modifier("Immunity: Frightened", target=Conditions.frightened, new_value=Defenses.immune)
)
assert not char.condition(Conditions.frightened)
# use a hit die
char.spend_hit_die(char.hit_dice["rogue"][0])
assert len([die for die in char.hit_dice_available if die.character_class.name == "rogue"]) == 0
# remove a class # remove a class
char.remove_class(rogue) char.remove_class(rogue)
db.add_or_update(char) db.add_or_update(char)
@ -130,11 +147,12 @@ def test_manage_character(db, bootstrap):
assert char.levels == {} assert char.levels == {}
assert char.class_attributes == {} assert char.class_attributes == {}
# verify the proficiencies added by the classes have been removed # verify the proficiencies etc. added by the classes have been removed
assert athletics not in char.skills assert athletics not in char.skills
assert acrobatics not in char.skills assert acrobatics not in char.skills
assert char.check_modifier(athletics) == 0 assert char.check_modifier(athletics) == 0
assert char.check_modifier(acrobatics) == 0 assert char.check_modifier(acrobatics) == 0
assert char.hit_dice == {}
# ensure we're not persisting any orphan records in the map tables # ensure we're not persisting any orphan records in the map tables
dump = json.loads(db.dump()) dump = json.loads(db.dump())
@ -162,6 +180,10 @@ def test_ancestries(db, bootstrap):
db.add_or_update(porc) db.add_or_update(porc)
assert endurance in porc.traits assert endurance in porc.traits
# add it again and assert nothing changes
porc.add_trait(endurance, level=1)
assert endurance in porc.traits
# add a +3 STR modifier # add a +3 STR modifier
str_bonus = schema.Modifier( str_bonus = schema.Modifier(
name="STR+3 (Pygmy Orc)", name="STR+3 (Pygmy Orc)",
@ -208,10 +230,18 @@ def test_modifiers(db, bootstrap):
slowed = schema.Modifier(target="speed", multiply_value=0.5, name="Slowed") slowed = schema.Modifier(target="speed", multiply_value=0.5, name="Slowed")
restrained = schema.Modifier(target="speed", absolute_value=0, name="Restrained") restrained = schema.Modifier(target="speed", absolute_value=0, name="Restrained")
reduced = schema.Modifier(target="size", new_value="Tiny", name="Reduced") reduced = schema.Modifier(target="size", new_value="Tiny", name="Reduced")
fly = schema.Modifier(target="fly_speed", absolute_value=30, name="Fly Spell")
# reduce speed by 10 # reduce speed by 10
assert carl.add_modifier(cold) assert carl.add_modifier(cold)
assert carl.speed == 20 assert carl.speed == 20
assert carl.climb_speed == 10
assert carl.swim_speed == 10
assert carl.fly_speed is None
# cast fly
carl.add_modifier(fly)
assert carl.fly_speed == 20
# make sure modifiers only apply to carl. Carl is having a bad day. # make sure modifiers only apply to carl. Carl is having a bad day.
assert marx.speed == 30 assert marx.speed == 30
@ -243,6 +273,9 @@ def test_modifiers(db, bootstrap):
assert carl.add_modifier(reduced) assert carl.add_modifier(reduced)
assert carl.size == "Tiny" assert carl.size == "Tiny"
# cannot remove a modifier that isn't applied
assert not carl.remove_modifier(cold)
# modifiers can be applied to skills, even if the character doesn't have a skill associated. # modifiers can be applied to skills, even if the character doesn't have a skill associated.
athletics = db.Skill.filter_by(name="athletics").one() athletics = db.Skill.filter_by(name="athletics").one()
assert athletics not in carl.skills assert athletics not in carl.skills
@ -283,6 +316,8 @@ def test_modifiers(db, bootstrap):
assert len([s for s in carl.skills if s == athletics]) == 1 assert len([s for s in carl.skills if s == athletics]) == 1
assert carl.check_modifier(athletics) == 1 assert carl.check_modifier(athletics) == 1
# ensure you can't remove a skill already removed
assert not carl.remove_skill(athletics, proficient=True, expert=False, character_class=None)
def test_defenses(db, bootstrap): def test_defenses(db, bootstrap):
with db.transaction(): with db.transaction():
@ -292,6 +327,16 @@ def test_defenses(db, bootstrap):
carl.apply_damage(5, DamageType.fire) carl.apply_damage(5, DamageType.fire)
assert carl.hit_points == 8 # half damage assert carl.hit_points == 8 # half damage
# add temp HP
carl.temp_hit_points = 3
assert carl.hit_points == 8
carl.apply_damage(1, DamageType.bludgeoning)
assert carl.hit_points == 8
assert carl.temp_hit_points == 2
carl.apply_damage(3, DamageType.bludgeoning)
assert carl.temp_hit_points == 0
assert carl.hit_points == 7
immunity = [ immunity = [
schema.Modifier("Fire Immunity", target=DamageType.fire, new_value=Defenses.immune), schema.Modifier("Fire Immunity", target=DamageType.fire, new_value=Defenses.immune),
] ]
@ -299,7 +344,7 @@ def test_defenses(db, bootstrap):
carl.add_modifier(i) carl.add_modifier(i)
assert carl.immune(DamageType.fire) assert carl.immune(DamageType.fire)
carl.apply_damage(5, DamageType.fire) carl.apply_damage(5, DamageType.fire)
assert carl.hit_points == 8 # no damage assert carl.hit_points == 7 # no damage
vulnerability = [ vulnerability = [
schema.Modifier("Fire Vulnerability", target=DamageType.fire, new_value=Defenses.vulnerable), schema.Modifier("Fire Vulnerability", target=DamageType.fire, new_value=Defenses.vulnerable),
@ -309,7 +354,7 @@ def test_defenses(db, bootstrap):
assert carl.vulnerable(DamageType.fire) assert carl.vulnerable(DamageType.fire)
assert not carl.immune(DamageType.fire) assert not carl.immune(DamageType.fire)
carl.apply_damage(2, DamageType.fire) carl.apply_damage(2, DamageType.fire)
assert carl.hit_points == 4 # double damage assert carl.hit_points == 3 # double damage
absorbs = [schema.Modifier("Absorbs Non-Magical Fire", target=DamageType.fire, new_value=Defenses.absorbs)] absorbs = [schema.Modifier("Absorbs Non-Magical Fire", target=DamageType.fire, new_value=Defenses.absorbs)]
carl.add_modifier(absorbs[0]) carl.add_modifier(absorbs[0])