patternpythonModerate
D&D Monster Organiser
Viewed 0 times
monsterorganiserstackoverflow
Problem
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.
Each text document is set up like the following example:
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
```
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 Monste
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 Monste
Solution
- Comments on your code
-
What you are doing here is creating and querying a persistent database, so it's a good idea to use an existing relational database rather than trying to implement your own in an ad-hoc fashion.
In particular, with a relational database you can issue queries ("select a chaotic evil monster with challenge rating 2"). Queries are going to be a pain to write if you pursue your current path of having lots of small text files containing assignments.
Another advantage of using a relational database is that you get to learn how to use and program relational databases. Time spent on this won't be wasted.
See section 2 below for an example of how you might go about this, using Python's built-in
sqlite3 module.-
Your constructor function (
Monster.__init__) does two different jobs: if the monster already exists in your database, it loads it, but if it doesn't exist, it prompts the user interactively to create it. This kind of dual-use method is usually a bad idea: it's better to separate the two uses into two methods (this makes the code easier to read, and is more flexible since you can pick which method you actually want).There could be several reasons why a monster can't be found (e.g. the program is being run in the wrong directory) and you don't always want to be prompted to create it.
-
Using
eval(value) is a bad idea because value is untrusted. It could contain code that you'd really rather not run. See "Eval really is dangerous" by Ned Batchelder.-
You write
self.addmonster() in one place. This should be addmonster(name) (or else addmonster should be made a method of the Monster class).-
The docstring for
Monster.__init__ says "If it is already loaded in LoadedMonsters, makes a copy", but this isn't true.-
The pathname code is Windows-specific and might not work on other operating systems. Python provides the
os.path module for manipulating pathnames in a platform-independent way.-
There's no input validation.
-
You use
setattr to update the attributes of the Monster object. This means that the attributes share the same namespace as the methods. So for example if the monster database contains an attack statistic, then this will overwrite the monster's attack method. It's probably best to keep the statistics in their own namespace, except for a few statistics that you refer to frequently.- Revised code
Here's some code that shows how you might start storing your data in table in a SQLite relational database, and how you might go about validating user input.
import collections
import itertools
import random
import re
import readline
import sqlite3
# Input validation functions.
def validate_integer(s):
return int(s)
def validate_positive(s):
s = int(s)
if s ".format(stat, value)
else:
prompt = "{0.desc} ({0.advice}) > ".format(stat)
while True:
try:
v = raw_input(prompt)
if not v and value:
return value
return stat.validator(v)
except ValueError as e:
print(e)
def update_interactively(self):
"""
Update a monster interactively (creating it if doesn't exist).
"""
action = 'create'
assert STATS[0].stat == 'name'
name = self.input(STATS[0])
if self.exists(name):
action = 'update'
c = self.execute('SELECT * FROM monster WHERE name = ?', (name,))
stats = dict(c.fetchone())
else:
stats = dict(name = name)
for stat in STATS[1:]:
stats[stat.stat] = self.input(stat, stats.get(stat.stat))
desc_width = max(len(stat.desc) for stat in STATS)
value_width = max(len(str(stats[stat.stat])) for stat in STATS)
print('\n{:{}} {}'.format('STAT', desc_width, 'VALUE'))
print('{:- ".format(action))[0].upper() != 'Y':
print('Abandoned.')
else:
self.update(**stats)
class Monster(object):
def __init__(self, **kwargs):
self.stat = kwargs
self.name = kwargs['name']
self.hp = roll_dice(kwargs['hitdice'])
def __repr__(self):
return ''.format(self.name, self.hp)
- Example interaction
>>> db = MonsterDB()
>>> db.update_interactively()
name (a string) > iron golem
hit dice (like "2d6+1") > 18d10+30
armour class (an integer) > 30
challenge rating (a positive integer) > 13
alignment (e.g. "chaotic evil") > neutral
STAT VALUE
---------------- ----------
name iron golem
hit dice 18d10+30
armour class 30
challenge rating 13
alignment neutral
OK to create? > yes
>>> db.load('iron golem')
>>> _.stat
{'hitdice': u'18d10+30', 'challenge': 13, 'armour': 30, 'alignment': u'neutral', 'name': u'iron golem'}
And in SQL you can issue queries:
`sqlite> select * from monster where challenge = 1;
name hitdice armour
Context
StackExchange Code Review Q#18717, answer score: 10
Revisions (0)
No revisions yet.