From d0f7c3bd1199a750e18cf38be900c258fff5ada5 Mon Sep 17 00:00:00 2001 From: dtookey Date: Sun, 3 Sep 2023 14:05:50 -0400 Subject: [PATCH] madness has taken hold of me --- src/main/kotlin/entries/BGSimulation.kt | 84 ++++++++++--- src/main/kotlin/simulation/AttackResult.kt | 37 ------ src/main/kotlin/simulation/MeleeAttack.kt | 37 ------ src/main/kotlin/simulation/Reporting.kt | 111 ++++++++++++++++++ src/main/kotlin/simulation/Simulator.kt | 4 +- .../kotlin/simulation/{ => dice}/CritDice.kt | 6 +- src/main/kotlin/simulation/{ => dice}/Dice.kt | 46 ++++---- src/main/kotlin/simulation/dice/DiceBag.kt | 19 +++ .../{Modifier.kt => dice/DiceModifier.kt} | 18 +-- src/main/kotlin/simulation/dice/RerollDice.kt | 32 +++++ .../simulation/fifthEd/ActionRollInfo.kt | 39 ++++++ .../simulation/{ => fifthEd}/AttackAction.kt | 47 ++++---- .../kotlin/simulation/fifthEd/AttackResult.kt | 10 ++ .../simulation/fifthEd/SpellSaveAttack.kt | 61 ++++++++++ src/test/kotlin/simulation/AttackDiceTest.kt | 15 ++- src/test/kotlin/simulation/DiceTest.kt | 26 ++-- src/test/kotlin/simulation/MeleeAttackTest.kt | 18 ++- 17 files changed, 435 insertions(+), 175 deletions(-) delete mode 100644 src/main/kotlin/simulation/AttackResult.kt delete mode 100644 src/main/kotlin/simulation/MeleeAttack.kt create mode 100644 src/main/kotlin/simulation/Reporting.kt rename src/main/kotlin/simulation/{ => dice}/CritDice.kt (93%) rename src/main/kotlin/simulation/{ => dice}/Dice.kt (81%) create mode 100644 src/main/kotlin/simulation/dice/DiceBag.kt rename src/main/kotlin/simulation/{Modifier.kt => dice/DiceModifier.kt} (73%) create mode 100644 src/main/kotlin/simulation/dice/RerollDice.kt create mode 100644 src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt rename src/main/kotlin/simulation/{ => fifthEd}/AttackAction.kt (70%) create mode 100644 src/main/kotlin/simulation/fifthEd/AttackResult.kt create mode 100644 src/main/kotlin/simulation/fifthEd/SpellSaveAttack.kt diff --git a/src/main/kotlin/entries/BGSimulation.kt b/src/main/kotlin/entries/BGSimulation.kt index 9856c16..c9ebe07 100644 --- a/src/main/kotlin/entries/BGSimulation.kt +++ b/src/main/kotlin/entries/BGSimulation.kt @@ -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(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>, + rounds: Int, + reportFactory: ReportBuilder +) { + val simulator = Simulator.getInstance(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.getDamage(): MetricResult? { + + forEach { + if (it.label == avgDamage) { + return it + } + } + return null +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/AttackResult.kt b/src/main/kotlin/simulation/AttackResult.kt deleted file mode 100644 index b09d837..0000000 --- a/src/main/kotlin/simulation/AttackResult.kt +++ /dev/null @@ -1,37 +0,0 @@ -package simulation - -import java.util.ArrayList - - -private fun ArrayList.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, 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) - } - } - - } - -} diff --git a/src/main/kotlin/simulation/MeleeAttack.kt b/src/main/kotlin/simulation/MeleeAttack.kt deleted file mode 100644 index b50d7f2..0000000 --- a/src/main/kotlin/simulation/MeleeAttack.kt +++ /dev/null @@ -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) - } - -} diff --git a/src/main/kotlin/simulation/Reporting.kt b/src/main/kotlin/simulation/Reporting.kt new file mode 100644 index 0000000..dc57bdd --- /dev/null +++ b/src/main/kotlin/simulation/Reporting.kt @@ -0,0 +1,111 @@ +package simulation + +import simulation.fifthEd.AttackResult +import kotlin.math.pow + +data class MetricReport(val name: String, val results: List) +data class MetricResult(val label: String, val metricValue: Double) + +interface Report { + val name: String + val metrics: List> + + companion object{ + fun formatReport(name: String, results: List): String { + val builder = StringBuilder() + results.forEach { + builder.append("${it.label}: %.2f".format(it.metricValue)) + .append('\t') + + } + return builder.toString() + } + } + + + fun computeResults(results: List): MetricReport{ + val m = ArrayList(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>) : Report + +interface ReportBuilder { + val metrics: MutableList> + + 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> = ArrayList() + +} + +interface Metric { + val metricName: String + fun mapToMetric(results: List): T + + fun formatResults(result: T): String + +} + +class AverageMetric(override val metricName: String, private val fieldMapFn: (AttackResult) -> Long) : Metric { + override fun mapToMetric(results: List): 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 { + override fun mapToMetric(results: List): 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 { + override fun mapToMetric(results: List): 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 \ No newline at end of file diff --git a/src/main/kotlin/simulation/Simulator.kt b/src/main/kotlin/simulation/Simulator.kt index d88588d..7f4efe3 100644 --- a/src/main/kotlin/simulation/Simulator.kt +++ b/src/main/kotlin/simulation/Simulator.kt @@ -17,7 +17,7 @@ interface Simulator { companion object { fun getInstance(nThreads: Int = Runtime.getRuntime().availableProcessors() / 2 ): Simulator { - return concreteSimulator(nThreads) + return SimulatorImpl(nThreads) } } @@ -65,5 +65,5 @@ interface Simulator { } -class concreteSimulator(override val nThreads: Int) : +class SimulatorImpl(override val nThreads: Int) : Simulator \ No newline at end of file diff --git a/src/main/kotlin/simulation/CritDice.kt b/src/main/kotlin/simulation/dice/CritDice.kt similarity index 93% rename from src/main/kotlin/simulation/CritDice.kt rename to src/main/kotlin/simulation/dice/CritDice.kt index d33a1fd..f965a8e 100644 --- a/src/main/kotlin/simulation/CritDice.kt +++ b/src/main/kotlin/simulation/dice/CritDice.kt @@ -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> = ArrayList() + override val modifiers: List> = ArrayList() ) : CritDice { override val nDice: Int override val dieSize: Int diff --git a/src/main/kotlin/simulation/Dice.kt b/src/main/kotlin/simulation/dice/Dice.kt similarity index 81% rename from src/main/kotlin/simulation/Dice.kt rename to src/main/kotlin/simulation/dice/Dice.kt index f79372d..74342fd 100644 --- a/src/main/kotlin/simulation/Dice.kt +++ b/src/main/kotlin/simulation/dice/Dice.kt @@ -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> + val modifiers: List> val nDice: Int val dieSize: Int - - companion object { - fun plainDice(rollString: String, modifiers: List> = ArrayList()): Dice { - return DiceImpl(rollString, modifiers) - } - - fun critDice(rollString: String, modifiers: List> = 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 { + 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> = ArrayList() + override val modifiers: List> = 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 } } \ No newline at end of file diff --git a/src/main/kotlin/simulation/dice/DiceBag.kt b/src/main/kotlin/simulation/dice/DiceBag.kt new file mode 100644 index 0000000..8f0fad5 --- /dev/null +++ b/src/main/kotlin/simulation/dice/DiceBag.kt @@ -0,0 +1,19 @@ +package simulation.dice + +object DiceBag { + fun plainDice(rollString: String, modifiers: List> = ArrayList()): Dice { + return DiceImpl(rollString, modifiers) + } + + fun critDice(rollString: String, modifiers: List> = ArrayList()): CritDice { + return AttackDiceImpl(rollString, modifiers) + } + + fun rerollDice( + rollString: String, + rerollThreshold: Int, + modifiers: List> = ArrayList() + ): RerollDice { + return RerollDiceImpl(rollString, rerollThreshold, modifiers) + } +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/Modifier.kt b/src/main/kotlin/simulation/dice/DiceModifier.kt similarity index 73% rename from src/main/kotlin/simulation/Modifier.kt rename to src/main/kotlin/simulation/dice/DiceModifier.kt index d97be39..5abe6f4 100644 --- a/src/main/kotlin/simulation/Modifier.kt +++ b/src/main/kotlin/simulation/dice/DiceModifier.kt @@ -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 { +interface DiceModifier { /** * Generates a bonus integer, potentially using the provided Random instance if needs be. @@ -21,16 +21,16 @@ interface Modifier { } /** - * 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 { +class DiceBonusModifier(private val dice: Dice) : DiceModifier { - 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 { } /** - * 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{ +class DicePenaltyModifier(private val dice: Dice): DiceModifier { 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 { +class FlatModifier(private val bonus: Int) : DiceModifier { override fun getBonus(r: Random, crit: Boolean): Int { return bonus } diff --git a/src/main/kotlin/simulation/dice/RerollDice.kt b/src/main/kotlin/simulation/dice/RerollDice.kt new file mode 100644 index 0000000..d04c5be --- /dev/null +++ b/src/main/kotlin/simulation/dice/RerollDice.kt @@ -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> = ArrayList() +) : RerollDice{ + override val nDice: Int + override val dieSize: Int + + init { + val rollPair = this.defaultParseFn(rollString) + nDice = rollPair.first + dieSize = rollPair.second + } +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt b/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt new file mode 100644 index 0000000..0d5b489 --- /dev/null +++ b/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt @@ -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) + } + +} diff --git a/src/main/kotlin/simulation/AttackAction.kt b/src/main/kotlin/simulation/fifthEd/AttackAction.kt similarity index 70% rename from src/main/kotlin/simulation/AttackAction.kt rename to src/main/kotlin/simulation/fifthEd/AttackAction.kt index 6482ce5..d8bc4f3 100644 --- a/src/main/kotlin/simulation/AttackAction.kt +++ b/src/main/kotlin/simulation/fifthEd/AttackAction.kt @@ -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>() - private val damageModifiers = ArrayList>() + private val attackModifiers = ArrayList>() + private val damageModifiers = ArrayList>() 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 ) diff --git a/src/main/kotlin/simulation/fifthEd/AttackResult.kt b/src/main/kotlin/simulation/fifthEd/AttackResult.kt new file mode 100644 index 0000000..5c26e85 --- /dev/null +++ b/src/main/kotlin/simulation/fifthEd/AttackResult.kt @@ -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 \ No newline at end of file diff --git a/src/main/kotlin/simulation/fifthEd/SpellSaveAttack.kt b/src/main/kotlin/simulation/fifthEd/SpellSaveAttack.kt new file mode 100644 index 0000000..e825866 --- /dev/null +++ b/src/main/kotlin/simulation/fifthEd/SpellSaveAttack.kt @@ -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 \ No newline at end of file diff --git a/src/test/kotlin/simulation/AttackDiceTest.kt b/src/test/kotlin/simulation/AttackDiceTest.kt index 6fc7e58..5936c10 100644 --- a/src/test/kotlin/simulation/AttackDiceTest.kt +++ b/src/test/kotlin/simulation/AttackDiceTest.kt @@ -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)) diff --git a/src/test/kotlin/simulation/DiceTest.kt b/src/test/kotlin/simulation/DiceTest.kt index e2080e9..f6acebd 100644 --- a/src/test/kotlin/simulation/DiceTest.kt +++ b/src/test/kotlin/simulation/DiceTest.kt @@ -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..(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)) } } \ No newline at end of file