From 89f3df2b1e58e28a57c2cd2fe474d759552db0a5 Mon Sep 17 00:00:00 2001 From: Marsh Macy Date: Sun, 21 Jan 2024 22:43:02 -0800 Subject: [PATCH 1/5] Basic treasure system + unit tests --- osrlib/osrlib/dice_roller.py | 32 +- osrlib/osrlib/enums.py | 17 +- osrlib/osrlib/treasure.py | 596 ++++++++++++++++++++--------------- tests/test_unit_treasure.py | 86 +++++ 4 files changed, 463 insertions(+), 268 deletions(-) create mode 100644 tests/test_unit_treasure.py diff --git a/osrlib/osrlib/dice_roller.py b/osrlib/osrlib/dice_roller.py index c69df74..fe4a3b1 100644 --- a/osrlib/osrlib/dice_roller.py +++ b/osrlib/osrlib/dice_roller.py @@ -43,33 +43,31 @@ def pretty_print(self): 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. + """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. + 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" as the ``notation`` parameter. The ``RollResult`` + returned when you pass an integer-as-string like this will always be a single roll on a die whose + number of sides is the ``notation`` value you passed, and its ``RollResult.total`` and + ``RollResult.total_with_modifier`` attribute values will also be the ``notation`` value you passed. + + Examples: + # TODO: Add examples, one each of a roll with no modifier, with modififer, and a guaranteed roll. 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() diff --git a/osrlib/osrlib/enums.py b/osrlib/osrlib/enums.py index e696d5f..3a946f6 100644 --- a/osrlib/osrlib/enums.py +++ b/osrlib/osrlib/enums.py @@ -111,10 +111,6 @@ class TreasureType(Enum): T = "T" U = "U" V = "V" - W = "W" - X = "X" - Y = "Y" - Z = "Z" class CoinType(Enum): @@ -123,7 +119,7 @@ class CoinType(Enum): ELECTRUM = ("electrum", 0.5) GOLD = ("gold", 1) PLATINUM = ("platinum", 5) - + def __init__(self, description: str, exchange_rate: float): self.description = description self.exchange_rate = exchange_rate @@ -140,7 +136,18 @@ def value_in_gold(coin_amounts: dict) -> float: float: The total value in gold pieces. Example usage: + >>> # Get total value in gold pieces from a dictionary of coin amounts. >>> CoinType.value_in_gold({CoinType.COPPER: 1000, CoinType.SILVER: 100}) 20.0 + >>> # Get total value in gold pieces from a dictionary with only one coin type. + >>> CoinType.value_in_gold({CoinType.GOLD: 100}) + 100.0 + >>> # Get the coin type from a string, then get the total value in gold pieces. + >>> coin_type_string = CoinType.GOLD.name + >>> coin_type_string + 'GOLD' + >>> coins = {CoinType[coin_type_string]: 100} + >>> CoinType.value_in_gold(coins) + 100.0 """ return sum(coin_type.exchange_rate * amount for coin_type, amount in coin_amounts.items()) diff --git a/osrlib/osrlib/treasure.py b/osrlib/osrlib/treasure.py index 7b9e0b6..645810a 100644 --- a/osrlib/osrlib/treasure.py +++ b/osrlib/osrlib/treasure.py @@ -1,267 +1,371 @@ +"""Implements the treasure generation system for a dungeon crawler RPG, inspired by the Dungeons & Dragons Basic Rules. + +The system is built around the `Treasure` class, which encapsulates the logic for generating treasure hauls for the the +player's party. Central to `Treasure` class is the `_treasure_types` attribute, a detailed mapping of various treasure +categories to their respective contents. These categories represent different types of treasure the party might obtain, +each with specified probabilities and quantities of items like coins, gems, jewelry, and magical items. +""" from typing import Dict, NamedTuple, Union from dataclasses import dataclass from osrlib.dice_roller import roll_dice from osrlib.enums import ItemType, TreasureType, CoinType from osrlib.item import Item, Weapon, Armor +from osrlib.game_manager import logger from enum import Enum @dataclass class TreasureDetail: - chance: float # Probability as a float between 0 and 1 - amount: str # Dice notation for the amount + """Defines the characteristics of a particular "unit" of treasure (coins, gems, magic items, ...) in a treasure type. + + This class dictatd the attributes of each item type within a defined treasure type. It outlines the chance of + occurrence, the amount (in dice notation or a fixed number), and whether the item is magical. This class is used + by the `Treasure` class, but you can also use it to customize the contents of treasure hauls by creating your own + treasure types. + + Attributes: + chance (int): The probability (as a percentage) of this item appearing in the treasure. + amount (str): The quantity of the item, expressed in dice notation (e.g., "1d6") or as a fixed number. + magical (bool): True if the item is magical, False otherwise (defaults to False). + + Example: + # Defining a custom treasure detail for a new treasure type + custom_treasure_detail = TreasureDetail(chance=40, amount="3d4", magical=True) + custom_treasure_type = { + CoinType.GOLD: custom_treasure_detail, + ItemType.MAGIC_ITEM: TreasureDetail(chance=25, amount="1", magical=True) + } + custom_treasure = Treasure.from_custom_type(custom_treasure_type) + # This creates a Treasure instance with the custom-defined treasure type + """ + chance: int # Probability of appearing in the treasure haul + amount: str # Dice notation for the amount/number of coins/items magical: bool = False # Whether the item should be magical -class Treasure(NamedTuple): - coins: Dict[CoinType, int] - other: Dict[ItemType, Union[str, int]] - total_gp_value: int - - -TreasureContent = Dict[Union[CoinType, ItemType], TreasureDetail] - - -treasure_types: Dict[TreasureType, TreasureContent] = { - TreasureType.A: { - CoinType.COPPER: TreasureDetail(chance=25, amount="1d6"), - CoinType.SILVER: TreasureDetail(chance=30, amount="1d6"), - CoinType.ELECTRUM: TreasureDetail(chance=20, amount="1d4"), - CoinType.GOLD: TreasureDetail(chance=35, amount="2d6"), - CoinType.PLATINUM: TreasureDetail(chance=25, amount="1d2"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="6d6"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=30, amount="3", magical=True), - }, - TreasureType.B: { - CoinType.COPPER: TreasureDetail(chance=50, amount="1d8"), - CoinType.SILVER: TreasureDetail(chance=25, amount="1d6"), - CoinType.ELECTRUM: TreasureDetail(chance=25, amount="1d4"), - CoinType.GOLD: TreasureDetail(chance=25, amount="1d3"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=25, amount="1d6"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=10, amount="1", magical=True), - }, - TreasureType.C: { - CoinType.COPPER: TreasureDetail(chance=20, amount="1d12"), - CoinType.SILVER: TreasureDetail(chance=30, amount="1d4"), - CoinType.ELECTRUM: TreasureDetail(chance=10, amount="1d4"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=25, amount="1d4"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=10, amount="2", magical=True), - }, - TreasureType.D: { - CoinType.COPPER: TreasureDetail(chance=10, amount="1d8"), - CoinType.SILVER: TreasureDetail(chance=15, amount="1d12"), - CoinType.GOLD: TreasureDetail(chance=60, amount="1d6"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=30, amount="1d8"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=15, amount="2+1", magical=True), - }, - TreasureType.E: { - CoinType.COPPER: TreasureDetail(chance=5, amount="1d10"), - CoinType.SILVER: TreasureDetail(chance=30, amount="1d12"), - CoinType.ELECTRUM: TreasureDetail(chance=25, amount="1d4"), - CoinType.GOLD: TreasureDetail(chance=25, amount="1d8"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=10, amount="1d10"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=25, amount="3+1", magical=True), - }, - TreasureType.F: { - CoinType.SILVER: TreasureDetail(chance=10, amount="2d10"), - CoinType.ELECTRUM: TreasureDetail(chance=20, amount="1d8"), - CoinType.GOLD: TreasureDetail(chance=45, amount="1d12"), - CoinType.PLATINUM: TreasureDetail(chance=30, amount="1d3"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=20, amount="2d12"), - ItemType.MAGIC_ITEM: TreasureDetail( - chance=30, - amount="Any 3 except weapons, + 1 potion, + 1 scroll", - ), - }, - TreasureType.G: { - CoinType.GOLD: TreasureDetail(chance=50, amount="10d4"), - CoinType.PLATINUM: TreasureDetail(chance=50, amount="1d6"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=25, amount="3d6"), - ItemType.MAGIC_ITEM: TreasureDetail( - chance=35, - amount="Any 4 + 1 scroll", - ), - }, - TreasureType.H: { - CoinType.COPPER: TreasureDetail(chance=25, amount="3d8"), - CoinType.SILVER: TreasureDetail(chance=50, amount="1d100"), - CoinType.ELECTRUM: TreasureDetail(chance=50, amount="10d4"), - CoinType.GOLD: TreasureDetail(chance=50, amount="10d6"), - CoinType.PLATINUM: TreasureDetail(chance=25, amount="5d4"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d100"), - ItemType.MAGIC_ITEM: TreasureDetail( - chance=15, - amount="Any 4 + 1 potion + 1 scroll", - ), - }, - TreasureType.I: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=30, amount="1d8"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="2d6"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=15, amount="Any 1"), - }, - TreasureType.J: { - CoinType.COPPER: TreasureDetail(chance=25, amount="1d4"), - CoinType.SILVER: TreasureDetail(chance=10, amount="1d3"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount=""), - }, - TreasureType.K: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=30, amount="1d6"), - CoinType.ELECTRUM: TreasureDetail(chance=10, amount="1d2"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0"), - }, - TreasureType.L: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=100, amount="1d100"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d6"), - ItemType.MAGIC_ITEM: TreasureDetail( - chance=30, - amount="Any 1 + 1 potion", - ), - }, - TreasureType.M: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=40, amount="2d8"), - CoinType.PLATINUM: TreasureDetail(chance=50, amount="5d30"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=55, amount="5d20"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount=""), - }, - TreasureType.N: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=45, amount="2d12"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=40, amount="2d8 potions"), - }, - TreasureType.O: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d4 scrolls"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount=""), - }, - TreasureType.P: { - CoinType.COPPER: TreasureDetail(chance=100, amount="4d6"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount=""), - }, - TreasureType.Q: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=100, amount="3d6"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount=""), - }, - TreasureType.R: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=100, amount="2d6"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount=""), - }, - TreasureType.S: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=100, amount="2d4"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount=""), - }, - TreasureType.T: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=100, amount="1d6"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount=""), - }, - TreasureType.U: { - CoinType.COPPER: TreasureDetail(chance=10, amount="1d100"), - CoinType.SILVER: TreasureDetail(chance=10, amount="1d100"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=5, amount="1d100"), - CoinType.PLATINUM: TreasureDetail(chance=100, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=5, amount="1d4"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=200, amount="Any 1"), - }, - TreasureType.V: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=10, amount="1d100"), - CoinType.ELECTRUM: TreasureDetail(chance=5, amount="1d100"), - CoinType.GOLD: TreasureDetail(chance=10, amount="1d100"), - CoinType.PLATINUM: TreasureDetail(chance=5, amount="1d100"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=10, amount="1d4"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=5, amount="Any 1"), - }, -} - - -def get_treasure(treasure_type: TreasureType) -> Treasure: - """Calculates the treasure based on the defined treasure types and computes the total value in gold pieces. - - Args: - treasure_type (TreasureType): The type of treasure to calculate. - - Returns: - Treasure: A named tuple containing the coins and other treasure items. +class Treasure: + """Represents a treasure haul within the game, encapsulating various types of loot. + + ``Treasure`` manages the generation and valuation of treasures comprising coins, gems, jewelry, and magical items. + The treasure is generated based on predefined types, each corresponding to different probabilities and amounts + of items. The class provides functionalities to generate treasure based on a specified type, calculate its total + value in gold pieces (gp), and retrieve the generated items. + + Attributes: + items (Dict[Union[CoinType, ItemType], int]): A dictionary holding the treasure items. The keys are instances + of either CoinType or ItemType enumerations, and the values are integers representing the quantity of each item. + + Methods: + __init__(treasure_type: TreasureType = None): Initializes a new Treasure instance, optionally generating + treasure contents based on the provided type. + total_gp_value(): Property that calculates and returns the total value of the treasure in gold pieces. + from_treasure_type(treasure_type: TreasureType): Class method to create a new Treasure instance with contents + generated based on the specified TreasureType. + + Example: + >>> treasure = Treasure.from_treasure_type(TreasureType.A) + >>> treasure.items + {: 12, : 6} + >>> treasure.total_gp_value + 12 """ - treasure_details = treasure_types[treasure_type] - treasure_haul = {"coins": {}, "other": {}, "total_gp_value": {}} - total_gp_value = 0 # Initialize the total gold pieces value - - for item_type, details in treasure_details.items(): - chance_roll = roll_dice("1d100").total - if chance_roll <= details.chance: - # Roll dice to determine the amount - amount_roll = roll_dice(details.amount) - amount = amount_roll.total + items: Dict[Union[CoinType, ItemType], int] + + _treasure_types: Dict[ + TreasureType, Dict[Union[CoinType, ItemType], TreasureDetail] + ] = { + TreasureType.A: { + CoinType.COPPER: TreasureDetail(chance=25, amount="1d6"), + CoinType.SILVER: TreasureDetail(chance=30, amount="1d6"), + CoinType.ELECTRUM: TreasureDetail(chance=20, amount="1d4"), + CoinType.GOLD: TreasureDetail(chance=35, amount="2d6"), + CoinType.PLATINUM: TreasureDetail(chance=25, amount="1d2"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="6d6"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=30, amount="3", magical=True), + }, + TreasureType.B: { + CoinType.COPPER: TreasureDetail(chance=50, amount="1d8"), + CoinType.SILVER: TreasureDetail(chance=25, amount="1d6"), + CoinType.ELECTRUM: TreasureDetail(chance=25, amount="1d4"), + CoinType.GOLD: TreasureDetail(chance=25, amount="1d3"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=25, amount="1d6"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=10, amount="1", magical=True), + }, + TreasureType.C: { + CoinType.COPPER: TreasureDetail(chance=20, amount="1d12"), + CoinType.SILVER: TreasureDetail(chance=30, amount="1d4"), + CoinType.ELECTRUM: TreasureDetail(chance=10, amount="1d4"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=25, amount="1d4"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=10, amount="2", magical=True), + }, + TreasureType.D: { + CoinType.COPPER: TreasureDetail(chance=10, amount="1d8"), + CoinType.SILVER: TreasureDetail(chance=15, amount="1d12"), + CoinType.GOLD: TreasureDetail(chance=60, amount="1d6"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=30, amount="1d8"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=15, amount="3", magical=True), + }, + TreasureType.E: { + CoinType.COPPER: TreasureDetail(chance=5, amount="1d10"), + CoinType.SILVER: TreasureDetail(chance=30, amount="1d12"), + CoinType.ELECTRUM: TreasureDetail(chance=25, amount="1d4"), + CoinType.GOLD: TreasureDetail(chance=25, amount="1d8"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=10, amount="1d10"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=25, amount="4", magical=True), + }, + TreasureType.F: { + CoinType.SILVER: TreasureDetail(chance=10, amount="2d10"), + CoinType.ELECTRUM: TreasureDetail(chance=20, amount="1d8"), + CoinType.GOLD: TreasureDetail(chance=45, amount="1d12"), + CoinType.PLATINUM: TreasureDetail(chance=30, amount="1d3"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=20, amount="2d12"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=30, amount="5", magical=True), + }, + TreasureType.G: { + CoinType.GOLD: TreasureDetail(chance=50, amount="10d4"), + CoinType.PLATINUM: TreasureDetail(chance=50, amount="1d6"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=25, amount="3d6"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=35, amount="5", magical=True), + }, + TreasureType.H: { + CoinType.COPPER: TreasureDetail(chance=25, amount="3d8"), + CoinType.SILVER: TreasureDetail(chance=50, amount="1d100"), + CoinType.ELECTRUM: TreasureDetail(chance=50, amount="10d4"), + CoinType.GOLD: TreasureDetail(chance=50, amount="10d6"), + CoinType.PLATINUM: TreasureDetail(chance=25, amount="5d4"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d100"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=15, amount="6", magical=True), + }, + TreasureType.I: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=0, amount="0"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=0, amount="0"), + CoinType.PLATINUM: TreasureDetail(chance=30, amount="1d8"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="2d6"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=15, amount="1", magical=True), + }, + TreasureType.J: { + CoinType.COPPER: TreasureDetail(chance=25, amount="1d4"), + CoinType.SILVER: TreasureDetail(chance=10, amount="1d3"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=0, amount="0"), + CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + }, + TreasureType.K: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=30, amount="1d6"), + CoinType.ELECTRUM: TreasureDetail(chance=10, amount="1d2"), + CoinType.GOLD: TreasureDetail(chance=0, amount="0"), + CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + }, + TreasureType.L: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=0, amount="0"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=100, amount="1d100"), + CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d6"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=30, amount="2", magical=True), + }, + TreasureType.M: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=0, amount="0"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=40, amount="2d8"), + CoinType.PLATINUM: TreasureDetail(chance=50, amount="5d30"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=55, amount="5d20"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + }, + TreasureType.N: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=0, amount="0"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=0, amount="0"), + CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=45, amount="2d12"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=40, amount="2d8", magical=True), + }, + TreasureType.O: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=0, amount="0"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=0, amount="0"), + CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d4"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + }, + TreasureType.P: { + CoinType.COPPER: TreasureDetail(chance=100, amount="4d6"), + CoinType.SILVER: TreasureDetail(chance=0, amount="0"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=0, amount="0"), + CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + }, + TreasureType.Q: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=100, amount="3d6"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=0, amount="0"), + CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + }, + TreasureType.R: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=0, amount="0"), + CoinType.ELECTRUM: TreasureDetail(chance=100, amount="2d6"), + CoinType.GOLD: TreasureDetail(chance=0, amount="0"), + CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + }, + TreasureType.S: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=0, amount="0"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=100, amount="2d4"), + CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + }, + TreasureType.T: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=0, amount="0"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=0, amount="0"), + CoinType.PLATINUM: TreasureDetail(chance=100, amount="1d6"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + }, + TreasureType.U: { + CoinType.COPPER: TreasureDetail(chance=10, amount="1d100"), + CoinType.SILVER: TreasureDetail(chance=10, amount="1d100"), + CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), + CoinType.GOLD: TreasureDetail(chance=5, amount="1d100"), + CoinType.PLATINUM: TreasureDetail(chance=100, amount="0"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=5, amount="1d4"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=200, amount="1", magical=True), + }, + TreasureType.V: { + CoinType.COPPER: TreasureDetail(chance=0, amount="0"), + CoinType.SILVER: TreasureDetail(chance=10, amount="1d100"), + CoinType.ELECTRUM: TreasureDetail(chance=5, amount="1d100"), + CoinType.GOLD: TreasureDetail(chance=10, amount="1d100"), + CoinType.PLATINUM: TreasureDetail(chance=5, amount="1d100"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=10, amount="1d4"), + ItemType.MAGIC_ITEM: TreasureDetail(chance=5, amount="1", magical=True), + }, + } + """Represents treasure configurations based on classic tabletop RPG game rules. + + This private class attribute maps each `TreasureType` to its possible contents, aligning with the variability and + randomness of treasure discovery as set forth in some classic RPGs. + """ + + def __init__(self, treasure_type: TreasureType = None): + self.items = {} + if treasure_type: + self._generate_treasure(treasure_type) + + def _generate_treasure(self, treasure_type: TreasureType) -> None: + """Populates the treasure's contents based on whether and how much of each valuable should be included according + to the treasure type. + + Args: + treasure_type (TreasureType): The type of treasure for which to calculate its contents. + """ + treasure_details = self._treasure_types[treasure_type] + for item_type, details in treasure_details.items(): + chance_roll = roll_dice("1d100").total + if chance_roll <= details.chance: + amount_roll = roll_dice(details.amount) + self.items[item_type] = amount_roll.total + + @property + def total_gp_value(self) -> int: + """Gets the total value in gold pieces (gp) of the treasure. + + Use this value when calculating the amount of experience points (XP) to award a party who obtains the treasure. + For example, at the end of an encounter, quest, or any other event in which the party receives it. + + Returns: + int: The total value in gold pieces (gp) of the coins, gems, jewelry, and items in the treasure. + """ + total_gp_value = 0 + # Calculate the value of coins + for item_type, amount in self.items.items(): if isinstance(item_type, CoinType): - treasure_haul["coins"][item_type] = amount - # Update the total value in gold pieces - total_gp_value += amount * item_type.exchange_rate - else: - treasure_haul["other"][item_type] = details.amount + total_gp_value += CoinType.value_in_gold({item_type: amount}) + + # TODO: Calculate the value of the other items in the treasure + # for item_type, amount in self.items.items(): + # if isinstance(item_type, ItemType): + # total_gp_value += item_type.value * amount + + return total_gp_value + + @classmethod + def from_treasure_type(cls, treasure_type: TreasureType) -> "Treasure": + """Generate a treasure haul by populating the treasure's contents based the given treasure type. - # Add the total_gp_value to the treasure haul - treasure_haul["total_gp_value"] = total_gp_value + Args: + treasure_type (TreasureType): The type of treasure for which to calculate its contents. - return Treasure(**treasure_haul) + Returns: + Treasure: The treasure haul. + """ + return cls(treasure_type) + @classmethod + def from_custom_type(cls, custom_type: Dict[Union[CoinType, ItemType], TreasureDetail]) -> "Treasure": + """Creates a Treasure instance using a custom-defined treasure type. + + This method allows for the generation of your own custom sets of treasure not included in the + standard treasure types. The custom_type parameter should be a dictionary mapping CoinType or + ItemType to TreasureDetail, similar to the predefined treasure types. + + Args: + custom_type (Dict[Union[CoinType, ItemType], TreasureDetail]): Custom-defined treasure type details. + + Returns: + Treasure: An instance of Treasure whose contents are based on the custom treasure type. + + Example: + python``` + # Define a custom treasure type with a 50% chance of 2d6 gold pieces, 40% chance of 1d4 gems/jewelry, + # and 15% chance of 1 magic item. + custom_treasure_type = { + CoinType.GOLD: TreasureDetail(chance=50, amount="2d6", magical=False), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=40, amount="1d4", magical=False), + ItemType.MAGIC_ITEM: TreasureDetail(chance=15, amount="1", magical=True) + } + # Creating a Treasure instance with the custom treasure type + custom_treasure = Treasure.from_custom_type(custom_treasure_type) + # The custom_treasure Treasure instance now contains items based on the custom type definition + ``` + """ + treasure = cls() + for item_type, details in custom_type.items(): + chance_roll = roll_dice("1d100").total + if chance_roll <= details.chance: + amount_roll = roll_dice(details.amount) + treasure.items[item_type] = amount_roll.total + return treasure # Example usage: -treasure = get_treasure(TreasureType.A) -print(f"Treasure haul: {treasure}") -# Calculate XP from the total gold pieces value -xp_from_treasure = treasure.total_gp_value -print(f"XP from treasure: {xp_from_treasure}") +treasure_haul_01 = Treasure.from_treasure_type(TreasureType.A) +treasure_haul_02 = Treasure(TreasureType.B) + +print(f"Treasure haul 1: {treasure_haul_01}") +print(f"Treasure haul 2: {treasure_haul_02}") + +xp_from_treasure = treasure_haul_01.total_gp_value + treasure_haul_02.total_gp_value +print(f"Total XP from treasure: {xp_from_treasure}") diff --git a/tests/test_unit_treasure.py b/tests/test_unit_treasure.py new file mode 100644 index 0000000..8807dec --- /dev/null +++ b/tests/test_unit_treasure.py @@ -0,0 +1,86 @@ +import pytest +from osrlib.enums import CoinType, ItemType +from osrlib.treasure import Treasure, TreasureDetail, TreasureType + + +def test_treasure_total_gold_piece_value(): + custom_type = { + CoinType.GOLD: TreasureDetail( + chance=100, amount="10", magical=False + ), # 10 gold coins + CoinType.SILVER: TreasureDetail( + chance=100, amount="100", magical=False + ), # 100 silver coins + } + treasure = Treasure.from_custom_type(custom_type) + expected_total_gp = 20 # 10 gold + 100 silver converted to gold (10 + 10) + assert treasure.total_gp_value == expected_total_gp + + +def test_treasure_from_custom_type(): + # Define a custom treasure type with specific items + custom_type = { + CoinType.GOLD: TreasureDetail( + chance=100, amount="5", magical=False + ), # Always 5 gold coins + ItemType.MAGIC_ITEM: TreasureDetail( + chance=50, amount="1", magical=True + ), # 50% chance of 1 magic item + } + + # Create a Treasure instance using the custom treasure type + custom_treasure = Treasure.from_custom_type(custom_type) + + # Check if the treasure contains the expected items + assert CoinType.GOLD in custom_treasure.items + assert custom_treasure.items[CoinType.GOLD] == 5 # Should always have 5 gold coins + + # Since magic item appearance is probabilistic, we test it statistically + magic_item_appearances = [ + Treasure.from_custom_type(custom_type).items.get(ItemType.MAGIC_ITEM, 0) > 0 + for _ in range(100) + ] + approx_magic_item_appearance_rate = sum(magic_item_appearances) / len( + magic_item_appearances + ) + + # Check if the appearance rate of the magic item is close to the expected 50% + assert 0.4 <= approx_magic_item_appearance_rate <= 0.6 + + +def test_treasure_predefined_treasure_type_statistics(): + iterations = 1000 # Number of iterations to average out randomness + type_a_probabilities = { + CoinType.GOLD: 35, # Probability in percent for CoinType.GOLD in TreasureType.A + ItemType.MAGIC_ITEM: 30, # Probability in percent for ItemType.MAGIC_ITEM in TreasureType.A + # TODO: add other item types and their probabilities for TreasureType.A + } + type_d_probabilities = { + CoinType.SILVER: 15, # Probability in percent for CoinType.SILVER in TreasureType.D + ItemType.GEMS_JEWELRY: 30, # Probability in percent for ItemType.GEMS_JEWELRY in TreasureType.D + # TODO: add other item types and their probabilities for TreasureType.D + } + + type_a_results = {item_type: 0 for item_type in type_a_probabilities} + type_d_results = {item_type: 0 for item_type in type_d_probabilities} + + # Generate treasures and record occurrences of each item type + for _ in range(iterations): + treasure_a = Treasure(TreasureType.A) + for item_type in type_a_probabilities: + if item_type in treasure_a.items: + type_a_results[item_type] += 1 + + treasure_d = Treasure(TreasureType.D) + for item_type in type_d_probabilities: + if item_type in treasure_d.items: + type_d_results[item_type] += 1 + + # Check if the occurrence rates are within an acceptable range of their probabilities + for item_type, probability in type_a_probabilities.items(): + occurrence_rate = (type_a_results[item_type] / iterations) * 100 + assert probability - 5 <= occurrence_rate <= probability + 5 + + for item_type, probability in type_d_probabilities.items(): + occurrence_rate = (type_d_results[item_type] / iterations) * 100 + assert probability - 5 <= occurrence_rate <= probability + 5 From 299027a7b612a144b2a829d2e6626621d68c98cd Mon Sep 17 00:00:00 2001 From: Marsh Macy Date: Mon, 22 Jan 2024 19:40:56 -0800 Subject: [PATCH 2/5] currency convert now returns int --- osrlib/osrlib/enums.py | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/osrlib/osrlib/enums.py b/osrlib/osrlib/enums.py index 3a946f6..2a4a42b 100644 --- a/osrlib/osrlib/enums.py +++ b/osrlib/osrlib/enums.py @@ -1,5 +1,5 @@ from enum import Enum - +from math import ceil class AbilityType(Enum): STRENGTH = "Strength" @@ -125,29 +125,23 @@ def __init__(self, description: str, exchange_rate: float): self.exchange_rate = exchange_rate @staticmethod - def value_in_gold(coin_amounts: dict) -> float: - """ - Calculate the combined value in gold pieces based on the coin amounts. + def value_in_gold(coin_amounts: dict) -> int: + """Calculate the combined value in gold pieces based on the coin amounts, rounded up to the nearest integer. Args: - coin_amounts (dict): A dictionary with CoinType as keys and amounts as values. + coin_amounts (dict): A dictionary with CoinType as key and quantity of that type as value. Returns: - float: The total value in gold pieces. - - Example usage: - >>> # Get total value in gold pieces from a dictionary of coin amounts. - >>> CoinType.value_in_gold({CoinType.COPPER: 1000, CoinType.SILVER: 100}) - 20.0 - >>> # Get total value in gold pieces from a dictionary with only one coin type. - >>> CoinType.value_in_gold({CoinType.GOLD: 100}) - 100.0 - >>> # Get the coin type from a string, then get the total value in gold pieces. - >>> coin_type_string = CoinType.GOLD.name - >>> coin_type_string - 'GOLD' - >>> coins = {CoinType[coin_type_string]: 100} - >>> CoinType.value_in_gold(coins) - 100.0 + int: The total value in gold pieces, rounded up to the nearest integer. + + Examples: + >>> CoinType.value_in_gold({CoinType.COPPER: 1000, CoinType.SILVER: 100}) + 20 + >>> CoinType.value_in_gold({CoinType.GOLD: 100}) + 100 + >>> coin_type_string = CoinType.GOLD.name + >>> coins = {CoinType[coin_type_string]: 100} + >>> CoinType.value_in_gold(coins) + 100 """ - return sum(coin_type.exchange_rate * amount for coin_type, amount in coin_amounts.items()) + return ceil(sum(coin_type.exchange_rate * amount for coin_type, amount in coin_amounts.items())) From 1e6813442485a3820489c6442abc8403994fcbbe Mon Sep 17 00:00:00 2001 From: Marsh Macy Date: Mon, 22 Jan 2024 20:03:08 -0800 Subject: [PATCH 3/5] docstring updates for dice_roller --- osrlib/osrlib/dice_roller.py | 59 ++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/osrlib/osrlib/dice_roller.py b/osrlib/osrlib/dice_roller.py index fe4a3b1..54bd95f 100644 --- a/osrlib/osrlib/dice_roller.py +++ b/osrlib/osrlib/dice_roller.py @@ -1,7 +1,12 @@ -"""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( @@ -9,15 +14,23 @@ class DiceRoll( ["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'). @@ -31,40 +44,34 @@ 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): - """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" as the ``notation`` parameter. The ``RollResult`` - returned when you pass an integer-as-string like this will always be a single roll on a die whose - number of sides is the ``notation`` value you passed, and its ``RollResult.total`` and - ``RollResult.total_with_modifier`` attribute values will also be the ``notation`` value you passed. +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. Examples: - # TODO: Add examples, one each of a roll with no modifier, with modififer, and a guaranteed roll. + 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 a string representing an integer, 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. From 6bd683f88e4296d1d81d4639bb7436c1113eaf72 Mon Sep 17 00:00:00 2001 From: Marsh Macy Date: Tue, 23 Jan 2024 06:49:22 -0800 Subject: [PATCH 4/5] ablility module docstrings + make _init_modifiers private --- osrlib/osrlib/ability.py | 140 +++++++++++++++++++++++++++------------ 1 file changed, 97 insertions(+), 43 deletions(-) diff --git a/osrlib/osrlib/ability.py b/osrlib/osrlib/ability.py index 07a24b5..0345cb1 100644 --- a/osrlib/osrlib/ability.py +++ b/osrlib/osrlib/ability.py @@ -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 = { @@ -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: @@ -62,14 +76,26 @@ 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, @@ -77,8 +103,17 @@ def to_dict(self) -> dict: @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 @@ -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): @@ -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() @@ -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): @@ -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() @@ -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): @@ -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() @@ -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): @@ -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() @@ -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): @@ -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() @@ -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): @@ -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() From 4a79a3e1b84f24a71ef3e6d4161d9204e5327d24 Mon Sep 17 00:00:00 2001 From: Marsh Macy Date: Tue, 23 Jan 2024 07:13:43 -0800 Subject: [PATCH 5/5] finalized treasure tables --- osrlib/osrlib/treasure.py | 85 ++++--------------------------------- tests/test_unit_treasure.py | 11 +++-- 2 files changed, 15 insertions(+), 81 deletions(-) diff --git a/osrlib/osrlib/treasure.py b/osrlib/osrlib/treasure.py index 645810a..9e9cb01 100644 --- a/osrlib/osrlib/treasure.py +++ b/osrlib/osrlib/treasure.py @@ -19,7 +19,7 @@ class TreasureDetail: """Defines the characteristics of a particular "unit" of treasure (coins, gems, magic items, ...) in a treasure type. - This class dictatd the attributes of each item type within a defined treasure type. It outlines the chance of + This class dictates the attributes of each item type within a defined treasure type. It outlines the chance of occurrence, the amount (in dice notation or a fixed number), and whether the item is magical. This class is used by the `Treasure` class, but you can also use it to customize the contents of treasure hauls by creating your own treasure types. @@ -138,10 +138,6 @@ class Treasure: ItemType.MAGIC_ITEM: TreasureDetail(chance=15, amount="6", magical=True), }, TreasureType.I: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), CoinType.PLATINUM: TreasureDetail(chance=30, amount="1d8"), ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="2d6"), ItemType.MAGIC_ITEM: TreasureDetail(chance=15, amount="1", magical=True), @@ -149,113 +145,48 @@ class Treasure: TreasureType.J: { CoinType.COPPER: TreasureDetail(chance=25, amount="1d4"), CoinType.SILVER: TreasureDetail(chance=10, amount="1d3"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), }, TreasureType.K: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), CoinType.SILVER: TreasureDetail(chance=30, amount="1d6"), CoinType.ELECTRUM: TreasureDetail(chance=10, amount="1d2"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), }, TreasureType.L: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=100, amount="1d100"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d6"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=30, amount="2", magical=True), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d4"), }, TreasureType.M: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=40, amount="2d8"), - CoinType.PLATINUM: TreasureDetail(chance=50, amount="5d30"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=55, amount="5d20"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + CoinType.GOLD: TreasureDetail(chance=40, amount="2d4"), + CoinType.PLATINUM: TreasureDetail(chance=50, amount="5d6"), + ItemType.GEMS_JEWELRY: TreasureDetail(chance=55, amount="5d4"), }, TreasureType.N: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=45, amount="2d12"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=40, amount="2d8", magical=True), + ItemType.MAGIC_ITEM: TreasureDetail(chance=40, amount="2d4", magical=True), }, TreasureType.O: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d4"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), + ItemType.MAGIC_ITEM: TreasureDetail(chance=50, amount="1d4", magical=True), }, TreasureType.P: { CoinType.COPPER: TreasureDetail(chance=100, amount="4d6"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), }, TreasureType.Q: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), CoinType.SILVER: TreasureDetail(chance=100, amount="3d6"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), }, TreasureType.R: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), CoinType.ELECTRUM: TreasureDetail(chance=100, amount="2d6"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), }, TreasureType.S: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), CoinType.GOLD: TreasureDetail(chance=100, amount="2d4"), - CoinType.PLATINUM: TreasureDetail(chance=0, amount="0"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), }, TreasureType.T: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), - CoinType.SILVER: TreasureDetail(chance=0, amount="0"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), - CoinType.GOLD: TreasureDetail(chance=0, amount="0"), CoinType.PLATINUM: TreasureDetail(chance=100, amount="1d6"), - ItemType.GEMS_JEWELRY: TreasureDetail(chance=0, amount="0"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=0, amount="0", magical=True), }, TreasureType.U: { CoinType.COPPER: TreasureDetail(chance=10, amount="1d100"), CoinType.SILVER: TreasureDetail(chance=10, amount="1d100"), - CoinType.ELECTRUM: TreasureDetail(chance=0, amount="0"), CoinType.GOLD: TreasureDetail(chance=5, amount="1d100"), - CoinType.PLATINUM: TreasureDetail(chance=100, amount="0"), ItemType.GEMS_JEWELRY: TreasureDetail(chance=5, amount="1d4"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=200, amount="1", magical=True), + ItemType.MAGIC_ITEM: TreasureDetail(chance=2, amount="1", magical=True), }, TreasureType.V: { - CoinType.COPPER: TreasureDetail(chance=0, amount="0"), CoinType.SILVER: TreasureDetail(chance=10, amount="1d100"), CoinType.ELECTRUM: TreasureDetail(chance=5, amount="1d100"), CoinType.GOLD: TreasureDetail(chance=10, amount="1d100"), diff --git a/tests/test_unit_treasure.py b/tests/test_unit_treasure.py index 8807dec..e4f3730 100644 --- a/tests/test_unit_treasure.py +++ b/tests/test_unit_treasure.py @@ -6,14 +6,16 @@ def test_treasure_total_gold_piece_value(): custom_type = { CoinType.GOLD: TreasureDetail( + # Always 10 GP chance=100, amount="10", magical=False - ), # 10 gold coins + ), CoinType.SILVER: TreasureDetail( + # Always 100 SP chance=100, amount="100", magical=False - ), # 100 silver coins + ), } treasure = Treasure.from_custom_type(custom_type) - expected_total_gp = 20 # 10 gold + 100 silver converted to gold (10 + 10) + expected_total_gp = 20 # 10 GP + 100 SP (10 SP = 1 GP) assert treasure.total_gp_value == expected_total_gp @@ -21,8 +23,9 @@ def test_treasure_from_custom_type(): # Define a custom treasure type with specific items custom_type = { CoinType.GOLD: TreasureDetail( + # Always 5 GP chance=100, amount="5", magical=False - ), # Always 5 gold coins + ), ItemType.MAGIC_ITEM: TreasureDetail( chance=50, amount="1", magical=True ), # 50% chance of 1 magic item