The goal

I continue to add stuff based on my Giant encounter generator script, this time, by writing a treasure generator. I don’t use the tables in the DM guide, and the generator is not a fancy (or ugly, depends on point of view) for those tables. In fact, you can use it without even looking at one. This generator uses a pool of treasure placed in a text file ( loot.txt ), to distribute the entire loot between the monster pool. I want such generator for three reasons; (a) Increasing randomness of monsters, I don’t want the standard issue 10 GP and 1 healing potion per group. (b) Speeding up the game play when it comes to generating monster loot. (c) Allowing me, as a GM, to set a number of healing potion, gold coins, magic swords, and whatever I wish to give the party in their current surrounding, and the script will distribute the treasure between the monsters. By surrounding, I mean that the entire moon knight army camp, the whole goblin cave, or, as in my case, every minion of the fire giant castle.

The logic

My loot.txt file looks somewhat similar to my ‘pool.txt’ from the original script, and it follows the same logic

healing potion 2d8+3, 10, ,dragon GP, 500, ,dragon head band of minor illusion, 1, , SP, 500, hill;dragon,

Each line has four parameters, the type, the number of items, the “only” monsters types, and the “exclude” monster types, that appear in a fixed order. The type is the name of this type of treasure items, healing potion , GP , SP , and head band of minor illusion 1, in the example file. This entry is essentially a free text, but it should be short though, as longer text will clutter the output. The second one is the number of items of this kind to be placed, in total, in the entire surrounding. The “only” entry indicates the only monster type(s) that will carry this loot, and the “exclude” the only monster type(s) that will never carry this loot. The last two entries are subdivided by semicolon ( ; ) and may be left empty, they also may contradict each other, so I figured a priority order for them.

If both are empty, the item may appear on any opponent the party will encounter.

If the only field has entry in it, it will be used without the exclude field .

field has entry in it, it will be used without the . The exclude field will be used only if the only field is empty.

Applying this rules to the loot.txt we can establish that there are 10 healing potions and 500 GP to be distributed between all the creatures but the dragons, one head band of minor illusion to be given to a single monster of any type, and 500 SP that will be placed either on hill giants or the dragons. I don’t have a mechanism, similar to the weight in the encounter generator itself, that will gravitate a type of treasure to a certain monster type. This can be done, but this will complicate the code, and the main effect can be mimicked easily enough by manipulating the loot.txt file. For example, if I want the gold coin to be more abundant with the hill giants, I would replace the `GP,500, ,dragon’ line with

GP,480,hill, GP,20,stone,

This will distribute more gold coin between the hill giants.

In game term, we can justify the distribution of treasure between the monsters by saying that the giants and dragons are fighting amongst themselves over each other’s treasure. The monsters that are excluded from a certain treasure type in the lot aren’t interested in it. Again, as with the encounter generator itself, I don’t update the loot.txt file automatically, as the loot doesn’t necessarily make its way into the party hands, or destroyed, by the end of the battle. Finally, I note that I don’t place here information about any specific encounters loot options. I can either create a specific entry for them, or, as I prefer to tailor this encounter more carefully.

The math

I wish to distribute, for example, 500 GP between 50 hill giants and 8 stone giants . As I noted above, I don’t prefer the gold coins to be placed on any giant type, this means that a single coin has the same probability, to be held by each of the giants

.

This is a uniform distribution which properties are well known (luckily for me). I can generate the treasure by generating a random number between 0 and one for each coin and if it is lower than give it to the monster. However, Wikipedia is already open, I might as well use it to fancy my code a little. The problem at hand is that given coins and monsters, what is the probability of having exactly coins at the possession of monster number 1 (the monster I’ve just picked). This problem is handled by the binomial distribution which I’m not going to expand about, but will rather only explain the parts that are essential to understand the script. Each coin, has a probability to be in this monster wallet (or claw if it is a dragon), and to be at the possession of some other monster (we don’t care which one). Since the coin cannot not exist, then . The probability for exactly coins to be held by my monster, is given by (odds are I got the letters wrong)



where



is called the binomial coefficient and



is called factorial.

The role of the binomial coefficient is to count how many ways can I place $n$ coins in such a way that exactly coins will find themselves at the hand of the specific monster. The multiplication by the probabilities account for the placement of exactly coins. Agian, read the Wikipedia page, or this guy blog post for detailed explanation about the binomial distribution to further understand this. The figure below show the normalized probability (solid line), cumulative probability (dot dash line), for the four treasures in the loot.txt file. The probability is normalized by the max probability for ease of viewing and is plotted properly in the inset. The mean is represented by the dotted line and is calculated using

.

Note that the probability of a giant to have the headband is quite low, and the average number of headbands a giant has is far less than 1. This is so because we have only 1 head band to distribute between all the creatures. Also note that there is a probability for the giants to have no coins which is not 0 (though it is very close to it). The probability for hill giants to have no healing potion is much higher and it is set at 1 in the plot due to my normalization.

The code

Luckily for me, python has a library that calculates that probability. The function reads three parameters which in our code boil down to

* The number of coins, or a list of number of coins, to be placed on the monster.

* The total number of coins

* The probability

The list returned hold probabilities corresponding to the number of coins at the appropriate place in the input list. In our case, since we’ll give the function the number of coins list in the form [0,1,2,3,...n] , the returned list will be actually the probability of having exactly the index of the list number of coins on the monster.

The code itself I’ve placed at the end of this post, and on my hard drive it is positioned in its own file (called Make_treasure.py ), I call it using

from Make_treasure import *

Then, in the main script file, I generate the treasure probabilities by the line

tres_pool = make_treasure(P)

where P is the same one as in the encounter generator itself. At the loop that generates the encounter, I’ve added

ctres = roll_treasure(tres_pool,monster)

with moster being the type of the monster, required for the “only” and “exclude” fields, to generate the treasure. I also need to update the probabilities after each monster I’ve picked, I do this by adding

#update the treasure pool for t in ctres: tres_pool[t].potential_removed(monster) tres_pool[t].items_removed(ctres[t])

right after I update the number of monsters. I call potential_removed to update the uniform probability ( ) and items_removed to update the loot inventory. This update doesn’t go into the loot.txt file.

Here is my code, again with as much comments I could think of.

from random import random from random import randint as rint from scipy.stats import binom import matplotlib.pyplot as plt class Treausre: #A class that store information and function related to treasure. #Created using the parameters #potentials: the creature type that may carry this treasure #number: the number of items of this treasure, and integer #inv_prov: the inver of the probability of this item appearance #for now, it is simply the total number of creautre that may #carry this item (not creature type, creatures) def __init__(self,potentials,number,inv_prob): self.potentials = potentials[:] self.number = number self.inv_prob = inv_prob self.prob = 1.0/self.inv_prob def potential_removed(self,pname): # a creature was taken out of the pool, if this creaure # is a potential carrier of this treasure type, update the prob if pname in self.potentials: self.inv_prob -= 1 self.prob = 1.0/self.inv_prob return def items_removed(self,n): #simply update the number of items of this treasure type self.number -= n return def make_treasure(pool): #This is a function to generate the probability of a certain treasure to be #each of the monsters. #monster must be a part of the pool dictionoary pool #Pool is of the form # #tressure name, number of items, targets with it, targets without it #Tressure name - the name, e.g. regeneration ring, magic scroll, etc. #number of items - an integer. #targets with it - if not empty means that only this kind of creatures will #have it. Doesn't mean that every individual of these creatures will have # separated with ';' #targets with it - if not empty, and targets with it is empty, the only #creatures that won't have it. Separated by ';' tres_file = open ('loot.txt').readlines()[:] tres = {} for line in tres_file: #going every entry in the file #Note there is no check that the file is valid key,n,only,exclude = line.strip().split(',') #getting the values, 'here' potentials = [] npot = 0 only = only.strip().split(';')#Splitting the cretures by names exclude = exclude.strip().split(';') n = int(n) has_excl = not exclude == ['']#a flag to note there are creatures to exlude has_only = not only == ['']# a flag to note the item belongs to some types put_all = not has_only and not has_excl # a flag saying all creature has this for ctype in pool: #Going throue all the creatures in the encoutner pool nc = pool[ctype][0] if put_all: #add the loot option to this monster potentials.append(ctype) npot += nc elif has_excl and ctype in exclude: continue #skip this creature elif has_only and not ctype in only: continue #skip this crearure potentials.append(ctype) npot += nc tres[key] = Treausre(potentials,n,npot)#Create new treasure return tres def roll_treasure(tres_pool,cname): #A function to generate treasure usning a treasure pool (tres_pool) #uses the Treasure class above #cname is the creature type/name, it must be in the tres_pool. out = {} for t in tres_pool: if not cname in tres_pool[t].potentials: # the creature is not listed for this treasure continue #Generate probabilities tables nt = tres_pool[t].number p_loot = tres_pool[t].prob pmf = binom.pmf(range(nt+1),nt,p_loot) #the number at each place of the pmf is the probability of giving that #number of items of this treasure type p = random() n_to_give = 0 while p > pmf[n_to_give]: p -= pmf[n_to_give] n_to_give += 1 out[t] = n_to_give return out def plot_treasure(tres_pool,cname): #this function plots the treasure porbability under specified conditoin #It follows the roll_treasure path, but is placed separately so it will #be both more readable, and the script will run faster fig = plt.figure() ax = fig.add_axes([0.05,0.22,0.9,0.72]) subax = fig.add_axes([0.6,0.3,0.25,0.25]) colors = ('red','green','blue','black') ci = 0 for t in tres_pool: if not cname in tres_pool[t].potentials: continue nt = tres_pool[t].number p_loot = tres_pool[t].prob pmf = binom.pmf(range(nt+1),nt,p_loot) x = range(len(pmf)) PMF = [pmf[0]] for i in range (1,len(pmf)): PMF.append(PMF[i-1]+pmf[i])#cummulative dist function mean = sum([i*pmf[i] for i in range (len(pmf))]) subax.plot(x,pmf,color=colors[ci],label=t) subax.plot((mean,mean),(0,1),ls=':',color=colors[ci]) pmf /= max(pmf) #Normalize the pmf, for easy reading ax.plot(x,pmf,color=colors[ci],label=t) ax.plot(x,PMF,color=colors[ci],ls='-.') ax.plot((mean,mean),(0,1),ls=':',color=colors[ci]) ci += 1 ax.set_xlim(0,4*mean) subax.set_xlim(0,4*mean) ax.set_ylim(0,1.01) ax.legend(loc=1) if not cname.lower() == 'dragon': fig.text(0.1,0.05,r''' Normalized by the max probability (solid line) and cumulative probability (dot dash line) as a function of number of items of treasure type. The mean is marked with a dotted line. Color coding specified by the legend. Inset: non narmalized probability''') else: fig.text(0.1,0.05,r''' Probability (solid line) and cummulative probability (dot,dash line) as a function of number of items of treasure type. The mean is marked with a dotted line''') fig.savefig('/tmp/play{cname}.png'.format(cname=cname))