I've made a python script to properly organise D&D monster stats in a directory. The script brings up the stats as instance variables of a Monster (class) instance.
For those unfamiliar with the game, keeping track of monsters (to me at least) is cumbersome and prone to many dead trees. You have to flip through the Monster Manual to find the monster, write down any change done to it (like health) on a piece of paper, keep track of the initiative order, different encounters...
There is a lot to do, so I decided to use technology to my advantage. I have folders set up under the Monsters folder. In each folder is a subfolder (the same types across all of them). In each subfolder is a .txt document with the stats (as dictated by the book) for each monster.
- Main Types = Animate, Beast, Humanoid, Magical Beast
- Sub Types = Abberant Elemental Fey Immortal Natural Shadow
Each text document is set up like the following example:
Example Monster # This is up here, and skipped, so I keep track of each one easily without having to remember to look at the name of the file.
HP = 50
Initiative = 5
Senses = [5, 'Darkvision'] # In auto, this is Perception and Vision combined
Defence = {"AC" : 5, "Fortitude" : 4, "Reflex" : 6, "Will" : 7}
Saving_Throw = 2
Speed = 6
AP = 1 # Action Points
Attacks = {'An attack that hurts the opponent and does permanent damage to its armour' : 'Example.alterstat({"HP" : -5, "Defence[0] : -3})'
Here is my entire Monster class, plus some unbound functions. What I want to know is if any of the functions can be improved (especially the __init__
, addmonster
, and LoadAll
)
import os
types = 'Animate Beast Humanoid Magical_Beast'.split()
sub_types = 'Abberant Elemental Fey Immortal Natural Shadow'.split()
LoadedMonsters = []
class Monster:
def __init__(self, monster, LoadedMonsters = LoadedMonsters):
'''Checks if monsters is defined. If it is defined, sets class attributes to be the monster's as decided in Monsters.txt. If it is already loaded in LoadedMonsters, makes a copy'''
self.name = monster.capitalize()
self.isdead = False
monstercheck = monsterfind(self.name)
if monstercheck != False:
with open(monstercheck, 'r') as monstercheck:
print monstercheck.next()
for stat in monstercheck:
print stat[:-1] # Show what stats are loaded
var, value = stat.split('=')
var, value = val.strip(), value.strip()
try:
value = eval(value) # Fixes non-string values
except:
pass
setattr(self, var, value)
self.Bloodied = self.HP / 2
AllMonsters = open('Monsters.txt', 'w+') # List (.txt) of all monsters
AllMonsters.read() # Get to end of text
AllMonsters.write(self.name)
AllMonsters.close()
monstercheck.close()
LoadedMonsters += [self.name] # A list of all loaded monsters
print 'Monster loaded'
else: # If can't find the monster in the folders
print '{} Not found, add it?'.format(self.name)
if raw_input('Y/n\n').capitalize() == 'Y':
self.addmonster()
self = Monster(self)
else:
self.name = 'UNKNOWN'
def setMonsterLevel(self, monster, level):
"Changes the given monster's level to the given value."
if monster not in LoadedMonsters:
print 'Monster not loaded'
raise NotImplementedError # I'll get to it, it's harder then it seems
def attack(self, monsters, changes, buff = None):
'''Takes a list of targets [Variable names!],a dictionary of stats and values w/ the stats as the key, and applies it to the list of targets. Optional parametre, buff, can be applied at the end as a dictionary'''
for monster in monsters:
for change in changes:
monster.alterstat( {change : changes.get(change)} )
if buff is not None:
self.alterstat(buff.keys()[0], buff.get(buff.keys()[0]))# Messy way of saying get key and value of dictionary
def monsterfind(name): # For use in __init__
'''name = monster Return folder path if monster exists, False otherwise'''
monster = name + '.txt'
for folder in types:
for subfolder in sub_types:
path = os.path.abspath('Monsters' + '\\{}\\{}\\{}'.format(
folder, subfolder, monster))
if os.path.exists(path):
return path
return False
def addmonster(name): # Bit wordy
'''Name = string representation of a monster name makes monster name'''
print 'Checking if available'
if os.path.exists(name): # Can't have names across sub folders
print '{} is taken!'.format(name)
name = raw_input('Choose a different name: ')
addmonster() # Start again
print 'Name available!'
while True:
option = raw_input(
'Choose the main folder:{}\n'.format(types)).capitalize()
if option in types:
while True:
sub_option = raw_input(
'Choose a sub-folder {}\n'.format(sub_types)).capitalize()
if sub_option in sub_types:
newmonster = open(
os.path.abspath(
'Monsters') + '\\{}\\{}\\{}.txt'.format(
option, sub_option, name), 'w')
break
break
if raw_input('Automatic data entry, or Manual? \n').capitalize() == ('Manual'):
print '''Enter your information. (Press enter for every line, press enter on a blank line to finish)'''
newmonster.write(name + ':\n')
while True:
newline = raw_input('>') + '\n' # Starts on newline, gives last
if newline == '\n': # a new line
break
newmonster.write(newline)
else:
while True:
Information = []
print '''Follow the steps below. If you do not want to add a value then leave it blank. This will NOT be saved until you confirm it at the end'''
print "Enter a number for the monster's health"
HP = 'HP = ' + raw_input('HP = ')
Information.append(HP)
print "Enter a # bonus (or penalty) for the monster's Initiative"
print "0 if no bonus/penalty"
Initiative = 'Initiative = ' + raw_input('Initiative = ')
Information.append(Initiative)
print "Enter a # bonus (or penalty) for the monster's perception"
Senses = list(raw_input('Perception = '))
print "Enter the vision type of the monster"
print "I.E: Darkvision, Normal, Low-Light, etc."
Senses.append(raw_input('Vision = '))
Senses = 'Sense =' + str(Senses)
Information.append(Senses)
print 'Enter the following defence stats as numbers:'
Defence = 'Defence = ' + str({
"AC" : input('AC = '),
"Fortitude": input('Fortitude = '),
"Reflex" : input('Reflex = '),
"Will" : input('Will = ')
})
Information.append(Defence)
print '''Enter a # bonus (or penalty) for the monster's Saving Throw'''
Saving_Throw = 'Saving Throw = ' + raw_input('Saving Throw = ')
Information.append(Saving_Throw)
print "Enter the value (not bonus) for speed. It is usually 6"
Speed = 'Speed = ' + raw_input('Speed = ')
Information.append(Speed)
print "Enter how many AP's (Action Points) the monster has"
AP = 'AP = ' + raw_input('AP = ')
Information.append(AP)
print 'Monster Stats:'
print
# Attacks will be added in GUI (if I do that): Too complicated for user here
for stat in Information:
print stat
print 'Verify that this is correct'
while True:
print 'Y/N\n'
answer = raw_input('>').capitalize()
if answer == 'Y':
newmonster.write(name + '\n')
AllMonsters = open('All Monsters.txt', 'w+')
AllMonsters.read()
AllMonsters.write(name + '\n')
AllMonsters.close()
for stat in Information:
newmonster.write(str(stat) + '\n')
newmonster.close()
return
elif answer == 'N':
'Press enter to start over'
newmonster.close()
raw_input()
continue
def LoadAll(): # This should probably do the last step too...
'''Opens Monsters.txt, a list of all monster names, and returns a list of instances of Monster to be loaded by iterating exec() it'''
AllMonsters = open(os.path.abspath('Monsters\\All Monsters.txt'), 'r')
for monster in AllMonsters:
yield '{0} = Monster("{0}")'.format(monster[:-1]) # -1 cause \n function
# Returns generator to load all monsters as instances of Monster class
If there are any mistakes let me know, as I had to rewrite the whole thing in spaces during a class period (curse bad programming habits!)