I apologize for the wall of text (130 lines) that here-follows. This is a very information-packed post.
I am going to present my approach to monster generation/balancing for my current roguelike. I am curious to know how others have approached this same subject, and would love to hear suggestions others have.
my latest project is a javascript roguelike. I have been working on it for about a month now.
I had a couple of very interesting ideas to help me implement the denizens of the fateful dungeons that you would encounter in my game.
I decided I would like to use mathematical progressions like cosine and Fibonacci to create the power curves of monsters on each level.
I am using cosine as a base to calculate how many monster types and if any bosses will be on each level.
I am using Fibonacci as the basis for the power curve.
Monster Strength/Power CurveI arbitrarily decided that I would have 20 levels in my game.
the cosine function (with some interference) generates a decent curve which allows for easier runs of levels with less varied monsters to be followed by similarly sized runs of levels with larger variety of monsters.
in my 20 levels, the equation I put together creates anywhere from 3 to 7 monster types per level with an added parameter increasing the number of monster types on later levels. any level with more than 5 monster types uses the types beyond 5 as bosses.
My current game is giving me a progression that looks like this:
level number | number of monster types | strengths of monsters | Average strength* | boss(es) strength* |
1 | 5 | 7, 8, 9, 11, 14 | 9.8 | |
2 | 4 | 8, 9, 10, 12 | 9.75 | |
3 | 3 | 11, 12, 14 | 12.33 | |
4 | 4 | 12, 13, 15, 18 | 14.5 | |
5 | 5 | 15, 17, 20, 25, 33 | 22 | |
6 | 5 | 16, 18, 21, 26, 34 | 23 | |
7 | 6 | 21, 24, 29, 37, 50 | 32.2 | 71 |
8 | 5 | 22, 25, 30, 38, 51 | 33.2 | |
9 | 4 | 29, 34, 42, 55 | 40 | |
10 | 4 | 30, 35, 43, 56 | 41 | |
11 | 6 | 41, 49, 62, 83, 117 | 70.4 | 172 |
12 | 6 | 58, 71, 92, 126, 181 | 105.6 | 270 |
13 | 6 | 59, 72, 93, 127, 182 | 106.6 | 271 |
14 | 7 | 86, 107, 141, 196, 285 | 163 | 429, 662 |
15 | 6 | 87, 108, 142, 197, 286 | 164 | 430 |
16 | 5 | 130, 164, 219, 308, 452 | 254.6 | |
17 | 6 | 131, 165, 220, 309, 453 | 255.6 | 686 |
18 | 7 | 200, 255, 344, 488, 721 | 401.6 | 1098, 1708 |
19 | 7 | 201, 256, 345, 489, 722 | 402.6 | 1099, 1709 |
20 | 7 | 312, 401, 545, 778, 1155 | 638.2 | 1765, 2752 |
* - average strength is off because I am using a probability distribution that results in lower powered enemies being more likely to spawn than higher leveled enemies. e.g. if there were 5 enemies at strengths [7,8,9,11,14] the spawn chances would arbitrarily be [50,40,30,20,10]. This results in 5x as many level 7 enemies vs level 14 enemiesstrength is generally about equal to total hitpoints for a given monster. Most other stats scale with strength as well. A strength value can generate a very generic monster, but then monster race and class are applied on-top of that generic monster to create the great variety that will exist.
I use Fibonacci as the basis for strength calculation... every couple of levels or so, I increment to the next Fibonacci as the level's base monster strength. This is visible in the nearly-always pattern of 2 levels with very similarly leveled monsters.
when a level is first created, it is populated with about 20 monsters from the table row. After a player has spent a significant amount of time on a floor, more monsters will begin to generate. These monsters follow different rules. They will instead be spawned from the pool of monsters between that level's average * .3 to average * 1.3. as a player spends more time on a level (barring death by hunger) the additionally generated monsters creep towards a bias for stronger monsters.
Effects on gameplay & Basic Player mechanicsIt becomes necessary for the player to spend some additional time on some levels or return to previous levels in order to keep up with the difficulty curve. I have always thought having to work your way towards more difficulty enemies is a fun part of roguelikes, as you see what the game can throw at you. The PC will have to be particularly powerful to advance through the later stages of the game.
A starting PC may begin with stats equivalent to a monster with strength of around 15 or 25. By the end, they will slay a boss 100 - 150x their original strength.
My intention is to make the PC rely heavily upon their gear to bring them through the final levels. A degree of crafting will be implemented in the game to achieve this balance. Crafting should eventually yield items as-powerful-as generated artifacts. Crafting is utterly useless if crafted items are of no use at the end-game.
Part of keeping the PC on their toes is making it very very difficult for them to max out their resistances. I have 6 damage types in the game (physical,cold,fire,poison,arcane,divine). A resistance of 10 means you take no damage from an attack of that type, 5 = 50%. going above 10 or below 0 makes the player recover from attack types or take extra damage from attack types.
The player shall never be able to become fully immune to physical damage, nor at any time shall the players total resistances cap 45. This will help the player to recognize the importance of tracking damage types from enemies and prepare their gear to counteract that damage.
A player can get a resistance above 10, but it would require a decent amount of gear pushing resistances for that damage type, sacrificing general resistances in the process. e.g. a rainbow or dual resistance item may offer 5 or 6 resistance by itself, but if an item offers 2 or 3 resistance to a single damage type, it is unlikely to have additional resistance modifiers.
Weapons will not be able to offer resistance bonuses, this leaves only 8 pieces of equipment from which to gain resistances (unless double-shielding, which would be 9). Therefore, no item shall ever give more than 1 physical resistance (shields and weapons 0), or more than 6 total resistance. (6 is going to be extremely difficult to achieve, even if it is distributed across all the damage types.)
Monster RacesI have also defined a set of races with accompanying monster types for those races. Each race is actually a base race, then 2 or 3 advanced or upgraded versions of that race.
here is one example:
{
'name':['dragon','aeldur dragon','ancient dragon'],
'character':['d','D','D'],
'description':'the {0} is a beast of magic and terrible strength',
'mods':'physRes2,absorb1,life4',
'classes':
[
{
'name':'red {0}',
'mods':'fireRes5,fireAttack4',
'color':'#FF1E32',
},
{
'name':'blue {0}',
'mods':'coldRes5',coldAttack4,
'color':'#539FEE',
},
{
'name':'green {0}',
'mods':'poisonRes5,poisonAttack4',
'color':'#277F2A',
},
{
'name':'white {0}',
'mods':'divineRes5,divineAttack4',
'color':'#CCCCCC',
},
{
'name':'black {0}',
'mods':'arcaneRes5,arcaneAttack4',
'color':'#333333',
}
]
},
{
'name':['goblin','hobgoblin','greater goblin'],
'character':['g','h','G'],
'mods':'',
'description':'{0}s are creatures of the deep, always seeking for riches',
},
NOTE: the '{0}' seen in some of the strings above is a placeholder, to be replaced with the basic name of the creature via string formatting. This allows for flexible enemy naming, while maintaining information about the monster in it's name. e.g. 'red {0}'.format('dragon') will return 'red dragon'
NOTE 2: the goblin family above does not yet have it's classes implemented, but I think you get the picture.
Races are chosen and ordered randomly for each game. Note the race definition at no-point defines a strength for any of the monsters. A different seed will create a different order/advancement of monster races. One game could start with goblins as the weakest enemies and dragons as the strongest, another could place them in completely opposite order.
the method of reusing a race later on as an 'upgraded race' makes for consistency in the game. you will see enemies of similar types, but much greater power.
It is possible that I may add additional parameters that make a race more likely to appear later in the game or earlier in the game based upon my notions of their strength compared to other races.
I have implemented 14 such race-definitions like the one in the code-box above. this results in about 45 different total races (some races have 4 tiers) and 225 total different enemies. Any given game currently uses about 20 different races only, so there will be great variety in which monsters you fight in each play-through. The higher leveled races of each race-definition can only appear if their lower version(s) appeared earlier in the game.
All races have different intrinsic stats that change their relative strengths if they were to be compared to another race at the same monster strength level. Dragons, for instance, tend to have more health, be resistant to damage and heal quickly. goblins, however don't have much of anything good going on for them. so a strength 50 goblin will be weaker than a strength 50 dragon because of intrinsic attributes.
When the game is choosing which monsters belong to which race, it first takes all the generated monster strengths (seen in the table above) and orders them lowest-to-highest into an array of unique values. It will then chop that array into smaller arrays of no more than 5 values where the largest value in the new array is no larger than the smaller * 1.3. This then becomes a single race. The resulting structure is power bands, where a race will always be more powerful than the race before it, but weaker than the race after it.
This organization also works well with the generated monster strengths above, as the levels tend to have mixed races of monsters. (because the values for minimum and maximum strength on a single level usually differ by much more than 30%)
Thanks for reading!