Remove encounters, refactor Actor into Entity, move more core capabilities into Entity

This commit is contained in:
Matthew DeMartino
2021-05-24 20:38:50 -04:00
parent 601bd0c9aa
commit ddb9d44354
14 changed files with 165 additions and 691 deletions
+7 -16
View File
@@ -23,14 +23,7 @@ from dungeonsheets import (
from dungeonsheets.stats import findattr from dungeonsheets.stats import findattr
from dungeonsheets.weapons import Weapon from dungeonsheets.weapons import Weapon
from dungeonsheets.readers import read_character_file from dungeonsheets.readers import read_character_file
from dungeonsheets.encounter.agent import Agent from dungeonsheets.entity import Entity
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
__version__ = read("../VERSION").strip()
dice_re = re.compile(r"(\d+)d(\d+)") dice_re = re.compile(r"(\d+)d(\d+)")
@@ -146,17 +139,15 @@ def _resolve_mechanic(mechanic, module, SuperClass, warning_message=None):
return Mechanic return Mechanic
class Character(Agent): class Character(Entity):
"""A generic player character.""" """A generic player character."""
# Character-specific
player_name = "" player_name = ""
dungeonsheets_version = __version__
xp = 0 xp = 0
inspiration = False inspiration = False
attacks_and_spellcasting = "" attacks_and_spellcasting = ""
class_list = list() class_list = list()
_race = None
_background = None _background = None
# Characteristics # Characteristics
@@ -172,7 +163,7 @@ class Character(Agent):
def __init__( def __init__(
self, self,
classes: Sequence = [], class_list: Sequence = [],
levels: Sequence[int] = [], levels: Sequence[int] = [],
subclasses: Sequence = [], subclasses: Sequence = [],
**attrs, **attrs,
@@ -184,7 +175,7 @@ class Character(Agent):
Parameters Parameters
========== ==========
classes class_list
Strings with class names, or character class definitions Strings with class names, or character class definitions
representing the characters various D&D classes. representing the characters various D&D classes.
levels levels
@@ -200,7 +191,7 @@ class Character(Agent):
super(Character, self).__init__() super(Character, self).__init__()
self.clear() self.clear()
# make sure class, race, background are set first # make sure class, race, background are set first
my_classes = classes my_classes = class_list
my_levels = levels my_levels = levels
my_subclasses = subclasses my_subclasses = subclasses
# backwards compatability # backwards compatability
@@ -931,7 +922,7 @@ class Character(Agent):
make_sheet(filename, character=self, flatten=kwargs.get("flatten", True)) make_sheet(filename, character=self, flatten=kwargs.get("flatten", True))
# Add backwards compatability for tests # Add backwards compatibility for tests
class Artificer(Character): class Artificer(Character):
def __init__(self, level=1, **attrs): def __init__(self, level=1, **attrs):
attrs["classes"] = ["Artificer"] attrs["classes"] = ["Artificer"]
-9
View File
@@ -1,9 +0,0 @@
class Condition:
"""A condition that can be held by an agent"""
pass
class Blinded(Condition):
pass
class Charmed(Condition):
pass
-1
View File
@@ -1 +0,0 @@
from dungeonsheets.encounter.encounter import Encounter
-45
View File
@@ -1,45 +0,0 @@
from abc import ABC, abstractmethod
from dungeonsheets.encounter.events import Event
class Executable(ABC):
"""Something (like an action) that can be executed.
Executing an action results in an event that is stored
"""
@abstractmethod
def execute(self, subj, obj=None):
"""Execute the given action"""
class Action(Executable):
pass
class BonusAction(Executable):
pass
class Reaction(Executable):
pass
class Movement(Executable):
pass
class LairAction(Executable):
pass
class LegendaryAction(Executable):
pass
class Attack(Action):
def __init__(self, subj, obj):
self.subj = subj
self.obj = obj
-185
View File
@@ -1,185 +0,0 @@
from dungeonsheets.conditions.conditions import Blinded, Charmed
from dungeonsheets.encounter.actions import Attack
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill, \
NumericalInitiative
from abc import ABC
from dungeonsheets.dice import roll
class Agent(ABC):
"""An actor in an encounter. Use Monster or Character, not this class directly!"""
# General attributes
name = ""
alignment = "Neutral"
# Hit points
hp_max = None
# Base stats (ability scores)
strength = Ability()
dexterity = Ability()
constitution = Ability()
intelligence = Ability()
wisdom = Ability()
charisma = Ability()
# Numerical things
armor_class = ArmorClass()
initiative = Initiative()
speed = Speed()
# Proficiencies and Languages
_saving_throw_proficiencies = tuple() # use to overwrite class proficiencies
other_weapon_proficiencies = tuple() # add to class/race proficiencies
skill_proficiencies = list()
skill_expertise = list()
languages = ""
# Skills
acrobatics = Skill(ability="dexterity")
animal_handling = Skill(ability="wisdom")
arcana = Skill(ability="intelligence")
athletics = Skill(ability="strength")
deception = Skill(ability="charisma")
history = Skill(ability="intelligence")
insight = Skill(ability="wisdom")
intimidation = Skill(ability="charisma")
investigation = Skill(ability="intelligence")
medicine = Skill(ability="wisdom")
nature = Skill(ability="intelligence")
perception = Skill(ability="wisdom")
performance = Skill(ability="charisma")
persuasion = Skill(ability="charisma")
religion = Skill(ability="intelligence")
sleight_of_hand = Skill(ability="dexterity")
stealth = Skill(ability="dexterity")
survival = Skill(ability="wisdom")
# Conditions
blinded = Blinded()
charmed = Charmed()
# TODO finish me!
# Inventory
cp = 0
sp = 0
ep = 0
gp = 0
pp = 0
equipment = ""
weapons = list()
magic_items = list()
armor = None
shield = None
# Magic
spellcasting_ability = None
_spells = list()
_spells_prepared = list()
infusions = list()
# Features IN MAJOR DEVELOPMENT
custom_features = list()
feature_choices = list()
# Current Status:
numerical_initiative = NumericalInitiative()
_initiative_roll = False
_current_hp = None
statuses = list()
# TODO: Pull in the monster class-variables here too
def __init__(self):
self.default_actions = list()
self.default_bonus_actions = list()
self.default_reactions = list()
self.default_legendary_actions = list()
self.default_lair_actions = list()
def roll_initiative(self):
init_mod, adv = self.numerical_initiative
val = roll(20)
if adv:
val = max(val, roll(20))
self._initiative_roll = val + init_mod
@property
def current_hp(self):
if self._current_hp is None:
self._current_hp = self.hp_max
return self._current_hp
@current_hp.setter
def current_hp(self, val):
if val < 0:
self._current_hp = 0
else:
self._current_hp = val
@property
def initiative_roll(self):
if self._initiative_roll is False:
self.roll_initiative()
return self._initiative_roll
def make_actions(self, encounter):
"""Return a series of actions"""
# TODO: Dramatically improve logic, consider healing,
# consider encounter state, consider strategy etc.
best_opponent = encounter.opponents(self)[0] # TODO: Choose opponent cleverly
action = self.actions[0](self, best_opponent)
event = action.execute()
encounter.events.append(event)
return [event] # TODO: Also allow bonus actions, etc.
def long_rest(self):
self.current_hp = self.hp_max
# TODO: Support spell slots
self.new_turn()
def new_turn(self):
self._actions = self.default_actions
self._bonus_actions = self.default_bonus_actions
self._reactions = self.default_reactions
self._legendary_actions = self.default_legendary_actions
self._lair_actions = self.default_lair_actions
def has_feature(self, *args, **kwargs):
return False # TODO: Save list of monster features as a list to check
# TODO: Consider having a single list of actions and gain or lose them each
# turn based on interrogating their sub-type instead, using isinstance or
# another method.
@property
def actions(self):
"""All the remaining things I can do in a turn"""
return self._actions
@property
def movement(self):
"""The rest of where I can go in a turn"""
return self._movement
@property
def bonus_actions(self):
"""The rest of the things I can do once in addition to an action"""
return self._bonus_actions
@property
def reactions(self):
"""The remaining things I can do in response to an action"""
return self._reactions
@property
def lair_actions(self):
"""Remaining things I can do at initiative count 20"""
return self._lair_actions
@property
def legendary_actions(self):
"""Remaining things I can do only so many times in a turn after another agent acts"""
return self._legendary_actions
-68
View File
@@ -1,68 +0,0 @@
class Encounter:
"""A combat encounter between two parties -- good guys and bad guys"""
def __init__(self, group_a, group_b):
self.group_a = group_a
self.group_b = group_b
self.all_agents = group_a + group_b
self.events = [] # Should be private?
def opponents(self, agent):
"""Who opposes the given agent in an encounter?"""
if agent in self.group_a:
return self.group_b
else:
return self.group_a
def allies(self, agent):
"""Who sides with the given agent in an encounter?"""
if agent in self.group_a:
return list(set(self.group_a) - set([agent]))
else:
return list(set(self.group_b) - set([agent]))
def reset(self):
self.events = []
self.long_rest()
def simulate(self):
"""Who will win?"""
# Initiative
for agent in self.all_agents:
agent.roll_initiative()
self.all_agents = sorted(self.all_agents, key=lambda a: a.initiative_roll)
# TODO: Support Lair Actions, cleverer loop
while not self.is_encounter_over():
self.new_turn()
for agent in self.all_agents:
agent.make_actions(self)
if self.is_encounter_over():
return self.events
return self.events # Should never get here -- self.is_encounter_over() will end it
def rating(self):
"""Encounter Rating"""
raise NotImplementedError()
def is_encounter_over(self):
"""If all members of one party are at HP <= 0, it's over"""
return (
all([agent.current_hp <= 0 for agent in self.group_a]) or
all([agent.current_hp <= 0 for agent in self.group_b])
)
def long_rest(self):
"""Resets all agents to have full actions, abilities, etc."""
for agent in self.all_agents:
agent.long_rest()
def new_turn(self):
"""Resets turn-based actions for all agents"""
for agent in self.all_agents:
agent.new_turn()
-28
View File
@@ -1,28 +0,0 @@
class Event:
"""An event between one and possibly more entities"""
def __init__(self, action, *args, **kwargs):
self.action = action
self.subj_hp = action.subj.current_hp
class AttackEvent(Event):
"""An attack action completed"""
def __init__(self, action, result, damage, is_hit):
super(AttackEvent, self).__init__(action)
if hasattr(self.action, "obj"):
self.obj_hp = self.action.obj.current_hp
self.result = result
self.damage = damage
self.is_hit = is_hit
def __str__(self):
if self.is_hit:
return f"{self.action.subj.name} Hit! with a {self.result} for {self.damage} damage, leaving {self.action.obj.name} with {self.obj_hp} hitpoints"
else:
return f"{self.action.subj.name} Missed! with a {self.result}. {self.action.obj.name} has {self.obj_hp} hp remaining."
# TODO: Support more events
+87
View File
@@ -0,0 +1,87 @@
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill,
from abc import ABC
import os
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
__version__ = read("../VERSION").strip()
class Entity(ABC):
"""A thing with stats. Use Monster or Character, not this class directly!"""
# General attributes
dungeonsheets_version = __version__
name = ""
alignment = "Neutral"
_race = None
# Hit points
hp_max = None
# Base stats (ability scores)
strength = Ability()
dexterity = Ability()
constitution = Ability()
intelligence = Ability()
wisdom = Ability()
charisma = Ability()
# Numerical things
armor_class = ArmorClass()
initiative = Initiative()
speed = Speed()
# Proficiencies and Languages
_saving_throw_proficiencies = tuple() # use to overwrite class proficiencies
other_weapon_proficiencies = tuple() # add to class/race proficiencies
skill_proficiencies = list()
skill_expertise = list()
languages = ""
senses = ""
# Skills
acrobatics = Skill(ability="dexterity")
animal_handling = Skill(ability="wisdom")
arcana = Skill(ability="intelligence")
athletics = Skill(ability="strength")
deception = Skill(ability="charisma")
history = Skill(ability="intelligence")
insight = Skill(ability="wisdom")
intimidation = Skill(ability="charisma")
investigation = Skill(ability="intelligence")
medicine = Skill(ability="wisdom")
nature = Skill(ability="intelligence")
perception = Skill(ability="wisdom")
performance = Skill(ability="charisma")
persuasion = Skill(ability="charisma")
religion = Skill(ability="intelligence")
sleight_of_hand = Skill(ability="dexterity")
stealth = Skill(ability="dexterity")
survival = Skill(ability="wisdom")
# Inventory
cp = 0
sp = 0
ep = 0
gp = 0
pp = 0
equipment = ""
weapons = list()
magic_items = list()
armor = None
shield = None
# Magic
spellcasting_ability = None
_spells = list()
_spells_prepared = list()
infusions = list()
# Features IN MAJOR DEVELOPMENT
custom_features = list()
feature_choices = list()
def __init__(self):
pass
+3 -13
View File
@@ -3,27 +3,17 @@ shape forms."""
from dungeonsheets.stats import Ability from dungeonsheets.stats import Ability
from dungeonsheets.encounter.agent import Agent from dungeonsheets.entity import Entity
class Monster(Agent): class Monster(Entity):
"""A monster that may be encountered when adventuring.""" """A monster that may be encountered when adventuring."""
name = "Generic Monster" name = "Generic Monster"
description = "" description = ""
challenge_rating = 0 challenge_rating = 0
armor_class = 0
skills = "Perception +3, Stealth +4" skills = "Perception +3, Stealth +4"
senses = "" swim_speed = 0 # TODO: Consider refactoring stats.Speed to consider all of these just like we do stats.Ability
languages = ""
strength = Ability()
dexterity = Ability()
constitution = Ability()
intelligence = Ability()
wisdom = Ability()
charisma = Ability()
speed = 30
swim_speed = 0
fly_speed = 0 fly_speed = 0
hp_max = 10 hp_max = 10
hit_dice = "1d6" hit_dice = "1d6"
+58 -62
View File
@@ -64,10 +64,6 @@ def findattr(obj, name):
def mod_str(modifier): def mod_str(modifier):
"""Converts a modifier to a string, eg 2 -> '+2'.""" """Converts a modifier to a string, eg 2 -> '+2'."""
return "{:+d}".format(modifier) return "{:+d}".format(modifier)
if modifier == 0:
return str(modifier)
else:
return "{:+}".format(modifier)
AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw")) AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw"))
@@ -90,25 +86,25 @@ class Ability:
# ability score dictionary exists but doesn't have this ability # ability score dictionary exists but doesn't have this ability
obj._ability_scores[self.ability_name] = self.default_value obj._ability_scores[self.ability_name] = self.default_value
def __get__(self, character, Character): def __get__(self, entity, Entity):
self._check_dict(character) self._check_dict(entity)
score = character._ability_scores[self.ability_name] score = entity._ability_scores[self.ability_name]
modifier = math.floor((score - 10) / 2) modifier = math.floor((score - 10) / 2)
# Check for proficiency # Check for proficiency
saving_throw = modifier saving_throw = modifier
if self.ability_name is not None and hasattr( if self.ability_name is not None and hasattr(
character, "saving_throw_proficiencies" entity, "saving_throw_proficiencies"
): ):
is_proficient = self.ability_name in character.saving_throw_proficiencies is_proficient = self.ability_name in entity.saving_throw_proficiencies
if is_proficient: if is_proficient:
saving_throw += character.proficiency_bonus saving_throw += entity.proficiency_bonus
# Create the named tuple # Create the named tuple
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw) value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw)
return value return value
def __set__(self, character, val): def __set__(self, entity, val):
self._check_dict(character) self._check_dict(entity)
character._ability_scores[self.ability_name] = val entity._ability_scores[self.ability_name] = val
self.value = val self.value = val
@@ -118,27 +114,27 @@ class Skill:
def __init__(self, ability): def __init__(self, ability):
self.ability_name = ability self.ability_name = ability
def __set_name__(self, character, name): def __set_name__(self, entity, name):
self.skill_name = name.lower().replace("_", " ") self.skill_name = name.lower().replace("_", " ")
self.character = character self.character = entity
def __get__(self, character, owner): def __get__(self, entity, owner):
ability = getattr(character, self.ability_name) ability = getattr(entity, self.ability_name)
modifier = ability.modifier modifier = ability.modifier
# Check for proficiency # Check for proficiency
is_proficient = self.skill_name in character.skill_proficiencies is_proficient = self.skill_name in entity.skill_proficiencies
if is_proficient: if is_proficient:
modifier += character.proficiency_bonus modifier += entity.proficiency_bonus
elif character.has_feature(JackOfAllTrades): elif entity.has_feature(JackOfAllTrades):
modifier += character.proficiency_bonus // 2 modifier += entity.proficiency_bonus // 2
elif character.has_feature(RemarkableAthelete): elif entity.has_feature(RemarkableAthelete):
if self.ability_name.lower() in ("strength", "dexterity", "constitution"): if self.ability_name.lower() in ("strength", "dexterity", "constitution"):
modifier += ceil(character.proficienc_bonus / 2.0) modifier += ceil(entity.proficienc_bonus / 2.0)
# Check for expertise # Check for expertise
is_expert = self.skill_name in character.skill_expertise is_expert = self.skill_name in entity.skill_expertise
if is_expert: if is_expert:
modifier += character.proficiency_bonus modifier += entity.proficiency_bonus
return modifier return modifier
@@ -147,36 +143,36 @@ class ArmorClass:
The Armor Class of a character The Armor Class of a character
""" """
def __get__(self, char, Character): def __get__(self, entity, Entity):
armor = char.armor or NoArmor() armor = entity.armor or NoArmor()
ac = armor.base_armor_class ac = armor.base_armor_class
# calculate and apply modifiers # calculate and apply modifiers
if armor.dexterity_mod_max is None: if armor.dexterity_mod_max is None:
ac += char.dexterity.modifier ac += entity.dexterity.modifier
else: else:
ac += min(char.dexterity.modifier, armor.dexterity_mod_max) ac += min(entity.dexterity.modifier, armor.dexterity_mod_max)
if char.has_feature(NaturalArmor): if entity.has_feature(NaturalArmor):
ac = max(ac, 13 + char.dexterity.modifier) ac = max(ac, 13 + entity.dexterity.modifier)
shield = char.shield or NoShield() shield = entity.shield or NoShield()
ac += shield.base_armor_class ac += shield.base_armor_class
# Compute feature-specific additions # Compute feature-specific additions
if char.has_feature(UnarmoredDefenseMonk): if entity.has_feature(UnarmoredDefenseMonk):
if isinstance(armor, NoArmor) and isinstance(shield, NoShield): if isinstance(armor, NoArmor) and isinstance(shield, NoShield):
ac += char.wisdom.modifier ac += entity.wisdom.modifier
if char.has_feature(UnarmoredDefenseBarbarian): if entity.has_feature(UnarmoredDefenseBarbarian):
if isinstance(armor, NoArmor): if isinstance(armor, NoArmor):
ac += char.constitution.modifier ac += entity.constitution.modifier
if char.has_feature(DraconicResilience): if entity.has_feature(DraconicResilience):
if isinstance(armor, NoArmor): if isinstance(armor, NoArmor):
ac += 3 ac += 3
if char.has_feature(Defense): if entity.has_feature(Defense):
if not isinstance(armor, NoArmor): if not isinstance(armor, NoArmor):
ac += 1 ac += 1
if char.has_feature(SoulOfTheForge): if entity.has_feature(SoulOfTheForge):
if isinstance(armor, HeavyArmor): if isinstance(armor, HeavyArmor):
ac += 1 ac += 1
# Check if any magic items add to AC # Check if any magic items add to AC
for mitem in char.magic_items: for mitem in entity.magic_items:
if hasattr(mitem, "ac_bonus"): if hasattr(mitem, "ac_bonus"):
ac += mitem.ac_bonus ac += mitem.ac_bonus
return ac return ac
@@ -187,25 +183,25 @@ class Speed:
The speed of a character The speed of a character
""" """
def __get__(self, char, Character): def __get__(self, entity, Entity):
speed = char.race.speed speed = entity.race.speed
other_speed = "" other_speed = ""
if isinstance(speed, str): if isinstance(speed, str):
other_speed = speed[2:] other_speed = speed[2:]
speed = int(speed[:2]) # ignore other speeds, like fly speed = int(speed[:2]) # ignore other speeds, like fly
if char.has_feature(FastMovement): if entity.has_feature(FastMovement):
if not isinstance(char.armor, HeavyArmor): if not isinstance(entity.armor, HeavyArmor):
speed += 10 speed += 10
if char.has_feature(SuperiorMobility): if entity.has_feature(SuperiorMobility):
speed += 10 speed += 10
if isinstance(char.armor, NoArmor) or (char.armor is None): if isinstance(entity.armor, NoArmor) or (entity.armor is None):
for f in char.features: for f in entity.features:
if isinstance(f, UnarmoredMovement): if isinstance(f, UnarmoredMovement):
speed += f.speed_bonus speed += f.speed_bonus
if char.has_feature(GiftOfTheDepths): if entity.has_feature(GiftOfTheDepths):
if "swim" not in other_speed: if "swim" not in other_speed:
other_speed += " ({:d} swim)".format(speed) other_speed += " ({:d} swim)".format(speed)
if char.has_feature(SeaSoul): if entity.has_feature(SeaSoul):
if "swim" not in other_speed: if "swim" not in other_speed:
other_speed += " (30 swim)" other_speed += " (30 swim)"
return "{:d}{:s}".format(speed, other_speed) return "{:d}{:s}".format(speed, other_speed)
@@ -214,19 +210,19 @@ class Speed:
class NumericalInitiative: class NumericalInitiative:
"""A numerical representation of initiative""" """A numerical representation of initiative"""
def __get__(self, char, Character): def __get__(self, entity, Entity):
ini = char.dexterity.modifier ini = entity.dexterity.modifier
if char.has_feature(QuickDraw): if entity.has_feature(QuickDraw):
ini += char.proficiency_bonus ini += entity.proficiency_bonus
if char.has_feature(DreadAmbusher): if entity.has_feature(DreadAmbusher):
ini += char.wisdom.modifier ini += entity.wisdom.modifier
if char.has_feature(RakishAudacity): if entity.has_feature(RakishAudacity):
ini += char.charisma.modifier ini += entity.charisma.modifier
has_advantage = ( has_advantage = (
char.has_feature(NaturalExplorerRevised) entity.has_feature(NaturalExplorerRevised)
or char.has_feature(FeralInstinct) or entity.has_feature(FeralInstinct)
or char.has_feature(AmbushMaster) or entity.has_feature(AmbushMaster)
) )
return ini, has_advantage return ini, has_advantage
@@ -234,8 +230,8 @@ class NumericalInitiative:
class Initiative(NumericalInitiative): class Initiative(NumericalInitiative):
"""A character's initiative""" """A character's initiative"""
def __get__(self, char, Character): def __get__(self, entity, Entity):
ini, has_advantage = super(Initiative, self).__get__(char, Character) ini, has_advantage = super(Initiative, self).__get__(entity, Entity)
ini = "{:+d}".format(ini) ini = "{:+d}".format(ini)
if has_advantage: if has_advantage:
ini += "(A)" ini += "(A)"
+3 -3
View File
@@ -64,7 +64,7 @@ class TestCharacter(TestCase):
self.assertEqual(char.spells[0].name, "my spell!") self.assertEqual(char.spells[0].name, "my spell!")
def test_homebrew_infusions(self): def test_homebrew_infusions(self):
char = Character(classes="artificer") char = Character(class_list="artificer")
class MyInfusion(infusions.Infusion): class MyInfusion(infusions.Infusion):
name = "my infusion!" name = "my infusion!"
@@ -74,7 +74,7 @@ class TestCharacter(TestCase):
self.assertIsInstance(char.infusions[0], infusions.Infusion) self.assertIsInstance(char.infusions[0], infusions.Infusion)
self.assertEqual(char.infusions[0].name, "my infusion!") self.assertEqual(char.infusions[0].name, "my infusion!")
# Pass a previously undefined infusion # Pass a previously undefined infusion
char = Character(classes="artificer") char = Character(class_list="artificer")
char.set_attrs(infusions=("spam_infusion",)) char.set_attrs(infusions=("spam_infusion",))
self.assertIsInstance(char.infusions[0], infusions.Infusion) self.assertIsInstance(char.infusions[0], infusions.Infusion)
self.assertEqual(char.infusions[0].name, "Spam Infusion") self.assertEqual(char.infusions[0].name, "Spam Infusion")
@@ -129,7 +129,7 @@ class TestCharacter(TestCase):
self.assertEqual(repr(char), "<Wizard: Inara>") self.assertEqual(repr(char), "<Wizard: Inara>")
def test_is_proficient(self): def test_is_proficient(self):
char = Character(classes=["Wizard"]) char = Character(class_list=["Wizard"])
char.weapon_proficiencies char.weapon_proficiencies
sword = Shortsword() sword = Shortsword()
# Check for not-proficient weapon # Check for not-proficient weapon
-254
View File
@@ -1,254 +0,0 @@
#!/usr/bin/env python
from unittest import TestCase, skip
from dungeonsheets.armor import ChainShirt
from dungeonsheets.character import Character
from dungeonsheets.encounter import Encounter
from dungeonsheets.encounter.actions import Attack
from dungeonsheets.encounter.events import AttackEvent
from dungeonsheets.monsters import Monster, GiantRat
from dungeonsheets.stats import Ability
from dungeonsheets.dice import roll
class TestEncounter(TestCase):
"""Tests for features and feature-related activities."""
def setUp(self):
char = SimpleRanger()
class StravajiaxenAttack(Attack):
# TODO: Get default player attacks as a result of
# creating a player character and classes
def __init__(self, subj, obj):
super(StravajiaxenAttack, self).__init__(subj, obj)
def execute(self):
result = roll(20) + self.subj.dexterity.modifier + self.subj.proficiency_bonus
damage = roll(1, 8) + self.subj.dexterity.modifier
is_hit = result >= self.obj.armor_class
if is_hit:
self.obj.current_hp -= damage
return AttackEvent(self, result, damage, is_hit)
char.default_actions.append(StravajiaxenAttack)
class GiantRatAttack(Attack):
def __init__(self, subj, obj):
super(GiantRatAttack, self).__init__(subj, obj)
def execute(self):
result = roll(20) + 4
damage = roll(1, 4) + 2
is_hit = result >= self.obj.armor_class
if is_hit:
self.obj.current_hp -= damage
return AttackEvent(self, result, damage, is_hit)
enemy = GiantRat()
enemy.name = "Nameless Rat" # I don't want things to be personal...
enemy.default_actions.append(GiantRatAttack)
self.default_player = char
self.default_enemy = enemy
def test_encounter_rating(self):
battle = Encounter([self.default_player], [self.default_enemy])
self.assertRaises(NotImplementedError, battle.rating)
def test_opponents(self):
battle = Encounter([self.default_player], [self.default_enemy])
self.assertEqual([self.default_enemy], battle.opponents(self.default_player))
self.assertEqual([self.default_player], battle.opponents(self.default_enemy))
def test_allies(self):
battle = Encounter([self.default_player], [self.default_enemy])
self.assertEqual(0, len(battle.allies(self.default_player)))
self.assertEqual(0, len(battle.allies(self.default_enemy)))
def test_simple_rat_fight(self):
"""A fight against a giant rat"""
battle = Encounter([self.default_player], [self.default_enemy])
battle.reset()
results = battle.simulate()
for event in results:
print(str(event))
print(results)
@skip('NotImplementedError')
def test_langdedrosa_fight(self):
"""Can I run an encounter against Langdedrosa Cyanwrath?"""
char = Character()
char.set_attrs(name="Stravajiaxen")
char.set_attrs(weapons=["greataxe"])
char.set_attrs(armor="split mail")
# Check that race gets set to an object
char.set_attrs(race="half orc")
char.set_attrs(inspiration=False)
class LangdedrosaCyanwrath(Monster):
"""
**Action Surge (Recharges on a Short or Long Rest).** On his turn, Langdedrosa
can take one additional action.
**Improved Critical.** Langdedrosa's weapon attacks score a critical hit on a
roll of 19 or 20.
**Multiattack:** Schlangdedrosa attacks twice, either with his greatsword or spear.
**Greatsword.** Melee Weapon Attack: +6 to hit, reach 5 ft., one target.
Hit: 11 (2d6 + 4) slashing damage.
**Spear.** Melee or Ranged Weapon Attack: +6 to hit, reach 5 ft. or
ranged 20/60 ft., one target. Hit: 7 (1d6 + 4) piercing damage.
**Lightning Breath (Recharge 5-6)**. Schlangdedrosa breathes lightning in a
30-foot line that is 5 feet wide. Each creature in the line must make a DC 13
Dexterity saving throw, taking 22 (4d10) lightning damage on a failed save, or
half as much damage on a successful one.
**Climbing speed:** 30 ft.
"""
name = "Langdedrosa Cyanwrath"
description = "Medium humanoid (half-dragon), lawful evil"
challenge_rating = 4
armor_class = 17
skills = "Athletics +6, Intimidation +3, Perception +4"
senses = "blindsight 10 ft., darkvision 60ft., passive Perception 14"
strength = Ability(19)
dexterity = Ability(13)
constitution = Ability(16)
intelligence = Ability(10)
wisdom = Ability(14)
charisma = Ability(12)
speed = 30
swim_speed = 0
fly_speed = 0
hp_max = 57
hit_dice = "6d12+18"
lang = LangdedrosaCyanwrath()
battle = Encounter([char], [lang])
results = battle.simulate()
class SimpleRanger(Character): # Taken from ranger2.py
"""This file describes the heroic adventurer Ranger2.
It's used primarily for saving characters from create-character,
where there will be many missing sections.
Modify this file as you level up and then re-generate the character
sheet by running ``makesheets`` from the command line.
"""
dungeonsheets_version = "0.9.4"
name = "Stravajiaxen"
player_name = "Ben"
# Be sure to list Primary class first
classes = ['Ranger'] # ex: ['Wizard'] or ['Rogue', 'Fighter']
levels = [3] # ex: [10] or [3, 2]
subclasses = ["Horizon Walker"] # ex: ['Necromacy'] or ['Thief', None]
background = "Uthgardt Tribe Member"
race = "Lizardfolk"
alignment = "Neutral good"
xp = 0
hp_max = 24
inspiration = 0 # integer inspiration value
# Ability Scores
strength = Ability(13)
dexterity = Ability(15)
constitution = Ability(12)
intelligence = Ability(8)
wisdom = Ability(15)
charisma = Ability(12)
# Select what skills you're proficient with
# ex: skill_proficiencies = ('athletics', 'acrobatics', 'arcana')
skill_proficiencies = ('athletics', 'insight', 'investigation')
# Any skills you have "expertise" (Bard/Rogue) in
skill_expertise = ()
# Named features / feats that aren't part of your classes, race, or background.
# Also include Eldritch Invocations and features you make multiple selection of
# (like Maneuvers for Fighter, Metamagic for Sorcerors, Trick Shots for
# Gunslinger, etc.)
# Example:
# features = ('Tavern Brawler',) # take the optional Feat from PHB
features = ()
# If selecting among multiple feature options: ex Fighting Style
# Example (Fighting Style):
# feature_choices = ('Archery',)
feature_choices = ('dueling',)
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Dwarvish, Common, Draconic"""
# Inventory
# TODO: Get yourself some money
cp = 0
sp = 0
ep = 0
gp = 0
pp = 0
# TODO: Put your equipped weapons and armor here
weapons = ('rapier', 'hand crossbow') # Example: ('shortsword', 'longsword')
magic_items = () # Example: ('ring of protection',)
armor = ChainShirt # Eg "leather armor"
shield = "" # Eg "shield"
equipment = """TODO: list the equipment and magic items your character carries"""
attacks_and_spellcasting = """TODO: Describe how your character usually attacks
or uses spells."""
# List of known spells
# Example: spells_prepared = ('magic missile', 'mage armor')
spells_prepared = () # Todo: Learn some spells
# Which spells have not been prepared
__spells_unprepared = ()
# all spells known
spells = spells_prepared + __spells_unprepared
# Wild shapes for Druid
wild_shapes = () # Ex: ('ape', 'wolf', 'ankylosaurus')
# Backstory
# Describe your backstory here
personality_traits = """TODO: How does your character behave? See the PHB for
examples of all the sections below"""
ideals = """TODO: What does your character believe in?"""
bonds = """TODO: Describe what debts your character has to pay,
and other commitments or ongoing quests they have."""
flaws = """TODO: Describe your characters interesting flaws.
"""
features_and_traits = """TODO: Describe other features and abilities your
character has."""
+7 -7
View File
@@ -13,24 +13,24 @@ class TestMulticlass(TestCase):
def test_constructor(self): def test_constructor(self):
char = Character( char = Character(
name="Multiclass", classes=["wizard", "fighter"], levels=[5, 4] name="Multiclass", class_list=["wizard", "fighter"], levels=[5, 4]
) )
self.assertIsInstance(char, Character) self.assertIsInstance(char, Character)
def test_level(self): def test_level(self):
char = Character( char = Character(
name="Multiclass", classes=["wizard", "fighter"], levels=[5, 4] name="Multiclass", class_list=["wizard", "fighter"], levels=[5, 4]
) )
self.assertEqual(char.level, 9) self.assertEqual(char.level, 9)
def test_spellcasting(self): def test_spellcasting(self):
char = Character( char = Character(
name="Multiclass", classes=["wizard", "fighter"], levels=[5, 4] name="Multiclass", class_list=["wizard", "fighter"], levels=[5, 4]
) )
self.assertEqual(len(char.spellcasting_classes), 1) self.assertEqual(len(char.spellcasting_classes), 1)
char = Character( char = Character(
name="Multiclass", name="Multiclass",
classes=["wizard", "fighter"], class_list=["wizard", "fighter"],
subclasses=[None, "Eldritch Knight"], subclasses=[None, "Eldritch Knight"],
levels=[5, 4], levels=[5, 4],
) )
@@ -43,12 +43,12 @@ class TestMulticlass(TestCase):
def test_proficiencies(self): def test_proficiencies(self):
char1 = Character( char1 = Character(
name="Multiclass", classes=["wizard", "fighter"], levels=[5, 4] name="Multiclass", class_list=["wizard", "fighter"], levels=[5, 4]
) )
for svt in ("intelligence", "wisdom"): for svt in ("intelligence", "wisdom"):
self.assertIn(svt, char1.saving_throw_proficiencies) self.assertIn(svt, char1.saving_throw_proficiencies)
char2 = Character(name="Multiclass", classes=["wizard", "rogue"], levels=[5, 4]) char2 = Character(name="Multiclass", class_list=["wizard", "rogue"], levels=[5, 4])
char3 = Character(name="Multiclass", classes=["rogue", "wizard"], levels=[4, 5]) char3 = Character(name="Multiclass", class_list=["rogue", "wizard"], levels=[4, 5])
sword = Shortsword() sword = Shortsword()
self.assertTrue(char1.is_proficient(sword)) self.assertTrue(char1.is_proficient(sword))
# multiclassing into Rogue doesn't give simple weapon proficiency # multiclassing into Rogue doesn't give simple weapon proficiency