PDA

View Full Version : NPC generator



Erith
2012-08-12, 03:45 PM
I wrote a tool in python for my dm that will randomly generate npcs in bulk according to the tables in the dmg. It doesn't give a statblock, but it shows alignment, race, class, and attributes. The tables it uses to generate class and race can be extended with your own classes and races with minimal work, and it supports a variety to attribute rolling methods.

I have 2 questions about this.
1. Would there be any interest in me posting this?
2. Would there be any issues due to the fact that it copies the tables from the dmg?

Flickerdart
2012-08-12, 03:50 PM
I don't think the sample NPCs are covered under the OGL.

Erith
2012-08-12, 03:56 PM
Does that mean the sample statblocks starting on page 113 of the dmg? This program only uses the tables starting on 110.

Flickerdart
2012-08-12, 04:03 PM
If it's not in the SRD, I don't think you're allowed to reproduce it. I mean, a lot of sites do anyway, but that doesn't mean they are allowed to.

Erith
2012-08-12, 04:06 PM
Would this still be useful to anyone if it didn't include the tables, but i posted instructions to make your own?

Forgot to mention above that i'm working on making it support templates.

Mithril Leaf
2012-08-12, 05:34 PM
I'd certainly be interested. Maybe make up some new stuff to fill in the gaps or use pathfinder ones? I think those are OGL.

Erith
2012-08-12, 06:37 PM
This is the main file. Name it what you want and make the extention '.py'

Edit: changed the program to pull attribute rolling style from problib. This file should no longer be editted


import random
from problib import alignment, classes, races, rollstyle
import os

#================================================= ===========================
def getrange(dic):
return max(dic.keys()) + 1
#================================================= ===========================
def rolltable(dic):
result = ''
done = False

while not done:
roll = random.randrange(1, getrange(dic))

keys = sorted(dic.keys())
temp = ''
for i in keys:
if roll <= i:
temp = dic[i]
break

if temp[0] == '#':
temp = temp[1:]
else:
done = True


if result.find(temp) < 0:
if len(result) > 0:
result += ' '
result += temp

return result
#================================================= ===========================
def rollatt(num=4, die=6, keep=3, reroll=0):
#if no arguments are passed use values from problib
if num == 4 and die == 6 and keep == 3 and reroll == 0:
num = rollstyle[0]
die = rollstyle[1]
keep = rollstyle[2]
reroll = rollstyle[3]
roll = []
for i in range(1, num+1):
j = 0
while j <= reroll:
j = random.randrange(1, die + 1)
roll.append(j)

while len(roll) > keep:
roll.sort()
del roll[0]

return sum(roll)
#================================================= ===========================
def rollattributes():
attributes = []
for i in range(6):
attributes.append(rollatt()) #add arguments to rollatt() to change rolling style
return attributes #default is 4d6 keep 3 no rerolls
#================================================= ===========================
def rollchar():
_alignment = rolltable(alignment)
_class = rolltable(classes[_alignment])
_race = rolltable(races[_alignment][_class])
_attributes = rollattributes()

returnval = ''
returnval += 'Alignment: {0}\n'.format(_alignment)
returnval += 'Class: {0}\n'.format(_class)
returnval += 'Race: {0}\n'.format(_race)
returnval += 'Str: {0}\n'.format(_attributes[0])
returnval += 'Dex: {0}\n'.format(_attributes[1])
returnval += 'Con: {0}\n'.format(_attributes[2])
returnval += 'Int: {0}\n'.format(_attributes[3])
returnval += 'Wis: {0}\n'.format(_attributes[4])
returnval += 'Cha: {0}\n'.format(_attributes[5])

return returnval
#================================================= ===========================
def writefile(contents, filename):
f=file(filename + ".txt", 'a')

try:
assert isinstance(contents, list)
for char in contents:
f.write(char)
f.write('========================================= =\n')
except:
f.write(contents)
f.write('========================================= =\n')
f.close()
#================================================= ===========================
def main():
again = 'y'
while again == 'y' or again == 'Y':
isnum = False
while not isnum:
num = raw_input('Enter the number of characters to generate: ')
try:
int(num)
isnum = True
except:
isnum = False

fileyn = 'p'
while fileyn != 'y' and fileyn != 'Y' and fileyn != 'n' and fileyn != 'N':
fileyn = raw_input('Do you want to write the characters to a file? (y/n): ')

if fileyn == 'y' or fileyn == 'Y':
filename = ''
while len(filename) < 1:
filename = raw_input('Enter the file name: ')

if fileyn == 'y' or fileyn == 'Y':
printtoscreen = 'p'
while printtoscreen != 'y' and printtoscreen != 'Y' and printtoscreen != 'n' and printtoscreen != 'N':
printtoscreen = raw_input('Do you want to print the characters on the screen? (y/n): ')
else:
printtoscreen = 'y'

for i in range(int(num)):
char = rollchar()
if fileyn == 'y' or fileyn == 'Y':
writefile(char, filename)
if printtoscreen == 'y' or printtoscreen == 'Y':
print char, '================================================= ==\n'

again = raw_input('Run again? (y/n): ')
if again == 'y' or again == 'Y':
os.system('cls' if os.name == 'nt' else 'clear')


#================================================= ===========================
main()

Erith
2012-08-12, 06:38 PM
This one needs to be named 'problib.py'



#edit this to change how attributes are rolled
#the first value is the number of dice to roll
#the second value is the type of die to roll
#the third value is how many dice to keep
#the fourth value is the highest number to reroll
#default is 4d6 keep 3 no rerolls
#examples:
#[5,6,3,1] will roll 5d6 keep 3 reroll 1s
#[3,8,3,2] will roll 3d8 keep 3 reroll 1s and 2s
rollstyle = [4,6,3,0]

#probability works the same for all tables
#a random number is generated with a minimum of 1 and a max of the highest value in the table
#each number in the table is the max result that will give that entry
#the min result is the previus table entry + 1
#for example if you have this table:
# 10 : x1
# 20 : x2
# 50 : x3
#the result will be x1 on a roll of 1-10
#x2 has a range of 11-20
#x3 has a range of 21-50

#a note on table formatting:
#i used a lot of white space to make the tables easier to read, but this is not necessary
#separate the probability from the class or race names with a colon
#each entry needs to be separated with a comma, but there shouldnt be one after the last entry of the table
#all strings (anything thats not a number) needs to be enclosed in either double("") or single('') quotes

#alignment is the simplest table
#just put in the probabilities you you want for each alignment
#the law-neutral-chaos axis is not rolled
alignment = { 1 : 'Good',
2 : 'Neutral',
3 : 'Evil'
}

#don't touch the good, neutral, and evil lines, they are needed for the programto work
#you can list as many classes as you want with any probability range
#classes should be copied into each of the good, neutral, and evil sections unless they are restricted by alignment
#watch the spelling, the class names you enter here will be used to generate the race later
classes = {
'Good' : {
5 : 'Example 1',
10 : 'Example 2',
30 : 'Example 3'
},
'Neutral' : {
5 : 'Example 1',
10 : 'Example 2',
15 : 'Example 3',
25 : 'Example 4'
},
'Evil' : {
10 : 'Example 1',
15 : 'Example 2',
35 : 'Example 3'
}
}

#as with the classes table, don't touch the good, neutral or evil lines
#the class lines, here the various 'example x' lines, should correspond exactly to the classes from the classes table
#any race entry starting with '#' will be interpreted as a template
#if a template is rolled the program will keep rolling until it gets a race
#this means its possible to stack templates
#note that the program doesn't check the legality of adding a template
races = {
'Good' : {
'Example 1' : {
2 : 'Race 1',
32 : 'Race 2',
34 : 'Race 3'
},
'Example 2' : {
1 : 'Race 1',
6 : 'Race 2',
11 : '#Template 1'
},
'Example 3' : {
1 : 'Race 1',
2 : 'Race 2',
22 : '#Template 1',
24 : '#Template 2'
}
},
'Neutral' : {
'Example 1' : {
1 : 'Race 1',
2 : 'Race 2',
13 : 'Race 3',
14 : 'Race 4'
},
'Example 2' : {
1 : 'Race 1',
3 : 'Race 2',
5 : 'Race 3'
},
'Example 3' : {
15 : 'Race 1',
25 : 'Race 2',
26 : 'Race 3',
27 : '#Template 1'
},
'Example 4' : {
1 : 'Race 1',
6 : 'Race 2',
11 : 'Race 4'
}
},
'Evil' : {
'Example 1' : {
1 : 'Race 2',
3 : 'Race 3',
4 : 'Race 4',
5 : '#Template 2'
},
'Example 2' : {
1 : 'Race 1',
2 : 'Race 2',
17 : 'Race 4'
},
'Example 3' : {
2 : 'Race 2',
3 : 'Race 4',
4 : '#Template 2'
}
}
}

#other notes:
#if a race table only has templates and no races the program will hang because it will keep rolling until it finds a race
#strictly speaking the alignment table can be extended, for example adding vile and exalted alignments
#this isn't recommended because you would then be required to add the new alignments to the classes and races tables or else the program could crash

Erith
2012-08-12, 06:40 PM
You'll need python 2.7 to run them. Let me know if there are any problems.

Sorry for the triple post.

Mithril Leaf
2012-08-12, 06:56 PM
Other than a lack of cls being an avaliable sh command, runs fine. Very cool.

Erith
2012-08-12, 07:00 PM
Sorry about that, I'm running windows and it works fine for me. Any suggestions to fix that?

Mithril Leaf
2012-08-12, 07:05 PM
You clear the screen at the end with system("cls"). It's not needed, if you delete it works fine with bash. You can also remove the importing of system at the beginning.

Erith
2012-08-12, 07:25 PM
Personally I don't like the idea of having old results being on the screen if you rerun it, and I can't figure out how to do it without system. This should at least make it a bit more platform independent, but it still won't clear the screen in idle. I also cleaned up the imports and some commented out lines.




#the only thing that should really be changed is how rollatt() is called in line 67 in the rollattributes() function
#as it is, attributes are rolled as 4d6 keep 3 best no rerolls
#this can be changed by supplying arguments to rollatt()
#You can changed the number of dice rolled, the type of dice rolled, how many to keep, and whether to reroll anything
#the num argument is the number of dice to roll
#the die argument is the type of die
#the keep argument is how many to keep
#the reroll argument is the highest result to be rerolled
#
#rollatt(num=3) will roll attributes as 3d6
#rollatt(num=5, keep=4) will roll 5d6 and keep the 4 highest
#rollatt(die=8) will roll 4d8 keep 3
#rollatt(reroll=2) will roll 4d6 keep 3 reroll 1s and 2s

import random
from problib import alignment, classes, races
import os

#================================================= ===========================
def getrange(dic):
return max(dic.keys()) + 1
#================================================= ===========================
def rolltable(dic):
result = ''
done = False

while not done:
roll = random.randrange(1, getrange(dic))

keys = sorted(dic.keys())
temp = ''
for i in keys:
if roll <= i:
temp = dic[i]
break

if temp[0] == '#':
temp = temp[1:]
else:
done = True


if result.find(temp) < 0:
if len(result) > 0:
result += ' '
result += temp

return result
#================================================= ===========================
def rollatt(num=4, die=6, keep=3, reroll=0):
roll = []
for i in range(1, num+1):
j = 0
while j <= reroll:
j = random.randrange(1, die + 1)
roll.append(j)

while len(roll) > keep:
roll.sort()
del roll[0]

return sum(roll)
#================================================= ===========================
def rollattributes():
attributes = []
for i in range(6):
attributes.append(rollatt()) #add arguments to rollatt() to change rolling style
return attributes #default is 4d6 keep 3 no rerolls
#================================================= ===========================
def rollchar():
_alignment = rolltable(alignment)
_class = rolltable(classes[_alignment])
_race = rolltable(races[_alignment][_class])
_attributes = rollattributes()

returnval = ''
returnval += 'Alignment: {0}\n'.format(_alignment)
returnval += 'Class: {0}\n'.format(_class)
returnval += 'Race: {0}\n'.format(_race)
returnval += 'Str: {0}\n'.format(_attributes[0])
returnval += 'Dex: {0}\n'.format(_attributes[1])
returnval += 'Con: {0}\n'.format(_attributes[2])
returnval += 'Int: {0}\n'.format(_attributes[3])
returnval += 'Wis: {0}\n'.format(_attributes[4])
returnval += 'Cha: {0}\n'.format(_attributes[5])

return returnval
#================================================= ===========================
def writefile(contents, filename):
f=file(filename + ".txt", 'a')

try:
assert isinstance(contents, list)
for char in contents:
f.write(char)
f.write('========================================= =\n')
except:
f.write(contents)
f.write('========================================= =\n')
f.close()
#================================================= ===========================
def main():
again = 'y'
while again == 'y' or again == 'Y':
isnum = False
while not isnum:
num = raw_input('Enter the number of characters to generate: ')
try:
int(num)
isnum = True
except:
isnum = False

fileyn = 'p'
while fileyn != 'y' and fileyn != 'Y' and fileyn != 'n' and fileyn != 'N':
fileyn = raw_input('Do you want to write the characters to a file? (y/n): ')

if fileyn == 'y' or fileyn == 'Y':
filename = ''
while len(filename) < 1:
filename = raw_input('Enter the file name: ')

if fileyn == 'y' or fileyn == 'Y':
printtoscreen = 'p'
while printtoscreen != 'y' and printtoscreen != 'Y' and printtoscreen != 'n' and printtoscreen != 'N':
printtoscreen = raw_input('Do you want to print the characters on the screen? (y/n): ')
else:
printtoscreen = 'y'

for i in range(int(num)):
char = rollchar()
if fileyn == 'y' or fileyn == 'Y':
writefile(char, filename)
if printtoscreen == 'y' or printtoscreen == 'Y':
print char, '================================================= ==\n'

again = raw_input('Run again? (y/n): ')
if again == 'y' or again == 'Y':
os.system('cls' if os.name == 'nt' else 'clear')


#================================================= ===========================
main()

Mithril Leaf
2012-08-12, 07:30 PM
Yep, works fine now. :smallsmile:

Erith
2012-08-12, 10:53 PM
I don't suppose the position on posting the tables would be any different if they were saved in unreadable binary? It could only be read by the program that way. Just thought I'd throw that out there.