renamed some things and snuck in some testing code

This commit is contained in:
dtookey 2023-09-09 19:56:53 -04:00
parent a985417f83
commit 483e67bb16
12 changed files with 263 additions and 172 deletions

View File

@ -1,7 +1,7 @@
package entries
import simulation.*
import simulation.dice.DiceBag
import simulation.dice.Dice
import simulation.dice.RollType
import simulation.fifthEd.AttackResult
import simulation.fifthEd.AttackSimulatorModel
@ -16,7 +16,8 @@ fun main() {
val rounds = 1_000_000
val start = System.currentTimeMillis()
// val computedRounds = doSimulation(rounds)
crunchBarbarian(21)
// crunchPally(21)
crunchRogue(18)
// crunchMonk()
val finish = System.currentTimeMillis()
// println("Simulation finished $computedRounds rounds in: ${finish - start}ms")
@ -34,7 +35,7 @@ fun crunchMonk(ac: Int= 18) {
val attackBless = MeleeAttackBuilder("1d20", "1d8", ac)
.withAtkBonus(11)
.withAtkBonus(DiceBag.plainDice("1d4"))
.withAtkBonus(Dice.regular("1d4"))
.withDmgBonus(7)
.build()
@ -77,6 +78,96 @@ fun crunchBarbarian(defense: Int = 16){
val attacks = listOf(Pair("Normal Attack", normalAttack), Pair("GWM Attack", gwmAttack))
generateCombatSuggestions(attacks, rounds, defaultReport())
}
fun crunchPally(defense: Int = 16){
val rounds = 1_000_000
val attackDice = "1d20"
val weaponDice = "1d10"
val normalAttack = MeleeAttackBuilder(attackDice, weaponDice, defense)
//attack
.withAtkBonus(5) // str mod
.withAtkBonus(4) // proficiency bonus
.withAtkBonus(4) // proficiency bonus
.withAtkBonus(2) // weapon bonus
.withAtkBonus(2) // arming
//damage
.withDmgBonus(2) // weapon bonus
.withDmgBonus(5) // str mod
.withDmgBonus(Dice.regular("1d4")) // polearm force
.withDmgBonus(Dice.regular("1d8")) // passive smite
.build()
val gwmAttack = MeleeAttackBuilder(attackDice, weaponDice, defense)
//attack
.withAtkBonus(5) // str mod
.withAtkBonus(-5) // GWM
.withAtkBonus(4) // proficiency bonus
.withAtkBonus(4) // proficiency bonus
.withAtkBonus(2) // weapon bonus
.withAtkBonus(2) // arming
//damage
.withDmgBonus(2) // weapon bonus
.withDmgBonus(5) // str mod
.withDmgBonus(10) // GWM
.withDmgBonus(Dice.regular("1d4")) // polearm force
.withDmgBonus(Dice.regular("1d8")) // passive smite
.build()
val attacks = listOf(Pair("Normal Attack", normalAttack), Pair("GWM Attack", gwmAttack))
generateCombatSuggestions(attacks, rounds, defaultReport())
}
fun crunchRogue(defense: Int = 18){
val attackDice = "1d20"
val bloodthirstDice = "1d20C19"
val weaponDice = "1d10"
val rounds = 1_000_000
val crimsonMischief = MeleeAttackBuilder(attackDice, weaponDice, defense)
//attack
.withAtkBonus(16) // bonus total
//damage
.withDmgBonus(Dice.regular("10d6")) //sneak attack
.withDmgBonus(7) //bonus total
.withDmgBonus(7) //bonus total
val bloodthirst = MeleeAttackBuilder(bloodthirstDice, weaponDice, defense)
//attack
.withAtkBonus(16) // bonus total
//damage
.withDmgBonus(Dice.regular("10d6")) //sneak attack
.withDmgBonus(7) //bonus total
val attacks = arrayListOf(
Pair("Crimson Mischief", crimsonMischief.build()),
Pair("Bloodthirst", bloodthirst.build())
)
crimsonMischief.withAtkBonus(-5).withDmgBonus(10)
bloodthirst.withAtkBonus(-5).withDmgBonus(10)
attacks.add(Pair("crimson Mischief sharpshooter", crimsonMischief.build()))
attacks.add(Pair("Bloodthirst sharpshooter", bloodthirst.build()))
generateCombatSuggestions(attacks, rounds, defaultReport())
}
@ -98,10 +189,12 @@ fun generateCombatSuggestions(
val report = reportFactory.build(label)
print("${attackInfo.first} $it\t")
val res = report.computeResults(results)
print("${attackInfo.first} $it\t")
println(Report.formatReport(res.name, res.results))
println()
res
}.toMap()
}
@ -128,6 +221,7 @@ fun generateCombatSuggestions(
}
}
fun defaultReport(): ReportBuilder {
return ReportBuilder.getInstance()
.addRateMetric("Accuracy") { it.rollSucceeded }

View File

@ -3,7 +3,7 @@ package simulation.dice
/**
* Dice with a modifiable crit threshold
*/
interface CritDice : Dice {
interface CritDiceRoller : DiceRoller {
val critThreshold: Int
@ -12,7 +12,7 @@ interface CritDice : Dice {
* for these attack dice.
*
* The critical hit threshold is determined based on the roll string used to construct
* this [CritDice] instance.
* this [CritDiceRoller] instance.
*
* @param result The [RollResult] to check for crit.
* @return True if result meets or exceeds the crit threshold.
@ -42,7 +42,7 @@ interface CritDice : Dice {
internal class CritDiceImpl(
override val rollString: String,
override val modifiers: List<DiceModifier<Int>> = ArrayList()
) : CritDice {
) : CritDiceRoller {
override val nDice: Int
override val dieSize: Int

View File

@ -1,111 +1,19 @@
package simulation.dice
import simulation.dice.RollType.*
import java.util.*
import kotlin.math.max
import kotlin.math.min
/**
* Enumeration of different dice roll types.
*
* @property Advantage Rolls the dice twice and takes the higher result.
* @property Normal Rolls the dice normally once.
* @property Disadvantage Rolls the dice twice and takes the lower result.
*/
enum class RollType {
/**
* Rolls the dice once normally.
*/
Normal,
/**
* Rolls the dice twice and returns the higher of the two results.
*/
Advantage,
/**
* Rolls the dice twice and returns the lower of the two results.
*/
Disadvantage
}
data class RollResult(val min: Int, val max: Int, val result: Int)
interface Dice {
val rollString: String
val modifiers: List<DiceModifier<Int>>
val nDice: Int
val dieSize: Int
/**
* Rolls the dice and returns the result.
*
* The roll result is determined based on the [rollType].
*
* The result is also modified by any [modifiers] that have been added to this [Dice].
*
* @param r The [Random] instance to use for the dice rolls
* @return The result of the dice roll with modifiers applied
* @see RollType
*/
fun roll(r: Random, rollType: RollType = Normal): RollResult {
val range = (dieSize * nDice) - nDice
val result = when (rollType) {
Advantage -> advantageRoll(r, nDice, range)
Disadvantage -> disadvantageRoll(r, nDice, range)
else -> normalRoll(r, nDice, range)
}
return RollResult(nDice, dieSize * nDice, result)
object Dice {
fun regular(rollString: String, modifiers: List<DiceModifier<Int>> = ArrayList()): DiceRoller {
return DiceImpl(rollString, modifiers)
}
private fun advantageRoll(r: Random, nDice: Int, range: Int): Int {
val roll1 = r.nextInt(range + 1)
val roll2 = r.nextInt(range + 1)
return max(roll1, roll2) + nDice
fun critDice(rollString: String, modifiers: List<DiceModifier<Int>> = ArrayList()): CritDiceRoller {
return CritDiceImpl(rollString, modifiers)
}
private fun disadvantageRoll(r: Random, nDice: Int, range: Int): Int {
val roll1 = r.nextInt(range + 1)
val roll2 = r.nextInt(range + 1)
return min(roll1, roll2) + nDice
}
private fun normalRoll(r: Random, nDice: Int, range: Int): Int {
return r.nextInt(range + 1) + nDice
}
/**
* Evaluates all the modifiers passed to this Dice instance and returns their sum.
*
* @param r The Random instance to pass to each modifier's getBonus() method
* @return The summed bonus values from all modifiers
*/
fun evaluateModifiers(r: Random, crit: Boolean = false): Int {
return modifiers.sumOf { it.getBonus(r, crit) }
}
fun defaultParseFn(rollString: String): Pair<Int, Int> {
val cleanRollString = rollString.lowercase()
val parts = cleanRollString.split('d')
return Pair(parts[0].toInt(), parts[1].toInt())
}
}
internal class DiceImpl(
override val rollString: String,
override val modifiers: List<DiceModifier<Int>> = ArrayList()
) : Dice {
override val nDice: Int
override val dieSize: Int
init {
val rollPair = defaultParseFn(rollString)
nDice = rollPair.first
dieSize = rollPair.second
fun rerollDice(
rollString: String,
rerollThreshold: Int,
modifiers: List<DiceModifier<Int>> = ArrayList()
): RerollDiceRoller {
return RerollDiceImpl(rollString, rerollThreshold, modifiers)
}
}

View File

@ -1,19 +0,0 @@
package simulation.dice
object DiceBag {
fun plainDice(rollString: String, modifiers: List<DiceModifier<Int>> = ArrayList()): Dice {
return DiceImpl(rollString, modifiers)
}
fun critDice(rollString: String, modifiers: List<DiceModifier<Int>> = ArrayList()): CritDice {
return CritDiceImpl(rollString, modifiers)
}
fun rerollDice(
rollString: String,
rerollThreshold: Int,
modifiers: List<DiceModifier<Int>> = ArrayList()
): RerollDice {
return RerollDiceImpl(rollString, rerollThreshold, modifiers)
}
}

View File

@ -21,16 +21,16 @@ interface DiceModifier<T> {
}
/**
* A [DiceModifier] that generates a random bonus integer based on a provided [Dice].
* A [DiceModifier] that generates a random bonus integer based on a provided [DiceRoller].
*
* On each call to [getBonus], it will roll the given [Dice] using the passed [Random]
* On each call to [getBonus], it will roll the given [DiceRoller] using the passed [Random]
* instance and return the result as a positive bonus amount.
*
* @param dice The [Dice] instance to use for generating bonus values.
* @param dice The [DiceRoller] instance to use for generating bonus values.
*/
class DiceBonusModifier(private val dice: Dice) : DiceModifier<Int> {
class DiceBonusModifier(private val dice: DiceRoller) : DiceModifier<Int> {
constructor(diceString: String):this(DiceBag.plainDice(diceString))
constructor(diceString: String):this(Dice.regular(diceString))
override fun getBonus(r: Random, crit: Boolean): Int {
return if (crit){
@ -43,14 +43,14 @@ class DiceBonusModifier(private val dice: Dice) : DiceModifier<Int> {
}
/**
* A [DiceModifier] that applies a random penalty based on a [Dice].
* A [DiceModifier] that applies a random penalty based on a [DiceRoller].
*
* On each call to [getBonus], it will roll the provided [Dice] object and return the
* On each call to [getBonus], it will roll the provided [DiceRoller] object and return the
* result as a negative number.
*
* @param dice The [Dice] to use for generating penalty values.
* @param dice The [DiceRoller] to use for generating penalty values.
*/
class DicePenaltyModifier(private val dice: Dice): DiceModifier<Int> {
class DicePenaltyModifier(private val dice: DiceRoller): DiceModifier<Int> {
override fun getBonus(r: Random, crit: Boolean): Int {//can penalties ever crit?
return -dice.roll(r).result
}

View File

@ -0,0 +1,111 @@
package simulation.dice
import simulation.dice.RollType.*
import java.util.*
import kotlin.math.max
import kotlin.math.min
/**
* Enumeration of different dice roll types.
*
* @property Advantage Rolls the dice twice and takes the higher result.
* @property Normal Rolls the dice normally once.
* @property Disadvantage Rolls the dice twice and takes the lower result.
*/
enum class RollType {
/**
* Rolls the dice once normally.
*/
Normal,
/**
* Rolls the dice twice and returns the higher of the two results.
*/
Advantage,
/**
* Rolls the dice twice and returns the lower of the two results.
*/
Disadvantage
}
data class RollResult(val min: Int, val max: Int, val result: Int)
interface DiceRoller {
val rollString: String
val modifiers: List<DiceModifier<Int>>
val nDice: Int
val dieSize: Int
/**
* Rolls the dice and returns the result.
*
* The roll result is determined based on the [rollType].
*
* The result is also modified by any [modifiers] that have been added to this [DiceRoller].
*
* @param r The [Random] instance to use for the dice rolls
* @return The result of the dice roll with modifiers applied
* @see RollType
*/
fun roll(r: Random, rollType: RollType = Normal): RollResult {
val range = (dieSize * nDice) - nDice
val result = when (rollType) {
Advantage -> advantageRoll(r, nDice, range)
Disadvantage -> disadvantageRoll(r, nDice, range)
else -> normalRoll(r, nDice, range)
}
return RollResult(nDice, dieSize * nDice, result)
}
private fun advantageRoll(r: Random, nDice: Int, range: Int): Int {
val roll1 = r.nextInt(range + 1)
val roll2 = r.nextInt(range + 1)
return max(roll1, roll2) + nDice
}
private fun disadvantageRoll(r: Random, nDice: Int, range: Int): Int {
val roll1 = r.nextInt(range + 1)
val roll2 = r.nextInt(range + 1)
return min(roll1, roll2) + nDice
}
private fun normalRoll(r: Random, nDice: Int, range: Int): Int {
return r.nextInt(range + 1) + nDice
}
/**
* Evaluates all the modifiers passed to this Dice instance and returns their sum.
*
* @param r The Random instance to pass to each modifier's getBonus() method
* @return The summed bonus values from all modifiers
*/
fun evaluateModifiers(r: Random, crit: Boolean = false): Int {
return modifiers.sumOf { it.getBonus(r, crit) }
}
fun defaultParseFn(rollString: String): Pair<Int, Int> {
val cleanRollString = rollString.lowercase()
val parts = cleanRollString.split('d')
return Pair(parts[0].toInt(), parts[1].toInt())
}
}
internal class DiceImpl(
override val rollString: String,
override val modifiers: List<DiceModifier<Int>> = ArrayList()
) : DiceRoller {
override val nDice: Int
override val dieSize: Int
init {
val rollPair = defaultParseFn(rollString)
nDice = rollPair.first
dieSize = rollPair.second
}
}

View File

@ -3,7 +3,7 @@ package simulation.dice
import java.util.*
import kotlin.collections.ArrayList
interface RerollDice : Dice {
interface RerollDiceRoller : DiceRoller {
val rollThreshold: Int
override fun roll(r: Random, rollType: RollType): RollResult {
val result = super.roll(r, rollType)
@ -20,7 +20,7 @@ class RerollDiceImpl(
override val rollString: String,
override val rollThreshold: Int,
override val modifiers: List<DiceModifier<Int>> = ArrayList()
) : RerollDice{
) : RerollDiceRoller{
override val nDice: Int
override val dieSize: Int

View File

@ -1,7 +1,6 @@
package simulation.fifthEd
import simulation.dice.CritDice
import simulation.dice.Dice
import java.util.*
import simulation.dice.CritDiceRoller
import simulation.dice.DiceRoller
data class ActionRollInfo(val actionRoll: CritDice, val damageRoll: Dice)
data class ActionRollInfo(val actionRoll: CritDiceRoller, val damageRoll: DiceRoller)

View File

@ -91,12 +91,12 @@ class MeleeAttackBuilder(
return this
}
fun withAtkBonus(dice: Dice): MeleeAttackBuilder {
fun withAtkBonus(dice: DiceRoller): MeleeAttackBuilder {
attackModifiers.add(DiceBonusModifier(dice))
return this
}
fun withAtkPenalty(dice: Dice): MeleeAttackBuilder {
fun withAtkPenalty(dice: DiceRoller): MeleeAttackBuilder {
attackModifiers.add(DicePenaltyModifier(dice))
return this
}
@ -106,12 +106,12 @@ class MeleeAttackBuilder(
return this
}
fun withDmgBonus(dice: Dice): MeleeAttackBuilder {
fun withDmgBonus(dice: DiceRoller): MeleeAttackBuilder {
damageModifiers.add(DiceBonusModifier(dice))
return this
}
fun withDmgPenalty(dice: Dice): MeleeAttackBuilder {
fun withDmgPenalty(dice: DiceRoller): MeleeAttackBuilder {
damageModifiers.add(DicePenaltyModifier(dice))
return this
}
@ -119,8 +119,8 @@ class MeleeAttackBuilder(
fun build(): SimpleMeleeAttackAction {
return SimpleMeleeAttackAction(
ActionRollInfo(
DiceBag.critDice(attackRollString, attackModifiers),
DiceBag.plainDice(dmgRollString, damageModifiers)
Dice.critDice(attackRollString, attackModifiers),
Dice.regular(dmgRollString, damageModifiers)
),
defense
)

View File

@ -2,7 +2,6 @@ package simulation
import org.junit.jupiter.api.Assertions
import simulation.dice.Dice
import simulation.dice.DiceBag
import simulation.dice.RollResult
import java.util.*
import kotlin.test.Test
@ -13,7 +12,7 @@ class AttackDiceTest {
fun testAttackDiceImplementation() {
val r = Random()
// Test no crit below threshold
val dice = DiceBag.critDice("1d20")
val dice = Dice.critDice("1d20")
val result = dice.roll(r)
if (result.result < 20) {
Assertions.assertFalse(dice.isCrit(result))
@ -21,7 +20,7 @@ class AttackDiceTest {
Assertions.assertTrue(dice.isCrit(result))
}
// Test no crit below threshold
val dice2 = DiceBag.critDice("1d20c10")
val dice2 = Dice.critDice("1d20c10")
val result2 = dice2.roll(r)
if (result2.result < 10) {
Assertions.assertFalse(dice2.isCrit(result2))
@ -30,7 +29,7 @@ class AttackDiceTest {
}
// Test crit threshold other than max
val dice3 = DiceBag.critDice("2d10c8")
val dice3 = Dice.critDice("2d10c8")
val result3 = dice3.roll(r)
if (result3.result >= 8) {
Assertions.assertTrue(dice3.isCrit(result3))
@ -44,9 +43,9 @@ class AttackDiceTest {
val trueCritResult = RollResult(1, 20, 20)
val fakeCritResult = RollResult(1, 20, 19)
val defaultRoll = DiceBag.critDice("1d20")
val verboseDefaultCrit = DiceBag.critDice("1d20c20")
val normalModifiedCrit = DiceBag.critDice("1d20c19")
val defaultRoll = Dice.critDice("1d20")
val verboseDefaultCrit = Dice.critDice("1d20c20")
val normalModifiedCrit = Dice.critDice("1d20c19")
Assertions.assertFalse(defaultRoll.isCrit(fakeCritResult))
Assertions.assertFalse(verboseDefaultCrit.isCrit(fakeCritResult))

View File

@ -4,19 +4,18 @@ import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import simulation.dice.Dice
import simulation.dice.DiceBag
import simulation.dice.FlatModifier
import simulation.dice.RollType
import java.util.*
internal class DiceTests {
internal class DiceRollerTests {
private val random = Random(1)
private val d20 = DiceBag.plainDice("1d20")
private val d20 = Dice.regular("1d20")
@Test
fun roll_normal() {
val dice = DiceBag.plainDice("2d6")
val dice = Dice.regular("2d6")
val result = dice.roll(random, RollType.Normal)
val result2 = dice.roll(random)
@ -30,7 +29,7 @@ internal class DiceTests {
@Test
fun roll_advantage() {
val dice = DiceBag.plainDice("2d6")
val dice = Dice.regular("2d6")
val result = dice.roll(random, RollType.Advantage)
assertEquals(2, result.min)
@ -41,7 +40,7 @@ internal class DiceTests {
@Test
fun roll_disadvantage() {
val dice = DiceBag.plainDice("2d6")
val dice = Dice.regular("2d6")
val result = dice.roll(random, RollType.Disadvantage)
assertEquals(2, result.min)
@ -53,7 +52,7 @@ internal class DiceTests {
fun evaluate_modifiers() {
val mod1 = FlatModifier(1)
val mod2 = FlatModifier(2)
val dice = DiceBag.plainDice("1d20", arrayListOf(mod1, mod2))
val dice = Dice.regular("1d20", arrayListOf(mod1, mod2))
val bonus = dice.evaluateModifiers(random)
@ -67,7 +66,7 @@ internal class DiceTests {
RollType.entries.parallelStream()
.forEach {
val dice = DiceBag.plainDice(rollString)
val dice = Dice.regular(rollString)
val r = Random(1)
val rollType = it
repeat(iterations) {
@ -99,7 +98,7 @@ internal class DiceTests {
RollType.entries.parallelStream()
.forEach {
val dice = DiceBag.plainDice(rollString)
val dice = Dice.regular(rollString)
val r = Random(1)
for (i in 0..<iterations) {
val res = dice.roll(r, it)
@ -129,7 +128,7 @@ internal class DiceTests {
val expectedAverageLowerBound = ((n + (n * max)) / 2) * (1 - tolerance)
val expectedAverageUpperBound = ((n + (n * max)) / 2) * (1 + tolerance)
val dice = DiceBag.plainDice(rollString)
val dice = Dice.regular(rollString)
var total = 0L
repeat(iterations) {
total += dice.roll(random).result.toLong()
@ -153,7 +152,7 @@ internal class DiceTests {
val expectedAverageUpperBound = ((n + (n * max)) / 2) * tolerance
val dice = DiceBag.plainDice(rollString)
val dice = Dice.regular(rollString)
var total = 0L
repeat(iterations) {
@ -177,7 +176,7 @@ internal class DiceTests {
val expectedAverageLowerBound = ((n + (n * max)) / 2) * tolerance
val dice = DiceBag.plainDice(rollString)
val dice = Dice.regular(rollString)
var total = 0L
@ -196,7 +195,7 @@ internal class DiceTests {
@Test
fun verifyDistribution() {
val size = 20
val dice = DiceBag.plainDice("1d20")
val dice = Dice.regular("1d20")
val iterations = 10_000_000
val tolerance = 0.05 //5% wiggle on distribution

View File

@ -1,6 +1,6 @@
package simulation
import simulation.dice.DiceBag
import simulation.dice.Dice
import simulation.dice.RollType
import simulation.fifthEd.ActionRollInfo
import simulation.fifthEd.AttackResult
@ -17,7 +17,7 @@ class MeleeAttackTest {
val itt = 1_000_000
val simulator = Simulator.getInstance<AttackResult>(Runtime.getRuntime().availableProcessors())
val attackAction = ActionRollInfo(DiceBag.critDice("1d20c19"), DiceBag.plainDice("1d8"))
val attackAction = ActionRollInfo(Dice.critDice("1d20c19"), Dice.regular("1d8"))
val critAttack = SimpleMeleeAttackAction(
attackAction,
10