From 0107aeffb019ba600ddb2ea02d5d4923f7c42b8e Mon Sep 17 00:00:00 2001 From: dtookey Date: Sun, 3 Sep 2023 09:04:01 -0400 Subject: [PATCH] cleaning things up, trying to get the API into a sensible place --- src/main/kotlin/entries/BGSimulation.kt | 68 ++++++++++++------ .../simulation/{Attack.kt => AttackAction.kt} | 71 +++++++++++++++++-- src/main/kotlin/simulation/AttackResult.kt | 2 +- .../simulation/{AttackDice.kt => CritDice.kt} | 46 ++++++------ src/main/kotlin/simulation/Dice.kt | 14 ++-- src/main/kotlin/simulation/MeleeAttack.kt | 12 ++-- src/main/kotlin/simulation/Modifier.kt | 2 +- .../controllers/TemporalControllerTest.kt | 42 ++--------- .../controllers/VisionControllerTest.kt | 23 ------ src/test/kotlin/simulation/AttackDiceTest.kt | 12 ++-- src/test/kotlin/simulation/DiceTest.kt | 41 ++++++----- src/test/kotlin/simulation/MeleeAttackTest.kt | 8 +-- src/test/kotlin/util/HelperFunctionsTest.kt | 10 +-- 13 files changed, 198 insertions(+), 153 deletions(-) rename src/main/kotlin/simulation/{Attack.kt => AttackAction.kt} (52%) rename src/main/kotlin/simulation/{AttackDice.kt => CritDice.kt} (88%) delete mode 100644 src/test/kotlin/controllers/VisionControllerTest.kt diff --git a/src/main/kotlin/entries/BGSimulation.kt b/src/main/kotlin/entries/BGSimulation.kt index 0d56bbe..9856c16 100644 --- a/src/main/kotlin/entries/BGSimulation.kt +++ b/src/main/kotlin/entries/BGSimulation.kt @@ -2,35 +2,61 @@ package entries import simulation.* -fun main(){ +fun main() { val start = System.currentTimeMillis() doSimulation() val finish = System.currentTimeMillis() - println("Simulation finished in: ${finish-start}ms") + println("Simulation finished in: ${finish - start}ms") } -fun doSimulation(){ - val itt = 10_000_000 + + +fun doSimulation() { + val rounds = 10_000_000 + + val defense = 18 val simulator = Simulator.getInstance(Runtime.getRuntime().availableProcessors()) - val attackModifiers = listOf(FlatModifier(4), DiceBonusModifier("1d4")) - - RollType.entries.parallelStream() - .forEach{ - val normalAttack = SimpleMeleeAttack( - actionRoll = AttackDice("1d20", it, attackModifiers), - damageRoll = Dice.makeDice("1d8"), - 19 - ) - val normalAttackModel = AttackSimulatorModel(itt, normalAttack) - val normalResults = simulator.doSimulation(normalAttackModel) + val attackDice = "1d20" + val weaponDice = "2d6" - AttackResult.printSimulationStatistics(normalResults, "Normal Attack (${it.name})") + val normalAttack = MeleeAttackBuilder(attackDice, weaponDice, defense) + .withAtkBonus(2) // weapon bonus + .withAtkBonus(4) // proficiency bonus + .withAtkBonus(5) // str mod + .withDmgBonus(2) // weapon bonus + .withDmgBonus(5) // str mod + .build() + + + val gwmAttack = MeleeAttackBuilder(attackDice, weaponDice, defense) + .withAtkBonus(2)// weapon bonus + .withAtkBonus(4)// proficiency bonus + .withAtkBonus(5)// str mod + .withAtkBonus(-5)// gwm penalty + .withDmgBonus(2) // weapon bonus + .withDmgBonus(5) // str mod + .withDmgBonus(10) // gwm bonus + .build() + + + mapOf(Pair("normalAttack", normalAttack), Pair("gwmAttack", gwmAttack)) + .entries + .forEach{ attackInfo -> + + val label = attackInfo.key + val attack = attackInfo.value + + RollType.entries.parallelStream() +// .filter{it == RollType.Advantage || it == RollType.Normal} + .forEach { + + val attackModel = AttackSimulatorModel(rounds, attack, it) + val normalResults = simulator.doSimulation(attackModel) + + AttackResult.printSimulationStatistics(normalResults, "$label (${it.name})") + } } - - - - -} \ No newline at end of file +} diff --git a/src/main/kotlin/simulation/Attack.kt b/src/main/kotlin/simulation/AttackAction.kt similarity index 52% rename from src/main/kotlin/simulation/Attack.kt rename to src/main/kotlin/simulation/AttackAction.kt index 416c167..6482ce5 100644 --- a/src/main/kotlin/simulation/Attack.kt +++ b/src/main/kotlin/simulation/AttackAction.kt @@ -6,14 +6,14 @@ import java.util.* * 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 Attack { +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: AttackDice + val actionRoll: CritDice /** * Similar to [actionRoll], we cannot call this 'defense', because it might be a spell DC. This is the value of the @@ -29,9 +29,9 @@ interface Attack { * - Calling onCriticalHit, onNormalHit or onMiss based on hit result * - Returning the AttackResult */ - fun calculateDamage(r: Random): AttackResult { + fun calculateDamage(r: Random, rollType: RollType): AttackResult { - val attackResult = actionRoll.roll(r) + val attackResult = actionRoll.roll(r, rollType) val attackBonus = actionRoll.evaluateModifiers(r, false) @@ -42,11 +42,14 @@ interface Attack { onNormalHit(r) } } else { - onMiss(r) + onMiss(r) } } private fun isHit(roll: RollResult, actionBonus: Int): Boolean { + if (roll.result == 1){ + return false + } //ties go to the roller return (roll.result + actionBonus) >= responseValue } @@ -63,8 +66,62 @@ interface Attack { * AttackSimulatorModel simulates attacks by running [sampleSize] simulations using the provided [attack] instance. * It implements [SimulationModel] to run the simulations and return [AttackResult]. */ -class AttackSimulatorModel(override val sampleSize: Int, private val attack: Attack) : SimulationModel { +class AttackSimulatorModel( + override val sampleSize: Int, + private val attack: AttackAction, + private val rollType: RollType +) : SimulationModel { override fun simulate(r: Random): AttackResult { - return attack.calculateDamage(r) + return attack.calculateDamage(r, rollType) } } + + +class MeleeAttackBuilder( + private val attackRollString: String, + private val dmgRollString: String, + private val defense: Int +) { + private val attackModifiers = ArrayList>() + private val damageModifiers = ArrayList>() + + fun withAtkBonus(flat: Int): MeleeAttackBuilder { + attackModifiers.add(FlatModifier(flat)) + return this + } + + fun withAtkBonus(dice: Dice): MeleeAttackBuilder { + attackModifiers.add(DiceBonusModifier(dice)) + return this + } + + fun withAtkPenalty(dice: Dice): MeleeAttackBuilder { + attackModifiers.add(DicePenaltyModifier(dice)) + return this + } + + fun withDmgBonus(flat: Int): MeleeAttackBuilder { + damageModifiers.add(FlatModifier(flat)) + return this + } + + fun withDmgBonus(dice: Dice): MeleeAttackBuilder { + damageModifiers.add(DiceBonusModifier(dice)) + return this + } + + fun withDmgPenalty(dice: Dice): MeleeAttackBuilder { + damageModifiers.add(DicePenaltyModifier(dice)) + return this + } + + fun build(): SimpleMeleeAttackAction { + return SimpleMeleeAttackAction( + MeleeAttack( + Dice.critDice(attackRollString, attackModifiers), + Dice.plainDice(dmgRollString, damageModifiers) + ), + defense + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/AttackResult.kt b/src/main/kotlin/simulation/AttackResult.kt index 04b1942..b09d837 100644 --- a/src/main/kotlin/simulation/AttackResult.kt +++ b/src/main/kotlin/simulation/AttackResult.kt @@ -26,7 +26,7 @@ data class AttackResult(val attackWasHit: Boolean, val attackWasCrit: Boolean, v val reportString = "Hit Rate: %.2f\tCrit Rate: %.2f\tAvg Dmg: %.2f".format(hitRate, critRate, avgDamage) if(label.isNotBlank()){ - println("[$label] $reportString") + println("[$label]\t$reportString") }else{ println(reportString) } diff --git a/src/main/kotlin/simulation/AttackDice.kt b/src/main/kotlin/simulation/CritDice.kt similarity index 88% rename from src/main/kotlin/simulation/AttackDice.kt rename to src/main/kotlin/simulation/CritDice.kt index fac1012..d33a1fd 100644 --- a/src/main/kotlin/simulation/AttackDice.kt +++ b/src/main/kotlin/simulation/CritDice.kt @@ -1,5 +1,28 @@ package simulation +/** + * Dice with a modifiable crit threshold + */ +interface CritDice : Dice { + val critThreshold: Int + + + /** + * Checks if the given [RollResult] meets or exceeds the critical hit threshold + * for these attack dice. + * + * The critical hit threshold is determined based on the roll string used to construct + * this [CritDice] instance. + * + * @param result The [RollResult] to check for crit. + * @return True if result meets or exceeds the crit threshold. + */ + fun isCrit(result: RollResult): Boolean { + return result.result >= critThreshold + } +} + + /** * AttackDice class represents dice used for attack rolls. * If the roll string contains a 'c' modifier, the value after 'c' is used as the crit threshold. @@ -16,15 +39,14 @@ package simulation * * The isCrit() method checks if a roll result meets the crit threshold. */ -class AttackDice( +class AttackDiceImpl( override val rollString: String, - override val rollType: RollType = RollType.Normal, override val modifiers: List> = ArrayList() -) : Dice { +) : CritDice { override val nDice: Int override val dieSize: Int - private val critThreshold: Int + override val critThreshold: Int init { val cleanRollString = rollString.lowercase() @@ -42,20 +64,4 @@ class AttackDice( critThreshold = dieSize * nDice } } - - - /** - * Checks if the given [RollResult] meets or exceeds the critical hit threshold - * for these attack dice. - * - * The critical hit threshold is determined based on the roll string used to construct - * this [AttackDice] instance. - * - * @param result The [RollResult] to check for crit. - * @return True if result meets or exceeds the crit threshold. - */ - fun isCrit(result: RollResult): Boolean { - return result.result >= critThreshold - } - } \ No newline at end of file diff --git a/src/main/kotlin/simulation/Dice.kt b/src/main/kotlin/simulation/Dice.kt index e6291c7..f79372d 100644 --- a/src/main/kotlin/simulation/Dice.kt +++ b/src/main/kotlin/simulation/Dice.kt @@ -35,15 +35,18 @@ data class RollResult(val min: Int, val max: Int, val result: Int) interface Dice { val rollString: String - val rollType: RollType val modifiers: List> val nDice: Int val dieSize: Int companion object { - fun makeDice(rollString: String, rollType: RollType = Normal, modifiers: List> = ArrayList()): Dice { - return SimpleDice(rollString, rollType, modifiers) + fun plainDice(rollString: String, modifiers: List> = ArrayList()): Dice { + return DiceImpl(rollString, modifiers) + } + + fun critDice(rollString: String, modifiers: List> = ArrayList()): CritDice{ + return AttackDiceImpl(rollString, modifiers) } } @@ -58,7 +61,7 @@ interface Dice { * @return The result of the dice roll with modifiers applied * @see RollType */ - fun roll(r: Random): RollResult { + fun roll(r: Random, rollType: RollType = Normal): RollResult { val range = (dieSize * nDice) - nDice val result = when (rollType) { Advantage -> advantageRoll(r, nDice, range) @@ -98,9 +101,8 @@ interface Dice { } } -private class SimpleDice( +private class DiceImpl( override val rollString: String, - override val rollType: RollType = Normal, override val modifiers: List> = ArrayList() ) : Dice { override val nDice: Int diff --git a/src/main/kotlin/simulation/MeleeAttack.kt b/src/main/kotlin/simulation/MeleeAttack.kt index 737b491..b50d7f2 100644 --- a/src/main/kotlin/simulation/MeleeAttack.kt +++ b/src/main/kotlin/simulation/MeleeAttack.kt @@ -1,6 +1,9 @@ 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. @@ -9,11 +12,12 @@ import java.util.* * @param damageRoll The dice roll used to determine damage if attack hits. * @param responseValue The defense value the attack must exceed to hit. */ -class SimpleMeleeAttack( - override val actionRoll: AttackDice, - val damageRoll: Dice, +class SimpleMeleeAttackAction( + attackInfo: MeleeAttack, override val responseValue: Int -) : Attack { +) : 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) diff --git a/src/main/kotlin/simulation/Modifier.kt b/src/main/kotlin/simulation/Modifier.kt index aa1af52..d97be39 100644 --- a/src/main/kotlin/simulation/Modifier.kt +++ b/src/main/kotlin/simulation/Modifier.kt @@ -30,7 +30,7 @@ interface Modifier { */ class DiceBonusModifier(private val dice: Dice) : Modifier { - constructor(diceString: String):this(Dice.makeDice(diceString)) + constructor(diceString: String):this(Dice.plainDice(diceString)) override fun getBonus(r: Random, crit: Boolean): Int { return if (crit){ diff --git a/src/test/kotlin/controllers/TemporalControllerTest.kt b/src/test/kotlin/controllers/TemporalControllerTest.kt index ef1e016..c0acd78 100644 --- a/src/test/kotlin/controllers/TemporalControllerTest.kt +++ b/src/test/kotlin/controllers/TemporalControllerTest.kt @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test import java.util.concurrent.TimeUnit import org.junit.jupiter.api.Assertions.* +import kotlin.math.max import kotlin.random.Random internal class TemporalControllerTest { @@ -11,25 +12,7 @@ internal class TemporalControllerTest { /** * Creates an instance of TemporalController for testing. */ - private val controller = object : TemporalController { - /** - * Sleeps for around the given duration, with variance. - * - * @param baseDuration the desired duration to sleep - * @param maxAdditionalDuration the amount of variance in the actual duration - */ - override fun sleep(baseDuration: Long, maxAdditionalDuration: Long) { - if (baseDuration < 0 || maxAdditionalDuration <= 1) { - return - } - - val dSize = (maxAdditionalDuration) / 2 - val r1 = Random.nextLong(dSize) - val r2 = Random.nextLong(dSize) - - sleep(baseDuration + r1 + r2) - } - } + private val controller = object : TemporalController{} /** * Tests that [TemporalController.sleep] blocks for the given duration. @@ -37,7 +20,7 @@ internal class TemporalControllerTest { @Test fun `sleep blocks for given duration`() { val start = System.currentTimeMillis() - controller.sleep(500) + controller.sleep(501) val end = System.currentTimeMillis() val elapsed = end - start @@ -58,9 +41,8 @@ internal class TemporalControllerTest { val end = System.currentTimeMillis() val elapsed = end - start - val lowerBound = (duration - variance / 2) - 1 - val upperBound = (duration + variance / 2) + 20 - println("elapsed: $elapsed, [$lowerBound, $upperBound]") + val lowerBound = duration + val upperBound = duration + variance assertTrue(elapsed >= lowerBound) assertTrue(elapsed <= upperBound) } @@ -79,18 +61,4 @@ internal class TemporalControllerTest { assertTrue(elapsed < 10) // assert sleep was very short } - /** - * Tests that [TemporalController.sleep] returns immediately - * if the variance parameter is 0. - */ - @Test - fun `sleepWithVariance returns immediately if variance is 0`() { - val start = System.currentTimeMillis() - controller.sleep(100, 0) - val end = System.currentTimeMillis() - val elapsed = end - start - - assertTrue(elapsed < 10) // assert sleep was very short - } - } diff --git a/src/test/kotlin/controllers/VisionControllerTest.kt b/src/test/kotlin/controllers/VisionControllerTest.kt deleted file mode 100644 index 77acfa2..0000000 --- a/src/test/kotlin/controllers/VisionControllerTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package controllers - -import java.awt.image.BufferedImage -import java.nio.file.Paths -import javax.imageio.ImageIO -import kotlin.test.Test -import kotlin.test.assertNotNull - -class VisionControllerTest { - - @Test - fun testImageCapture() { - val vc = ConcreteVisionController() - val bi = vc.takeScreenshotOfForeground() - assertNotNull(bi) - - for (i in 0..= 8) { Assertions.assertTrue(dice3.isCrit(result3)) @@ -41,9 +41,9 @@ class AttackDiceTest { val trueCritResult = RollResult(1, 20, 20) val fakeCritResult = RollResult(1, 20, 19) - val defaultRoll = AttackDice("1d20") - val verboseDefaultCrit = AttackDice("1d20c20") - val normalModifiedCrit = AttackDice("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)) diff --git a/src/test/kotlin/simulation/DiceTest.kt b/src/test/kotlin/simulation/DiceTest.kt index cca3be4..e2080e9 100644 --- a/src/test/kotlin/simulation/DiceTest.kt +++ b/src/test/kotlin/simulation/DiceTest.kt @@ -7,23 +7,27 @@ import java.util.* internal class DiceTests { private val random = Random(1) - private val d20 = Dice.makeDice("1d20") + private val d20 = Dice.plainDice("1d20") @Test fun roll_normal() { - val dice = Dice.makeDice("2d6") - val result = dice.roll(random) + val dice = Dice.plainDice("2d6") + val result = dice.roll(random, RollType.Normal) + val result2 = dice.roll(random) assertEquals(2, result.min) assertEquals(12, result.max) Assertions.assertTrue(result.min <= result.result && result.result <= result.max) + assertEquals(2, result2.min) + assertEquals(12, result2.max) + Assertions.assertTrue(result2.min <= result2.result && result2.result <= result2.max) } @Test fun roll_advantage() { - val dice = Dice.makeDice("2d6", RollType.Advantage) - val result = dice.roll(random) + val dice = Dice.plainDice("2d6") + val result = dice.roll(random, RollType.Advantage) assertEquals(2, result.min) assertEquals(12, result.max) @@ -33,8 +37,8 @@ internal class DiceTests { @Test fun roll_disadvantage() { - val dice = Dice.makeDice("2d6", RollType.Disadvantage) - val result = dice.roll(random) + val dice = Dice.plainDice("2d6") + val result = dice.roll(random, RollType.Disadvantage) assertEquals(2, result.min) assertEquals(12, result.max) @@ -45,7 +49,7 @@ internal class DiceTests { fun evaluate_modifiers() { val mod1 = FlatModifier(1) val mod2 = FlatModifier(2) - val dice = Dice.makeDice("1d20", RollType.Normal, arrayListOf(mod1, mod2)) + val dice = Dice.plainDice("1d20", arrayListOf(mod1, mod2)) val bonus = dice.evaluateModifiers(random) @@ -59,10 +63,11 @@ internal class DiceTests { RollType.entries.parallelStream() .forEach { - val dice = Dice.makeDice(rollString, it) + val dice = Dice.plainDice(rollString) val r = Random(1) + val rollType = it repeat(iterations) { - val res = dice.roll(r) + val res = dice.roll(r, rollType) Assertions.assertTrue(res.min <= res.result) Assertions.assertTrue(res.result <= res.max) } @@ -90,10 +95,10 @@ internal class DiceTests { RollType.entries.parallelStream() .forEach { - val dice = Dice.makeDice(rollString, it) + val dice = Dice.plainDice(rollString) val r = Random(1) for (i in 0..(Runtime.getRuntime().availableProcessors()) - val critAttack = SimpleMeleeAttack( - actionRoll = AttackDice("1d20c19"), - damageRoll = Dice.makeDice("1d8"), + val attackAction = MeleeAttack(Dice.critDice("1d20c19"), Dice.plainDice("1d8")) + val critAttack = SimpleMeleeAttackAction( + attackAction, 10 ) - val normalAttackModel = AttackSimulatorModel(itt, critAttack) + val normalAttackModel = AttackSimulatorModel(itt, critAttack, RollType.Normal) val normalResults = simulator.doSimulation(normalAttackModel) diff --git a/src/test/kotlin/util/HelperFunctionsTest.kt b/src/test/kotlin/util/HelperFunctionsTest.kt index 8c9f542..cf0dbd8 100644 --- a/src/test/kotlin/util/HelperFunctionsTest.kt +++ b/src/test/kotlin/util/HelperFunctionsTest.kt @@ -9,7 +9,7 @@ class HelperFunctionsTest { @Test - fun `test getRandomLongFromNormalDistribution generates normal distribution`() { + fun `test getApproximatelyNormalLong generates normal distribution`() { // Generate a large number of samples val numSamples = 10000 @@ -28,7 +28,7 @@ class HelperFunctionsTest { } - @Test + fun `test benchmark getNextLongJanky`() { val iterations = 1000000 val upperBound = 1000L @@ -52,7 +52,7 @@ class HelperFunctionsTest { } } - @Test + fun `test benchmark getNextLongGaussian`() { val iterations = 1000000 @@ -91,9 +91,9 @@ class HelperFunctionsTest { } - @Test + fun `test Direct Comparison of performance`() { - val heats = 1000 + val heats = 10 val iterations = 1000000 val upperBound = 1000L