Compare commits

...

2 Commits

Author SHA1 Message Date
dtookey
24b8122273 cleanup 2023-09-10 20:27:06 -04:00
dtookey
82acb36334 added some cheap docs 2023-09-10 19:24:52 -04:00
4 changed files with 111 additions and 27 deletions

View File

@ -5,7 +5,10 @@ import kotlinx.coroutines.runBlocking
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
/**
* Interface for simulation models.
* Defines a sample size and simulates individual samples.
*/
interface SimulationModel<T : Any> { interface SimulationModel<T : Any> {
val sampleSize: Int val sampleSize: Int
@ -13,9 +16,18 @@ interface SimulationModel<T : Any> {
fun simulate(r: Random): T fun simulate(r: Random): T
} }
/**
* Interface for running simulations concurrently using multiple threads.
* Defines the number of threads to use and runs simulations to generate results.
*/
interface Simulator<T : Any> { interface Simulator<T : Any> {
companion object { companion object {
/**
* Gets an instance of the Simulator interface implementation.
* @param nThreads Number of threads to use for simulations. Defaults to half the available CPU cores.
* @return Simulator instance with the specified number of threads.
*/
fun <T: Any> getInstance(nThreads: Int = Runtime.getRuntime().availableProcessors() / 2 ): Simulator<T> { fun <T: Any> getInstance(nThreads: Int = Runtime.getRuntime().availableProcessors() / 2 ): Simulator<T> {
return SimulatorImpl(nThreads) return SimulatorImpl(nThreads)
} }
@ -23,6 +35,15 @@ interface Simulator<T : Any> {
val nThreads: Int val nThreads: Int
/**
* Runs simulations using the provided model and returns the results.
*
* This divides the sample size into nThreads chunks and runs simulations concurrently on each chunk using coroutines.
* The results from each thread are collected into a synchronized list.
*
* @param model The simulation model to use for generating samples.
* @return A list containing all the simulated sample results.
*/
fun doSimulation(model: SimulationModel<T>): ArrayList<T> { fun doSimulation(model: SimulationModel<T>): ArrayList<T> {
val results = Collections.synchronizedList(ArrayList<T>(model.sampleSize)) val results = Collections.synchronizedList(ArrayList<T>(model.sampleSize))
@ -53,7 +74,10 @@ interface Simulator<T : Any> {
return results.toCollection(ArrayList()) return results.toCollection(ArrayList())
} }
/**
* Generates simulation results by running [steps] simulations using [model] and [Random].
* Returns the results in an [ArrayList].
*/
private fun generateResults(steps: Int, model: SimulationModel<T>): ArrayList<T> { private fun generateResults(steps: Int, model: SimulationModel<T>): ArrayList<T> {
val results = ArrayList<T>(steps) val results = ArrayList<T>(steps)
val r = Random() val r = Random()
@ -65,5 +89,5 @@ interface Simulator<T : Any> {
} }
class SimulatorImpl<T : Any>(override val nThreads: Int) : internal class SimulatorImpl<T : Any>(override val nThreads: Int) :
Simulator<T> Simulator<T>

View File

@ -8,8 +8,8 @@ import kotlin.math.min
/** /**
* Enumeration of different dice roll types. * 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 Normal Rolls the dice normally once.
* @property Advantage Rolls the dice twice and takes the higher result.
* @property Disadvantage Rolls the dice twice and takes the lower result. * @property Disadvantage Rolls the dice twice and takes the lower result.
*/ */
enum class RollType { enum class RollType {
@ -33,6 +33,10 @@ data class RollResult(val min: Int, val max: Int, val result: Int)
interface DiceRoller { interface DiceRoller {
/**
* string representation of the base dice in {n}d{size} format
* i.e. 1d20, 2d7, 30d100
*/
val rollString: String val rollString: String
val modifiers: List<DiceModifier<Int>> val modifiers: List<DiceModifier<Int>>
val nDice: Int val nDice: Int
@ -40,7 +44,7 @@ interface DiceRoller {
companion object { companion object {
internal fun defaultDiceStringParseFn(rollString: String): Pair<Int, Int> { fun defaultDiceStringParseFn(rollString: String): Pair<Int, Int> {
val cleanRollString = rollString.lowercase() val cleanRollString = rollString.lowercase()
val parts = cleanRollString.split('d') val parts = cleanRollString.split('d')
return Pair(parts[0].toInt(), parts[1].toInt()) return Pair(parts[0].toInt(), parts[1].toInt())

View File

@ -77,7 +77,9 @@ class AttackSimulatorModel(
} }
} }
/**
* MeleeAttackBuilder builds a [SimpleMeleeAttackAction] by configuring the attack and damage rolls and modifiers.
*/
class MeleeAttackBuilder( class MeleeAttackBuilder(
private val attackRollString: String, private val attackRollString: String,
private val dmgRollString: String, private val dmgRollString: String,
@ -86,36 +88,83 @@ class MeleeAttackBuilder(
private val attackModifiers = ArrayList<DiceModifier<Int>>() private val attackModifiers = ArrayList<DiceModifier<Int>>()
private val damageModifiers = ArrayList<DiceModifier<Int>>() private val damageModifiers = ArrayList<DiceModifier<Int>>()
/**
* Adds a flat bonus to the attack roll.
*
* @param flat the flat bonus amount to add to or subtract from the attack roll.
* @return this MeleeAttackBuilder instance.
*/
fun withAtkBonus(flat: Int): MeleeAttackBuilder { fun withAtkBonus(flat: Int): MeleeAttackBuilder {
attackModifiers.add(FlatModifier(flat)) return withAtkModifier(FlatModifier(flat))
return this
} }
/**
* Adds a dice-roll bonus to the attack roll.
*
* @param dice The DiceRoller instance to add as a bonus.
* @return This MeleeAttackBuilder instance.
*/
fun withAtkBonus(dice: DiceRoller): MeleeAttackBuilder { fun withAtkBonus(dice: DiceRoller): MeleeAttackBuilder {
attackModifiers.add(DiceBonusModifier(dice)) return withAtkModifier(DiceBonusModifier(dice))
return this
} }
/**
* Adds a dice-roll penalty to the attack roll.
*
* @param dice The DiceRoller instance to add as a penalty. The result will be subtracted from the total.
* @return This MeleeAttackBuilder instance.
*/
fun withAtkPenalty(dice: DiceRoller): MeleeAttackBuilder { fun withAtkPenalty(dice: DiceRoller): MeleeAttackBuilder {
attackModifiers.add(DicePenaltyModifier(dice)) return withAtkModifier(DicePenaltyModifier(dice))
}
fun withAtkModifier(modifier: DiceModifier<Int>): MeleeAttackBuilder{
attackModifiers.add(modifier)
return this return this
} }
/**
* Adds a flat bonus to the damage roll.
*
* @param flat the flat bonus amount to add to or subtract from the damage roll.
* @return this MeleeAttackBuilder instance.
*/
fun withDmgBonus(flat: Int): MeleeAttackBuilder { fun withDmgBonus(flat: Int): MeleeAttackBuilder {
damageModifiers.add(FlatModifier(flat)) return withDmgModifier(FlatModifier(flat))
return this
} }
/**
* Adds a dice-roll bonus to the damage roll.
*
* @param dice The DiceRoller instance to add as a bonus.
* @return This MeleeAttackBuilder instance.
*/
fun withDmgBonus(dice: DiceRoller): MeleeAttackBuilder { fun withDmgBonus(dice: DiceRoller): MeleeAttackBuilder {
damageModifiers.add(DiceBonusModifier(dice)) return withDmgModifier(DiceBonusModifier(dice))
return this
} }
/**
* Adds a dice-roll penalty to the damage roll.
*
* @param dice The DiceRoller instance to add as a penalty. The result will be subtracted from the total.
* @return This MeleeAttackBuilder instance.
*/
fun withDmgPenalty(dice: DiceRoller): MeleeAttackBuilder { fun withDmgPenalty(dice: DiceRoller): MeleeAttackBuilder {
damageModifiers.add(DicePenaltyModifier(dice)) return withDmgModifier(DicePenaltyModifier(dice))
}
fun withDmgModifier(modifier: DiceModifier<Int>): MeleeAttackBuilder{
this.damageModifiers.add(modifier)
return this return this
} }
/**
* Builds and returns a SimpleMeleeAttackAction instance with the configured attack and damage rolls,
* modifiers, and defense value.
*/
fun build(): SimpleMeleeAttackAction { fun build(): SimpleMeleeAttackAction {
return SimpleMeleeAttackAction( return SimpleMeleeAttackAction(
ActionRollInfo( ActionRollInfo(

View File

@ -82,26 +82,32 @@ internal class DiceRollerTests {
verifyBoundariesForTypes(1, 20) verifyBoundariesForTypes(1, 20)
verifyBoundariesForTypes(2, 6) verifyBoundariesForTypes(2, 6)
verifyBoundariesForTypes(5, 8) verifyBoundariesForTypes(5, 8)
verifyBoundariesForTypes(1000, 6)
verifyBoundariesForTypes(6, 1000) //we have to increase the iteration count by a few orders of magnitude in order to have a chance of hitting a max
// roll w/disadvantage
verifyBoundariesForTypes(6, 1000, 100_000_000)
verifyBoundariesForTypes(1000, 6, 100_000_000)
} }
private fun verifyBoundariesForTypes(nDice: Int, dieSize: Int) { private fun verifyBoundariesForTypes(nDice: Int, dieSize: Int, iterationCount: Long = 10_000) {
val rollString = "${nDice}d${dieSize}" val rollString = "${nDice}d${dieSize}"
val iterations = 10_000_000
val max = nDice * dieSize val max = nDice * dieSize
var observedMin = false
var observedMax = false
RollType.entries.parallelStream() RollType.entries.parallelStream()
.forEach { .forEach {
var observedMin = false
var observedMax = false
val dice = Dice.regular(rollString) val dice = Dice.regular(rollString)
val r = Random(1) val r = Random(1)
for (i in 0..<iterations) {
for (i in 0..<iterationCount) {
val res = dice.roll(r, it) val res = dice.roll(r, it)
//make sure it isn't bigger or smaller than it's supposed to be
Assertions.assertTrue(res.result in nDice..max)
if (!observedMin && res.result == nDice) { if (!observedMin && res.result == nDice) {
observedMin = true observedMin = true
} }
@ -112,17 +118,18 @@ internal class DiceRollerTests {
break break
} }
} }
Assertions.assertTrue(observedMin) Assertions.assertTrue(observedMin)
Assertions.assertTrue(observedMax) Assertions.assertTrue(observedMax)
} }
} }
@Test @Test
fun verifyNormalRollWithinExpectedBounds() { fun verifyNormalRollMeanWithinExpectedBounds() {
val n = 2 val n = 2
val max = 100 val max = 100
val rollString = "${n}d${max}" val rollString = "${n}d${max}"
val tolerance = 0.05 //we expect more than a 25% improvement val tolerance = 0.05 //we want a "wiggle" around the mean of < 5%
val iterations = 100_000_000 val iterations = 100_000_000
val expectedAverageLowerBound = ((n + (n * max)) / 2) * (1 - tolerance) val expectedAverageLowerBound = ((n + (n * max)) / 2) * (1 - tolerance)