diff --git a/src/main/kotlin/entries/BGSimulation.kt b/src/main/kotlin/entries/BGSimulation.kt index 656f81d..61c7214 100644 --- a/src/main/kotlin/entries/BGSimulation.kt +++ b/src/main/kotlin/entries/BGSimulation.kt @@ -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 } diff --git a/src/main/kotlin/simulation/dice/CritDice.kt b/src/main/kotlin/simulation/dice/CritDiceRoller.kt similarity index 94% rename from src/main/kotlin/simulation/dice/CritDice.kt rename to src/main/kotlin/simulation/dice/CritDiceRoller.kt index 7640807..8ab1be6 100644 --- a/src/main/kotlin/simulation/dice/CritDice.kt +++ b/src/main/kotlin/simulation/dice/CritDiceRoller.kt @@ -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> = ArrayList() -) : CritDice { +) : CritDiceRoller { override val nDice: Int override val dieSize: Int diff --git a/src/main/kotlin/simulation/dice/Dice.kt b/src/main/kotlin/simulation/dice/Dice.kt index 74342fd..0ed8beb 100644 --- a/src/main/kotlin/simulation/dice/Dice.kt +++ b/src/main/kotlin/simulation/dice/Dice.kt @@ -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> - 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> = 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> = 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 { - 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> = 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> = ArrayList() + ): RerollDiceRoller { + return RerollDiceImpl(rollString, rerollThreshold, modifiers) } } \ No newline at end of file diff --git a/src/main/kotlin/simulation/dice/DiceBag.kt b/src/main/kotlin/simulation/dice/DiceBag.kt deleted file mode 100644 index 052a2f5..0000000 --- a/src/main/kotlin/simulation/dice/DiceBag.kt +++ /dev/null @@ -1,19 +0,0 @@ -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 CritDiceImpl(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/dice/DiceModifier.kt b/src/main/kotlin/simulation/dice/DiceModifier.kt index 5abe6f4..1d2b777 100644 --- a/src/main/kotlin/simulation/dice/DiceModifier.kt +++ b/src/main/kotlin/simulation/dice/DiceModifier.kt @@ -21,16 +21,16 @@ interface DiceModifier { } /** - * 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 { +class DiceBonusModifier(private val dice: DiceRoller) : DiceModifier { - 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 { } /** - * 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 { +class DicePenaltyModifier(private val dice: DiceRoller): DiceModifier { override fun getBonus(r: Random, crit: Boolean): Int {//can penalties ever crit? return -dice.roll(r).result } diff --git a/src/main/kotlin/simulation/dice/DiceRoller.kt b/src/main/kotlin/simulation/dice/DiceRoller.kt new file mode 100644 index 0000000..e1aed99 --- /dev/null +++ b/src/main/kotlin/simulation/dice/DiceRoller.kt @@ -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> + 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 { + 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> = ArrayList() +) : DiceRoller { + override val nDice: Int + override val dieSize: Int + + init { + val rollPair = defaultParseFn(rollString) + nDice = rollPair.first + dieSize = rollPair.second + } +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/dice/RerollDice.kt b/src/main/kotlin/simulation/dice/RerollDiceRoller.kt similarity index 91% rename from src/main/kotlin/simulation/dice/RerollDice.kt rename to src/main/kotlin/simulation/dice/RerollDiceRoller.kt index d04c5be..79ab896 100644 --- a/src/main/kotlin/simulation/dice/RerollDice.kt +++ b/src/main/kotlin/simulation/dice/RerollDiceRoller.kt @@ -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> = ArrayList() -) : RerollDice{ +) : RerollDiceRoller{ override val nDice: Int override val dieSize: Int diff --git a/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt b/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt index 1049486..a3e11b0 100644 --- a/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt +++ b/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt @@ -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) diff --git a/src/main/kotlin/simulation/fifthEd/AttackAction.kt b/src/main/kotlin/simulation/fifthEd/AttackAction.kt index d8bc4f3..e29bfd2 100644 --- a/src/main/kotlin/simulation/fifthEd/AttackAction.kt +++ b/src/main/kotlin/simulation/fifthEd/AttackAction.kt @@ -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 ) diff --git a/src/test/kotlin/simulation/AttackDiceTest.kt b/src/test/kotlin/simulation/AttackDiceTest.kt index 5936c10..6d9d8cc 100644 --- a/src/test/kotlin/simulation/AttackDiceTest.kt +++ b/src/test/kotlin/simulation/AttackDiceTest.kt @@ -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)) diff --git a/src/test/kotlin/simulation/DiceTest.kt b/src/test/kotlin/simulation/DiceTest.kt index f6acebd..158e4f6 100644 --- a/src/test/kotlin/simulation/DiceTest.kt +++ b/src/test/kotlin/simulation/DiceTest.kt @@ -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..(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