Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Spellbook Functionality #23

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
13 changes: 13 additions & 0 deletions dnd_character/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,20 @@ def give_item(self, item: _Item) -> None:
If the item is armor or a shield, the armor_class attribute will be set
and any other armor/shields in the inventory will be removed.
"""
# Applying armor class if the item is armor or a shield
self.apply_armor_class(item)

# Checking if the item's index property equals "spellbook"
if getattr(item, 'index', None) == 'spellbook':
# Creating a new Spellbook instance with necessary arguments
# Assuming the Spellbook constructor accepts the same arguments as the item
spellbook = Spellbook(contents=item.contents, cost=item.cost, desc=item.desc, index=item.index,
name=item.name, properties=item.properties, special=item.special,
url=item.url, equipment_category=item.equipment_category)
# Replacing the placeholder item with the new Spellbook instance
item = spellbook

# Adding the item or Spellbook instance to the inventory
self.inventory.append(item)

def remove_item(self, item: _Item) -> None:
Expand Down
160 changes: 160 additions & 0 deletions dnd_character/spellbook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import dnd_character.character
from dnd_character.equipment import _Item
import json
import os
import dnd_character


# Spellbook is a subclass of the _Item class that only accepts wizard spells as contents
class Spellbook(_Item):
# Constructor for initializing a new Spellbook instance
MAX_SPELLS = 100 # Class variable to define the maximum number of spells

def __init__(self, contents, cost, desc, index, name, properties, special, url, equipment_category, *args,
**kwargs):
super().__init__(contents=contents, cost=cost, desc=desc, index=index, name=name, properties=properties,
special=special, url=url, equipment_category=equipment_category, *args, **kwargs)
self.type = 'Spellbook'

# Method to add a spell to the spellbook if it passes validation
def add_spell(self, spell, char_instance):
if len(self.contents) >= Spellbook.MAX_SPELLS:
print("Spellbook is full. Cannot add more spells.")
return

if self.validate_spell(spell, char_instance):
self.contents.append(spell)
else:
print("Invalid spell. Only spells can be added to a spellbook.")

# Method to remove a spell from a spellbook
def remove_spell(self, spell):
if spell in self.contents:
self.contents.remove(spell)
print(f"Removed {spell} from the spellbook.")
else:
print(f"{spell} not found in the spellbook.")

def check_components(self, char_instance, cost):
return char_instance.wealth >= cost

# Method to validate a spell based on D&D wizard spellcasting rules
def validate_spell(self, spell, char_instance):
# Fetch the list of wizard spells by level from the SRD data
wizard_spells_by_level = fetch_wizard_spells_from_json()
print(f"Validating spell: {spell.name}, for character: {char_instance.name}") # Print spell and character info
# Check if character is a wizard
if not char_instance.classs.name == "Wizard":
print(f"{char_instance.name} is not a wizard and cannot scribe spells.")
return False

# Check if the spell level exists in the dictionary
if spell.level not in wizard_spells_by_level:
print(f"Spell level {spell.level} not found in wizard spell list.")
return False

# Check if the spell is of a level the wizard can cast
if spell.level > max_level_for_wizard(char_instance.level):
return False

# Check if the spell is in the wizard spell list
if spell.name not in wizard_spells_by_level[spell.level]:
return False

# Check for component restrictions
cost = spell.level * 50 * dnd_character.character.coin_value['gp']

if not self.check_components(char_instance, cost):
print(f"Insufficient gold to scribe {spell.name}.")
return False
print(f"Char detailed wealth before transaction: {char_instance.wealth_detailed}") # Debug line
print(f"Char wealth before transaction: {char_instance.wealth}") # Debug line
try:
print(f"Cost of transaction: {cost}") # Debug line
char_instance.change_wealth(gp=-cost, conversion=True) # deduct cost
print(f"Char detailed wealth after transaction: {char_instance.wealth_detailed}") # New debug line
return True
except ValueError: # Insufficient funds
print("Exception occurred: ValueError - Insufficient funds")
return False


def max_level_for_wizard(wizard_level):
if wizard_level < 1:
return 0 # Invalid wizard level
elif wizard_level < 3:
return 1 # Level 1 spells
elif wizard_level < 5:
return 2 # Level 2 spells
elif wizard_level < 7:
return 3 # Level 3 spells
elif wizard_level < 9:
return 4 # Level 4 spells
elif wizard_level < 11:
return 5 # Level 5 spells
elif wizard_level < 13:
return 6 # Level 6 spells
elif wizard_level < 15:
return 7 # Level 7 spells
elif wizard_level < 17:
return 8 # Level 8 spells
else:
return 9 # Level 9 spells


# Function to retrieve the wizard spell data from the JSON cache
def fetch_wizard_spells_from_json():
current_script_path = os.path.dirname(__file__)
json_cache_path = os.path.join(current_script_path, 'json_cache', 'api_spells.json')

if not os.path.exists(json_cache_path):
print("JSON file does not exist at the path.")
return None

wizard_spells_by_level = {}

try:
with open(json_cache_path, 'r') as f:
spells = json.load(f)["results"]

for spell in spells:
spell_file_path = os.path.join(current_script_path, 'json_cache', f"api_spells_{spell['index']}.json")

if not os.path.exists(spell_file_path):
continue

with open(spell_file_path, 'r') as spell_file:
spell_data = json.load(spell_file)

if 'wizard' in [cls['name'].lower() for cls in spell_data.get('classes', [])]:
level = spell_data['level']
wizard_spells_by_level.setdefault(level, []).append(spell_data['name'])

except (FileNotFoundError, json.JSONDecodeError):
print("Error encountered while processing JSON.")

try:
with open(json_cache_path, 'r') as f:
spells = json.load(f)["results"]
for spell in spells:
spell_data_path = os.path.join(current_script_path, 'json_cache', f"api_spells_{spell['index']}.json")
# print(f"Attempting to open additional JSON file at {spell_data_path}...")
try:
with open(spell_data_path, 'r') as f:
spell_data = json.load(f)
# print("Additional JSON file read successfully. Content:", spell_data)
except FileNotFoundError:
print(f"Additional JSON file {spell_data_path} not found.")
continue
except json.JSONDecodeError:
print("Error decoding additional JSON file.")
continue
if 'wizard' in [cls['name'].lower() for cls in spell_data.get('classes', [])]:
level = spell_data['level']
if level not in wizard_spells_by_level:
wizard_spells_by_level[level] = []
wizard_spells_by_level[level].append(spell_data['name'])
except (FileNotFoundError, json.JSONDecodeError):
print("Error encountered while processing JSON.")

return wizard_spells_by_level
57 changes: 57 additions & 0 deletions tests/test_spellbook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from dnd_character.spellbook import Spellbook
from dnd_character.classes import Wizard

# Initialize a Spellbook object
my_spellbook = Spellbook(contents=[], cost={}, desc=[], index="", name="", properties=[], special=[], url="",
equipment_category={})

lvl_1_wizard = Wizard(
name="Level 1 Wizard ",
level=1,
wealth=75
)
lvl_3_wizard = Wizard(
name="Level 3 Wizard ",
level=3,
wealth=750
)
lvl_5_wizard = Wizard(
name="Level 3 Wizard ",
level=5,
wealth=700
)
lvl_7_wizard = Wizard(
name="Level 7 Wizard ",
level=7,
wealth=100
)


def run_tests():
# Create mock spell objects
level_2_spell = type('Spell', (object,), {'level': 2, 'name': 'acid-arrow'})
nonwizard_spell = type('Spell', (object,), {'level': 2, 'name': 'Animal Friendship'})
level_3_spell = type('Spell', (object,), {'level': 3, 'name': 'Fireball'})
mock_spell4 = type('Spell', (object,), {'level': 3, 'name': 'Vampiric Touch'})

# Test Case 1: Level 1 Wizard tries to add a Level 2 spell (Should fail)
result1 = my_spellbook.validate_spell(spell=level_2_spell, char_instance=lvl_1_wizard)
assert result1 == False, "Test Case 1 Failed"

# Test Case 2: Level 3 Wizard tries to add a Level 1 spell not in wizard spell list (Should fail)
result2 = my_spellbook.validate_spell(spell=nonwizard_spell, char_instance=lvl_3_wizard)
assert result2 == False, "Test Case 2 Failed"

# Test Case 3: Level 5 Wizard tries to add a Level 3 spell in wizard spell list (Should pass)
result3 = my_spellbook.validate_spell(spell=level_3_spell, char_instance=lvl_5_wizard)
assert result3 == True, "Test Case 3 Failed"

# Test Case 4: Level 5 tries to add a Level 3 spell but does not have enough gold (Should fail)
result4 = my_spellbook.validate_spell(spell=mock_spell4, char_instance=lvl_7_wizard)
assert result4 == False, "Test Case 4 Failed"

print("All test cases passed!")


# Run the tests
run_tests()