split dice up a bit better, and it should be easier to implement future roll types like reroll-once and other things
This commit is contained in:
parent
2ac759deb4
commit
3ce527b07f
@ -3,19 +3,78 @@ package simulation
|
||||
import java.util.*
|
||||
|
||||
interface Attack {
|
||||
fun attackerSuccessful(r: Random): Boolean
|
||||
|
||||
fun resultingDamage(r: Random, attackSuccessful: Boolean): Int
|
||||
val actionRoll: Dice
|
||||
val passValue: Int
|
||||
|
||||
fun getResultingDamage(r: Random): Int{
|
||||
val success = attackerSuccessful(r)
|
||||
return resultingDamage(r, success)
|
||||
fun calculateDamage(r: Random): AttackResult {
|
||||
|
||||
val attackResult = actionRoll.roll(r)
|
||||
val attackBonus = actionRoll.evaluateModifiers(r, false)
|
||||
|
||||
|
||||
return if (isHit(attackResult, attackBonus)) {
|
||||
if (isCrit(attackResult)) {
|
||||
onCriticalHit(r)
|
||||
} else {
|
||||
onNormalHit(r)
|
||||
}
|
||||
} else {
|
||||
onMiss(r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun isCrit(roll: RollResult): Boolean {
|
||||
return roll.max == roll.result
|
||||
}
|
||||
|
||||
private fun isHit(roll: RollResult, attackBonus: Int): Boolean {
|
||||
//ties go to the roller
|
||||
return (roll.result + attackBonus) >= passValue
|
||||
}
|
||||
|
||||
fun onNormalHit(r: Random): AttackResult
|
||||
|
||||
fun onCriticalHit(r: Random): AttackResult
|
||||
|
||||
fun onMiss(r: Random): AttackResult
|
||||
|
||||
}
|
||||
|
||||
private fun ArrayList<AttackResult>.sumBools(fn: (AttackResult) -> Boolean): Int {
|
||||
return sumOf { boolToInt(fn(it)) }
|
||||
}
|
||||
|
||||
class AttackSimulatorModel(override val sampleSize: Int, private val attack: Attack) : SimulationModel<Int>{
|
||||
override fun simulate(r: Random): Int {
|
||||
return attack.getResultingDamage(r)
|
||||
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 totalHits = results.sumBools { it.attackWasHit }
|
||||
val hitRate = (totalHits.toDouble() / results.size.toDouble()) * 100.0
|
||||
|
||||
val totalCriticals = results.sumBools { it.attackWasCrit }
|
||||
val critRate = (totalCriticals.toDouble() / results.size.toDouble()) * 100.0
|
||||
|
||||
val sumDamage = results.sumOf { it.resultingDamage }
|
||||
val avgDamage = (sumDamage.toDouble() / results.size.toDouble()) * 100.0
|
||||
|
||||
val reportString = "Hit Rate: %.2f\tCrit Rate: %.2f\tAvg Dmg: %.2f".format(hitRate, critRate, avgDamage)
|
||||
|
||||
if(label.isNotBlank()){
|
||||
println("[$label] $reportString")
|
||||
}else{
|
||||
println(reportString)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AttackSimulatorModel(override val sampleSize: Int, private val attack: Attack) : SimulationModel<AttackResult> {
|
||||
override fun simulate(r: Random): AttackResult {
|
||||
return attack.calculateDamage(r)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,21 +11,26 @@ import kotlin.math.min
|
||||
* @property Normal Rolls the dice normally once.
|
||||
* @property Disadvantage Rolls the dice twice and takes the lower result.
|
||||
*/
|
||||
enum class RollType{
|
||||
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 lower of the two results.
|
||||
*/
|
||||
Disadvantage
|
||||
}
|
||||
|
||||
data class RollResult(val min: Int, val max: Int, val result: Int)
|
||||
|
||||
|
||||
/**
|
||||
* Represents dice that can be rolled with different roll types and modifiers.
|
||||
*
|
||||
@ -33,7 +38,11 @@ enum class RollType{
|
||||
* @param rollType The roll type to use, which determines how the dice are rolled
|
||||
* @param modifiers Optional modifier functions that add a bonus to the roll result
|
||||
*/
|
||||
class Dice(rollString: String, private val rollType: RollType = RollType.Normal, private vararg val modifiers: Modifier<Int>) {
|
||||
class Dice(
|
||||
rollString: String,
|
||||
private val rollType: RollType = RollType.Normal,
|
||||
private vararg val modifiers: Modifier<Int>
|
||||
) {
|
||||
private val nDice: Int
|
||||
private val dieSize: Int
|
||||
|
||||
@ -54,24 +63,34 @@ class Dice(rollString: String, private val rollType: RollType = RollType.Normal,
|
||||
* @return The result of the dice roll with modifiers applied
|
||||
* @see RollType
|
||||
*/
|
||||
fun roll(r: Random): Int {
|
||||
val result = when(rollType){
|
||||
RollType.Advantage->{
|
||||
val range1 = (dieSize * nDice) - nDice
|
||||
val range2 = (dieSize * nDice) - nDice
|
||||
max(range1, range2) + nDice
|
||||
fun roll(r: Random): RollResult {
|
||||
val result = when (rollType) {
|
||||
RollType.Advantage -> onAdvantage(r, nDice, dieSize)
|
||||
RollType.Disadvantage -> onDisadvantage(r, nDice, dieSize)
|
||||
else -> onNormalRoll(r, nDice, dieSize)
|
||||
}
|
||||
RollType.Disadvantage->{
|
||||
val range1 = (dieSize * nDice) - nDice
|
||||
val range2 = (dieSize * nDice) - nDice
|
||||
min(range1, range2) + nDice
|
||||
return RollResult(nDice, dieSize * nDice, result)
|
||||
}
|
||||
else->{
|
||||
val range = (dieSize * nDice) - nDice
|
||||
r.nextInt(range) + nDice
|
||||
|
||||
private fun onAdvantage(r: Random, nDice: Int, dieSize: Int): Int {
|
||||
val range = (dieSize * nDice)
|
||||
val roll1 = r.nextInt(range) + nDice
|
||||
val roll2 = r.nextInt(range) + nDice
|
||||
|
||||
return max(roll1, roll2)
|
||||
}
|
||||
|
||||
private fun onDisadvantage(r: Random, nDice: Int, dieSize: Int): Int {
|
||||
val range = (dieSize * nDice)
|
||||
val roll1 = r.nextInt(range) + nDice
|
||||
val roll2 = r.nextInt(range) + nDice
|
||||
|
||||
return min(roll1, roll2)
|
||||
}
|
||||
return result + evaluateModifiers(r)
|
||||
|
||||
private fun onNormalRoll(r: Random, nDice: Int, dieSize: Int): Int {
|
||||
val range = (dieSize * nDice)
|
||||
return r.nextInt(range) + nDice
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,7 +99,7 @@ class Dice(rollString: String, private val rollType: RollType = RollType.Normal,
|
||||
* @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): Int{
|
||||
return modifiers.sumOf{ it.getBonus(r) }
|
||||
fun evaluateModifiers(r: Random, crit: Boolean = false): Int {
|
||||
return modifiers.sumOf { it.getBonus(r, crit) }
|
||||
}
|
||||
}
|
||||
@ -5,27 +5,29 @@ import java.util.*
|
||||
/**
|
||||
* Represents a simple melee attack in a simulation.
|
||||
*
|
||||
* @param attackRoll The dice roll used to determine if an attack hits.
|
||||
* @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 defense The defense value the attack must exceed to hit.
|
||||
* @param passValue The defense value the attack must exceed to hit.
|
||||
*/
|
||||
class SimpleMeleeAttack(
|
||||
val attackRoll: Dice,
|
||||
override val actionRoll: Dice,
|
||||
val damageRoll: Dice,
|
||||
val defense: Int
|
||||
override val passValue: Int
|
||||
) : Attack {
|
||||
override fun attackerSuccessful(r: Random): Boolean {
|
||||
val attackTotal = attackRoll.roll(r)
|
||||
|
||||
return attackTotal >= defense
|
||||
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 resultingDamage(r: Random, attackSuccessful: Boolean): Int {
|
||||
return if(attackSuccessful){
|
||||
damageRoll.roll(r)
|
||||
}else{
|
||||
0
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ interface Modifier<T> {
|
||||
* @param r Random instance to use for random number generation
|
||||
* @return a generated bonus integer
|
||||
*/
|
||||
fun getBonus(r: Random): T
|
||||
fun getBonus(r: Random, crit: Boolean): T
|
||||
}
|
||||
|
||||
/**
|
||||
@ -29,8 +29,12 @@ interface Modifier<T> {
|
||||
* @param dice The [Dice] instance to use for generating bonus values.
|
||||
*/
|
||||
class DiceBonus(private val dice: Dice) : Modifier<Int> {
|
||||
override fun getBonus(r: Random): Int {
|
||||
return dice.roll(r)
|
||||
override fun getBonus(r: Random, crit: Boolean): Int {
|
||||
return if (crit){
|
||||
dice.roll(r).result + dice.roll(r).result
|
||||
}else{
|
||||
dice.roll(r).result
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -44,8 +48,8 @@ class DiceBonus(private val dice: Dice) : Modifier<Int> {
|
||||
* @param dice The [Dice] to use for generating penalty values.
|
||||
*/
|
||||
class DicePenalty(private val dice: Dice): Modifier<Int>{
|
||||
override fun getBonus(r: Random): Int {
|
||||
return -dice.roll(r)
|
||||
override fun getBonus(r: Random, crit: Boolean): Int {//can penalties ever crit?
|
||||
return -dice.roll(r).result
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +61,7 @@ class DicePenalty(private val dice: Dice): Modifier<Int>{
|
||||
* @param bonus The fixed bonus amount to apply.
|
||||
*/
|
||||
class FlatModifier(private val bonus: Int) : Modifier<Int> {
|
||||
override fun getBonus(r: Random): Int {
|
||||
override fun getBonus(r: Random, crit: Boolean): Int {
|
||||
return bonus
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,17 +6,17 @@ import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
interface SimulationModel<T : Number> {
|
||||
interface SimulationModel<T : Any> {
|
||||
val sampleSize: Int
|
||||
|
||||
//has to be pure or else you're going to have a bad time
|
||||
fun simulate(r: Random): T
|
||||
}
|
||||
|
||||
interface Simulator<T : Number> {
|
||||
interface Simulator<T : Any> {
|
||||
|
||||
companion object {
|
||||
fun <T: Number> getInstance(nThreads: Int = Runtime.getRuntime().availableProcessors() / 2 ): Simulator<T> {
|
||||
fun <T: Any> getInstance(nThreads: Int = Runtime.getRuntime().availableProcessors() / 2 ): Simulator<T> {
|
||||
return concreteSimulator(nThreads)
|
||||
}
|
||||
}
|
||||
@ -65,5 +65,5 @@ interface Simulator<T : Number> {
|
||||
|
||||
}
|
||||
|
||||
class concreteSimulator<T : Number>(override val nThreads: Int) :
|
||||
class concreteSimulator<T : Any>(override val nThreads: Int) :
|
||||
Simulator<T>
|
||||
56
src/test/kotlin/simulation/DiceTest.kt
Normal file
56
src/test/kotlin/simulation/DiceTest.kt
Normal file
@ -0,0 +1,56 @@
|
||||
package simulation
|
||||
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.*
|
||||
|
||||
internal class DiceTests {
|
||||
|
||||
@Test
|
||||
fun roll_normal() {
|
||||
val dice = Dice("2d6")
|
||||
val random = Random(1)
|
||||
val result = dice.roll(random)
|
||||
|
||||
assertEquals(2, result.min)
|
||||
assertEquals(12, result.max)
|
||||
Assertions.assertTrue(result.min <= result.result && result.result <= result.max)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun roll_advantage() {
|
||||
val dice = Dice("2d6", RollType.Advantage)
|
||||
val random = Random(1)
|
||||
val result = dice.roll(random)
|
||||
|
||||
assertEquals(2, result.min)
|
||||
assertEquals(12, result.max)
|
||||
Assertions.assertTrue(result.min <= result.result && result.result <= result.max)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun roll_disadvantage() {
|
||||
val dice = Dice("2d6", RollType.Disadvantage)
|
||||
val random = Random(1)
|
||||
val result = dice.roll(random)
|
||||
|
||||
assertEquals(2, result.min)
|
||||
assertEquals(12, result.max)
|
||||
Assertions.assertTrue(result.min <= result.result && result.result <= result.max)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun evaluate_modifiers() {
|
||||
val mod1 = FlatModifier(1)
|
||||
val mod2 = FlatModifier(2)
|
||||
val dice = Dice("1d20", RollType.Normal, mod1, mod2)
|
||||
|
||||
val random = Random(1)
|
||||
val bonus = dice.evaluateModifiers(random)
|
||||
|
||||
assertEquals(3, bonus)
|
||||
}
|
||||
|
||||
}
|
||||
@ -5,20 +5,20 @@ import kotlin.test.Test
|
||||
|
||||
class SimulatorTest {
|
||||
@Test
|
||||
fun testStats(){
|
||||
fun testStats() {
|
||||
val itt = 10_000_000
|
||||
val model = testSimulationModel(itt)
|
||||
val simulator = Simulator.getInstance<Int>(Runtime.getRuntime().availableProcessors())
|
||||
val start = System.nanoTime()
|
||||
val results = simulator.doSimulation(model)
|
||||
val finish = System.nanoTime()
|
||||
println("${results.size} simulations performed in ${finish - start}ns (${(finish-start)/results.size}ns/simulation)")
|
||||
println("${results.size} simulations performed in ${finish - start}ns (${(finish - start) / results.size}ns/simulation)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAttack(){
|
||||
val itt = 10_000_000
|
||||
val simulator = Simulator.getInstance<Int>(Runtime.getRuntime().availableProcessors())
|
||||
fun testAttack() {
|
||||
val itt = 1_000_000
|
||||
val simulator = Simulator.getInstance<AttackResult>(Runtime.getRuntime().availableProcessors())
|
||||
|
||||
val attack = SimpleMeleeAttack(
|
||||
Dice("1d20", RollType.Normal, FlatModifier(5)),
|
||||
@ -27,7 +27,7 @@ class SimulatorTest {
|
||||
)
|
||||
|
||||
val attackWithAdvantageAndBless = SimpleMeleeAttack(
|
||||
Dice("1d20", RollType.Advantage,FlatModifier(5), DiceBonus(Dice("1d4"))),
|
||||
Dice("1d20", RollType.Advantage, FlatModifier(5), DiceBonus(Dice("1d4"))),
|
||||
Dice("2d6", RollType.Normal, FlatModifier(5)),
|
||||
15
|
||||
)
|
||||
@ -38,14 +38,16 @@ class SimulatorTest {
|
||||
val buffedAttackModel = AttackSimulatorModel(itt, attackWithAdvantageAndBless)
|
||||
val buffedResults = simulator.doSimulation(buffedAttackModel)
|
||||
|
||||
println("Average normal damage: ${normalResults.average()}\nAverage buffed damage: ${buffedResults.average()}")
|
||||
|
||||
AttackResult.printSimulationStatistics(normalResults, "Normal Attack")
|
||||
AttackResult.printSimulationStatistics(buffedResults, "Buffed Attack")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class testSimulationModel(override val sampleSize: Int) : SimulationModel<Int>{
|
||||
class testSimulationModel(override val sampleSize: Int) : SimulationModel<Int> {
|
||||
override fun simulate(r: Random): Int {
|
||||
return r.nextInt(20)+1
|
||||
return r.nextInt(20) + 1
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user