Compare commits
No commits in common. "d0f7c3bd1199a750e18cf38be900c258fff5ada5" and "20be0861989000e0bcaba1d31cdbdf006ac4a393" have entirely different histories.
d0f7c3bd11
...
20be086198
@ -1,112 +1,36 @@
|
|||||||
package entries
|
package entries
|
||||||
|
|
||||||
import simulation.*
|
import simulation.*
|
||||||
import simulation.dice.RollType
|
|
||||||
import simulation.fifthEd.AttackResult
|
|
||||||
import simulation.fifthEd.AttackSimulatorModel
|
|
||||||
import simulation.fifthEd.MeleeAttackBuilder
|
|
||||||
import simulation.fifthEd.SimpleMeleeAttackAction
|
|
||||||
|
|
||||||
fun main() {
|
fun main(){
|
||||||
val rounds = 1_000_000
|
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val computedRounds = doSimulation(rounds)
|
doSimulation()
|
||||||
val finish = System.currentTimeMillis()
|
val finish = System.currentTimeMillis()
|
||||||
println("Simulation finished $computedRounds rounds in: ${finish - start}ms")
|
println("Simulation finished in: ${finish-start}ms")
|
||||||
}
|
}
|
||||||
|
|
||||||
const val avgDamage = "Avg Dmg"
|
fun doSimulation(){
|
||||||
|
val itt = 10_000_000
|
||||||
|
val simulator = Simulator.getInstance<AttackResult>(Runtime.getRuntime().availableProcessors())
|
||||||
|
|
||||||
fun doSimulation(rounds: Int): Long {
|
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 defense = 18
|
AttackResult.printSimulationStatistics(normalResults, "Normal Attack (${it.name})")
|
||||||
|
|
||||||
val attackDice = "1d20"
|
|
||||||
val weaponDice = "2d6"
|
|
||||||
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
val attacks = listOf(Pair("Normal Attack", normalAttack), Pair("GWM Attack", gwmAttack))
|
|
||||||
val reportFactory = ReportBuilder.getInstance()
|
|
||||||
.addRateMetric("Accuracy") { it.rollSucceeded }
|
|
||||||
.addRateMetric("Crit Rate") { it.rollSucceeded }
|
|
||||||
.addAverageMetric(avgDamage) { it.resultingDamage.toLong() }
|
|
||||||
|
|
||||||
generateCombatSuggestions(attacks, rounds, reportFactory)
|
|
||||||
|
|
||||||
return rounds.toLong() * attacks.size.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateCombatSuggestions(
|
|
||||||
attacks: List<Pair<String, SimpleMeleeAttackAction>>,
|
|
||||||
rounds: Int,
|
|
||||||
reportFactory: ReportBuilder
|
|
||||||
) {
|
|
||||||
val simulator = Simulator.getInstance<AttackResult>(128)
|
|
||||||
val results = attacks
|
|
||||||
.associateWith { attackInfo ->
|
|
||||||
|
|
||||||
val label = "${attackInfo.first}"
|
|
||||||
val attack = attackInfo.second
|
|
||||||
|
|
||||||
RollType.entries.associateWith {
|
|
||||||
val attackModel = AttackSimulatorModel(rounds, attack, it)
|
|
||||||
val results = simulator.doSimulation(attackModel)
|
|
||||||
|
|
||||||
val report = reportFactory.build(label)
|
|
||||||
|
|
||||||
report.computeResults(results)
|
|
||||||
}.toMap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
RollType.entries.forEach { rollType ->
|
|
||||||
val builder = StringBuilder("[$rollType]\t")
|
|
||||||
|
|
||||||
var choice: MetricReport? = null
|
|
||||||
|
|
||||||
results.forEach { attackAction ->
|
|
||||||
val current = attackAction.value[rollType]
|
|
||||||
if(choice == null){
|
|
||||||
choice = current
|
|
||||||
}else{
|
|
||||||
if(current?.results?.getDamage()!!.metricValue > choice!!.results.getDamage()!!.metricValue){
|
|
||||||
choice = current
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.append("Use ${choice!!.name}: ${Report.formatReport(choice!!.name, choice!!.results)}")
|
|
||||||
println(builder.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun List<MetricResult>.getDamage(): MetricResult? {
|
|
||||||
|
|
||||||
forEach {
|
|
||||||
if (it.label == avgDamage) {
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
70
src/main/kotlin/simulation/Attack.kt
Normal file
70
src/main/kotlin/simulation/Attack.kt
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to [actionRoll], we cannot call this 'defense', because it might be a spell DC. This is the value of the
|
||||||
|
* responder to the action. In a melee attack, this is AC. For a spell check, this would be the DC.
|
||||||
|
*/
|
||||||
|
val responseValue: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* calculateDamage calculates the damage for an attack by:
|
||||||
|
* - Rolling the relevant action
|
||||||
|
* - Evaluating action modifiers
|
||||||
|
* - Checking if action hits by comparing roll + modifiers to response value
|
||||||
|
* - Calling onCriticalHit, onNormalHit or onMiss based on hit result
|
||||||
|
* - Returning the AttackResult
|
||||||
|
*/
|
||||||
|
fun calculateDamage(r: Random): AttackResult {
|
||||||
|
|
||||||
|
val attackResult = actionRoll.roll(r)
|
||||||
|
val attackBonus = actionRoll.evaluateModifiers(r, false)
|
||||||
|
|
||||||
|
|
||||||
|
return if (isHit(attackResult, attackBonus)) {
|
||||||
|
if (actionRoll.isCrit(attackResult)) {
|
||||||
|
onCriticalHit(r)
|
||||||
|
} else {
|
||||||
|
onNormalHit(r)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onMiss(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isHit(roll: RollResult, actionBonus: Int): Boolean {
|
||||||
|
//ties go to the roller
|
||||||
|
return (roll.result + actionBonus) >= responseValue
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNormalHit(r: Random): AttackResult
|
||||||
|
|
||||||
|
fun onCriticalHit(r: Random): AttackResult
|
||||||
|
|
||||||
|
fun onMiss(r: Random): AttackResult
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<AttackResult> {
|
||||||
|
override fun simulate(r: Random): AttackResult {
|
||||||
|
return attack.calculateDamage(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,27 +1,4 @@
|
|||||||
package simulation.dice
|
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.
|
* AttackDice class represents dice used for attack rolls.
|
||||||
@ -39,14 +16,15 @@ interface CritDice : Dice {
|
|||||||
*
|
*
|
||||||
* The isCrit() method checks if a roll result meets the crit threshold.
|
* The isCrit() method checks if a roll result meets the crit threshold.
|
||||||
*/
|
*/
|
||||||
internal class AttackDiceImpl(
|
class AttackDice(
|
||||||
override val rollString: String,
|
override val rollString: String,
|
||||||
override val modifiers: List<DiceModifier<Int>> = ArrayList()
|
override val rollType: RollType = RollType.Normal,
|
||||||
) : CritDice {
|
override val modifiers: List<Modifier<Int>> = ArrayList()
|
||||||
|
) : Dice {
|
||||||
override val nDice: Int
|
override val nDice: Int
|
||||||
override val dieSize: Int
|
override val dieSize: Int
|
||||||
|
|
||||||
override val critThreshold: Int
|
private val critThreshold: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val cleanRollString = rollString.lowercase()
|
val cleanRollString = rollString.lowercase()
|
||||||
@ -64,4 +42,20 @@ internal class AttackDiceImpl(
|
|||||||
critThreshold = dieSize * nDice
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
37
src/main/kotlin/simulation/AttackResult.kt
Normal file
37
src/main/kotlin/simulation/AttackResult.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
|
||||||
|
private fun ArrayList<AttackResult>.sumBools(fn: (AttackResult) -> Boolean): Int {
|
||||||
|
return sumOf { boolToInt(fn(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 sampleSize = results.size.toDouble()
|
||||||
|
|
||||||
|
val totalHits = results.sumBools { it.attackWasHit }
|
||||||
|
val hitRate = (totalHits.toDouble() / sampleSize) * 100.0
|
||||||
|
|
||||||
|
val totalCriticals = results.sumBools { it.attackWasCrit }
|
||||||
|
val critRate = (totalCriticals.toDouble() / sampleSize) * 100.0
|
||||||
|
|
||||||
|
val sumDamage = results.sumOf { it.resultingDamage.toLong() }
|
||||||
|
val avgDamage = sumDamage.toDouble() / sampleSize
|
||||||
|
|
||||||
|
val reportString = "Hit Rate: %.2f\tCrit Rate: %.2f\tAvg Dmg: %.2f".format(hitRate, critRate, avgDamage)
|
||||||
|
|
||||||
|
if(label.isNotBlank()){
|
||||||
|
println("[$label] $reportString")
|
||||||
|
}else{
|
||||||
|
println(reportString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
package simulation.dice
|
package simulation
|
||||||
|
|
||||||
import simulation.dice.RollType.*
|
import simulation.RollType.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@ -13,16 +14,16 @@ import kotlin.math.min
|
|||||||
* @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 {
|
||||||
/**
|
|
||||||
* Rolls the dice once normally.
|
|
||||||
*/
|
|
||||||
Normal,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rolls the dice twice and returns the higher of the two results.
|
* Rolls the dice twice and returns the higher of the two results.
|
||||||
*/
|
*/
|
||||||
Advantage,
|
Advantage,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rolls the dice once normally.
|
||||||
|
*/
|
||||||
|
Normal,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rolls the dice twice and returns the lower of the two results.
|
* Rolls the dice twice and returns the lower of the two results.
|
||||||
*/
|
*/
|
||||||
@ -34,10 +35,18 @@ data class RollResult(val min: Int, val max: Int, val result: Int)
|
|||||||
|
|
||||||
interface Dice {
|
interface Dice {
|
||||||
val rollString: String
|
val rollString: String
|
||||||
val modifiers: List<DiceModifier<Int>>
|
val rollType: RollType
|
||||||
|
val modifiers: List<Modifier<Int>>
|
||||||
val nDice: Int
|
val nDice: Int
|
||||||
val dieSize: Int
|
val dieSize: Int
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun makeDice(rollString: String, rollType: RollType = Normal, modifiers: List<Modifier<Int>> = ArrayList()): Dice {
|
||||||
|
return SimpleDice(rollString, rollType, modifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rolls the dice and returns the result.
|
* Rolls the dice and returns the result.
|
||||||
*
|
*
|
||||||
@ -49,7 +58,7 @@ interface Dice {
|
|||||||
* @return The result of the dice roll with modifiers applied
|
* @return The result of the dice roll with modifiers applied
|
||||||
* @see RollType
|
* @see RollType
|
||||||
*/
|
*/
|
||||||
fun roll(r: Random, rollType: RollType = Normal): RollResult {
|
fun roll(r: Random): RollResult {
|
||||||
val range = (dieSize * nDice) - nDice
|
val range = (dieSize * nDice) - nDice
|
||||||
val result = when (rollType) {
|
val result = when (rollType) {
|
||||||
Advantage -> advantageRoll(r, nDice, range)
|
Advantage -> advantageRoll(r, nDice, range)
|
||||||
@ -87,25 +96,20 @@ interface Dice {
|
|||||||
fun evaluateModifiers(r: Random, crit: Boolean = false): Int {
|
fun evaluateModifiers(r: Random, crit: Boolean = false): Int {
|
||||||
return modifiers.sumOf { it.getBonus(r, crit) }
|
return modifiers.sumOf { it.getBonus(r, crit) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun defaultParseFn(rollString: String): Pair<Int, Int> {
|
|
||||||
val cleanRollString = rollString.lowercase()
|
|
||||||
val parts = cleanRollString.split('d')
|
|
||||||
return Pair(parts[0].toInt(), parts[1].toInt())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DiceImpl(
|
private class SimpleDice(
|
||||||
override val rollString: String,
|
override val rollString: String,
|
||||||
override val modifiers: List<DiceModifier<Int>> = ArrayList()
|
override val rollType: RollType = Normal,
|
||||||
|
override val modifiers: List<Modifier<Int>> = ArrayList()
|
||||||
) : Dice {
|
) : Dice {
|
||||||
override val nDice: Int
|
override val nDice: Int
|
||||||
override val dieSize: Int
|
override val dieSize: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val rollPair = defaultParseFn(rollString)
|
val cleanRollString = rollString.lowercase()
|
||||||
nDice = rollPair.first
|
val parts = cleanRollString.split('d')
|
||||||
dieSize = rollPair.second
|
nDice = parts[0].toInt()
|
||||||
|
dieSize = parts[1].toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
33
src/main/kotlin/simulation/MeleeAttack.kt
Normal file
33
src/main/kotlin/simulation/MeleeAttack.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a simple melee attack in a simulation.
|
||||||
|
*
|
||||||
|
* @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 responseValue The defense value the attack must exceed to hit.
|
||||||
|
*/
|
||||||
|
class SimpleMeleeAttack(
|
||||||
|
override val actionRoll: AttackDice,
|
||||||
|
val damageRoll: Dice,
|
||||||
|
override val responseValue: Int
|
||||||
|
) : Attack {
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package simulation.dice
|
package simulation
|
||||||
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ import java.util.*
|
|||||||
*
|
*
|
||||||
* A [Random] object is provided if the bonus is variable.
|
* A [Random] object is provided if the bonus is variable.
|
||||||
*/
|
*/
|
||||||
interface DiceModifier<T> {
|
interface Modifier<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a bonus integer, potentially using the provided Random instance if needs be.
|
* Generates a bonus integer, potentially using the provided Random instance if needs be.
|
||||||
@ -21,16 +21,16 @@ interface DiceModifier<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [DiceModifier] that generates a random bonus integer based on a provided [Dice].
|
* A [Modifier] that generates a random bonus integer based on a provided [Dice].
|
||||||
*
|
*
|
||||||
* 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 [Dice] using the passed [Random]
|
||||||
* instance and return the result as a positive bonus amount.
|
* instance and return the result as a positive bonus amount.
|
||||||
*
|
*
|
||||||
* @param dice The [Dice] instance to use for generating bonus values.
|
* @param dice The [Dice] instance to use for generating bonus values.
|
||||||
*/
|
*/
|
||||||
class DiceBonusModifier(private val dice: Dice) : DiceModifier<Int> {
|
class DiceBonusModifier(private val dice: Dice) : Modifier<Int> {
|
||||||
|
|
||||||
constructor(diceString: String):this(DiceBag.plainDice(diceString))
|
constructor(diceString: String):this(Dice.makeDice(diceString))
|
||||||
|
|
||||||
override fun getBonus(r: Random, crit: Boolean): Int {
|
override fun getBonus(r: Random, crit: Boolean): Int {
|
||||||
return if (crit){
|
return if (crit){
|
||||||
@ -43,27 +43,27 @@ class DiceBonusModifier(private val dice: Dice) : DiceModifier<Int> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [DiceModifier] that applies a random penalty based on a [Dice].
|
* A [Modifier] that applies a random penalty based on a [Dice].
|
||||||
*
|
*
|
||||||
* 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 [Dice] object and return the
|
||||||
* result as a negative number.
|
* result as a negative number.
|
||||||
*
|
*
|
||||||
* @param dice The [Dice] to use for generating penalty values.
|
* @param dice The [Dice] to use for generating penalty values.
|
||||||
*/
|
*/
|
||||||
class DicePenaltyModifier(private val dice: Dice): DiceModifier<Int> {
|
class DicePenaltyModifier(private val dice: Dice): Modifier<Int>{
|
||||||
override fun getBonus(r: Random, crit: Boolean): Int {//can penalties ever crit?
|
override fun getBonus(r: Random, crit: Boolean): Int {//can penalties ever crit?
|
||||||
return -dice.roll(r).result
|
return -dice.roll(r).result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [DiceModifier] that applies a fixed bonus amount.
|
* A [Modifier] that applies a fixed bonus amount.
|
||||||
*
|
*
|
||||||
* The bonus value is set on construction and does not vary.
|
* The bonus value is set on construction and does not vary.
|
||||||
*
|
*
|
||||||
* @param bonus The fixed bonus amount to apply.
|
* @param bonus The fixed bonus amount to apply.
|
||||||
*/
|
*/
|
||||||
class FlatModifier(private val bonus: Int) : DiceModifier<Int> {
|
class FlatModifier(private val bonus: Int) : Modifier<Int> {
|
||||||
override fun getBonus(r: Random, crit: Boolean): Int {
|
override fun getBonus(r: Random, crit: Boolean): Int {
|
||||||
return bonus
|
return bonus
|
||||||
}
|
}
|
||||||
@ -1,111 +0,0 @@
|
|||||||
package simulation
|
|
||||||
|
|
||||||
import simulation.fifthEd.AttackResult
|
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
data class MetricReport(val name: String, val results: List<MetricResult>)
|
|
||||||
data class MetricResult(val label: String, val metricValue: Double)
|
|
||||||
|
|
||||||
interface Report {
|
|
||||||
val name: String
|
|
||||||
val metrics: List<Metric<Double>>
|
|
||||||
|
|
||||||
companion object{
|
|
||||||
fun formatReport(name: String, results: List<MetricResult>): String {
|
|
||||||
val builder = StringBuilder()
|
|
||||||
results.forEach {
|
|
||||||
builder.append("${it.label}: %.2f".format(it.metricValue))
|
|
||||||
.append('\t')
|
|
||||||
|
|
||||||
}
|
|
||||||
return builder.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun computeResults(results: List<AttackResult>): MetricReport{
|
|
||||||
val m = ArrayList<MetricResult>(metrics.size)
|
|
||||||
metrics.forEach {
|
|
||||||
val value = it.mapToMetric(results)
|
|
||||||
m.add(MetricResult(it.metricName, value))
|
|
||||||
}
|
|
||||||
return MetricReport(name, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ReportImpl(override val name: String, override val metrics: List<Metric<Double>>) : Report
|
|
||||||
|
|
||||||
interface ReportBuilder {
|
|
||||||
val metrics: MutableList<Metric<Double>>
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getInstance(): ReportBuilder {
|
|
||||||
return ReportBuilderImpl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun build(reportName: String): Report {
|
|
||||||
return ReportImpl(reportName, metrics)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addAverageMetric(metricName: String, fieldMapFn: (AttackResult) -> Long): ReportBuilder {
|
|
||||||
metrics.add(AverageMetric(metricName, fieldMapFn))
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addRateMetric(metricName: String, fieldMapFn: (AttackResult) -> Boolean): ReportBuilder {
|
|
||||||
metrics.add(RateMetric(metricName, fieldMapFn))
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReportBuilderImpl : ReportBuilder {
|
|
||||||
override val metrics: MutableList<Metric<Double>> = ArrayList()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Metric<T> {
|
|
||||||
val metricName: String
|
|
||||||
fun mapToMetric(results: List<AttackResult>): T
|
|
||||||
|
|
||||||
fun formatResults(result: T): String
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class AverageMetric(override val metricName: String, private val fieldMapFn: (AttackResult) -> Long) : Metric<Double> {
|
|
||||||
override fun mapToMetric(results: List<AttackResult>): Double {
|
|
||||||
return results.map(fieldMapFn).average()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun formatResults(result: Double): String {
|
|
||||||
return "$metricName: %.2f".format(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class RateMetric(override val metricName: String, private val fieldMapFn: (AttackResult) -> Boolean) : Metric<Double> {
|
|
||||||
override fun mapToMetric(results: List<AttackResult>): Double {
|
|
||||||
return results.map(fieldMapFn).map { boolToInt(it) }.average() * 100.0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun formatResults(result: Double): String {
|
|
||||||
return "$metricName: %.2f".format(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class StdDevMetric(override val metricName: String, private val fieldMapFn: (AttackResult) -> Long) : Metric<Double> {
|
|
||||||
override fun mapToMetric(results: List<AttackResult>): Double {
|
|
||||||
val mean = results.map(fieldMapFn).average()
|
|
||||||
return results.map(fieldMapFn).map { (it - mean).pow(2) }.average().pow(0.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun formatResults(result: Double): String {
|
|
||||||
return "$metricName: %.2f".format(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun boolToInt(b: Boolean): Int = if (b) 1 else 0
|
|
||||||
@ -17,7 +17,7 @@ interface Simulator<T : Any> {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
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 concreteSimulator(nThreads)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,5 +65,5 @@ interface Simulator<T : Any> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SimulatorImpl<T : Any>(override val nThreads: Int) :
|
class concreteSimulator<T : Any>(override val nThreads: Int) :
|
||||||
Simulator<T>
|
Simulator<T>
|
||||||
@ -1,19 +0,0 @@
|
|||||||
package simulation.dice
|
|
||||||
|
|
||||||
object DiceBag {
|
|
||||||
fun plainDice(rollString: String, modifiers: List<DiceModifier<Int>> = ArrayList()): Dice {
|
|
||||||
return DiceImpl(rollString, modifiers)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun critDice(rollString: String, modifiers: List<DiceModifier<Int>> = ArrayList()): CritDice {
|
|
||||||
return AttackDiceImpl(rollString, modifiers)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rerollDice(
|
|
||||||
rollString: String,
|
|
||||||
rerollThreshold: Int,
|
|
||||||
modifiers: List<DiceModifier<Int>> = ArrayList()
|
|
||||||
): RerollDice {
|
|
||||||
return RerollDiceImpl(rollString, rerollThreshold, modifiers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
package simulation.dice
|
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
interface RerollDice : Dice {
|
|
||||||
val rollThreshold: Int
|
|
||||||
override fun roll(r: Random, rollType: RollType): RollResult {
|
|
||||||
val result = super.roll(r, rollType)
|
|
||||||
|
|
||||||
return if (result.result <= rollThreshold) {
|
|
||||||
super.roll(r, rollType)
|
|
||||||
} else {
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RerollDiceImpl(
|
|
||||||
override val rollString: String,
|
|
||||||
override val rollThreshold: Int,
|
|
||||||
override val modifiers: List<DiceModifier<Int>> = ArrayList()
|
|
||||||
) : RerollDice{
|
|
||||||
override val nDice: Int
|
|
||||||
override val dieSize: Int
|
|
||||||
|
|
||||||
init {
|
|
||||||
val rollPair = this.defaultParseFn(rollString)
|
|
||||||
nDice = rollPair.first
|
|
||||||
dieSize = rollPair.second
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
package simulation.fifthEd
|
|
||||||
|
|
||||||
import simulation.dice.CritDice
|
|
||||||
import simulation.dice.Dice
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
data class ActionRollInfo(val actionRoll: CritDice, val damageRoll: Dice)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a simple melee attack in a simulation.
|
|
||||||
*
|
|
||||||
* @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 responseValue The defense value the attack must exceed to hit.
|
|
||||||
*/
|
|
||||||
class SimpleMeleeAttackAction(
|
|
||||||
override val actionRollInfo: ActionRollInfo,
|
|
||||||
override val responseValue: Int
|
|
||||||
) : AttackAction {
|
|
||||||
|
|
||||||
|
|
||||||
override fun onNormalAction(r: Random): AttackResult {
|
|
||||||
val damage = actionRollInfo.damageRoll.roll(r).result + actionRollInfo.damageRoll.evaluateModifiers(r, false)
|
|
||||||
return AttackResult(rollSucceeded = true, rollWasCritical = false, damage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCriticalAction(r: Random): AttackResult {
|
|
||||||
val damage = actionRollInfo.damageRoll.roll(r).result +
|
|
||||||
actionRollInfo.damageRoll.roll(r).result +
|
|
||||||
actionRollInfo.damageRoll.evaluateModifiers(r, true)
|
|
||||||
|
|
||||||
return AttackResult(rollSucceeded = true, rollWasCritical = true, damage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionFailure(r: Random): AttackResult {
|
|
||||||
return AttackResult(rollSucceeded = false, rollWasCritical = false, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
package simulation.fifthEd
|
|
||||||
|
|
||||||
import simulation.*
|
|
||||||
import simulation.dice.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
interface ActionResult{}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 AttackAction {
|
|
||||||
|
|
||||||
val actionRollInfo :ActionRollInfo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Similar to [actionRollInfo], we cannot call this 'defense', because it might be a spell DC. This is the value of the
|
|
||||||
* responder to the action. In a melee attack, this is AC. For a spell check, this would be the DC.
|
|
||||||
*/
|
|
||||||
val responseValue: Int
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* calculateDamage calculates the damage for an attack by:
|
|
||||||
* - Rolling the relevant action
|
|
||||||
* - Evaluating action modifiers
|
|
||||||
* - Checking if action hits by comparing roll + modifiers to response value
|
|
||||||
* - Calling onCriticalHit, onNormalHit or onMiss based on hit result
|
|
||||||
* - Returning the AttackResult
|
|
||||||
*/
|
|
||||||
fun calculateDamage(r: Random, rollType: RollType): AttackResult {
|
|
||||||
|
|
||||||
val attackResult = actionRollInfo.actionRoll.roll(r, rollType)
|
|
||||||
val attackBonus = actionRollInfo.actionRoll.evaluateModifiers(r, false)
|
|
||||||
|
|
||||||
|
|
||||||
return if (isHit(attackResult, attackBonus)) {
|
|
||||||
if (actionRollInfo.actionRoll.isCrit(attackResult)) {
|
|
||||||
onCriticalAction(r)
|
|
||||||
} else {
|
|
||||||
onNormalAction(r)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onActionFailure(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
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onNormalAction(r: Random): AttackResult
|
|
||||||
|
|
||||||
fun onCriticalAction(r: Random): AttackResult
|
|
||||||
|
|
||||||
fun onActionFailure(r: Random): AttackResult
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: AttackAction,
|
|
||||||
private val rollType: RollType
|
|
||||||
) : SimulationModel<AttackResult> {
|
|
||||||
override fun simulate(r: Random): AttackResult {
|
|
||||||
return attack.calculateDamage(r, rollType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class MeleeAttackBuilder(
|
|
||||||
private val attackRollString: String,
|
|
||||||
private val dmgRollString: String,
|
|
||||||
private val defense: Int
|
|
||||||
) {
|
|
||||||
private val attackModifiers = ArrayList<DiceModifier<Int>>()
|
|
||||||
private val damageModifiers = ArrayList<DiceModifier<Int>>()
|
|
||||||
|
|
||||||
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(
|
|
||||||
ActionRollInfo(
|
|
||||||
DiceBag.critDice(attackRollString, attackModifiers),
|
|
||||||
DiceBag.plainDice(dmgRollString, damageModifiers)
|
|
||||||
),
|
|
||||||
defense
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package simulation.fifthEd
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the result of an attack in a simulation.
|
|
||||||
*
|
|
||||||
* @param rollSucceeded Whether the attack successfully hit the target.
|
|
||||||
* @param rollWasCritical Whether the attack resulted in a critical hit on the target.
|
|
||||||
* @param resultingDamage The amount of damage dealt to the target from this attack.
|
|
||||||
*/
|
|
||||||
data class AttackResult(val rollSucceeded: Boolean, val rollWasCritical: Boolean, val resultingDamage: Int): ActionResult
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
package simulation.fifthEd
|
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
enum class SpellMitigation(val modifier: Int) {
|
|
||||||
Half(2),
|
|
||||||
Quarter(4),
|
|
||||||
NoDamage(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface SpellSaveAttack : AttackAction {
|
|
||||||
|
|
||||||
val saveDenominator: SpellMitigation
|
|
||||||
val perfectDefense: Boolean
|
|
||||||
override fun onNormalAction(r: Random): AttackResult {
|
|
||||||
return when (saveDenominator) {
|
|
||||||
//save-or-suck style spells
|
|
||||||
SpellMitigation.NoDamage -> AttackResult(
|
|
||||||
rollSucceeded = true,
|
|
||||||
rollWasCritical = false,
|
|
||||||
resultingDamage = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
//normal partial mitigation
|
|
||||||
val damageRoll = actionRollInfo.damageRoll.roll(r)
|
|
||||||
|
|
||||||
AttackResult(
|
|
||||||
rollSucceeded = true,
|
|
||||||
rollWasCritical = false,
|
|
||||||
resultingDamage = damageRoll.result / saveDenominator.modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCriticalAction(r: Random): AttackResult { // read this as "on critical save"
|
|
||||||
return if (perfectDefense) {
|
|
||||||
AttackResult(rollSucceeded = true, rollWasCritical = true, resultingDamage = 0)
|
|
||||||
} else {
|
|
||||||
onNormalAction(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionFailure(r: Random): AttackResult { //read this on
|
|
||||||
val damageRoll = actionRollInfo.damageRoll.roll(r)
|
|
||||||
return AttackResult(rollSucceeded = false, rollWasCritical = false, damageRoll.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SpellSaveAttackImp(
|
|
||||||
override val actionRollInfo: ActionRollInfo,
|
|
||||||
override val responseValue: Int,
|
|
||||||
override val saveDenominator: SpellMitigation,
|
|
||||||
override val perfectDefense: Boolean = false
|
|
||||||
) : SpellSaveAttack
|
|
||||||
@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions.*
|
import org.junit.jupiter.api.Assertions.*
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
internal class TemporalControllerTest {
|
internal class TemporalControllerTest {
|
||||||
@ -12,7 +11,25 @@ internal class TemporalControllerTest {
|
|||||||
/**
|
/**
|
||||||
* Creates an instance of TemporalController for testing.
|
* Creates an instance of TemporalController for testing.
|
||||||
*/
|
*/
|
||||||
private val controller = object : TemporalController{}
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests that [TemporalController.sleep] blocks for the given duration.
|
* Tests that [TemporalController.sleep] blocks for the given duration.
|
||||||
@ -20,7 +37,7 @@ internal class TemporalControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `sleep blocks for given duration`() {
|
fun `sleep blocks for given duration`() {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
controller.sleep(501)
|
controller.sleep(500)
|
||||||
val end = System.currentTimeMillis()
|
val end = System.currentTimeMillis()
|
||||||
val elapsed = end - start
|
val elapsed = end - start
|
||||||
|
|
||||||
@ -41,8 +58,9 @@ internal class TemporalControllerTest {
|
|||||||
val end = System.currentTimeMillis()
|
val end = System.currentTimeMillis()
|
||||||
val elapsed = end - start
|
val elapsed = end - start
|
||||||
|
|
||||||
val lowerBound = duration
|
val lowerBound = (duration - variance / 2) - 1
|
||||||
val upperBound = duration + variance
|
val upperBound = (duration + variance / 2) + 20
|
||||||
|
println("elapsed: $elapsed, [$lowerBound, $upperBound]")
|
||||||
assertTrue(elapsed >= lowerBound)
|
assertTrue(elapsed >= lowerBound)
|
||||||
assertTrue(elapsed <= upperBound)
|
assertTrue(elapsed <= upperBound)
|
||||||
}
|
}
|
||||||
@ -61,4 +79,18 @@ internal class TemporalControllerTest {
|
|||||||
assertTrue(elapsed < 10) // assert sleep was very short
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/test/kotlin/controllers/VisionControllerTest.kt
Normal file
23
src/test/kotlin/controllers/VisionControllerTest.kt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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..<bi.resolutionVariants.size) {
|
||||||
|
val path = Paths.get("image$i.png")
|
||||||
|
val image = bi.resolutionVariants[i] as BufferedImage
|
||||||
|
ImageIO.write(image , "png", path.toFile())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,6 @@
|
|||||||
package simulation
|
package simulation
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions
|
import org.junit.jupiter.api.Assertions
|
||||||
import simulation.dice.Dice
|
|
||||||
import simulation.dice.DiceBag
|
|
||||||
import simulation.dice.RollResult
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
@ -13,7 +10,7 @@ class AttackDiceTest {
|
|||||||
fun testAttackDiceImplementation() {
|
fun testAttackDiceImplementation() {
|
||||||
val r = Random()
|
val r = Random()
|
||||||
// Test no crit below threshold
|
// Test no crit below threshold
|
||||||
val dice = DiceBag.critDice("1d20")
|
val dice = AttackDice("1d20")
|
||||||
val result = dice.roll(r)
|
val result = dice.roll(r)
|
||||||
if (result.result < 20) {
|
if (result.result < 20) {
|
||||||
Assertions.assertFalse(dice.isCrit(result))
|
Assertions.assertFalse(dice.isCrit(result))
|
||||||
@ -21,7 +18,7 @@ class AttackDiceTest {
|
|||||||
Assertions.assertTrue(dice.isCrit(result))
|
Assertions.assertTrue(dice.isCrit(result))
|
||||||
}
|
}
|
||||||
// Test no crit below threshold
|
// Test no crit below threshold
|
||||||
val dice2 = DiceBag.critDice("1d20c10")
|
val dice2 = AttackDice("1d20c19")
|
||||||
val result2 = dice2.roll(r)
|
val result2 = dice2.roll(r)
|
||||||
if (result2.result < 10) {
|
if (result2.result < 10) {
|
||||||
Assertions.assertFalse(dice2.isCrit(result2))
|
Assertions.assertFalse(dice2.isCrit(result2))
|
||||||
@ -30,7 +27,7 @@ class AttackDiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test crit threshold other than max
|
// Test crit threshold other than max
|
||||||
val dice3 = DiceBag.critDice("2d10c8")
|
val dice3 = AttackDice("2d10c8")
|
||||||
val result3 = dice3.roll(r)
|
val result3 = dice3.roll(r)
|
||||||
if (result3.result >= 8) {
|
if (result3.result >= 8) {
|
||||||
Assertions.assertTrue(dice3.isCrit(result3))
|
Assertions.assertTrue(dice3.isCrit(result3))
|
||||||
@ -44,9 +41,9 @@ class AttackDiceTest {
|
|||||||
val trueCritResult = RollResult(1, 20, 20)
|
val trueCritResult = RollResult(1, 20, 20)
|
||||||
val fakeCritResult = RollResult(1, 20, 19)
|
val fakeCritResult = RollResult(1, 20, 19)
|
||||||
|
|
||||||
val defaultRoll = DiceBag.critDice("1d20")
|
val defaultRoll = AttackDice("1d20")
|
||||||
val verboseDefaultCrit = DiceBag.critDice("1d20c20")
|
val verboseDefaultCrit = AttackDice("1d20c20")
|
||||||
val normalModifiedCrit = DiceBag.critDice("1d20c19")
|
val normalModifiedCrit = AttackDice("1d20c19")
|
||||||
|
|
||||||
Assertions.assertFalse(defaultRoll.isCrit(fakeCritResult))
|
Assertions.assertFalse(defaultRoll.isCrit(fakeCritResult))
|
||||||
Assertions.assertFalse(verboseDefaultCrit.isCrit(fakeCritResult))
|
Assertions.assertFalse(verboseDefaultCrit.isCrit(fakeCritResult))
|
||||||
|
|||||||
@ -3,35 +3,27 @@ package simulation
|
|||||||
import org.junit.jupiter.api.Assertions
|
import org.junit.jupiter.api.Assertions
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Test
|
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.*
|
import java.util.*
|
||||||
|
|
||||||
internal class DiceTests {
|
internal class DiceTests {
|
||||||
private val random = Random(1)
|
private val random = Random(1)
|
||||||
private val d20 = DiceBag.plainDice("1d20")
|
private val d20 = Dice.makeDice("1d20")
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun roll_normal() {
|
fun roll_normal() {
|
||||||
val dice = DiceBag.plainDice("2d6")
|
val dice = Dice.makeDice("2d6")
|
||||||
val result = dice.roll(random, RollType.Normal)
|
val result = dice.roll(random)
|
||||||
val result2 = dice.roll(random)
|
|
||||||
|
|
||||||
assertEquals(2, result.min)
|
assertEquals(2, result.min)
|
||||||
assertEquals(12, result.max)
|
assertEquals(12, result.max)
|
||||||
Assertions.assertTrue(result.min <= result.result && result.result <= 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
|
@Test
|
||||||
fun roll_advantage() {
|
fun roll_advantage() {
|
||||||
val dice = DiceBag.plainDice("2d6")
|
val dice = Dice.makeDice("2d6", RollType.Advantage)
|
||||||
val result = dice.roll(random, RollType.Advantage)
|
val result = dice.roll(random)
|
||||||
|
|
||||||
assertEquals(2, result.min)
|
assertEquals(2, result.min)
|
||||||
assertEquals(12, result.max)
|
assertEquals(12, result.max)
|
||||||
@ -41,8 +33,8 @@ internal class DiceTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun roll_disadvantage() {
|
fun roll_disadvantage() {
|
||||||
val dice = DiceBag.plainDice("2d6")
|
val dice = Dice.makeDice("2d6", RollType.Disadvantage)
|
||||||
val result = dice.roll(random, RollType.Disadvantage)
|
val result = dice.roll(random)
|
||||||
|
|
||||||
assertEquals(2, result.min)
|
assertEquals(2, result.min)
|
||||||
assertEquals(12, result.max)
|
assertEquals(12, result.max)
|
||||||
@ -53,7 +45,7 @@ internal class DiceTests {
|
|||||||
fun evaluate_modifiers() {
|
fun evaluate_modifiers() {
|
||||||
val mod1 = FlatModifier(1)
|
val mod1 = FlatModifier(1)
|
||||||
val mod2 = FlatModifier(2)
|
val mod2 = FlatModifier(2)
|
||||||
val dice = DiceBag.plainDice("1d20", arrayListOf(mod1, mod2))
|
val dice = Dice.makeDice("1d20", RollType.Normal, arrayListOf(mod1, mod2))
|
||||||
|
|
||||||
val bonus = dice.evaluateModifiers(random)
|
val bonus = dice.evaluateModifiers(random)
|
||||||
|
|
||||||
@ -67,11 +59,10 @@ internal class DiceTests {
|
|||||||
|
|
||||||
RollType.entries.parallelStream()
|
RollType.entries.parallelStream()
|
||||||
.forEach {
|
.forEach {
|
||||||
val dice = DiceBag.plainDice(rollString)
|
val dice = Dice.makeDice(rollString, it)
|
||||||
val r = Random(1)
|
val r = Random(1)
|
||||||
val rollType = it
|
|
||||||
repeat(iterations) {
|
repeat(iterations) {
|
||||||
val res = dice.roll(r, rollType)
|
val res = dice.roll(r)
|
||||||
Assertions.assertTrue(res.min <= res.result)
|
Assertions.assertTrue(res.min <= res.result)
|
||||||
Assertions.assertTrue(res.result <= res.max)
|
Assertions.assertTrue(res.result <= res.max)
|
||||||
}
|
}
|
||||||
@ -99,10 +90,10 @@ internal class DiceTests {
|
|||||||
|
|
||||||
RollType.entries.parallelStream()
|
RollType.entries.parallelStream()
|
||||||
.forEach {
|
.forEach {
|
||||||
val dice = DiceBag.plainDice(rollString)
|
val dice = Dice.makeDice(rollString, it)
|
||||||
val r = Random(1)
|
val r = Random(1)
|
||||||
for (i in 0..<iterations) {
|
for (i in 0..<iterations) {
|
||||||
val res = dice.roll(r, it)
|
val res = dice.roll(r)
|
||||||
if (!observedMin && res.result == nDice) {
|
if (!observedMin && res.result == nDice) {
|
||||||
observedMin = true
|
observedMin = true
|
||||||
}
|
}
|
||||||
@ -129,7 +120,7 @@ internal class DiceTests {
|
|||||||
val expectedAverageLowerBound = ((n + (n * max)) / 2) * (1 - tolerance)
|
val expectedAverageLowerBound = ((n + (n * max)) / 2) * (1 - tolerance)
|
||||||
val expectedAverageUpperBound = ((n + (n * max)) / 2) * (1 + tolerance)
|
val expectedAverageUpperBound = ((n + (n * max)) / 2) * (1 + tolerance)
|
||||||
|
|
||||||
val dice = DiceBag.plainDice(rollString)
|
val dice = Dice.makeDice(rollString, RollType.Normal)
|
||||||
var total = 0L
|
var total = 0L
|
||||||
repeat(iterations) {
|
repeat(iterations) {
|
||||||
total += dice.roll(random).result.toLong()
|
total += dice.roll(random).result.toLong()
|
||||||
@ -153,11 +144,11 @@ internal class DiceTests {
|
|||||||
|
|
||||||
val expectedAverageUpperBound = ((n + (n * max)) / 2) * tolerance
|
val expectedAverageUpperBound = ((n + (n * max)) / 2) * tolerance
|
||||||
|
|
||||||
val dice = DiceBag.plainDice(rollString)
|
val dice = Dice.makeDice(rollString, RollType.Advantage)
|
||||||
|
|
||||||
var total = 0L
|
var total = 0L
|
||||||
repeat(iterations) {
|
repeat(iterations) {
|
||||||
total += dice.roll(random, RollType.Advantage).result.toLong()
|
total += dice.roll(random).result.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
val avg = total.toDouble() / iterations.toDouble()
|
val avg = total.toDouble() / iterations.toDouble()
|
||||||
@ -177,13 +168,13 @@ internal class DiceTests {
|
|||||||
|
|
||||||
val expectedAverageLowerBound = ((n + (n * max)) / 2) * tolerance
|
val expectedAverageLowerBound = ((n + (n * max)) / 2) * tolerance
|
||||||
|
|
||||||
val dice = DiceBag.plainDice(rollString)
|
val dice = Dice.makeDice(rollString, RollType.Disadvantage)
|
||||||
|
|
||||||
|
|
||||||
var total = 0L
|
var total = 0L
|
||||||
|
|
||||||
repeat(iterations) {
|
repeat(iterations) {
|
||||||
total += dice.roll(random, RollType.Disadvantage).result.toLong()
|
total += dice.roll(random).result.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
val avg = total.toDouble() / iterations.toDouble()
|
val avg = total.toDouble() / iterations.toDouble()
|
||||||
@ -196,7 +187,7 @@ internal class DiceTests {
|
|||||||
@Test
|
@Test
|
||||||
fun verifyDistribution() {
|
fun verifyDistribution() {
|
||||||
val size = 20
|
val size = 20
|
||||||
val dice = DiceBag.plainDice("1d20")
|
val dice = Dice.makeDice("1d20")
|
||||||
val iterations = 10_000_000
|
val iterations = 10_000_000
|
||||||
val tolerance = 0.05 //5% wiggle on distribution
|
val tolerance = 0.05 //5% wiggle on distribution
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,5 @@
|
|||||||
package simulation
|
package simulation
|
||||||
|
|
||||||
import simulation.dice.DiceBag
|
|
||||||
import simulation.dice.RollType
|
|
||||||
import simulation.fifthEd.ActionRollInfo
|
|
||||||
import simulation.fifthEd.AttackResult
|
|
||||||
import simulation.fifthEd.AttackSimulatorModel
|
|
||||||
import simulation.fifthEd.SimpleMeleeAttackAction
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
class MeleeAttackTest {
|
class MeleeAttackTest {
|
||||||
@ -15,23 +9,19 @@ class MeleeAttackTest {
|
|||||||
val itt = 1_000_000
|
val itt = 1_000_000
|
||||||
val simulator = Simulator.getInstance<AttackResult>(Runtime.getRuntime().availableProcessors())
|
val simulator = Simulator.getInstance<AttackResult>(Runtime.getRuntime().availableProcessors())
|
||||||
|
|
||||||
val attackAction = ActionRollInfo(DiceBag.critDice("1d20c19"), DiceBag.plainDice("1d8"))
|
val critAttack = SimpleMeleeAttack(
|
||||||
val critAttack = SimpleMeleeAttackAction(
|
actionRoll = AttackDice("1d20c19"),
|
||||||
attackAction,
|
damageRoll = Dice.makeDice("1d8"),
|
||||||
10
|
10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
val normalAttackModel = AttackSimulatorModel(itt, critAttack, RollType.Normal)
|
|
||||||
|
val normalAttackModel = AttackSimulatorModel(itt, critAttack)
|
||||||
val normalResults = simulator.doSimulation(normalAttackModel)
|
val normalResults = simulator.doSimulation(normalAttackModel)
|
||||||
|
|
||||||
|
|
||||||
val report = ReportBuilder.getInstance()
|
AttackResult.printSimulationStatistics(normalResults, "Normal Attack")
|
||||||
.addRateMetric("Hit Rate"){it.rollSucceeded}
|
|
||||||
.addRateMetric("Crit Rate"){it.rollWasCritical}
|
|
||||||
.addAverageMetric("Avg Dmg") { it.resultingDamage.toLong() }
|
|
||||||
.build("Normal Attack")
|
|
||||||
val metrics = report.computeResults(normalResults)
|
|
||||||
println(Report.formatReport(report.name, metrics.results))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,7 +9,7 @@ class HelperFunctionsTest {
|
|||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test getApproximatelyNormalLong generates normal distribution`() {
|
fun `test getRandomLongFromNormalDistribution generates normal distribution`() {
|
||||||
|
|
||||||
// Generate a large number of samples
|
// Generate a large number of samples
|
||||||
val numSamples = 10000
|
val numSamples = 10000
|
||||||
@ -28,7 +28,7 @@ class HelperFunctionsTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
fun `test benchmark getNextLongJanky`() {
|
fun `test benchmark getNextLongJanky`() {
|
||||||
val iterations = 1000000
|
val iterations = 1000000
|
||||||
val upperBound = 1000L
|
val upperBound = 1000L
|
||||||
@ -52,7 +52,7 @@ class HelperFunctionsTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
fun `test benchmark getNextLongGaussian`() {
|
fun `test benchmark getNextLongGaussian`() {
|
||||||
|
|
||||||
val iterations = 1000000
|
val iterations = 1000000
|
||||||
@ -91,9 +91,9 @@ class HelperFunctionsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
fun `test Direct Comparison of performance`() {
|
fun `test Direct Comparison of performance`() {
|
||||||
val heats = 10
|
val heats = 1000
|
||||||
val iterations = 1000000
|
val iterations = 1000000
|
||||||
val upperBound = 1000L
|
val upperBound = 1000L
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user