# What are the effects of using this stat generation method?

I have recently discovered the game Knave, by Ben Milton from the Questing Beast YouTube channel, and I find the stat generation procedure pretty interesting. I'm thinking of implementing it in some of my 5e games. I'm curious on the effect this can have on the game balance and the feel from the player's point of view. Especially since my players are not used to old-school habits like rolling for stats.

The system from Knave.

When creating a PC, roll 3d6 for each of their abilities, in order. The lowest of the three dice on each roll is that ability’s bonus. Add 10 to find its defense. After you’ve finished rolling, you may optionally swap the scores of two abilities.

Example: You roll a 2, 2, and 6 for Strength. The lowest die is a 2, so your PC’s Strength has a bonus of +2 and a defense of 12. Repeat this process for the rest of the abilities.

For the 5e implementation, the same example would yield a bonus of +2 and a score of 14 to match.

The defense mentioned in the quote is used as both a save DC when rolling as defenders and AC when rolling to hit as the attacker. The math is meant to be equivalent whether an attack is rolled on the attacker or defender side. I would not port this concept to 5e.

I'm especially looking for experience on a similar system for a D&D-like game, especially 5e itself, or from playing Knave itself. Or a more system-focused analysis for 5e. When comparing, you can assume that no character will live to level 8 and I would only allow standard-human or variant-human (unsure which, probably the same for everyone). And if a player is lucky enough to get triple-6, I am willing to let him bypass the maximum of 20. But ASIs or race could not go above 20.

## The Knave rolling system will give substantially higher modifers than is normal for starting characters in D&D 5e.

The modifiers using the Knave rolls range from +1 to +6, while starting D&D characters have modifiers that range from -4 to +4 (with typical scores in the range of -1 to +3).

The average modifier is more than a point higher using Knave rolls than when using 4d6 drop the lowest, and the distribution is also quite different. Here are the two methods graphed together, via this AnyDice program:

If you were to subtract one from the modifier you got from the Knave roll, you'd be a lot closer to the modifiers of a typical starting D&D character. The average would still be very slightly better with Knave rolls, but the distribution might not be as good (fewer +2s and +3s in exchange for no negative scores at all).

• I guess the fact it would be higher was to be expected. But the comparison is interesting. The +4 is more frequent than I expected. I like the idea of Knave-1 if I end up using this. Apr 4 '21 at 3:15
• And I think 4d6 is actually still higher than standard array, right? Apr 4 '21 at 3:16
• @3C273 Yes, standard array is (on average) 1 lower on each score. Apr 4 '21 at 7:35
• I think this is a great answer to the first half of the question, addressing the typical stat, but that it would be improved by addressing the "in order" part of the system. Apr 5 '21 at 3:35
• @minnmass I have to admit the down-the-line part is another departure from standard play. But for what it's worth, even in my own answer I disregard the order since I assume players will pick a class that fits at least 2 of their high stats (or what they can get with the free stat switch). Apr 5 '21 at 16:20

I rewrite this answer after finding a much better way to show what I found. So here it goes.

TLDR : the Question's method gives systematically higher numbers but the stats have roughly the same shape. Doing Blckknght's Knave-1 gives a rough euqivalence without negative bonuses. Doing a Knave method where the stats are produced instead of the bonus gives more 0s and 1s, with rare 2s.

So I made a python script (the code is at the end for those interested) to generate arrays and try to see if the proposed method would encourage different kind of strategies and if the method would generate a different spread of attribute. The program generated arrays, sort them in ascending order, then compare the highest stat with other highest, then second-highest with other second-highest. And so on until the lowest one.

I don't feel bad sorting the numbers since I assume players will choose a class based on which stats can get the highest bonuses.

I tested five strategies when choosing a race. My assumption with this is that some methods could encourage different uses of the racial bonuses.

• Use standard human for +1 everywhere
• Optimizer : Try to get a +1 bonus to two stats. Aiming to get the biggest bonus in the main stats.
• Generalist : Try to get +1 in the highest and third highest. Aiming to get good bonus in all useful stats of a MAD class like the monk.

I tested three methods.

• Standard array (it made for an interesting control case actually)
• 4d6 drop lowest.
• the Knave generation method mentionned in the question (KnaveBonus).
• Knave-1, taken from Blckknght
• KnaveScore. Rolling 2d6, using the lowest as the attribute's score. So a 5 and a 2 gives a 12 for the score and a +1 bonus.

# Results

Like @Blckknght showed, the Knave array has average bonuses 1 higher than 4d6 (so +2 compared to the standard array). The maximum and minimum are also 1 higher than 4d6 on average. So I assume, like @Blcknght mentionned in his answer, that using the Knave method but reducing all bonuses by 1 would yield a similar results in most cases.

## Effects of races

More interesting, to me at least, is that the different strategies don't affect the numbers of highest and second-highest stats that much. As can be expected, increasing the two main stats give higher top stats than increasing the main and third-main stats. And using standard human gives the best lower stats at the cost of the top stats.

The following table shows the expected range of the bonus for the three main strategies using the proposed Knave method. For example, the (0.87 / 1.59) pair means an average of +1.23 with 66% of rolls between 0.87 and 1.59. Since we're dealing with dices and integer, most rolls would be ones, with more twos than zeroes.

Test Lowest bonus second highest highest
standard Human (0.87 / 1.59) (1.18 / 2.20) (1.70 / 2.55) (1.99 / 3.24) (2.57 / 3.98) (3.37 / 5.11)
Optimizer (0.96 / 1.11) (0.87 / 1.56) (1.00 / 2.15) (1.55 / 2.60) (3.00 / 4.43) (3.82 / 5.50)
Generalist (0.96 / 1.10) (0.87 / 1.53) (1.00 / 2.13) (1.92 / 3.24) (2.59 / 3.77) (3.80 / 5.50)

## Stat generation methods

Below is a table describing the arrays for the Optimizer strategy.

Test Lowest bonus second highest highest
4d6 (-1.7 / -0.2) (-0.6 / 0.56) (-0.0 / 1.29) (0.58 / 1.84) (2.22 / 3.42) (2.93 / 4.23)
Knave (0.96 / 1.11) (0.87 / 1.56) (0.99 / 2.13) (1.55 / 2.63) (3.00 / 4.44) (3.83 / 5.52)
Knave-1 (-0.0 / 0.11) (-0.1 / 0.56) (0.01 / 1.16) (0.55 / 1.63) (2.01 / 3.43) (2.82 / 4.51)
Knave Score (-0.0 / 0.32) (-0.0 / 0.90) (0.35 / 1.18) (0.78 / 1.41) (1.94 / 2.99) (2.64 / 3.33)
• Comparing the 4d6 and Knave array, we can see that 4d6 is on average a full -1 lower than the Knave method at every roll. Even more so for the lowest stat where Knave cannot go negative. Since it can't go below +1, we can guess that the 0.96 is due to the average being very close to 1. Such that most rolls are ones, but a few are twos.
• Comparing 4d6 and the Knave-1. The numbers are a lot closer. Accept that Knave-1 does not go into the negatives.
• Comparing 4d6 with the KnaveScore. KnaveScore has lower highs and higher lows. The highest score is actually a full number behind 4d6.

## The Code

Here's what I used, this should be standard Python3 code.

               # -*- coding: utf-8 -*-

from random import randint

"""
Score generation
"""
def ConvertScoreToBonus(_score):
if( _score < 10):
_score -= 1 #Need to adjust  so that 9=>-1
return int( (_score-10) / 2 )

def GetStandardArray():
official = [15, 14, 13, 12, 10, 8]
official.sort()
return official

def Get4d6Array():
def oneRoll():
oneroll = [randint(1,6) for i in range(4)]
oneroll.remove(min(oneroll))
return sum(oneroll)

ret = [ oneRoll() for stat in range(6) ]
ret.sort()
return ret

def Get3d6Array():
def oneRoll():
oneroll = [randint(1,6) for i in range(3)]
return sum(oneroll)

ret = [ oneRoll() for stat in range(6) ]
ret.sort()
return ret

def GetKnaveBonusArray():
def bonus():
rolls = [randint(1,6) for i in range(3)]
return min(rolls)

ret = [ 10+2*bonus() + (1 if randint(1,2)==1 else 0)\
for stat in range(6) ]
ret.sort()
return ret

def GetKnaveMinusOneArray():
def bonus():
rolls = [randint(1,6) for i in range(3)]
return min(rolls) - 1

ret = [ 10+2*bonus() + (1 if randint(1,2)==1 else 0)\
for stat in range(6) ]
ret.sort()
return ret

def GetKnaveScoreArray():
def score():
rolls = [randint(1,6) for i in range(2)]
return 10+min(rolls)

ret = [ score() for stat in range(6) ]
ret.sort()
return ret

"""
Player race selection and strategy
"""
def player_NoRace( _stats ):
return [ConvertScoreToBonus(s) for s in _stats]

def player_UseHuman( _stats ):
return [ConvertScoreToBonus(s+1) for s in _stats]

def player_OptimizerHuman( _stats ):
_stats[-1] += 2
_stats[-2] += 2
return [ConvertScoreToBonus(s) for s in _stats]

def player_GeneralistHuman( _stats ):
_stats[-1] += 2
_stats[-3] += 2
_stats.sort()
return [ConvertScoreToBonus(s) for s in _stats]

def player_OptimizerSingleBonus( _stats ):
_stats[-1] += 2
_stats[-2] += 1
_stats.sort()
return [ConvertScoreToBonus(s) for s in _stats]

def player_GeneralistSingleBonus( _stats ):
_stats[-1] += 1
_stats[-2] += 2
_stats.sort()
return [ConvertScoreToBonus(s) for s in _stats]

"""
Statistic stuff
"""
class StatsForRoll:
def __init__( self, _array ):
self.m_array = list(_array)

self.m_max = max(self.m_array)
self.m_countOfMax = self.m_array.count( self.m_max )
self.m_countOfSecond = self.m_array.count( StatsForRoll.getSecond(self.m_array) )
self.m_min = min(self.m_array)
self.m_average = sum(self.m_array) / len(self.m_array)

def getSecond( _array ):
localcopy = list(_array)
maximum = max(localcopy)
while( maximum in localcopy and len(localcopy)>1 ):
localcopy.remove( maximum )
return max(localcopy)

def GetAverageArrays( _arrayOfRolls ):
#_arrayOfRolls = list(_arrayOfRolls)
divider = 0
stats = [ 0 for i in range(6)]
for roll in _arrayOfRolls:
divider += 1
for i in range(len(roll.m_array)):
stats[i] += roll.m_array[i]
for i in range(6):
stats[i] /= divider

return stats

def GetErrorBarOnArray( _arrayOfRolls, _averageArray ):
"""
Returns the array as tuples of average +- Average Absolute Deviation
"""
#_arrayOfRolls = list(_arrayOfRolls)
divider = 0
absoluteDeviations = [0 for i in range(6)]
for roll in _arrayOfRolls:
divider += 1
for i in range(len(roll.m_array)):
absoluteDeviations [i] += abs(roll.m_array[i] - _averageArray[i])
for i in range(6):
absoluteDeviations[i] /= divider

return [ (_averageArray[i]-absoluteDeviations[i],\
_averageArray[i]+absoluteDeviations[i] )\
for i in range(6)]

if __name__ == "__main__":
class TestToDo:
def __init__(self, _statCreator, _player, _testLength = 5000):
self.m_statCreator = _statCreator
self.m_player = _player
self.m_testLength = _testLength
testsToDo = []
#testsToDo.append( TestToDo(GetStandardArray, player_UseHuman, 1) )
#testsToDo.append( TestToDo(GetStandardArray, player_OptimizerHuman, 1) )
#testsToDo.append( TestToDo(GetStandardArray, player_GeneralistHuman, 1) )

#testsToDo.append( TestToDo(Get3d6Array, player_UseHuman) )

testsToDo.append( TestToDo(Get4d6Array, player_OptimizerHuman) )
testsToDo.append( TestToDo(GetKnaveBonusArray, player_OptimizerHuman) )
testsToDo.append( TestToDo(GetKnaveMinusOneArray, player_OptimizerHuman) )
testsToDo.append( TestToDo(GetKnaveScoreArray, player_OptimizerHuman) )

#testsToDo.append( TestToDo(Get4d6Array, player_GeneralistHuman) )

#testsToDo.append( TestToDo(Get4d6Array, player_OptimizerSingleBonus) )

#testsToDo.append( TestToDo(Get4d6Array, player_GeneralistSingleBonus) )

for test in testsToDo:
rolls = list(StatsForRoll(test.m_player(test.m_statCreator()))\
for i in range(test.m_testLength))

averageArray = GetAverageArrays( rolls )
errorBars = GetErrorBarOnArray( rolls, averageArray)
testName = test.m_statCreator.__name__ #+ test.m_player.__name__

def fmtNumber(x):
return str(x)[:4]

numbers = ("("+fmtNumber(t[0]) + " / "+ fmtNumber(t[1])+")" \
for t in errorBars)
line = "{0} | {1} |".format(testName, " | ".join(numbers))

print( line )
print()


## 5E isn't made to force scores onto attributes

The key effect of forcing scores onto specific attributes is locking players into characters they didn't want to play. +4 Str, +1 int? Guess you aren't playing a wizard. It will also make it harder to build a class that depends on multiple ability scores, like Monk or Paladin. Different classes need different scores, and this method forces players into characters instead of letting players choose.

As to the "fun" of rolling, most players aren't familiar with rolling because those of us who are familiar with it are glad to be rid of it. It just isn't fun to play a character significantly weaker than other party member.

• Given so many different ways to approach playing this game and what makes things fun, I'm not entirely sure your premise is totally supportable. Apr 5 '21 at 14:48
• @NautArch as a player who rolls 3d6-down-the-line whenever allowed, I can't agree with you more.
– nitsua60
Apr 5 '21 at 14:54
• The described method does allow for swapping two stats, so your single ability dependent wizard can put his +4 into int. Multi dependent characters would have a harder time of it in general. Apr 5 '21 at 16:03
• @NautArch While I certainly believe that's true, it seemed too far off topic to go into. Apr 5 '21 at 21:13
• This answer is really only focusing on the "in order" component of the proposed system. It would benefit your answer if you also commented on the attribute generation method itself. Apr 7 '21 at 11:41