mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-07 05:03:31 +02:00
Finish encounter, test, dice unittest, attack actions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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)])
|
||||
@@ -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
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user