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.


3 Answers 3


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:

graphs of ability modifiers rolled using Knave rolls, versus D&D 5e rolls

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).

  • \$\begingroup\$ 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. \$\endgroup\$
    – 3C273
    Commented Apr 4, 2021 at 3:15
  • \$\begingroup\$ And I think 4d6 is actually still higher than standard array, right? \$\endgroup\$
    – 3C273
    Commented Apr 4, 2021 at 3:16
  • 1
    \$\begingroup\$ @3C273 Yes, standard array is (on average) 1 lower on each score. \$\endgroup\$ Commented Apr 4, 2021 at 7:35
  • \$\begingroup\$ 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. \$\endgroup\$
    – minnmass
    Commented Apr 5, 2021 at 3:35
  • \$\begingroup\$ @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). \$\endgroup\$
    – 3C273
    Commented Apr 5, 2021 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.


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]
            return official
        def Get4d6Array():
            def oneRoll():        
                oneroll = [randint(1,6) for i in range(4)]
                return sum(oneroll)
            ret = [ oneRoll() for stat in range(6) ]
            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) ]
            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) ]
            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) ]
            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) ]
            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
            return [ConvertScoreToBonus(s) for s in _stats]
        def player_OptimizerSingleBonus( _stats ):
            _stats[-1] += 2
            _stats[-2] += 1
            return [ConvertScoreToBonus(s) for s in _stats]
        def player_GeneralistSingleBonus( _stats ):
            _stats[-1] += 1
            _stats[-2] += 2
            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 )

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.

  • 5
    \$\begingroup\$ 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. \$\endgroup\$
    – NotArch
    Commented Apr 5, 2021 at 14:48
  • 3
    \$\begingroup\$ @NautArch as a player who rolls 3d6-down-the-line whenever allowed, I can't agree with you more. \$\endgroup\$
    – nitsua60
    Commented Apr 5, 2021 at 14:54
  • 4
    \$\begingroup\$ 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. \$\endgroup\$ Commented Apr 5, 2021 at 16:03
  • 1
    \$\begingroup\$ @NautArch While I certainly believe that's true, it seemed too far off topic to go into. \$\endgroup\$ Commented Apr 5, 2021 at 21:13
  • 1
    \$\begingroup\$ 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. \$\endgroup\$
    – linksassin
    Commented Apr 7, 2021 at 11:41

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .