This is not the first script I’ve written for my RPG needs, but the first one I wish to “publish”. The goal of this script is to manage a pool of monsters which the characters will encounter on random. Granted, this is fully logical and after several giants are gone missing, the castle members will be more alert. But this logical condition for small groups still may be found, and the script should be easy enough to tweak for harder encounters. The goal of my scripts in general is to avoid the dice rolling and and page flipping on session while trying to build a tense atmosphere. This one isn’t suppose to contribute anything during the encounter, only help it start, and provide some randomness (and fun). Note that the generated random numbers are not really random, but rather pseudo random. This is good enough for our needs, but one should keep that in mind.

The setup is that the group (two 4th level and three 5th level) is about to reach a castle controlled by a red giant. I figured that there would be some patrols. Initially, I’ve figured there will be some patrols of two hills giants. The thing is that I searched for a scenario where giants live in such conditions and I realize that they are probably enslaving some humanoids, so they must be roaming around tormenting their slave. I’ve also enjoyed the prospect of white dragon wyrmlings serving as guard dog. With that in mind I have created a wondering monster hierarchy and pool

8 stone giants that I consider as commanders with weight of 25

12 white dragon wymlings that I consider as guard dogs with weight of 50

50 hill giants that I consider as guards with weight of 90

The weight mentioned are not the actual weight, but a concept imported from GUI design and I use here in to calculate the probability. My goal is to get a random number between 0 and 1 and map it to a monster type. In math term I require that

In statistics there is a concept of partition function which I’m not going to bore you with. The important part, is that I do something similar, First I set A value which is equal to the number of monster of each kind multiplied by their weight

And I use this to generate the probabilities

Finally, I use a function of a sigmoid type which generates data that looks similar to cumulative distribution function, Again not going to bore you with math here, but I’ll need to use that sigmoid to decide if I should accept the encounter as generated, or turn it harder by adding a monster, which is, again, generated randomly. The sigmoid I’ve used is

where and are the constant I’ve determined by trial an error. You can do something more precise, but I find it that it doesn’t worth the effort. To decide if I should accept an encounter, I calculate the cumulative probability of a certain XP value, and roll a number between 0 and 1. If the number is higher than the cumulative probability the script adds another monster. Otherwise, the script stops.

This is the code I use, with as much comments I could think of.

#This program released for everybody to use and tweak. If you manage to break #your computer with it, that's your problem, and you'll have to handle it. #Essentially, this is GPL licence #The program was run and tested for total of about 10(!) times on my machine, #With opensuse as an OS. It should work on every Linux os, and break on Windows #Machines. Mac machine, should probably work too. You can remove the plotting #part for it to work on your machine. #You need numpy for the program, to run, and pyplot for the plot, but you can #throw this part away from random import random from random import randint as rint import numpy as np import matplotlib.pyplot as plt class Amonster: # A small class to print the important stuff about the monster. A proper way # to do this is to throw all the data in a text file and find the # appropriate line at the file. I guess that eventually I'll do that def __init__(self,kind): self.kind = kind print '\033[1m' print kind + '\033[0m' # Print the name of the monster in bold face print 'page: '+self.page() print 'HP: ' + self.hp_roller() print 'Init: ' + self.init_roller() def page (self): #The page in the monster manual if self.kind == 'hill': return '155' elif self.kind == 'stone': return '156' elif self.kind == 'dragon': return '102' def init_roller(self): #initiative roller. The bonus is base on the MM if self.kind == 'hill': bonus = -1 elif self.kind == 'stone': bonus = 2 elif self.kind == 'dragon': bonus = 0 return str(rint (1,20) + bonus) def hp_roller(self): #Rolling hit points # out - the number to return starts with the HP bonus the monster has # d the die type # nd the number of dies to throw if self.kind == 'hill': out =40 d = 12 nd = 10 elif self.kind == 'stone': out = 60 d = 12 nd = 12 elif self.kind == 'dragon': out = 10 d = 8 nd = 5 i = 0 while i &amp;lt; nd: out += rint (1,d) i += 1 return str(out) #End of Amonster class def count(name): #Unlike the monster class above, here I do have everything in a file called #pool.txt. The first number is number of individuals remained in the pool #file. The DM responsibility is to update the pool whenever a monster dies f = open('pool.txt').readlines()[:] for line in f: words = line.split('=') if name == words[0].lower().strip(): words = words[1].split() # the first number is the number of monsters in the pool # the second of the number is the &amp;quot;weight of the monster&amp;quot; # the higher that number is, the higher the probability of this # monster to be picked by the generator return [int(words[0]),int(words[1])] #end of count class hill = count('hill') dragon = count('dragon') stone = count('stone') #The XP and CR of each monster in the pool xpcr = {'hill':[1800,5],'stone':[2900,6],'dragon':[450,2]} # the number and weigt of each number in the pool, a proper way to do this is in # the counter function above. Probebly next time P = {'hill':hill,'stone':stone,'dragon':dragon} #XP difficulty values, based on the PCs level xptable ={ 'easy':1000, 'medium':2000, 'hard':3000, 'deadly':4300} #The probability to pick a certain monster def RP_Calc(Pin): out = {} pcom = 0 #the total value of the number and the weights. Serves as a poor man partition #function tp = sum([Pin[p][0]*Pin[p][1] for p in Pin]) if tp == 0: print &amp;quot;All the monsters are dead now

go home&amp;quot; exit() #Stop the script for name in P: #This is the actualy probabilities calculation pname = Pin[name][0]*Pin[name][1]*1.0/tp if pname == 0: continue pcom += pname out[pcom] = name return out #End of RP calculation RP = RP_Calc(P) xpval = 0 def fprob (x): #This is the cumulative probability function. #The higher the XP (X) you put it, the height the value it produce. #It is far from a proper probability function, but it is good enough for our #needs. The 0.7 and 0.2 are empirical values, and you can change them X = x-xptable['hard']*0.7 X /= xptable['hard']*0.2 return 1.0/(1.0 + np.exp(-X)) #Here I plot the probability function X= np.linspace(0,xptable['deadly'],1000) dx = X[2] - X[1] Y = fprob(X) dY = [Y[i+1]/dx - Y[i]/dx for i in range (len(Y)-1)] dY.append(0) dY = np.array(dY)/max(dY) fig = plt.figure() ax = fig.add_subplot(111) ax.plot(X,fprob(X),label='P (cumulative)') ax.plot(X,dY,label='p') #marking XP levels for hard in xptable: x = xptable[hard] y = fprob(x) ax.annotate(hard,xy=(x,y))#the name ax.scatter(x,y,color='black')#the value #tidy up ax.plot((min(X),max(X)),(0.5,0.5),ls='-.',color='black') ax.set_xlim(0,xptable['deadly']) ax.set_ylim(0,1) ax.set_xlabel('Total encounter XP Value') ax.set_ylabel('Probability of accepting the encoutner') ax.legend(loc=2) fig.savefig('/tmp/play.png')# store the plot n_monsters = 0 while True: #Generate the encoutner p = random() # Create a random number between 0 and 1 for pm in sorted(RP.keys()): # go through the monsters and pick the appropriate one if p &amp;lt; pm:#If the number is less than the monster probability, take it monster = RP[pm] break if P[monster][0] == 0: #Exhusted the monsters of this kind from the pool. Restart the loop continue xpval += xpcr[monster][0]#The xp value, without the multiplayer # determine the multipplier. See the DM basic rules, page 57 n_monsters += 1 if n_monsters == 1: multi = 1 elif n_monsters == 2: multi = 1.5 elif n_monsters &amp;lt;= 6: multi = 2 elif n_monsters &amp;lt;= 10: multi = 2.5 elif n_monsters &amp;lt;= 14: multi = 3 else: multi = 4 P[monster][0] -= 1 #remove one monster from the pool. #Note that this doesn't change the pool.txt file p = random()# Generate new random number RP = RP_Calc(P)# We have removed a monster from the pool, new probabilities! Amonster(monster)# The class above. Essentially, a nice wrapper print data if p &amp;lt; fprob(xpval*multi): #Exit the loop print 'Encounter XP: ' + str(xpval) print 'Diffculty XP: ' + str(xpval*multi) break

and the pull.txt file

1 Hill = 50 90 2 Dragon = 12 50 3 Stone = 8 25

Finally the bash output

dragon page: 102 HP: 34 Init: 16 hill page: 155 HP: 102 Init: 2 Encounter XP: 2250 Diffculty XP: 3375.0

And this is the cumulative probability I generated (the dash line is at one half)