Compare commits
No commits in common. "24b81222735b0043bfb294ec94077a2ca5e22b4c" and "9c3d3f2d70514bbf5f2a066995a74c8f059ee06c" have entirely different histories.
24b8122273
...
9c3d3f2d70
@ -5,10 +5,7 @@ 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
|
||||||
|
|
||||||
@ -16,18 +13,9 @@ 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)
|
||||||
}
|
}
|
||||||
@ -35,15 +23,6 @@ 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))
|
||||||
|
|
||||||
@ -74,10 +53,7 @@ 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()
|
||||||
@ -89,5 +65,5 @@ interface Simulator<T : Any> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class SimulatorImpl<T : Any>(override val nThreads: Int) :
|
class SimulatorImpl<T : Any>(override val nThreads: Int) :
|
||||||
Simulator<T>
|
Simulator<T>
|
||||||
@ -8,8 +8,8 @@ import kotlin.math.min
|
|||||||
/**
|
/**
|
||||||
* Enumeration of different dice roll types.
|
* Enumeration of different dice roll types.
|
||||||
*
|
*
|
||||||
* @property Normal Rolls the dice normally once.
|
|
||||||
* @property Advantage Rolls the dice twice and takes the higher result.
|
* @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.
|
* @property Disadvantage Rolls the dice twice and takes the lower result.
|
||||||
*/
|
*/
|
||||||
enum class RollType {
|
enum class RollType {
|
||||||
@ -33,10 +33,6 @@ 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
|
||||||
@ -44,7 +40,7 @@ interface DiceRoller {
|
|||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun defaultDiceStringParseFn(rollString: String): Pair<Int, Int> {
|
internal 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())
|
||||||
|
|||||||
@ -77,9 +77,7 @@ 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,
|
||||||
@ -88,83 +86,36 @@ 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 {
|
||||||
return withAtkModifier(FlatModifier(flat))
|
attackModifiers.add(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 {
|
||||||
return withAtkModifier(DiceBonusModifier(dice))
|
attackModifiers.add(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 {
|
||||||
return withAtkModifier(DicePenaltyModifier(dice))
|
attackModifiers.add(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 {
|
||||||
return withDmgModifier(FlatModifier(flat))
|
damageModifiers.add(FlatModifier(flat))
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun withDmgBonus(dice: DiceRoller): MeleeAttackBuilder {
|
||||||
* Adds a dice-roll bonus to the damage roll.
|
damageModifiers.add(DiceBonusModifier(dice))
|
||||||
*
|
return this
|
||||||
* @param dice The DiceRoller instance to add as a bonus.
|
}
|
||||||
* @return This MeleeAttackBuilder instance.
|
|
||||||
*/
|
fun withDmgPenalty(dice: DiceRoller): MeleeAttackBuilder {
|
||||||
fun withDmgBonus(dice: DiceRoller): MeleeAttackBuilder {
|
damageModifiers.add(DicePenaltyModifier(dice))
|
||||||
return withDmgModifier(DiceBonusModifier(dice))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
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(
|
||||||
|
|||||||
@ -82,32 +82,26 @@ internal class DiceRollerTests {
|
|||||||
verifyBoundariesForTypes(1, 20)
|
verifyBoundariesForTypes(1, 20)
|
||||||
verifyBoundariesForTypes(2, 6)
|
verifyBoundariesForTypes(2, 6)
|
||||||
verifyBoundariesForTypes(5, 8)
|
verifyBoundariesForTypes(5, 8)
|
||||||
|
verifyBoundariesForTypes(1000, 6)
|
||||||
//we have to increase the iteration count by a few orders of magnitude in order to have a chance of hitting a max
|
verifyBoundariesForTypes(6, 1000)
|
||||||
// roll w/disadvantage
|
|
||||||
verifyBoundariesForTypes(6, 1000, 100_000_000)
|
|
||||||
verifyBoundariesForTypes(1000, 6, 100_000_000)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun verifyBoundariesForTypes(nDice: Int, dieSize: Int, iterationCount: Long = 10_000) {
|
private fun verifyBoundariesForTypes(nDice: Int, dieSize: Int) {
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -118,18 +112,17 @@ internal class DiceRollerTests {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Assertions.assertTrue(observedMin)
|
Assertions.assertTrue(observedMin)
|
||||||
Assertions.assertTrue(observedMax)
|
Assertions.assertTrue(observedMax)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun verifyNormalRollMeanWithinExpectedBounds() {
|
fun verifyNormalRollWithinExpectedBounds() {
|
||||||
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 want a "wiggle" around the mean of < 5%
|
val tolerance = 0.05 //we expect more than a 25% improvement
|
||||||
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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user