diff --git a/docs/reference/game_manager.md b/docs/reference/game_manager.md deleted file mode 100644 index 945779b..0000000 --- a/docs/reference/game_manager.md +++ /dev/null @@ -1 +0,0 @@ -::: osrlib.game_manager diff --git a/mkdocs.yml b/mkdocs.yml index b004382..65b0b30 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -120,7 +120,6 @@ nav: - dungeon_assistant: reference/dungeon_assistant.md - encounter: reference/encounter.md - enums: reference/enums.md - - game_manager: reference/game_manager.md - inventory: reference/inventory.md - item: reference/item.md - item_factories: reference/item_factories.md diff --git a/osrgame/osrgame/osrgame.py b/osrgame/osrgame/osrgame.py index 8f61df4..d0c845b 100644 --- a/osrgame/osrgame/osrgame.py +++ b/osrgame/osrgame/osrgame.py @@ -10,8 +10,8 @@ from osrlib.constants import ADVENTURE_NAMES, DUNGEON_NAMES from osrlib.dungeon import Dungeon from osrlib.dungeon_assistant import DungeonAssistant -from osrlib.game_manager import logger -from osrlib.party import get_default_party +from osrlib.utils import logger +from osrlib.party import Party from osrlib.enums import OpenAIModelVersion @@ -86,7 +86,7 @@ def set_active_adventure(self, adventure: Adventure = None) -> None: default_adventure.add_dungeon(dungeon) default_adventure.set_active_dungeon(dungeon) - default_adventure.set_active_party(get_default_party()) + default_adventure.set_active_party(Party.get_default_party()) self.adventure = default_adventure def start_session(self) -> str: diff --git a/osrgame/poetry.lock b/osrgame/poetry.lock index 6231297..382678f 100644 --- a/osrgame/poetry.lock +++ b/osrgame/poetry.lock @@ -1063,7 +1063,7 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] [[package]] name = "osrlib" -version = "0.1.81" +version = "0.1.83" description = "Turn-based dungeon-crawler game engine for OSR-style RPGs." optional = false python-versions = "^3.11" diff --git a/osrlib/osrlib/__init__.py b/osrlib/osrlib/__init__.py index 9ab4f6b..bf7e234 100644 --- a/osrlib/osrlib/__init__.py +++ b/osrlib/osrlib/__init__.py @@ -1 +1 @@ -"""OSR Library is a Python library for powering old-school turn-based role-playing games (OSRPGs).""" +"""Python library for powering old-school turn-based role-playing games.""" diff --git a/osrlib/osrlib/ability.py b/osrlib/osrlib/ability.py index 3f28ab4..9887283 100644 --- a/osrlib/osrlib/ability.py +++ b/osrlib/osrlib/ability.py @@ -1,12 +1,22 @@ """Defines `PlayerCharacter` abilities and their modifiers. -Abilities are inherent traits that every [PlayerCharacter][osrlib.player_character.PlayerCharacter] possesses in -varying degrees. 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][osrlib.ability.Dexterity] score or whether they know additional languages because of a high +An [Ability][osrlib.ability.Ability] is an inherent trait possessed by every +[PlayerCharacter][osrlib.player_character.PlayerCharacter]. There are several ability types, each of which can provide +modifiers (bonuses or penalties) that affect gameplay mechanics like dice rolls or other properties of the character. +For example, a high [Dexterity][osrlib.ability.Dexterity] score can make the character harder to hit in combat due to +an AC modifier, and the character might know an additional language or two because they also have a high [Intelligence][osrlib.ability.Intelligence] score. +## Usage: +You typically wouldn't create an instance of an [Ability][osrlib.ability.Ability] directly. Instead, you create an +instance of [PlayerCharacter][osrlib.player_character.PlayerCharacter], and its abilities are instantiated as attributes +of the `PlayerCharacter` instance automatically. You can then "roll" the character's ability scores by calling a method +on `PlayerCharacter`. + +```python +---8<--- "tests/test_doc_player_character.py:player_character_create" +``` """ from abc import ABC, abstractmethod @@ -152,12 +162,11 @@ def _init_modifiers(self) -> None: """Initialize Strength-specific ability modifiers. Modifiers: - - 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. + 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() @@ -171,9 +180,7 @@ class Intelligence(Ability): magical aptitude. Modifiers: - - - LANGUAGES (ModifierType.LANGUAGES): Modifies the number of additional languages the - character can read and write. + - LANGUAGES (ModifierType.LANGUAGES): Modifies the number of additional languages the character can read and write. """ def __init__(self, score: int): @@ -203,11 +210,10 @@ def _init_modifiers(self) -> None: """Initialize Intelligence-specific ability modifiers. Modifiers: - - 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. + The modifier is calculated based on the Intelligence score of the character. """ self.modifiers[ModifierType.LANGUAGES] = self._get_modifier() @@ -218,9 +224,7 @@ class Wisdom(Ability): Wisdom measures a character's common sense, intuition, and willpower. Modifiers: - - - SAVING_THROWS (ModifierType.SAVING_THROWS): Modifies 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): @@ -237,10 +241,9 @@ def _init_modifiers(self) -> None: """Initialize Wisdom-specific ability modifiers. Modifiers: - SAVING_THROWS (ModifierType.SAVING_THROWS): Modifies 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. + Each modifier is calculated based on the Wisdom score of the character. """ self.modifiers[ModifierType.SAVING_THROWS] = self._get_modifier() @@ -295,12 +298,11 @@ def _init_modifiers(self) -> None: """Initialize Dexterity-specific ability modifiers. Modifiers: - - 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. + Each modifier is calculated based on the Dexterity score of the character. """ self.modifiers[ModifierType.AC] = -self._get_modifier() self.modifiers[ModifierType.TO_HIT] = self._get_modifier() @@ -314,8 +316,8 @@ class Constitution(Ability): Modifiers: - - HP (ModifierType.HP): Modifies hit point (HP) rolls. For example, when initially rolling the - character or when the character gains a 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): @@ -331,10 +333,10 @@ def _init_modifiers(self) -> None: """Initialize Constitution-specific ability modifiers. Modifiers: - HP (ModifierType.HP): Modifies hit point (HP) rolls. For example, when initially rolling - the character or when the character gains a 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. + The modifier is calculated based on the Constitution score of the character. """ self.modifiers[ModifierType.HP] = self._get_modifier() @@ -345,7 +347,6 @@ class Charisma(Ability): Charisma measures force of personality, leadership ability, and physical attractiveness. Modifiers: - - REACTION (ModifierType.REACTION): Modifies reaction rolls when interacting with NPCs. """ @@ -378,9 +379,8 @@ def _init_modifiers(self) -> None: """Initialize Charisma-specific ability modifiers. Modifiers: - - REACTION (ModifierType.REACTION): Modifies reaction rolls when interacting with NPCs. - The modifier is calculated based on the charisma score of the character. + The modifier is calculated based on the Charisma score of the character. """ self.modifiers[ModifierType.REACTION] = self._get_modifier() diff --git a/osrlib/osrlib/adventure.py b/osrlib/osrlib/adventure.py index 4a11892..9c6d270 100644 --- a/osrlib/osrlib/adventure.py +++ b/osrlib/osrlib/adventure.py @@ -10,7 +10,7 @@ DungeonAlreadyExistsError: Raised for duplicate dungeon additions. """ import json, os, datetime -from osrlib.game_manager import logger +from osrlib.utils import logger from osrlib.dungeon import Dungeon from osrlib.party import Party from osrlib.quest import Quest diff --git a/osrlib/osrlib/character_classes.py b/osrlib/osrlib/character_classes.py index f40cf1b..a2d8025 100644 --- a/osrlib/osrlib/character_classes.py +++ b/osrlib/osrlib/character_classes.py @@ -4,7 +4,7 @@ from osrlib.dice_roller import DiceRoll, roll_dice from osrlib.enums import CharacterClassType from osrlib.saving_throws import get_saving_throws_for_class_and_level -from osrlib.game_manager import logger +from osrlib.utils import logger class ClassLevel: diff --git a/osrlib/osrlib/dungeon.py b/osrlib/osrlib/dungeon.py index 75360ce..74d4193 100644 --- a/osrlib/osrlib/dungeon.py +++ b/osrlib/osrlib/dungeon.py @@ -26,7 +26,7 @@ import random, json, uuid from openai import OpenAI from osrlib.enums import Direction, OpenAIModelVersion -from osrlib.game_manager import logger +from osrlib.utils import logger from osrlib.encounter import Encounter from osrlib.dice_roller import roll_dice diff --git a/osrlib/osrlib/dungeon_assistant.py b/osrlib/osrlib/dungeon_assistant.py index 1d13c0a..ac2458d 100644 --- a/osrlib/osrlib/dungeon_assistant.py +++ b/osrlib/osrlib/dungeon_assistant.py @@ -1,10 +1,9 @@ """The `dungeon_assistant` module contains the `DungeonAssistant` class that interfaces with the OpenAI API and performs the duties of the game's referee and guide (*game master* or *dungeon master* in some tabletop RPGs).""" -import asyncio from openai import OpenAI from osrlib.adventure import Adventure from osrlib.enums import OpenAIModelVersion -from osrlib.game_manager import logger +from osrlib.utils import logger dm_init_message = ( diff --git a/osrlib/osrlib/encounter.py b/osrlib/osrlib/encounter.py index 7aebdc8..caecf92 100644 --- a/osrlib/osrlib/encounter.py +++ b/osrlib/osrlib/encounter.py @@ -5,7 +5,7 @@ from osrlib.party import Party from osrlib.monster import MonsterParty from osrlib.monster_manual import monster_stats_blocks -from osrlib.game_manager import logger, last_message_handler as pylog +from osrlib.utils import logger, last_message_handler as pylog from osrlib.dice_roller import roll_dice from osrlib.treasure import Treasure, TreasureType diff --git a/osrlib/osrlib/game_manager.py b/osrlib/osrlib/game_manager.py deleted file mode 100644 index b3ad1c1..0000000 --- a/osrlib/osrlib/game_manager.py +++ /dev/null @@ -1,51 +0,0 @@ -"""The GameManager module provides the main API surface for the game.""" -from enum import Enum -import json -import logging -import warnings -import logging -import queue -import threading - - -class LastMessageHandler(logging.Handler): - def __init__(self): - super().__init__() - self.last_message = None - - def emit(self, record): - self.last_message = self.format(record) - - def format(self, record): - # Return only the message part of the log record - return record.getMessage() - -# Configure logging -logging.basicConfig( - format="%(asctime)s [%(levelname)s][%(module)s::%(funcName)s] %(message)s", -) - -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) -last_message_handler = LastMessageHandler() -logger.addHandler(last_message_handler) - - -class GameManager: - """The GameManager class provides facilities for working with parties and their adventures. - - Attributes: - parties (list): A list of the available parties. - adventures (list): A list of the available adventures. - """ - def __init__( - self, - parties: list = [], - adventures: list = [], - ): - logger.debug("Initializing the GameManager...") - self.adventures = adventures - self.parties = parties - logger.debug( - f"GameManager initialized." - ) diff --git a/osrlib/osrlib/inventory.py b/osrlib/osrlib/inventory.py index 85615d0..746703b 100644 --- a/osrlib/osrlib/inventory.py +++ b/osrlib/osrlib/inventory.py @@ -37,7 +37,7 @@ class Inventory: ``` """ def __init__(self, player_character_owner: "PlayerCharacter"): - """Creates a `PlayerCharacter` instance. + """Creates an `Inventory` instance and assigns the specified `PlayerCharacter` as its owner. Args: player_character_owner (PlayerCharacter): The player character that owns the inventory. diff --git a/osrlib/osrlib/item.py b/osrlib/osrlib/item.py index 004e801..1012ad6 100644 --- a/osrlib/osrlib/item.py +++ b/osrlib/osrlib/item.py @@ -5,51 +5,6 @@ from osrlib.enums import CharacterClassType, ItemType -class ItemAlreadyHasOwnerError(Exception): - """Exception raised when an item already has an owner.""" - - pass - - -class ItemAlreadyInInventoryError(Exception): - """Exception raised when trying to add an item to a player character's (PC) inventory already in the inventory.""" - - pass - - -class ItemEquippedError(Exception): - """Exception raised when trying to equip an item the player character (PC) already has equipped.""" - - pass - - -class ItemNotEquippedError(Exception): - """Exception raised when trying to unequip an item the player character (PC) doesn't have equipped.""" - - pass - - -class ItemNotInInventoryError(Exception): - """Exception raised when trying to remove an item from a player character's (PC) inventory that's not in the inventory.""" - - pass - - -class ItemNotUsableError(Exception): - """Exception raised when trying to use an item that the player character (PC) can't use. - - The inability to use an item is typically due to a character class restriction. For example, a magic user can't use - a sword and a thief can't wear plate mail armor.""" - - pass - - -class ItemAlreadyInQuestError(Exception): - """Exception raised when trying to assign an item to a quest that's already been assigned to a quest.""" - - pass - - class Item: """An item represents a piece of equipment, a weapon, spell, quest piece, any other item that can be owned by a player character (PC). @@ -71,7 +26,7 @@ class Item: Example: ```python - # Create an item that is a sword usable by fighters and thieves + # Create a sword usable by Fighters and Thieves usable_by = {CharacterClassType.FIGHTER, CharacterClassType.THIEF} item = Item("Sword", ItemType.WEAPON, usable_by, max_equipped=1, gp_value=10) ``` @@ -89,7 +44,7 @@ def __init__( """Initialize an item with the specified properties. Don't call the methods on this class directory. Instead, use a PlayerCharacter's InventoryManager (pc.inventory) - to add/remove this item from a PC's inventor or add it to a Quest. + to add/remove this item to/from a PC's inventory or add it to a Quest. Args: name (str): Name of the item. @@ -410,3 +365,48 @@ def from_dict(cls, spell_dict: dict) -> "Spell": max_equipped=base_item.max_equipped, gp_value=base_item.gp_value, ) + + +class ItemAlreadyHasOwnerError(Exception): + """Exception raised when an item already has an owner.""" + + pass + + +class ItemAlreadyInInventoryError(Exception): + """Exception raised when trying to add an item to a player character's (PC) inventory already in the inventory.""" + + pass + + +class ItemEquippedError(Exception): + """Exception raised when trying to equip an item the player character (PC) already has equipped.""" + + pass + + +class ItemNotEquippedError(Exception): + """Exception raised when trying to unequip an item the player character (PC) doesn't have equipped.""" + + pass + + +class ItemNotInInventoryError(Exception): + """Exception raised when trying to remove an item from a player character's (PC) inventory that's not in the inventory.""" + + pass + + +class ItemNotUsableError(Exception): + """Exception raised when trying to use an item that the player character (PC) can't use. + + The inability to use an item is typically due to a character class restriction. For example, a magic user can't use + a sword and a thief can't wear plate mail armor.""" + + pass + + +class ItemAlreadyInQuestError(Exception): + """Exception raised when trying to assign an item to a quest that's already been assigned to a quest.""" + + pass diff --git a/osrlib/osrlib/item_factories.py b/osrlib/osrlib/item_factories.py index b518af1..d893e55 100644 --- a/osrlib/osrlib/item_factories.py +++ b/osrlib/osrlib/item_factories.py @@ -151,7 +151,7 @@ class ArmorFactory: @staticmethod def create_armor(armor_name: str): - armor_info = armor_data.get(armor_name) + armor_info = (armor_data | magic_armor_data).get(armor_name) if armor_info: return Armor( name=armor_name, @@ -181,11 +181,11 @@ def create_item(item_name): class WeaponFactory: - """Factory class to create items of type ``Weapon``.""" + """Factory class to create items of type `Weapon`.""" @staticmethod def create_weapon(weapon_name: str): - weapon_info = weapon_data.get(weapon_name) + weapon_info = (weapon_data| magic_weapon_data).get(weapon_name) if weapon_info: return Weapon( name=weapon_name, @@ -194,7 +194,6 @@ def create_weapon(weapon_name: str): usable_by_classes=weapon_info["usable_by"], range=weapon_info.get("range"), ) - else: raise ItemDataNotFoundError(weapon_name) @@ -340,6 +339,5 @@ def get_random_item(item_type: ItemType, magical: bool = False) -> Item: data = magic_weapon_data if magical else weapon_data item_name = random.choice(list(data.keys())) return WeaponFactory.create_weapon(item_name) - # TODO: Add support for SPELL, EQUIPMENT, and MAGIC_ITEM else: raise ValueError(f"No item selection logic for {item_type}") diff --git a/osrlib/osrlib/monster.py b/osrlib/osrlib/monster.py index c36ff37..d567f6c 100644 --- a/osrlib/osrlib/monster.py +++ b/osrlib/osrlib/monster.py @@ -3,7 +3,7 @@ from osrlib.enums import CharacterClassType, TreasureType from osrlib.player_character import Alignment from osrlib.saving_throws import get_saving_throws_for_class_and_level -from osrlib.game_manager import logger +from osrlib.utils import logger from osrlib.treasure import Treasure monster_xp = { diff --git a/osrlib/osrlib/monster_manual.py b/osrlib/osrlib/monster_manual.py index 03ccaa6..afcccc9 100644 --- a/osrlib/osrlib/monster_manual.py +++ b/osrlib/osrlib/monster_manual.py @@ -4,7 +4,10 @@ berserker_stats = MonsterStatsBlock( name="Berserker", - description="Berserkers are simple fighters who go mad in battle. They react normally at first, but once a battle starts they will always fight to the death, sometimes attacking their comrades in their blind rage. When fighting humans or human-like creatures, they add +2 to 'to hit' rolls due to this ferocity. They never retreat, surrender, or take prisoners. Treasure Type (B) is only found in the wilderness.", + description="Berserkers are fighters that go mad in battle. They react normally at first, but when battle starts, " + "they always fight to the death and sometimes attack their comrades in their blind rage. When fighting humans or " + "human-like creatures, they add +2 to 'to hit' rolls due to this ferocity. They never retreat, surrender, or take " + "prisoners.", armor_class=7, hit_dice="1d8+1", num_appearing="1d6", @@ -15,7 +18,7 @@ save_as_class=CharacterClassType.FIGHTER, save_as_level=1, morale=12, - treasure_type=TreasureType.B, + treasure_type=TreasureType.P, # TreasureType.B is only found in wilderness alignment=Alignment.NEUTRAL ) @@ -72,14 +75,15 @@ cyclops_stats = MonsterStatsBlock( name="Cyclops", - description="A rare type of giant, the cyclops is noted for its great size and single eye in the center of its forehead. Cyclops have poor depth perception due to their single eye.", + description="A rare type of giant, the cyclops is noted for its great size and single eye in the center of its " + "forehead. Cyclops have poor depth perception due to their single eye.", armor_class=5, hit_dice="13d8", num_appearing="d1", movement=90, num_special_abilities=1, attacks_per_round=1, # TODO: Add support for attack and damage modifiers (Cyclops has -2 on attack rolls) - damage_per_attack="3d10", + damage_per_attack="3d10", # TODO: Add support for ranged attacks (Cyclops can throw rocks up to 200' at 3d6 damage) save_as_class=CharacterClassType.FIGHTER, save_as_level=13, morale=9, @@ -91,12 +95,12 @@ name="Ghoul", description="Ghouls are undead creatures. They are hideous, beast-like humans who attack anything living. Attacks by a ghoul will paralyze any creature of ogre-size or smaller, except elves, unless the victim saves vs. Paralysis. Once an opponent is paralyzed, the ghoul will turn and attack another opponent until either the ghoul or all the opponents are paralyzed or dead. The paralysis lasts 2-8 turns unless removed by a cure light wounds spell.", armor_class=6, - hit_dice="2d8", # The asterisk (*) in the rule book indicates a special ability, which in this case is paralysis - num_appearing="1d6", # For individual encounters + hit_dice="2d8", + num_appearing="1d6", movement=90, num_special_abilities=1, # For the paralysis ability attacks_per_round=3, # Two claws and one bite - damage_per_attack="1d3+1", # Assuming the special damage applies to each attack + damage_per_attack="1d3+1", # Special damage applies to each attack save_as_class=CharacterClassType.FIGHTER, save_as_level=2, morale=9, diff --git a/osrlib/osrlib/party.py b/osrlib/osrlib/party.py index bb98038..92f37a6 100644 --- a/osrlib/osrlib/party.py +++ b/osrlib/osrlib/party.py @@ -4,7 +4,7 @@ from typing import List from osrlib.player_character import PlayerCharacter -from osrlib.game_manager import logger +from osrlib.utils import logger from osrlib.enums import CharacterClassType from osrlib.item_factories import equip_party from osrlib.dice_roller import roll_dice @@ -63,6 +63,7 @@ class NoMembersInPartyError(Exception): pass + class Party: """Manages a collection of player characters (PCs) that comprise an adventuring party. @@ -528,21 +529,22 @@ def from_dict(cls, party_dict: dict) -> "Party": party.set_active_character(party.members[0]) return party -def get_default_party(party_name: str = "Default Party") -> Party: # pragma: no cover - """Get a party of six (6) first-level characters: a Fighter, Elf, Dwarf, Thief, Halfling, and Magic User. + @staticmethod + def get_default_party(party_name: str = "Default Party") -> "Party": # pragma: no cover + """Get a party of first-level characters of each class: a Fighter, Elf, Cleric, Dwarf, Thief, Halfling, and Magic User. - Returns: - Party: A party with six (6) player characters at first level (zero experience points). - """ - party = Party(party_name) - party.create_character(random.choice(FIGHTER_NAMES), CharacterClassType.FIGHTER, 1) - party.create_character(random.choice(ELF_NAMES), CharacterClassType.ELF, 1) - party.create_character(random.choice(CLERIC_NAMES), CharacterClassType.CLERIC, 1) - party.create_character(random.choice(DWARF_NAMES), CharacterClassType.DWARF, 1) - party.create_character(random.choice(THIEF_NAMES), CharacterClassType.THIEF, 1) - party.create_character(random.choice(HALFLING_NAMES), CharacterClassType.HALFLING, 1) - party.create_character(random.choice(MAGIC_USER_NAMES), CharacterClassType.MAGIC_USER, 1) - - equip_party(party) - - return party + Returns: + Party: A party of first-level player characters in each character class. + """ + party = Party(party_name) + party.create_character(random.choice(FIGHTER_NAMES), CharacterClassType.FIGHTER, 1) + party.create_character(random.choice(ELF_NAMES), CharacterClassType.ELF, 1) + party.create_character(random.choice(CLERIC_NAMES), CharacterClassType.CLERIC, 1) + party.create_character(random.choice(DWARF_NAMES), CharacterClassType.DWARF, 1) + party.create_character(random.choice(THIEF_NAMES), CharacterClassType.THIEF, 1) + party.create_character(random.choice(HALFLING_NAMES), CharacterClassType.HALFLING, 1) + party.create_character(random.choice(MAGIC_USER_NAMES), CharacterClassType.MAGIC_USER, 1) + + equip_party(party) + + return party diff --git a/osrlib/osrlib/player_character.py b/osrlib/osrlib/player_character.py index b19d01a..e854841 100644 --- a/osrlib/osrlib/player_character.py +++ b/osrlib/osrlib/player_character.py @@ -14,7 +14,7 @@ from osrlib.enums import AbilityType, CharacterClassType, ModifierType from osrlib.inventory import Inventory from osrlib.dice_roller import roll_dice, DiceRoll -from osrlib.game_manager import logger +from osrlib.utils import logger from osrlib.utils import get_data_dir_path, create_dir_tree_if_not_exist @@ -34,7 +34,9 @@ class PlayerCharacter: abilities (dict): A dictionary of the character's abilities. character_class (CharacterClass): The character's class. inventory (Inventory): The character's inventory. - xp_adjustment_percentage (int): The character's XP adjustment based on the scores of ability their prime requisite(s). This value is set when the character's class is set, or when restoring a saved character. + xp_adjustment_percentage (int): The character's XP adjustment based on the scores of ability their prime + requisite(s). This value is set when the character's class is set, or when + restoring a saved character. """ def __init__( @@ -267,7 +269,7 @@ def roll_abilities(self): ) def roll_hp(self) -> DiceRoll: - """Rolls the character's hit points, taking into account their Constitution modifier, if any. + """Rolls the character's hit dice and applies their Constitution modifier, if any. The total value of the roll with modifier can be negative after if the roll was low and the character has a negative Constitution modifier. You should clamp the value to 1 before applying it to the character's HP. diff --git a/osrlib/osrlib/saving_throws.py b/osrlib/osrlib/saving_throws.py index 60fc13e..15a62e7 100644 --- a/osrlib/osrlib/saving_throws.py +++ b/osrlib/osrlib/saving_throws.py @@ -1,5 +1,5 @@ from osrlib.enums import CharacterClassType, AttackType -from osrlib.game_manager import logger +from osrlib.utils import logger saving_throws = { CharacterClassType.CLERIC: { diff --git a/osrlib/osrlib/treasure.py b/osrlib/osrlib/treasure.py index de819bb..fd6e19e 100644 --- a/osrlib/osrlib/treasure.py +++ b/osrlib/osrlib/treasure.py @@ -5,12 +5,14 @@ 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 +import random +from typing import Dict, Union, List 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 osrlib.item import Item +from osrlib.item_factories import get_random_item +from osrlib.utils import logger from enum import Enum @@ -57,7 +59,8 @@ class Treasure: 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. + of either CoinType or ItemType enumerations, and the values are + integers representing the quantity of each item. Example: @@ -73,96 +76,98 @@ class Treasure: ``` """ items: Dict[Union[CoinType, ItemType], int] + magic_items: List[Item] _treasure_types: Dict[ TreasureType, Dict[Union[CoinType, ItemType], TreasureDetail] ] = { TreasureType.NONE: {}, 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"), + CoinType.COPPER: TreasureDetail(chance=25, amount="1000d6"), + CoinType.SILVER: TreasureDetail(chance=30, amount="1000d6"), + CoinType.ELECTRUM: TreasureDetail(chance=20, amount="1000d4"), + CoinType.GOLD: TreasureDetail(chance=35, amount="2000d6"), + CoinType.PLATINUM: TreasureDetail(chance=25, amount="1000d2"), ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="6d6"), - ItemType.MAGIC_ITEM: TreasureDetail(chance=30, amount="3", magical=True), + ItemType.ARMOR: TreasureDetail(chance=30, amount="1", magical=True), + ItemType.WEAPON: TreasureDetail(chance=30, amount="1", 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"), + CoinType.COPPER: TreasureDetail(chance=50, amount="1000d8"), + CoinType.SILVER: TreasureDetail(chance=25, amount="1000d6"), + CoinType.ELECTRUM: TreasureDetail(chance=25, amount="1000d4"), + CoinType.GOLD: TreasureDetail(chance=25, amount="1000d3"), 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"), + CoinType.COPPER: TreasureDetail(chance=20, amount="1000d12"), + CoinType.SILVER: TreasureDetail(chance=30, amount="1000d4"), + CoinType.ELECTRUM: TreasureDetail(chance=10, amount="1000d4"), 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"), + CoinType.COPPER: TreasureDetail(chance=10, amount="1000d8"), + CoinType.SILVER: TreasureDetail(chance=15, amount="1000d12"), + CoinType.GOLD: TreasureDetail(chance=60, amount="1000d6"), 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"), + CoinType.COPPER: TreasureDetail(chance=5, amount="1000d10"), + CoinType.SILVER: TreasureDetail(chance=30, amount="1000d12"), + CoinType.ELECTRUM: TreasureDetail(chance=25, amount="1000d4"), + CoinType.GOLD: TreasureDetail(chance=25, amount="1000d8"), 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"), + CoinType.SILVER: TreasureDetail(chance=10, amount="2000d10"), + CoinType.ELECTRUM: TreasureDetail(chance=20, amount="1000d8"), + CoinType.GOLD: TreasureDetail(chance=45, amount="1000d12"), + CoinType.PLATINUM: TreasureDetail(chance=30, amount="1000d3"), 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"), + CoinType.GOLD: TreasureDetail(chance=50, amount="10000d4"), + CoinType.PLATINUM: TreasureDetail(chance=50, amount="1000d6"), 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"), + CoinType.COPPER: TreasureDetail(chance=25, amount="3000d8"), + CoinType.SILVER: TreasureDetail(chance=50, amount="1000d100"), + CoinType.ELECTRUM: TreasureDetail(chance=50, amount="10000d4"), + CoinType.GOLD: TreasureDetail(chance=50, amount="10000d6"), + CoinType.PLATINUM: TreasureDetail(chance=25, amount="5000d4"), ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d100"), ItemType.MAGIC_ITEM: TreasureDetail(chance=15, amount="6", magical=True), }, TreasureType.I: { - CoinType.PLATINUM: TreasureDetail(chance=30, amount="1d8"), + CoinType.PLATINUM: TreasureDetail(chance=30, amount="1000d8"), 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.COPPER: TreasureDetail(chance=25, amount="1000d4"), + CoinType.SILVER: TreasureDetail(chance=10, amount="1000d3"), }, TreasureType.K: { - CoinType.SILVER: TreasureDetail(chance=30, amount="1d6"), - CoinType.ELECTRUM: TreasureDetail(chance=10, amount="1d2"), + CoinType.SILVER: TreasureDetail(chance=30, amount="1000d6"), + CoinType.ELECTRUM: TreasureDetail(chance=10, amount="1000d2"), }, TreasureType.L: { ItemType.GEMS_JEWELRY: TreasureDetail(chance=50, amount="1d4"), }, TreasureType.M: { - CoinType.GOLD: TreasureDetail(chance=40, amount="2d4"), - CoinType.PLATINUM: TreasureDetail(chance=50, amount="5d6"), + CoinType.GOLD: TreasureDetail(chance=40, amount="2000d4"), + CoinType.PLATINUM: TreasureDetail(chance=50, amount="5000d6"), ItemType.GEMS_JEWELRY: TreasureDetail(chance=55, amount="5d4"), }, TreasureType.N: { - ItemType.MAGIC_ITEM: TreasureDetail(chance=40, amount="2d4", magical=True), + ItemType.MAGIC_ITEM: TreasureDetail(chance=40, amount="2000d4", magical=True), }, TreasureType.O: { ItemType.MAGIC_ITEM: TreasureDetail(chance=50, amount="1d4", magical=True), @@ -206,11 +211,34 @@ class Treasure: def __init__(self, treasure_type: TreasureType = TreasureType.NONE): self.items = {} + self.magic_items = [] self._generate_treasure(treasure_type) + self.treasure_type = treasure_type + + def __str__(self) -> str: + """Returns a string representation of the treasure in a multi-line format, showing each type of treasure with its quantity on separate lines, followed by the total value in gold pieces (GP) on a separate line. + + Returns: + str: A multi-line description of the treasure's contents and total GP value. + """ + lines = [] + lines.append(f"{str(self.treasure_type)} ({self.total_gp_value} GP value)") + + for item_type, amount in self.items.items(): + if isinstance(item_type, CoinType): + lines.append(f"{item_type.name.capitalize()}: {amount}") + elif isinstance(item_type, ItemType): + # Adjusting item name formatting to be more readable + item_name = item_type.name.replace('_', ' ').capitalize() + lines.append(f"{item_name}: {amount}") + else: + # Fallback for any item types not accounted for + lines.append(f"Unknown item: {amount}") + + return " | ".join(lines) 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. + """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. @@ -220,7 +248,12 @@ def _generate_treasure(self, treasure_type: TreasureType) -> None: chance_roll = roll_dice("1d100").total if chance_roll <= details.chance: amount_roll = roll_dice(details.amount) - self.items[item_type] = amount_roll.total + if isinstance(item_type, CoinType): + self.items[item_type] = amount_roll.total_with_modifier + elif item_type == ItemType.ARMOR or item_type == ItemType.WEAPON: + magic_item = get_random_item(item_type, magical=True) + self.magic_items.append(magic_item) + logger.debug(f"Added {magic_item} to {treasure_type}") @property def total_gp_value(self) -> int: diff --git a/osrlib/osrlib/utils.py b/osrlib/osrlib/utils.py index 254f8e3..2347dee 100644 --- a/osrlib/osrlib/utils.py +++ b/osrlib/osrlib/utils.py @@ -1,4 +1,8 @@ -import os, platform, textwrap, re +import os +import platform +import textwrap +import re +import logging from pathlib import Path from osrlib.ability import ModifierType @@ -149,3 +153,25 @@ def get_data_dir_path(app_name: str) -> Path: raise ValueError("Unsupported operating system.") return base_dir / sanitize_path_element(app_name) + +class LastMessageHandler(logging.Handler): + def __init__(self): + super().__init__() + self.last_message = None + + def emit(self, record): + self.last_message = self.format(record) + + def format(self, record): + # Return only the message part of the log record + return record.getMessage() + +# Configure logging +logging.basicConfig( + format="%(asctime)s [%(levelname)s][%(module)s::%(funcName)s] %(message)s", +) + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +last_message_handler = LastMessageHandler() +logger.addHandler(last_message_handler) \ No newline at end of file diff --git a/osrlib/pyproject.toml b/osrlib/pyproject.toml index fb89b28..b92e394 100644 --- a/osrlib/pyproject.toml +++ b/osrlib/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "osrlib" -version = "0.1.81" +version = "0.1.83" description = "Turn-based dungeon-crawler game engine for OSR-style RPGs." authors = ["Marsh Macy "] license = "MIT" diff --git a/tests/test_doc_player_character.py b/tests/test_doc_player_character.py new file mode 100644 index 0000000..18a0397 --- /dev/null +++ b/tests/test_doc_player_character.py @@ -0,0 +1,22 @@ +"""Testable code examples for `PlayerCharacter` and related classes. + +The code in this module is ingested by and displayed in documentation for the `osrlib` package. +""" + +import pytest + +# --8<-- [start:player_character_create] +from osrlib.enums import CharacterClassType +from osrlib.player_character import PlayerCharacter + +def test_doc_player_character_create(): + + # Create a fighter PC and roll their abilities and hit points (HP) + fighter_pc = PlayerCharacter("Sckricko", CharacterClassType.FIGHTER) + fighter_pc.roll_abilities() + fighter_pc.roll_hp() +# --8<-- [end:player_character_create] + + assert len(fighter_pc.abilities) > 0 + assert fighter_pc.hit_points >= 1 + assert fighter_pc.name == "Sckricko" diff --git a/tests/test_integration_dungeon_master.py b/tests/test_integration_dungeon_master.py index b31fa2e..d817172 100644 --- a/tests/test_integration_dungeon_master.py +++ b/tests/test_integration_dungeon_master.py @@ -8,7 +8,7 @@ from osrlib.dungeon import Dungeon, Location, Exit, Direction from osrlib.dungeon_assistant import DungeonAssistant from osrlib.adventure import Adventure -from osrlib.game_manager import logger +from osrlib.utils import logger @pytest.mark.optin @pytest.mark.flaky(reruns=0, reruns_delay=0) diff --git a/tests/test_integration_saveload.py b/tests/test_integration_saveload.py index 816caae..0445d21 100644 --- a/tests/test_integration_saveload.py +++ b/tests/test_integration_saveload.py @@ -2,10 +2,10 @@ import pytest from tinydb import Query, TinyDB -from osrlib.party import Party, get_default_party +from osrlib.party import Party from osrlib.player_character import PlayerCharacter from osrlib.item import Item, ItemType, ItemNotUsableError, Armor, Weapon, Spell -from osrlib.game_manager import logger +from osrlib.utils import logger from osrlib.ability import ( Strength, Intelligence, @@ -521,7 +521,7 @@ def test_player_character_saveload(db, test_fighter): def test_party_saveload(db): - pc_party = get_default_party() + pc_party = Party.get_default_party() # Give one party member some gear armor = Armor( @@ -589,7 +589,7 @@ def test_party_saveload(db): # Test whether a "dead" party stays dead after saving and loading def test_party_saveload_dead(db): - pc_party = get_default_party() + pc_party = Party.get_default_party() # Kill the party for character in pc_party.members: diff --git a/tests/test_unit_adventure.py b/tests/test_unit_adventure.py index ab95487..542038c 100644 --- a/tests/test_unit_adventure.py +++ b/tests/test_unit_adventure.py @@ -1,7 +1,7 @@ import pytest, os, json from osrlib.adventure import Adventure from osrlib.dungeon import Dungeon -from osrlib.party import get_default_party +from osrlib.party import Party from osrlib.enums import OpenAIModelVersion @pytest.fixture @@ -12,7 +12,7 @@ def sample_adventure() -> Adventure: return Adventure(name="Test Adventure", description="A small test adventure.", dungeons=[dungeon1, dungeon2]) def test_adventure_to_dict(sample_adventure): - default_party = get_default_party() + default_party = Party.get_default_party() sample_adventure.set_active_party(default_party) sample_adventure.set_active_dungeon(sample_adventure.dungeons[0]) @@ -26,7 +26,7 @@ def test_adventure_to_dict(sample_adventure): assert adventure_dict["active_party"]["name"] == default_party.name def test_adventure_from_dict(sample_adventure): - default_party = get_default_party() + default_party = Party.get_default_party() sample_adventure.set_active_party(default_party) sample_adventure.set_active_dungeon(sample_adventure.dungeons[0]) @@ -46,7 +46,7 @@ def test_save_adventure(sample_adventure, tmp_path): """ Test that an adventure can be successfully saved to a JSON file. """ - default_party = get_default_party() + default_party = Party.get_default_party() sample_adventure.set_active_party(default_party) sample_adventure.set_active_dungeon(sample_adventure.dungeons[0]) @@ -66,7 +66,7 @@ def test_load_adventure(sample_adventure, tmp_path): """ Test that an adventure can be successfully loaded from a JSON file. """ - default_party = get_default_party() + default_party = Party.get_default_party() sample_adventure.set_active_party(default_party) sample_adventure.set_active_dungeon(sample_adventure.dungeons[0]) diff --git a/tests/test_unit_armor.py b/tests/test_unit_armor.py index 6694394..dab800b 100644 --- a/tests/test_unit_armor.py +++ b/tests/test_unit_armor.py @@ -1,12 +1,9 @@ -import logging - import pytest from osrlib.enums import AbilityType, CharacterClassType, ModifierType from osrlib.item import Armor from osrlib.player_character import PlayerCharacter - -logger = logging.getLogger(__name__) +from osrlib.utils import logger CLASSES_THAT_CAN_USE_ALL_ARMOR = { CharacterClassType.FIGHTER, diff --git a/tests/test_unit_character_classes.py b/tests/test_unit_character_classes.py index 15eea47..24fda67 100644 --- a/tests/test_unit_character_classes.py +++ b/tests/test_unit_character_classes.py @@ -1,33 +1,38 @@ -import logging, math +import math import pytest from osrlib.enums import CharacterClassType from osrlib.character_classes import class_levels from osrlib.player_character import PlayerCharacter - -logger = logging.getLogger(__name__) - - -def generate_player_character_params(starting_level: int = 1): - params = [] - for character_class_type in CharacterClassType: - pc = PlayerCharacter(f"Test {character_class_type.value}", character_class_type, starting_level) - params.append(pc) - return params - - -@pytest.mark.parametrize("pc", generate_player_character_params(starting_level=1)) -def test_characters_init_sane_level_one(pc: PlayerCharacter): - logger.debug(pc) - assert pc.character_class.current_level.level_num == 1 - assert pc.character_class.max_hp > 0 - assert pc.character_class.xp == 0 - -@pytest.mark.parametrize("pc", generate_player_character_params(starting_level=0)) -def test_character_can_reach_all_available_levels(pc: PlayerCharacter): +from osrlib.utils import logger + + +# Fixture to generate player characters of all available classes at level 0 +@pytest.fixture(scope="function", params=[class_type for class_type in CharacterClassType]) +def pc_level_zero(request): + pc = PlayerCharacter(f"Test {request.param.value}", request.param, 0) + return pc + +# Fixture to generate player characters of all available classes at level 1 +@pytest.fixture(scope="function", params=[class_type for class_type in CharacterClassType]) +def pc_level_one(request): + pc = PlayerCharacter(f"Test {request.param.value}", request.param, 1) + return pc + +# Use the fixture by specifying its name as an argument to the test function +def test_characters_init_sane_level_one(pc_level_one: PlayerCharacter): + logger.debug(pc_level_one) + assert pc_level_one.character_class.current_level.level_num == 1 + assert pc_level_one.character_class.max_hp > 0 + assert pc_level_one.character_class.xp == 0 + +# Use the fixture by specifying its name as an argument to the test function +def test_character_can_reach_all_available_levels(pc_level_zero: PlayerCharacter): + pc = pc_level_zero logger.debug( f"Testing whether {pc.name.upper()} can reach all {pc.character_class.class_type.value.upper()} levels ..." ) + # Rest of the test implementation remains the same while pc.character_class.current_level.level_num < len(class_levels[pc.character_class.class_type]) - 1: level_before = pc.character_class.current_level.level_num diff --git a/tests/test_unit_combat.py b/tests/test_unit_combat.py index 50f9100..274f02f 100644 --- a/tests/test_unit_combat.py +++ b/tests/test_unit_combat.py @@ -1,9 +1,9 @@ import pytest from osrlib.character_classes import CharacterClassType, class_levels from osrlib.encounter import Encounter -from osrlib.game_manager import logger +from osrlib.utils import logger from osrlib.monster import MonsterParty, MonsterStatsBlock -from osrlib.party import get_default_party +from osrlib.party import Party from osrlib.player_character import Alignment from osrlib.treasure import TreasureType @@ -45,7 +45,7 @@ @pytest.fixture def pc_party(): - yield get_default_party() + yield Party.get_default_party() @pytest.fixture def goblin_party(): @@ -175,9 +175,9 @@ def test_encounter_start_and_end(pc_party, goblin_encounter): assert goblin_encounter.is_ended == True def test_monster_thac0(hobgoblin_encounter, kobold_encounter, cyclops_party): - hobgoblin_encounter.start_encounter(get_default_party()) + hobgoblin_encounter.start_encounter(Party.get_default_party()) - kobold_encounter.start_encounter(get_default_party()) + kobold_encounter.start_encounter(Party.get_default_party()) cyclops_encounter = Encounter("Cyclops", "This thing has 13 HD and a special ability.", cyclops_party) - cyclops_encounter.start_encounter(get_default_party()) + cyclops_encounter.start_encounter(Party.get_default_party()) diff --git a/tests/test_unit_dice_roller.py b/tests/test_unit_dice_roller.py index 337f86b..226f172 100644 --- a/tests/test_unit_dice_roller.py +++ b/tests/test_unit_dice_roller.py @@ -1,14 +1,9 @@ -import logging from unittest.mock import patch import pytest from osrlib.dice_roller import DiceRoll, roll_dice -logger = logging.getLogger(__name__) -logger.debug("Testing dice_roller.py") - - def test_valid_notation(): """Tests valid dice notation. diff --git a/tests/test_unit_dungeon.py b/tests/test_unit_dungeon.py index 083283d..3a51e14 100644 --- a/tests/test_unit_dungeon.py +++ b/tests/test_unit_dungeon.py @@ -1,15 +1,9 @@ import json import pytest -import osrlib -from osrlib.dungeon import Dungeon, Location, Exit, Direction, LocationNotFoundError +from osrlib.dungeon import Dungeon, Location, Exit, Direction from osrlib.encounter import Encounter -from osrlib.game_manager import logger from osrlib.enums import OpenAIModelVersion - -logger.info(osrlib.__file__) - - @pytest.fixture def sample_dungeon(): # Create a small dungeon with a couple of locations and exits @@ -143,7 +137,7 @@ def test_random_dungeon(): description="The first level of the home of the ancient wizard lich Glofarnux, its " "entrance hidden in a forgotten glade deep in the cursed Mystic Forest.", num_locations=20, - openai_model=OpenAIModelVersion.DEFAULT, + openai_model=OpenAIModelVersion.NONE, ) # Validate Dungeon diff --git a/tests/test_unit_encounter.py b/tests/test_unit_encounter.py index 5fb8a9c..f9deb3e 100644 --- a/tests/test_unit_encounter.py +++ b/tests/test_unit_encounter.py @@ -3,7 +3,7 @@ from osrlib.encounter import Encounter from osrlib.monster import MonsterParty from osrlib.monster_manual import monster_stats_blocks -from osrlib.game_manager import logger +from osrlib.utils import logger @pytest.fixture def random_monster_party(): diff --git a/tests/test_unit_inventory.py b/tests/test_unit_inventory.py index 916cd11..2c26033 100644 --- a/tests/test_unit_inventory.py +++ b/tests/test_unit_inventory.py @@ -1,9 +1,8 @@ +import pytest from osrlib.inventory import Inventory from osrlib.item import Item from osrlib.player_character import PlayerCharacter, CharacterClassType from osrlib.item import ItemType -import pytest - @pytest.fixture def inventory(): diff --git a/tests/test_unit_item.py b/tests/test_unit_item.py index ce1f1f8..fe81e1a 100644 --- a/tests/test_unit_item.py +++ b/tests/test_unit_item.py @@ -1,20 +1,14 @@ -import logging - import pytest from osrlib.enums import CharacterClassType from osrlib.item import ( - ItemAlreadyHasOwnerError, ItemAlreadyInInventoryError, - ItemEquippedError, ItemNotEquippedError, ItemNotInInventoryError, - ItemNotUsableError, Weapon, ) from osrlib.player_character import PlayerCharacter - -logger = logging.getLogger(__name__) +from osrlib.utils import logger CLASSES_THAT_CAN_USE_ALL_WEAPONS = { CharacterClassType.FIGHTER, diff --git a/tests/test_unit_monster_party.py b/tests/test_unit_monster_party.py index 1322cc8..87e3003 100644 --- a/tests/test_unit_monster_party.py +++ b/tests/test_unit_monster_party.py @@ -1,4 +1,5 @@ -import pytest, random +import pytest +import random from osrlib.monster import MonsterParty from osrlib.monster_manual import monster_stats_blocks diff --git a/tests/test_unit_monster_stats_block.py b/tests/test_unit_monster_stats_block.py index 52f0681..39d263a 100644 --- a/tests/test_unit_monster_stats_block.py +++ b/tests/test_unit_monster_stats_block.py @@ -1,12 +1,11 @@ import pytest -from unittest.mock import Mock from osrlib.monster import MonsterStatsBlock, CharacterClassType, TreasureType, Alignment @pytest.fixture def default_monster_stats_block(): return MonsterStatsBlock(name="Test Monster") -def test_initialization_with_default_values(default_monster_stats_block): +def test_initialization_with_default_values(default_monster_stats_block: MonsterStatsBlock): assert default_monster_stats_block.name == "Test Monster" assert default_monster_stats_block.description == "" assert default_monster_stats_block.armor_class == 10 @@ -54,7 +53,7 @@ def test_initialization_with_custom_values(): assert custom_monster.treasure_type == TreasureType.A assert custom_monster.alignment == Alignment.CHAOTIC -def test_to_dict(default_monster_stats_block): +def test_to_dict(default_monster_stats_block: MonsterStatsBlock): monster_dict = default_monster_stats_block.to_dict() assert monster_dict["name"] == "Test Monster" assert monster_dict["description"] == "" diff --git a/tests/test_unit_party.py b/tests/test_unit_party.py index 35d2f96..0ea1915 100644 --- a/tests/test_unit_party.py +++ b/tests/test_unit_party.py @@ -1,13 +1,17 @@ import pytest -from osrlib import party, player_character, character_classes +from osrlib.enums import CharacterClassType +from osrlib.party import Party +from osrlib.party import CharacterNotInPartyError +from osrlib.party import PartyAtCapacityError +from osrlib.player_character import PlayerCharacter @pytest.fixture def setup_party(): - empty_test_party = party.Party("The B-Team", 3) - character1 = player_character.PlayerCharacter("Sckricko", character_classes.CharacterClassType.FIGHTER, 1) - character2 = player_character.PlayerCharacter("Mazpar", character_classes.CharacterClassType.MAGIC_USER, 1) - character3 = player_character.PlayerCharacter("Slick", character_classes.CharacterClassType.THIEF, 1) + empty_test_party = Party("The B-Team", 3) + character1 = PlayerCharacter("Sckricko", CharacterClassType.FIGHTER, 1) + character2 = PlayerCharacter("Mazpar", CharacterClassType.MAGIC_USER, 1) + character3 = PlayerCharacter("Slick", CharacterClassType.THIEF, 1) return empty_test_party, character1, character2, character3 @@ -23,8 +27,8 @@ def test_add_character_at_capacity(setup_party): # Fill the party to max_capacity - 1 with uniquely named characters for i in range(max_capacity - 1): - new_character = player_character.PlayerCharacter( - f"Test Character {i}", character_classes.CharacterClassType.FIGHTER, 1 + new_character = PlayerCharacter( + f"Test Character {i}", CharacterClassType.FIGHTER, 1 ) test_party.add_character(new_character) @@ -32,7 +36,7 @@ def test_add_character_at_capacity(setup_party): test_party.add_character(character1) # Adding one more beyond that should raise PartyAtCapacityError - with pytest.raises(party.PartyAtCapacityError): + with pytest.raises( PartyAtCapacityError): test_party.add_character(character2) @@ -45,7 +49,7 @@ def test_remove_character(setup_party): def test_remove_character_not_in_party(setup_party): test_party, character1, _, _ = setup_party - with pytest.raises(party.CharacterNotInPartyError): + with pytest.raises(CharacterNotInPartyError): test_party.remove_character(character1) @@ -85,7 +89,7 @@ def test_get_character_index(setup_party): def test_get_character_index_not_in_party(setup_party): test_party, character1, _, _ = setup_party - with pytest.raises(party.CharacterNotInPartyError): + with pytest.raises(CharacterNotInPartyError): test_party.get_character_index(character1) @@ -100,7 +104,7 @@ def test_move_character_to_index(setup_party): def test_move_character_to_index_not_in_party(setup_party): test_party, character1, _, _ = setup_party - with pytest.raises(party.CharacterNotInPartyError): + with pytest.raises(CharacterNotInPartyError): test_party.move_character_to_index(character1, 0) diff --git a/tests/test_unit_treasure.py b/tests/test_unit_treasure.py index de35e97..7e8b13e 100644 --- a/tests/test_unit_treasure.py +++ b/tests/test_unit_treasure.py @@ -1,7 +1,17 @@ import pytest from osrlib.enums import CoinType, ItemType from osrlib.treasure import Treasure, TreasureDetail, TreasureType +from osrlib.utils import logger +def test_treasure_create_all_types(): + # Loop through the TreasureType enum and create Treasure instances for every type + for t in TreasureType: + treasure = Treasure(t) + + assert isinstance(treasure, Treasure), "Failed to create instance of type 'Treasure'" + assert treasure.treasure_type == t, "Treasure type does not match the expected enum value." + + logger.debug(treasure) def test_treasure_total_gold_piece_value(): custom_type = { @@ -21,18 +31,19 @@ def test_treasure_total_gold_piece_value(): @pytest.mark.flaky(reruns=5) # Flaky because we can't guarantee an average of exactly 50% of getting a magic item. def test_treasure_from_custom_type(): # Define a custom treasure type with specific items - custom_type = { + custom_treasure_type = { CoinType.GOLD: TreasureDetail( # Always 5 GP chance=100, amount="5", magical=False ), - ItemType.MAGIC_ITEM: TreasureDetail( + ItemType.WEAPON: TreasureDetail( + # 50% chance of 1 magic weapon 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) + custom_treasure = Treasure.from_custom_type(custom_treasure_type) # Check if the treasure contains the expected items assert CoinType.GOLD in custom_treasure.items @@ -40,7 +51,7 @@ def test_treasure_from_custom_type(): # 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 + Treasure.from_custom_type(custom_treasure_type).items.get(ItemType.WEAPON, 0) > 0 for _ in range(100) ] approx_magic_item_appearance_rate = sum(magic_item_appearances) / len( diff --git a/tests/test_unit_utils.py b/tests/test_unit_utils.py index 482fa54..9d2e3f4 100644 --- a/tests/test_unit_utils.py +++ b/tests/test_unit_utils.py @@ -1,10 +1,6 @@ -import logging - from osrlib.ability import ModifierType from osrlib.utils import format_modifiers - -logger = logging.getLogger(__name__) - +from osrlib.utils import logger def test_format_modifiers(): """Test function for the format_modifiers() utility function.