Finish encounter, test, dice unittest, attack actions

This commit is contained in:
Matthew DeMartino
2021-05-23 14:52:18 -04:00
parent 4bc844a6bf
commit 89143c5cbc
10 changed files with 262 additions and 93 deletions
+1
View File
@@ -197,6 +197,7 @@ class Character(Agent):
character.
"""
super(Character, self).__init__()
self.clear()
# make sure class, race, background are set first
my_classes = classes
+9
View File
@@ -1,3 +1,4 @@
import random
import re
from collections import namedtuple
@@ -22,3 +23,11 @@ def read_dice_str(dice_str):
raise DiceError(f"Cannot interpret dice string {dice_str}")
dice = Dice(num=int(match.group(1)), faces=int(match.group(2)))
return dice
def roll(a, b=None):
"""roll(20) means roll 1d20, roll(2, 6) means roll 2d6"""
if b is None:
return random.randint(1, a)
else:
return sum([random.randint(1, b) for _ in range(a)])
+1 -17
View File
@@ -1,15 +1,6 @@
from abc import ABC, abstractmethod
class Event:
"""An event between one and possibly more entities"""
subj = None
obj = None
def __init__(self, action, subj, obj):
self.action = action
self.subj = subj
self.obj = obj
from dungeonsheets.encounter.events import Event
class Executable(ABC):
@@ -52,10 +43,3 @@ class Attack(Action):
def __init__(self, subj, obj):
self.subj = subj
self.obj = obj
def execute(self):
# Subject makes an attack roll
# Compare attack roll to object's AC
# Store the results to look into the event later
pass # TODO: Write how to do this
+27 -54
View File
@@ -3,7 +3,7 @@ from dungeonsheets.encounter.actions import Attack
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill, \
NumericalInitiative
from abc import ABC
from dungeonsheets.utils import roll
from dungeonsheets.dice import roll
class Agent(ABC):
@@ -92,6 +92,11 @@ class Agent(ABC):
# 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()
self.long_rest()
def roll_initiative(self):
@@ -107,6 +112,13 @@ class Agent(ABC):
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:
@@ -116,76 +128,37 @@ class Agent(ABC):
def make_actions(self, encounter):
"""Return a series of actions"""
# TODO: Dramatically improve logic, consider healing, consider encounter state, etc.
# TODO: Dramatically improve logic, consider healing,
# consider encounter state, consider strategy etc.
best_opponent = encounter.opponents(self)[0] # TODO: Choose opponent cleverly
action = Attack(self, best_opponent)
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
self._free_actions = self._default_free_actions
# 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
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 their sub-type instead.
@property
def default_actions(self):
"""All the things I can do in a turn"""
return []
@property
def default_free_actions(self):
"""Stuff I can do as much as I want in a turn"""
return []
@property
def default_movement(self):
"""Where I can go in a turn"""
return []
@property
def default_bonus_actions(self):
"""Things I can do once in addition to an action"""
return []
@property
def default_reactions(self):
"""Things I can do in response to an action"""
return []
@property
def default_lair_actions(self):
"""Things I can do at initiative count 20"""
return []
@property
def default_legendary_actions(self):
"""Things I can do only so many times in a turn after another agent acts"""
return []
@property
def default_long_rest_actions(self):
"""Actions I gain back if I've had a long rest"""
# 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 free_actions(self):
"""Stuff I can do as much as I want in a turn"""
return self._free_actions
@property
def movement(self):
+2 -12
View File
@@ -6,7 +6,7 @@ class Encounter:
self.group_b = group_b
self.all_agents = group_a + group_b
self._events = []
self.events = [] # Should be private?
def opponents(self, agent):
"""Who opposes the given agent in an encounter?"""
@@ -23,7 +23,7 @@ class Encounter:
return list(set(self.group_b) - set(agent))
def reset(self):
self._events = []
self.events = []
self.long_rest()
def rating(self):
@@ -48,8 +48,6 @@ class Encounter:
return self.events # Should never get here -- self.is_encounter_over() will end it
raise NotImplementedError() # TODO: Finish the encounter
def is_encounter_over(self):
"""If all members of one party are at HP <= 0, it's over"""
return (
@@ -67,11 +65,3 @@ class Encounter:
for agent in self.all_agents:
agent.new_turn()
@property
def events(self):
"""What series of events went down in the encounter?"""
return self._events
def analyze(self):
"""So, really... how deadly *is* it?"""
raise NotImplementedError() # TODO: Run a Monte-Carlo simulation
+28
View File
@@ -0,0 +1,28 @@
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
+3
View File
@@ -28,6 +28,9 @@ class Monster(Agent):
hp_max = 10
hit_dice = "1d6"
def __init__(self):
super(Monster, self).__init__()
@property
def is_beast(self):
is_beast = "beast" in self.description.lower()
-6
View File
@@ -1,6 +0,0 @@
import random
def roll(d, n=1):
"""roll(6, 2) means roll 2d6"""
return sum([random.randint(1, d) for _ in range(n)])
+20
View File
@@ -1,10 +1,12 @@
from unittest import TestCase
from dungeonsheets.dice import roll
from dungeonsheets.exceptions import DiceError
from dungeonsheets import dice
class TestDice(TestCase):
def test_read_dice_str(self):
out = dice.read_dice_str("1d6")
self.assertEqual(out.faces, 6)
@@ -16,3 +18,21 @@ class TestDice(TestCase):
# Check a bad value
with self.assertRaises(DiceError):
dice.read_dice_str("Ed15")
def test_simple_rolling(self):
num_tests = 100
for _ in range(num_tests):
result = roll(6)
self.assertGreaterEqual(result, 1)
self.assertLessEqual(result, 6)
def test_multi_rolling(self):
num_tests = 100
rolls = []
for _ in range(num_tests):
result = roll(2, 4) # Roll 2d4
self.assertGreaterEqual(result, 2)
self.assertLessEqual(result, 8)
rolls.append(result)
print(rolls)
self.assertGreaterEqual(max(rolls), 7) # Must sometimes get a 7 or 8 after rolling 2d4 100 times
+171 -4
View File
@@ -1,17 +1,72 @@
#!/usr/bin/env python
from unittest import TestCase
from unittest import TestCase, skip
from dungeonsheets.armor import ChainShirt
from dungeonsheets.character import Character
from dungeonsheets.encounter import Encounter
from dungeonsheets.monsters import Monster
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 test_simulation(self):
def test_simple_rat_fight(self):
"""A fight against a giant rat"""
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):
self.subj = subj
self.obj = 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):
self.subj = subj
self.obj = 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)
battle = Encounter([char], [enemy])
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")
@@ -67,4 +122,116 @@ class TestEncounter(TestCase):
lang = LangdedrosaCyanwrath()
battle = Encounter([char], [lang])
results = battle.simulate()
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."""