madness has taken hold of me

This commit is contained in:
dtookey 2023-09-03 14:05:50 -04:00
parent 0107aeffb0
commit d0f7c3bd11
17 changed files with 435 additions and 175 deletions

View File

@ -1,21 +1,26 @@
package entries
import simulation.*
import simulation.dice.RollType
import simulation.fifthEd.AttackResult
import simulation.fifthEd.AttackSimulatorModel
import simulation.fifthEd.MeleeAttackBuilder
import simulation.fifthEd.SimpleMeleeAttackAction
fun main() {
val rounds = 1_000_000
val start = System.currentTimeMillis()
doSimulation()
val computedRounds = doSimulation(rounds)
val finish = System.currentTimeMillis()
println("Simulation finished in: ${finish - start}ms")
println("Simulation finished $computedRounds rounds in: ${finish - start}ms")
}
const val avgDamage = "Avg Dmg"
fun doSimulation(rounds: Int): Long {
fun doSimulation() {
val rounds = 10_000_000
val defense = 18
val simulator = Simulator.getInstance<AttackResult>(Runtime.getRuntime().availableProcessors())
val attackDice = "1d20"
val weaponDice = "2d6"
@ -40,23 +45,68 @@ fun doSimulation() {
.withDmgBonus(10) // gwm bonus
.build()
val attacks = listOf(Pair("Normal Attack", normalAttack), Pair("GWM Attack", gwmAttack))
val reportFactory = ReportBuilder.getInstance()
.addRateMetric("Accuracy") { it.rollSucceeded }
.addRateMetric("Crit Rate") { it.rollSucceeded }
.addAverageMetric(avgDamage) { it.resultingDamage.toLong() }
mapOf(Pair("normalAttack", normalAttack), Pair("gwmAttack", gwmAttack))
.entries
.forEach{ attackInfo ->
generateCombatSuggestions(attacks, rounds, reportFactory)
val label = attackInfo.key
val attack = attackInfo.value
return rounds.toLong() * attacks.size.toLong()
}
RollType.entries.parallelStream()
// .filter{it == RollType.Advantage || it == RollType.Normal}
.forEach {
fun generateCombatSuggestions(
attacks: List<Pair<String, SimpleMeleeAttackAction>>,
rounds: Int,
reportFactory: ReportBuilder
) {
val simulator = Simulator.getInstance<AttackResult>(128)
val results = attacks
.associateWith { attackInfo ->
val attackModel = AttackSimulatorModel(rounds, attack, it)
val normalResults = simulator.doSimulation(attackModel)
val label = "${attackInfo.first}"
val attack = attackInfo.second
AttackResult.printSimulationStatistics(normalResults, "$label (${it.name})")
}
RollType.entries.associateWith {
val attackModel = AttackSimulatorModel(rounds, attack, it)
val results = simulator.doSimulation(attackModel)
val report = reportFactory.build(label)
report.computeResults(results)
}.toMap()
}
RollType.entries.forEach { rollType ->
val builder = StringBuilder("[$rollType]\t")
var choice: MetricReport? = null
results.forEach { attackAction ->
val current = attackAction.value[rollType]
if(choice == null){
choice = current
}else{
if(current?.results?.getDamage()!!.metricValue > choice!!.results.getDamage()!!.metricValue){
choice = current
}
}
}
builder.append("Use ${choice!!.name}: ${Report.formatReport(choice!!.name, choice!!.results)}")
println(builder.toString())
}
}
fun List<MetricResult>.getDamage(): MetricResult? {
forEach {
if (it.label == avgDamage) {
return it
}
}
return null
}

View File

@ -1,37 +0,0 @@
package simulation
import java.util.ArrayList
private fun ArrayList<AttackResult>.sumBools(fn: (AttackResult) -> Boolean): Int {
return sumOf { boolToInt(fn(it)) }
}
private fun boolToInt(b: Boolean): Int = if (b) 1 else 0
data class AttackResult(val attackWasHit: Boolean, val attackWasCrit: Boolean, val resultingDamage: Int) {
companion object {
fun printSimulationStatistics(results: ArrayList<AttackResult>, label: String = "") {
val sampleSize = results.size.toDouble()
val totalHits = results.sumBools { it.attackWasHit }
val hitRate = (totalHits.toDouble() / sampleSize) * 100.0
val totalCriticals = results.sumBools { it.attackWasCrit }
val critRate = (totalCriticals.toDouble() / sampleSize) * 100.0
val sumDamage = results.sumOf { it.resultingDamage.toLong() }
val avgDamage = sumDamage.toDouble() / sampleSize
val reportString = "Hit Rate: %.2f\tCrit Rate: %.2f\tAvg Dmg: %.2f".format(hitRate, critRate, avgDamage)
if(label.isNotBlank()){
println("[$label]\t$reportString")
}else{
println(reportString)
}
}
}
}

View File

@ -1,37 +0,0 @@
package simulation
import java.util.*
import kotlin.collections.ArrayList
data class MeleeAttack(val attackRoll: CritDice, val damageRoll: Dice)
/**
* Represents a simple melee attack in a simulation.
*
* @param actionRoll The dice roll used to determine if an attack hits.
* @param damageRoll The dice roll used to determine damage if attack hits.
* @param responseValue The defense value the attack must exceed to hit.
*/
class SimpleMeleeAttackAction(
attackInfo: MeleeAttack,
override val responseValue: Int
) : AttackAction {
override val actionRoll: CritDice = attackInfo.attackRoll
private val damageRoll: Dice = attackInfo.damageRoll
override fun onNormalHit(r: Random): AttackResult {
val damage = damageRoll.roll(r).result + damageRoll.evaluateModifiers(r, false)
return AttackResult(attackWasHit = true, attackWasCrit = false, damage)
}
override fun onCriticalHit(r: Random): AttackResult {
val damage =
damageRoll.roll(r).result + damageRoll.roll(r).result + damageRoll.evaluateModifiers(r, true)
return AttackResult(attackWasHit = true, attackWasCrit = true, damage)
}
override fun onMiss(r: Random): AttackResult {
return AttackResult(attackWasHit = false, attackWasCrit = false, 0)
}
}

View File

@ -0,0 +1,111 @@
package simulation
import simulation.fifthEd.AttackResult
import kotlin.math.pow
data class MetricReport(val name: String, val results: List<MetricResult>)
data class MetricResult(val label: String, val metricValue: Double)
interface Report {
val name: String
val metrics: List<Metric<Double>>
companion object{
fun formatReport(name: String, results: List<MetricResult>): String {
val builder = StringBuilder()
results.forEach {
builder.append("${it.label}: %.2f".format(it.metricValue))
.append('\t')
}
return builder.toString()
}
}
fun computeResults(results: List<AttackResult>): MetricReport{
val m = ArrayList<MetricResult>(metrics.size)
metrics.forEach {
val value = it.mapToMetric(results)
m.add(MetricResult(it.metricName, value))
}
return MetricReport(name, m)
}
}
internal class ReportImpl(override val name: String, override val metrics: List<Metric<Double>>) : Report
interface ReportBuilder {
val metrics: MutableList<Metric<Double>>
companion object {
fun getInstance(): ReportBuilder {
return ReportBuilderImpl()
}
}
fun build(reportName: String): Report {
return ReportImpl(reportName, metrics)
}
fun addAverageMetric(metricName: String, fieldMapFn: (AttackResult) -> Long): ReportBuilder {
metrics.add(AverageMetric(metricName, fieldMapFn))
return this
}
fun addRateMetric(metricName: String, fieldMapFn: (AttackResult) -> Boolean): ReportBuilder {
metrics.add(RateMetric(metricName, fieldMapFn))
return this
}
}
class ReportBuilderImpl : ReportBuilder {
override val metrics: MutableList<Metric<Double>> = ArrayList()
}
interface Metric<T> {
val metricName: String
fun mapToMetric(results: List<AttackResult>): T
fun formatResults(result: T): String
}
class AverageMetric(override val metricName: String, private val fieldMapFn: (AttackResult) -> Long) : Metric<Double> {
override fun mapToMetric(results: List<AttackResult>): Double {
return results.map(fieldMapFn).average()
}
override fun formatResults(result: Double): String {
return "$metricName: %.2f".format(result)
}
}
class RateMetric(override val metricName: String, private val fieldMapFn: (AttackResult) -> Boolean) : Metric<Double> {
override fun mapToMetric(results: List<AttackResult>): Double {
return results.map(fieldMapFn).map { boolToInt(it) }.average() * 100.0
}
override fun formatResults(result: Double): String {
return "$metricName: %.2f".format(result)
}
}
class StdDevMetric(override val metricName: String, private val fieldMapFn: (AttackResult) -> Long) : Metric<Double> {
override fun mapToMetric(results: List<AttackResult>): Double {
val mean = results.map(fieldMapFn).average()
return results.map(fieldMapFn).map { (it - mean).pow(2) }.average().pow(0.5)
}
override fun formatResults(result: Double): String {
return "$metricName: %.2f".format(result)
}
}
private fun boolToInt(b: Boolean): Int = if (b) 1 else 0

View File

@ -17,7 +17,7 @@ interface Simulator<T : Any> {
companion object {
fun <T: Any> getInstance(nThreads: Int = Runtime.getRuntime().availableProcessors() / 2 ): Simulator<T> {
return concreteSimulator(nThreads)
return SimulatorImpl(nThreads)
}
}
@ -65,5 +65,5 @@ interface Simulator<T : Any> {
}
class concreteSimulator<T : Any>(override val nThreads: Int) :
class SimulatorImpl<T : Any>(override val nThreads: Int) :
Simulator<T>

View File

@ -1,4 +1,4 @@
package simulation
package simulation.dice
/**
* Dice with a modifiable crit threshold
@ -39,9 +39,9 @@ interface CritDice : Dice {
*
* The isCrit() method checks if a roll result meets the crit threshold.
*/
class AttackDiceImpl(
internal class AttackDiceImpl(
override val rollString: String,
override val modifiers: List<Modifier<Int>> = ArrayList()
override val modifiers: List<DiceModifier<Int>> = ArrayList()
) : CritDice {
override val nDice: Int
override val dieSize: Int

View File

@ -1,8 +1,7 @@
package simulation
package simulation.dice
import simulation.RollType.*
import simulation.dice.RollType.*
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.max
import kotlin.math.min
@ -14,16 +13,16 @@ import kotlin.math.min
* @property Disadvantage Rolls the dice twice and takes the lower result.
*/
enum class RollType {
/**
* Rolls the dice twice and returns the higher of the two results.
*/
Advantage,
/**
* 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.
*/
@ -35,21 +34,10 @@ data class RollResult(val min: Int, val max: Int, val result: Int)
interface Dice {
val rollString: String
val modifiers: List<Modifier<Int>>
val modifiers: List<DiceModifier<Int>>
val nDice: Int
val dieSize: Int
companion object {
fun plainDice(rollString: String, modifiers: List<Modifier<Int>> = ArrayList()): Dice {
return DiceImpl(rollString, modifiers)
}
fun critDice(rollString: String, modifiers: List<Modifier<Int>> = ArrayList()): CritDice{
return AttackDiceImpl(rollString, modifiers)
}
}
/**
* Rolls the dice and returns the result.
*
@ -99,19 +87,25 @@ interface Dice {
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())
}
}
private class DiceImpl(
internal class DiceImpl(
override val rollString: String,
override val modifiers: List<Modifier<Int>> = ArrayList()
override val modifiers: List<DiceModifier<Int>> = ArrayList()
) : Dice {
override val nDice: Int
override val dieSize: Int
init {
val cleanRollString = rollString.lowercase()
val parts = cleanRollString.split('d')
nDice = parts[0].toInt()
dieSize = parts[1].toInt()
val rollPair = defaultParseFn(rollString)
nDice = rollPair.first
dieSize = rollPair.second
}
}

View File

@ -0,0 +1,19 @@
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 AttackDiceImpl(rollString, modifiers)
}
fun rerollDice(
rollString: String,
rerollThreshold: Int,
modifiers: List<DiceModifier<Int>> = ArrayList()
): RerollDice {
return RerollDiceImpl(rollString, rerollThreshold, modifiers)
}
}

View File

@ -1,4 +1,4 @@
package simulation
package simulation.dice
import java.util.*
@ -9,7 +9,7 @@ import java.util.*
*
* A [Random] object is provided if the bonus is variable.
*/
interface Modifier<T> {
interface DiceModifier<T> {
/**
* Generates a bonus integer, potentially using the provided Random instance if needs be.
@ -21,16 +21,16 @@ interface Modifier<T> {
}
/**
* A [Modifier] that generates a random bonus integer based on a provided [Dice].
* A [DiceModifier] that generates a random bonus integer based on a provided [Dice].
*
* On each call to [getBonus], it will roll the given [Dice] 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.
*/
class DiceBonusModifier(private val dice: Dice) : Modifier<Int> {
class DiceBonusModifier(private val dice: Dice) : DiceModifier<Int> {
constructor(diceString: String):this(Dice.plainDice(diceString))
constructor(diceString: String):this(DiceBag.plainDice(diceString))
override fun getBonus(r: Random, crit: Boolean): Int {
return if (crit){
@ -43,27 +43,27 @@ class DiceBonusModifier(private val dice: Dice) : Modifier<Int> {
}
/**
* A [Modifier] that applies a random penalty based on a [Dice].
* A [DiceModifier] that applies a random penalty based on a [Dice].
*
* On each call to [getBonus], it will roll the provided [Dice] object and return the
* result as a negative number.
*
* @param dice The [Dice] to use for generating penalty values.
*/
class DicePenaltyModifier(private val dice: Dice): Modifier<Int>{
class DicePenaltyModifier(private val dice: Dice): DiceModifier<Int> {
override fun getBonus(r: Random, crit: Boolean): Int {//can penalties ever crit?
return -dice.roll(r).result
}
}
/**
* A [Modifier] that applies a fixed bonus amount.
* A [DiceModifier] that applies a fixed bonus amount.
*
* The bonus value is set on construction and does not vary.
*
* @param bonus The fixed bonus amount to apply.
*/
class FlatModifier(private val bonus: Int) : Modifier<Int> {
class FlatModifier(private val bonus: Int) : DiceModifier<Int> {
override fun getBonus(r: Random, crit: Boolean): Int {
return bonus
}

View File

@ -0,0 +1,32 @@
package simulation.dice
import java.util.*
import kotlin.collections.ArrayList
interface RerollDice : Dice {
val rollThreshold: Int
override fun roll(r: Random, rollType: RollType): RollResult {
val result = super.roll(r, rollType)
return if (result.result <= rollThreshold) {
super.roll(r, rollType)
} else {
result
}
}
}
class RerollDiceImpl(
override val rollString: String,
override val rollThreshold: Int,
override val modifiers: List<DiceModifier<Int>> = ArrayList()
) : RerollDice{
override val nDice: Int
override val dieSize: Int
init {
val rollPair = this.defaultParseFn(rollString)
nDice = rollPair.first
dieSize = rollPair.second
}
}

View File

@ -0,0 +1,39 @@
package simulation.fifthEd
import simulation.dice.CritDice
import simulation.dice.Dice
import java.util.*
data class ActionRollInfo(val actionRoll: CritDice, val damageRoll: Dice)
/**
* Represents a simple melee attack in a simulation.
*
* @param actionRoll The dice roll used to determine if an attack hits.
* @param damageRoll The dice roll used to determine damage if attack hits.
* @param responseValue The defense value the attack must exceed to hit.
*/
class SimpleMeleeAttackAction(
override val actionRollInfo: ActionRollInfo,
override val responseValue: Int
) : AttackAction {
override fun onNormalAction(r: Random): AttackResult {
val damage = actionRollInfo.damageRoll.roll(r).result + actionRollInfo.damageRoll.evaluateModifiers(r, false)
return AttackResult(rollSucceeded = true, rollWasCritical = false, damage)
}
override fun onCriticalAction(r: Random): AttackResult {
val damage = actionRollInfo.damageRoll.roll(r).result +
actionRollInfo.damageRoll.roll(r).result +
actionRollInfo.damageRoll.evaluateModifiers(r, true)
return AttackResult(rollSucceeded = true, rollWasCritical = true, damage)
}
override fun onActionFailure(r: Random): AttackResult {
return AttackResult(rollSucceeded = false, rollWasCritical = false, 0)
}
}

View File

@ -1,26 +1,27 @@
package simulation
package simulation.fifthEd
import simulation.*
import simulation.dice.*
import java.util.*
interface ActionResult{}
/**
* Attack defines the interface for performing an attack action.
* It handles rolling attack dice, calculating modifiers, checking for hits/crits, and triggering damage calculation.
*/
interface AttackAction {
/**
* We can't call this an 'attack' roll because it could be the case that we're attacking by forcing a DC. So whoever
* is taking the positive action makes this roll. For melee attacks, this is a normal attack. For spell checks this
* would be the save roll.
*/
val actionRoll: CritDice
val actionRollInfo :ActionRollInfo
/**
* Similar to [actionRoll], we cannot call this 'defense', because it might be a spell DC. This is the value of the
* Similar to [actionRollInfo], we cannot call this 'defense', because it might be a spell DC. This is the value of the
* responder to the action. In a melee attack, this is AC. For a spell check, this would be the DC.
*/
val responseValue: Int
/**
* calculateDamage calculates the damage for an attack by:
* - Rolling the relevant action
@ -31,34 +32,34 @@ interface AttackAction {
*/
fun calculateDamage(r: Random, rollType: RollType): AttackResult {
val attackResult = actionRoll.roll(r, rollType)
val attackBonus = actionRoll.evaluateModifiers(r, false)
val attackResult = actionRollInfo.actionRoll.roll(r, rollType)
val attackBonus = actionRollInfo.actionRoll.evaluateModifiers(r, false)
return if (isHit(attackResult, attackBonus)) {
if (actionRoll.isCrit(attackResult)) {
onCriticalHit(r)
if (actionRollInfo.actionRoll.isCrit(attackResult)) {
onCriticalAction(r)
} else {
onNormalHit(r)
onNormalAction(r)
}
} else {
onMiss(r)
onActionFailure(r)
}
}
private fun isHit(roll: RollResult, actionBonus: Int): Boolean {
if (roll.result == 1){
if (roll.result == 1) {
return false
}
//ties go to the roller
return (roll.result + actionBonus) >= responseValue
}
fun onNormalHit(r: Random): AttackResult
fun onNormalAction(r: Random): AttackResult
fun onCriticalHit(r: Random): AttackResult
fun onCriticalAction(r: Random): AttackResult
fun onMiss(r: Random): AttackResult
fun onActionFailure(r: Random): AttackResult
}
@ -82,8 +83,8 @@ class MeleeAttackBuilder(
private val dmgRollString: String,
private val defense: Int
) {
private val attackModifiers = ArrayList<Modifier<Int>>()
private val damageModifiers = ArrayList<Modifier<Int>>()
private val attackModifiers = ArrayList<DiceModifier<Int>>()
private val damageModifiers = ArrayList<DiceModifier<Int>>()
fun withAtkBonus(flat: Int): MeleeAttackBuilder {
attackModifiers.add(FlatModifier(flat))
@ -117,9 +118,9 @@ class MeleeAttackBuilder(
fun build(): SimpleMeleeAttackAction {
return SimpleMeleeAttackAction(
MeleeAttack(
Dice.critDice(attackRollString, attackModifiers),
Dice.plainDice(dmgRollString, damageModifiers)
ActionRollInfo(
DiceBag.critDice(attackRollString, attackModifiers),
DiceBag.plainDice(dmgRollString, damageModifiers)
),
defense
)

View File

@ -0,0 +1,10 @@
package simulation.fifthEd
/**
* Represents the result of an attack in a simulation.
*
* @param rollSucceeded Whether the attack successfully hit the target.
* @param rollWasCritical Whether the attack resulted in a critical hit on the target.
* @param resultingDamage The amount of damage dealt to the target from this attack.
*/
data class AttackResult(val rollSucceeded: Boolean, val rollWasCritical: Boolean, val resultingDamage: Int): ActionResult

View File

@ -0,0 +1,61 @@
package simulation.fifthEd
import java.util.*
enum class SpellMitigation(val modifier: Int) {
Half(2),
Quarter(4),
NoDamage(-1);
}
interface SpellSaveAttack : AttackAction {
val saveDenominator: SpellMitigation
val perfectDefense: Boolean
override fun onNormalAction(r: Random): AttackResult {
return when (saveDenominator) {
//save-or-suck style spells
SpellMitigation.NoDamage -> AttackResult(
rollSucceeded = true,
rollWasCritical = false,
resultingDamage = 0
)
else -> {
//normal partial mitigation
val damageRoll = actionRollInfo.damageRoll.roll(r)
AttackResult(
rollSucceeded = true,
rollWasCritical = false,
resultingDamage = damageRoll.result / saveDenominator.modifier
)
}
}
}
override fun onCriticalAction(r: Random): AttackResult { // read this as "on critical save"
return if (perfectDefense) {
AttackResult(rollSucceeded = true, rollWasCritical = true, resultingDamage = 0)
} else {
onNormalAction(r)
}
}
override fun onActionFailure(r: Random): AttackResult { //read this on
val damageRoll = actionRollInfo.damageRoll.roll(r)
return AttackResult(rollSucceeded = false, rollWasCritical = false, damageRoll.result)
}
}
class SpellSaveAttackImp(
override val actionRollInfo: ActionRollInfo,
override val responseValue: Int,
override val saveDenominator: SpellMitigation,
override val perfectDefense: Boolean = false
) : SpellSaveAttack

View File

@ -1,6 +1,9 @@
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
@ -10,7 +13,7 @@ class AttackDiceTest {
fun testAttackDiceImplementation() {
val r = Random()
// Test no crit below threshold
val dice = Dice.critDice("1d20")
val dice = DiceBag.critDice("1d20")
val result = dice.roll(r)
if (result.result < 20) {
Assertions.assertFalse(dice.isCrit(result))
@ -18,7 +21,7 @@ class AttackDiceTest {
Assertions.assertTrue(dice.isCrit(result))
}
// Test no crit below threshold
val dice2 = Dice.critDice("1d20c10")
val dice2 = DiceBag.critDice("1d20c10")
val result2 = dice2.roll(r)
if (result2.result < 10) {
Assertions.assertFalse(dice2.isCrit(result2))
@ -27,7 +30,7 @@ class AttackDiceTest {
}
// Test crit threshold other than max
val dice3 = Dice.critDice("2d10c8")
val dice3 = DiceBag.critDice("2d10c8")
val result3 = dice3.roll(r)
if (result3.result >= 8) {
Assertions.assertTrue(dice3.isCrit(result3))
@ -41,9 +44,9 @@ class AttackDiceTest {
val trueCritResult = RollResult(1, 20, 20)
val fakeCritResult = RollResult(1, 20, 19)
val defaultRoll = Dice.critDice("1d20")
val verboseDefaultCrit = Dice.critDice("1d20c20")
val normalModifiedCrit = Dice.critDice("1d20c19")
val defaultRoll = DiceBag.critDice("1d20")
val verboseDefaultCrit = DiceBag.critDice("1d20c20")
val normalModifiedCrit = DiceBag.critDice("1d20c19")
Assertions.assertFalse(defaultRoll.isCrit(fakeCritResult))
Assertions.assertFalse(verboseDefaultCrit.isCrit(fakeCritResult))

View File

@ -3,16 +3,20 @@ package simulation
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 {
private val random = Random(1)
private val d20 = Dice.plainDice("1d20")
private val d20 = DiceBag.plainDice("1d20")
@Test
fun roll_normal() {
val dice = Dice.plainDice("2d6")
val dice = DiceBag.plainDice("2d6")
val result = dice.roll(random, RollType.Normal)
val result2 = dice.roll(random)
@ -26,7 +30,7 @@ internal class DiceTests {
@Test
fun roll_advantage() {
val dice = Dice.plainDice("2d6")
val dice = DiceBag.plainDice("2d6")
val result = dice.roll(random, RollType.Advantage)
assertEquals(2, result.min)
@ -37,7 +41,7 @@ internal class DiceTests {
@Test
fun roll_disadvantage() {
val dice = Dice.plainDice("2d6")
val dice = DiceBag.plainDice("2d6")
val result = dice.roll(random, RollType.Disadvantage)
assertEquals(2, result.min)
@ -49,7 +53,7 @@ internal class DiceTests {
fun evaluate_modifiers() {
val mod1 = FlatModifier(1)
val mod2 = FlatModifier(2)
val dice = Dice.plainDice("1d20", arrayListOf(mod1, mod2))
val dice = DiceBag.plainDice("1d20", arrayListOf(mod1, mod2))
val bonus = dice.evaluateModifiers(random)
@ -63,7 +67,7 @@ internal class DiceTests {
RollType.entries.parallelStream()
.forEach {
val dice = Dice.plainDice(rollString)
val dice = DiceBag.plainDice(rollString)
val r = Random(1)
val rollType = it
repeat(iterations) {
@ -95,7 +99,7 @@ internal class DiceTests {
RollType.entries.parallelStream()
.forEach {
val dice = Dice.plainDice(rollString)
val dice = DiceBag.plainDice(rollString)
val r = Random(1)
for (i in 0..<iterations) {
val res = dice.roll(r, it)
@ -125,7 +129,7 @@ internal class DiceTests {
val expectedAverageLowerBound = ((n + (n * max)) / 2) * (1 - tolerance)
val expectedAverageUpperBound = ((n + (n * max)) / 2) * (1 + tolerance)
val dice = Dice.plainDice(rollString)
val dice = DiceBag.plainDice(rollString)
var total = 0L
repeat(iterations) {
total += dice.roll(random).result.toLong()
@ -149,7 +153,7 @@ internal class DiceTests {
val expectedAverageUpperBound = ((n + (n * max)) / 2) * tolerance
val dice = Dice.plainDice(rollString)
val dice = DiceBag.plainDice(rollString)
var total = 0L
repeat(iterations) {
@ -173,7 +177,7 @@ internal class DiceTests {
val expectedAverageLowerBound = ((n + (n * max)) / 2) * tolerance
val dice = Dice.plainDice(rollString)
val dice = DiceBag.plainDice(rollString)
var total = 0L
@ -192,7 +196,7 @@ internal class DiceTests {
@Test
fun verifyDistribution() {
val size = 20
val dice = Dice.plainDice("1d20")
val dice = DiceBag.plainDice("1d20")
val iterations = 10_000_000
val tolerance = 0.05 //5% wiggle on distribution

View File

@ -1,5 +1,11 @@
package simulation
import simulation.dice.DiceBag
import simulation.dice.RollType
import simulation.fifthEd.ActionRollInfo
import simulation.fifthEd.AttackResult
import simulation.fifthEd.AttackSimulatorModel
import simulation.fifthEd.SimpleMeleeAttackAction
import kotlin.test.Test
class MeleeAttackTest {
@ -9,19 +15,23 @@ class MeleeAttackTest {
val itt = 1_000_000
val simulator = Simulator.getInstance<AttackResult>(Runtime.getRuntime().availableProcessors())
val attackAction = MeleeAttack(Dice.critDice("1d20c19"), Dice.plainDice("1d8"))
val attackAction = ActionRollInfo(DiceBag.critDice("1d20c19"), DiceBag.plainDice("1d8"))
val critAttack = SimpleMeleeAttackAction(
attackAction,
10
)
val normalAttackModel = AttackSimulatorModel(itt, critAttack, RollType.Normal)
val normalResults = simulator.doSimulation(normalAttackModel)
AttackResult.printSimulationStatistics(normalResults, "Normal Attack")
val report = ReportBuilder.getInstance()
.addRateMetric("Hit Rate"){it.rollSucceeded}
.addRateMetric("Crit Rate"){it.rollWasCritical}
.addAverageMetric("Avg Dmg") { it.resultingDamage.toLong() }
.build("Normal Attack")
val metrics = report.computeResults(normalResults)
println(Report.formatReport(report.name, metrics.results))
}
}