Skip to content

Commit

Permalink
Merge pull request #36 from mmacy/treasure-and-char-work-3
Browse files Browse the repository at this point in the history
Basic treasure system + unit tests
  • Loading branch information
mmacy committed Jan 23, 2024
2 parents 2224104 + 4a79a3e commit 016da12
Show file tree
Hide file tree
Showing 5 changed files with 518 additions and 334 deletions.
140 changes: 97 additions & 43 deletions osrlib/osrlib/ability.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ class Ability(ABC):
"""Abstract base class for character abilities.
Abilities are inherent traits that every character possesses in varying degrees.
They provide different kinds of modifiers which can affect different game actions.
They provide different modifiers (bonuses or penalties) that can affect gameplay
mechanics like dice rolls during game play or core aspects of the character like
whether they're especially hard to hit (lower AC) because of a high dexterity
score or whether they know additional languages because of a high intelligence score.
Attributes:
score (int): The raw ability score.
modifiers (dict): A mapping of the ability's modifier types to modifier values based on the ability score.
modifiers (dict): A mapping of the ability's modifier types to modifier values based on the
ability score.
"""

_MODIFIER_MAP = {
Expand All @@ -40,16 +44,26 @@ def __init__(self, score: int):
self.ability_type = None
self.score = score
self.modifiers = {}
self.init_modifiers()
self._init_modifiers()
self.prime_requisite_classes = []

def __str__(self):
"""Return string representation of the ability."""
modifiers_str = ", ".join(f"{mod.value}: {'+' if val > 0 else ''}{val}" for mod, val in self.modifiers.items())
modifiers_str = ", ".join(
f"{mod.value}: {'+' if val > 0 else ''}{val}"
for mod, val in self.modifiers.items()
)
return f"{self.ability_type.value}: {self.score} - {modifiers_str}"

def get_earned_xp_adjustment(self):
"""Get the experience adjustment for the ability score."""
def get_earned_xp_adjustment(self) -> float:
"""Calculate the experience points adjustment based on the ability score.
Determines the percentage adjustment to experience points earned, based on the character's
ability score. This adjustment can be positive or negative depending on the score.
Returns:
float: The experience point adjustment as a decimal percentage.
"""
if self.score >= 16:
return 0.10
elif self.score >= 13:
Expand All @@ -62,23 +76,44 @@ def get_earned_xp_adjustment(self):
return -0.20

@abstractmethod
def init_modifiers(self):
"""Abstract method to initialize ability modifiers."""
def _init_modifiers(self) -> None:
"""Initialize ability modifiers in subclasses.
Subclasses must implement this method to define specific modifiers for each ability type.
These modifiers adjust various gameplay mechanics like bonuses and penalties for dice rolls
or adjustments to experience points earned.
"""
pass

def _get_modifier(self) -> int:
return self._MODIFIER_MAP.get(self.score, 0)

def to_dict(self) -> dict:
"""Convert ability instance to a dictionary for serialization.
Useful for serializing and transferring the ability's data during a game save operation.
Returns:
dict: Dictionary containing the ability's type and score.
"""
return {
"ability_type": self.ability_type.name,
"score": self.score,
}

@classmethod
def from_dict(cls, data: dict) -> "Ability":
ability = cls(score=data["score"])
"""Create an ability instance from a dictionary.
Useful for deserializing the ability's data during a game load operation.
Args:
data (dict): Dictionary containing the ability's type and score.
Returns:
Ability: Instance of the Ability class or its subclasses.
"""
ability = cls(score=data["score"])
return ability


Expand All @@ -89,9 +124,9 @@ class Strength(Ability):
It primarily influences hand-to-hand combat and opening doors.
Modifiers:
TO_HIT: Modifier to hand-to-hand attack rolls.
DAMAGE: Modifier to damage in hand-to-hand combat.
OPEN_DOORS: Modifier to chances of opening stuck doors.
TO_HIT (ModifierType.TO_HIT): Modifies melee (hand-to-hand) attack rolls.
DAMAGE (ModifierType.DAMAGE): Modifies damage in melee combat.
OPEN_DOORS (ModifierType.OPEN_DOORS): Modifies chances of opening stuck doors.
"""

def __init__(self, score: int):
Expand All @@ -109,13 +144,15 @@ def __init__(self, score: int):
CharacterClassType.HALFLING,
]

def init_modifiers(self):
"""Initialize the Strength modifiers.
def _init_modifiers(self) -> None:
"""Initialize Strength-specific ability modifiers.
Modifiers:
TO_HIT: Modifier to hand-to-hand attack rolls.
DAMAGE: Modifier to damage in hand-to-hand combat.
OPEN_DOORS: Modifier to chances of opening stuck doors.
TO_HIT (ModifierType.TO_HIT): Modifies melee (hand-to-hand) attack rolls.
DAMAGE (ModifierType.DAMAGE): Modifies damage in melee combat.
OPEN_DOORS (ModifierType.OPEN_DOORS): Modifies chances of opening stuck doors.
Each modifier is calculated based on the strength score of the character.
"""
self.modifiers[ModifierType.TO_HIT] = self._get_modifier()
self.modifiers[ModifierType.DAMAGE] = self._get_modifier()
Expand All @@ -125,10 +162,12 @@ def init_modifiers(self):
class Intelligence(Ability):
"""Represents the Intelligence ability for characters.
Intelligence is a measure of problem-solving ability, linguistic capability, and magical aptitude.
Intelligence is a measure of problem-solving ability, linguistic capability, and
magical aptitude.
Modifiers:
LANGUAGES: Modifier to the number of additional languages the character can learn.
LANGUAGES (ModifierType.LANGUAGES): Modifies the number of additional languages the
character can read and write.
"""

def __init__(self, score: int):
Expand All @@ -154,11 +193,14 @@ def __init__(self, score: int):
18: 3,
}

def init_modifiers(self):
"""Initialize the Intelligence modifiers.
def _init_modifiers(self) -> None:
"""Initialize Intelligence-specific ability modifiers.
Modifiers:
LANGUAGES: Modifier to the number of additional languages the character can learn.
LANGUAGES (ModifierType.LANGUAGES): Modifies the number of additional languages
the character can read and write.
The modifier is calculated based on the intelligence score of the character.
"""
self.modifiers[ModifierType.LANGUAGES] = self._get_modifier()

Expand All @@ -169,7 +211,8 @@ class Wisdom(Ability):
Wisdom measures a character's common sense, intuition, and willpower.
Modifiers:
SAVING_THROWS: Modifier to saving throws against spells and magical effects.
SAVING_THROWS (ModifierType.SAVING_THROWS): Modifies saving throws against spells and
magical effects.
"""

def __init__(self, score: int):
Expand All @@ -182,11 +225,14 @@ def __init__(self, score: int):
self.ability_type = AbilityType.WISDOM
self.prime_requisite_classes = [CharacterClassType.CLERIC]

def init_modifiers(self):
"""Initialize the Wisdom modifiers.
def _init_modifiers(self) -> None:
"""Initialize Wisdom-specific ability modifiers.
Modifiers:
SAVING_THROWS: Modifier to saving throws against spells and magical effects.
SAVING_THROWS (ModifierType.SAVING_THROWS): Modifies saving throws against spells
and magical effects.
Each modifier is calculated based on the wisdom score of the character.
"""
self.modifiers[ModifierType.SAVING_THROWS] = self._get_modifier()

Expand All @@ -197,9 +243,9 @@ class Dexterity(Ability):
Dexterity measures agility, reflexes, and coordination.
Modifiers:
TO_HIT: Modifier to ranged attack rolls.
AC: Modifier to Armor Class (inverts standard modifier since lower AC is better).
INITIATIVE: Modifier to initiative rolls.
TO_HIT (ModifierType.TO_HIT): Modifies ranged attack rolls.
AC (ModifierType.AC): Modifies armor class (lower is better).
INITIATIVE (ModifierType.INITIATIVE): Modifies initiative rolls.
"""

def __init__(self, score: int):
Expand Down Expand Up @@ -236,15 +282,17 @@ def __init__(self, score: int):
def _get_initiative_modifier(self) -> int:
return self._INITIATIVE_MODIFIER_MAP.get(self.score, 0)

def init_modifiers(self):
"""Initialize the Dexterity modifiers.
def _init_modifiers(self) -> None:
"""Initialize Dexterity-specific ability modifiers.
Modifiers:
TO_HIT: Modifier to ranged attack rolls.
AC: Modifier to Armor Class (inverts standard modifier since lower AC is better).
INITIATIVE: Modifier to initiative rolls.
TO_HIT (ModifierType.TO_HIT): Modifies ranged attack rolls.
AC (ModifierType.AC): Modifies armor class (lower is better).
INITIATIVE (ModifierType.INITIATIVE): Modifies initiative rolls.
Each modifier is calculated based on the dexterity score of the character.
"""
self.modifiers[ModifierType.AC] = -self._get_modifier() # Lower-is-better for AC
self.modifiers[ModifierType.AC] = -self._get_modifier()
self.modifiers[ModifierType.TO_HIT] = self._get_modifier()
self.modifiers[ModifierType.INITIATIVE] = self._get_initiative_modifier()

Expand All @@ -255,7 +303,8 @@ class Constitution(Ability):
Constitution measures stamina, endurance, and overall health.
Modifiers:
HP: Modifier to Hit Points gained per level.
HP (ModifierType.HP): Modifies hit point (HP) rolls. For example, when initially rolling the
character or when the character gains a level.
"""

def __init__(self, score: int):
Expand All @@ -267,11 +316,14 @@ def __init__(self, score: int):
super().__init__(score)
self.ability_type = AbilityType.CONSTITUTION

def init_modifiers(self):
"""Initialize the Constitution modifiers.
def _init_modifiers(self) -> None:
"""Initialize Constitution-specific ability modifiers.
Modifiers:
HP: Modifier to Hit Points gained per level.
HP (ModifierType.HP): Modifies hit point (HP) rolls. For example, when initially rolling
the character or when the character gains a level.
The modifier is calculated based on the constitution score of the character.
"""
self.modifiers[ModifierType.HP] = self._get_modifier()

Expand All @@ -282,7 +334,7 @@ class Charisma(Ability):
Charisma measures force of personality, leadership ability, and physical attractiveness.
Modifiers:
REACTION: Modifier to reaction rolls when interacting with NPCs.
REACTION (ModifierType.REACTION): Modifies reaction rolls when interacting with NPCs.
"""

def __init__(self, score: int):
Expand Down Expand Up @@ -310,10 +362,12 @@ def __init__(self, score: int):
18: 2,
}

def init_modifiers(self):
"""Initialize the Charisma modifiers.
def _init_modifiers(self) -> None:
"""Initialize Charisma-specific ability modifiers.
Modifiers:
REACTION: Modifier to reaction rolls when interacting with NPCs.
REACTION (ModifierType.REACTION): Modifies reaction rolls when interacting with NPCs.
The modifier is calculated based on the charisma score of the character.
"""
self.modifiers[ModifierType.REACTION] = self._get_modifier()
63 changes: 34 additions & 29 deletions osrlib/osrlib/dice_roller.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
"""Dice roller module for rolling dice based on the nDn or Dn notation, supporting modifiers."""
import random, re
from collections import namedtuple
"""Dice roller module for rolling dice based on the nDn or Dn notation, supporting modifiers.
This module provides functionality for rolling dice using traditional RPG notations such as '3d6' (three six-sided dice)
and '1d20+5' (one twenty-sided die plus a modifier of 5). It supports modifiers and the option to drop the lowest roll.
"""

import random
import re
from collections import namedtuple

class DiceRoll(
namedtuple(
"RollResultBase",
["num_dice", "num_sides", "total", "modifier", "total_with_modifier", "rolls"],
)
):
"""Roll dice based on the nDn or Dn notation, optionally including a modifier like '3d6+2' or '1d20-1'.
Args:
namedtuple (RollResult): The named tuple containing the number of dice, number of sides, base roll, modifier, total roll with modifier, and the individual rolls.
"""Represent the result of a dice roll.
This class is a named tuple containing details of a dice roll, including the number of dice, number of sides,
base roll, modifier, total roll with modifier, and the individual rolls.
Attributes:
num_dice (int): Number of dice rolled.
num_sides (int): Number of sides on each die.
total (int): Total value of the dice rolls without modifiers.
modifier (int): Modifier value added to or subtracted from the total roll.
total_with_modifier (int): Total value of the dice rolls including the modifier.
rolls (list): List of individual die roll results.
"""

def __str__(self):
"""
Returns a string representation of the dice roll based on the ndn notation, including modifiers if applicable.
Return a string representation of the dice roll in ndn notation, including modifiers if applicable.
Returns:
str: A string in ndn notation (e.g., '3d6', '1d20+5', '2d8-4').
Expand All @@ -31,45 +44,37 @@ def __str__(self):
return base

def pretty_print(self):
"""Returns a human-readable string representation of the dice roll, including the total roll and any modifiers.
"""Return a human-readable string representation of the dice roll, including the total roll and any modifiers.
Returns:
str: A string describing the dice roll and its outcome (e.g., 'Rolled 3d6 and got 11 (11)', 'Rolled 1d20+3 and got 9 (6 + 3)').
str: A string describing the dice roll and its outcome (e.g., 'Rolled 3d6 and got 11 (11)',
'Rolled 1d20+3 and got 9 (6 + 3)').
"""
base_str = f"{self.total}"
if self.modifier != 0:
base_str += f" {'+' if self.modifier > 0 else '-'} {abs(self.modifier)}"
return f"Rolled {self.total_with_modifier} on {self} ({base_str})"

def roll_dice(notation: str, modifier: int = 0, drop_lowest: bool = False) -> DiceRoll:
"""Roll dice based on the nDn or Dn notation and factor in optional modifiers.
def roll_dice(notation: str, modifier: int = 0, drop_lowest: bool = False):
"""Rolls dice based on the nDn or Dn notation and factors in optional modifiers. Also accepts a string representing a single integer value.
To guarantee the result of the roll, specify a single string-formatted integer for ``notation``. For example, to
guarantee a roll of 20, pass "20" in the ``notation`` parameter. The ``RollResult`` that's returned will always be a
single roll on a die whose number of sides is the ``notation`` value as are its ``RollResult.total`` and
``RollResult.total_with_modifier`` attribute values.
Examples:
roll_dice('3d6') -> DiceRoll object representing a roll of three six-sided dice.
roll_dice('1d20+5') -> DiceRoll object for rolling one twenty-sided die with a +5 modifier.
roll_dice('20') -> DiceRoll object for a guaranteed roll of 20.
Args:
notation (str): A string representation of a dice roll in ndn format with optional modifiers like '3d6', '1d20+5', or '2d8-4'. Or specify single integer as string like '1', '20', or '18'.
notation (str): A string representation of a dice roll in ndn format with optional modifiers like '3d6',
'1d20+5', or '2d8-4', or a string representing an integer, like '1', '20', or '18'.
modifier (int): An optional additional integer modifier to add to the roll. Defaults to 0.
drop_lowest (bool): Whether to drop the lowest dice roll. Defaults to False.
Returns:
DiceRoll: A named tuple containing the number of dice, number of sides, base roll, modifier, total roll with modifier, and the individual rolls.
DiceRoll: A named tuple containing the number of dice, number of sides, base roll, modifier, total roll
with modifier, and the individual rolls.
Raises:
ValueError: If the notation or dice sides are invalid.
Example usage:
>>> result = roll_dice('3d6')
>>> print(result.pretty_print())
>>> result = roll_dice('1d20+5')
>>> print(result.pretty_print())
>>> result = roll_dice('4d6', drop_lowest=True)
>>> print(result.pretty_print())
"""
notation = notation.replace(" ", "").lower()

Expand Down
Loading

0 comments on commit 016da12

Please sign in to comment.