Compare commits
No commits in common. "20be0861989000e0bcaba1d31cdbdf006ac4a393" and "3ce527b07f180e563e279a2f5c0c0e123c4873c9" have entirely different histories.
20be086198
...
3ce527b07f
7
src/main/kotlin/Entry.kt
Normal file
7
src/main/kotlin/Entry.kt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import game_logic.runescape.RunescapeRoutines
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
RunescapeRoutines.fullRunIncense( 0, 0, 0, 1839)
|
||||||
|
// RunescapeRoutines.processInventoryAtFurnace(2500)
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
package entries
|
|
||||||
|
|
||||||
import simulation.*
|
|
||||||
|
|
||||||
fun main(){
|
|
||||||
val start = System.currentTimeMillis()
|
|
||||||
doSimulation()
|
|
||||||
val finish = System.currentTimeMillis()
|
|
||||||
println("Simulation finished in: ${finish-start}ms")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doSimulation(){
|
|
||||||
val itt = 10_000_000
|
|
||||||
val simulator = Simulator.getInstance<AttackResult>(Runtime.getRuntime().availableProcessors())
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
AttackResult.printSimulationStatistics(normalResults, "Normal Attack (${it.name})")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package entries
|
|
||||||
|
|
||||||
import game_logic.runescape.RunescapeRoutines
|
|
||||||
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
RunescapeRoutines.processInventoryAtFurnace(1839)
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package entries
|
|
||||||
|
|
||||||
import game_logic.runescape.RunescapeRoutines
|
|
||||||
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
RunescapeRoutines.fullRunIncense(0, 0, 0, 1839)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -2,33 +2,11 @@ package simulation
|
|||||||
|
|
||||||
import java.util.*
|
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 {
|
interface Attack {
|
||||||
|
|
||||||
/**
|
val actionRoll: Dice
|
||||||
* We can't call this an 'attack' roll because it could be the case that we're attacking by forcing a DC. So whoever
|
val passValue: Int
|
||||||
* 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 {
|
fun calculateDamage(r: Random): AttackResult {
|
||||||
|
|
||||||
val attackResult = actionRoll.roll(r)
|
val attackResult = actionRoll.roll(r)
|
||||||
@ -36,7 +14,7 @@ interface Attack {
|
|||||||
|
|
||||||
|
|
||||||
return if (isHit(attackResult, attackBonus)) {
|
return if (isHit(attackResult, attackBonus)) {
|
||||||
if (actionRoll.isCrit(attackResult)) {
|
if (isCrit(attackResult)) {
|
||||||
onCriticalHit(r)
|
onCriticalHit(r)
|
||||||
} else {
|
} else {
|
||||||
onNormalHit(r)
|
onNormalHit(r)
|
||||||
@ -46,9 +24,14 @@ interface Attack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isHit(roll: RollResult, actionBonus: Int): Boolean {
|
|
||||||
|
private fun isCrit(roll: RollResult): Boolean {
|
||||||
|
return roll.max == roll.result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isHit(roll: RollResult, attackBonus: Int): Boolean {
|
||||||
//ties go to the roller
|
//ties go to the roller
|
||||||
return (roll.result + actionBonus) >= responseValue
|
return (roll.result + attackBonus) >= passValue
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNormalHit(r: Random): AttackResult
|
fun onNormalHit(r: Random): AttackResult
|
||||||
@ -59,10 +42,37 @@ interface Attack {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun ArrayList<AttackResult>.sumBools(fn: (AttackResult) -> Boolean): Int {
|
||||||
* AttackSimulatorModel simulates attacks by running [sampleSize] simulations using the provided [attack] instance.
|
return sumOf { boolToInt(fn(it)) }
|
||||||
* It implements [SimulationModel] to run the simulations and return [AttackResult].
|
}
|
||||||
*/
|
|
||||||
|
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> {
|
class AttackSimulatorModel(override val sampleSize: Int, private val attack: Attack) : SimulationModel<AttackResult> {
|
||||||
override fun simulate(r: Random): AttackResult {
|
override fun simulate(r: Random): AttackResult {
|
||||||
return attack.calculateDamage(r)
|
return attack.calculateDamage(r)
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
package simulation
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AttackDice class represents dice used for attack rolls.
|
|
||||||
* If the roll string contains a 'c' modifier, the value after 'c' is used as the crit threshold.
|
|
||||||
* Otherwise, the crit threshold defaults to the max possible roll (number of dice * die size).
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Example rollStrings:
|
|
||||||
*
|
|
||||||
* "1d20"
|
|
||||||
*
|
|
||||||
* "1d20c18"
|
|
||||||
*
|
|
||||||
* "3d8c15"
|
|
||||||
*
|
|
||||||
* The isCrit() method checks if a roll result meets the crit threshold.
|
|
||||||
*/
|
|
||||||
class AttackDice(
|
|
||||||
override val rollString: String,
|
|
||||||
override val rollType: RollType = RollType.Normal,
|
|
||||||
override val modifiers: List<Modifier<Int>> = ArrayList()
|
|
||||||
) : Dice {
|
|
||||||
override val nDice: Int
|
|
||||||
override val dieSize: Int
|
|
||||||
|
|
||||||
private val critThreshold: Int
|
|
||||||
|
|
||||||
init {
|
|
||||||
val cleanRollString = rollString.lowercase()
|
|
||||||
|
|
||||||
if (cleanRollString.contains('c')) {
|
|
||||||
val critModifierParts = cleanRollString.split("c")
|
|
||||||
val parts = critModifierParts[0].split('d')
|
|
||||||
nDice = parts[0].toInt()
|
|
||||||
dieSize = parts[1].toInt()
|
|
||||||
critThreshold = critModifierParts[1].toInt()
|
|
||||||
} else {
|
|
||||||
val parts = cleanRollString.split('d')
|
|
||||||
nDice = parts[0].toInt()
|
|
||||||
dieSize = parts[1].toInt()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
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,8 +1,6 @@
|
|||||||
package simulation
|
package simulation
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@ -33,18 +31,25 @@ enum class RollType {
|
|||||||
data class RollResult(val min: Int, val max: Int, val result: Int)
|
data class RollResult(val min: Int, val max: Int, val result: Int)
|
||||||
|
|
||||||
|
|
||||||
interface Dice {
|
/**
|
||||||
val rollString: String
|
* Represents dice that can be rolled with different roll types and modifiers.
|
||||||
val rollType: RollType
|
*
|
||||||
val modifiers: List<Modifier<Int>>
|
* @param rollString The dice roll notation, e.g. "2d6"
|
||||||
val nDice: Int
|
* @param rollType The roll type to use, which determines how the dice are rolled
|
||||||
val dieSize: Int
|
* @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>
|
||||||
|
) {
|
||||||
|
private val nDice: Int
|
||||||
|
private val dieSize: Int
|
||||||
|
|
||||||
|
init {
|
||||||
companion object {
|
val parts = rollString.lowercase().split("d")
|
||||||
fun makeDice(rollString: String, rollType: RollType = Normal, modifiers: List<Modifier<Int>> = ArrayList()): Dice {
|
nDice = parts[0].toInt()
|
||||||
return SimpleDice(rollString, rollType, modifiers)
|
dieSize = parts[1].toInt()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,32 +64,33 @@ interface Dice {
|
|||||||
* @see RollType
|
* @see RollType
|
||||||
*/
|
*/
|
||||||
fun roll(r: Random): RollResult {
|
fun roll(r: Random): RollResult {
|
||||||
val range = (dieSize * nDice) - nDice
|
|
||||||
val result = when (rollType) {
|
val result = when (rollType) {
|
||||||
Advantage -> advantageRoll(r, nDice, range)
|
RollType.Advantage -> onAdvantage(r, nDice, dieSize)
|
||||||
Disadvantage -> disadvantageRoll(r, nDice, range)
|
RollType.Disadvantage -> onDisadvantage(r, nDice, dieSize)
|
||||||
else -> normalRoll(r, nDice, range)
|
else -> onNormalRoll(r, nDice, dieSize)
|
||||||
}
|
}
|
||||||
return RollResult(nDice, dieSize * nDice, result)
|
return RollResult(nDice, dieSize * nDice, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
private fun advantageRoll(r: Random, nDice: Int, range: Int): Int {
|
return max(roll1, roll2)
|
||||||
val roll1 = r.nextInt(range + 1)
|
|
||||||
val roll2 = r.nextInt(range + 1)
|
|
||||||
|
|
||||||
return max(roll1, roll2) + nDice
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun disadvantageRoll(r: Random, nDice: Int, range: Int): Int {
|
private fun onDisadvantage(r: Random, nDice: Int, dieSize: Int): Int {
|
||||||
val roll1 = r.nextInt(range + 1)
|
val range = (dieSize * nDice)
|
||||||
val roll2 = r.nextInt(range + 1)
|
val roll1 = r.nextInt(range) + nDice
|
||||||
|
val roll2 = r.nextInt(range) + nDice
|
||||||
|
|
||||||
return min(roll1, roll2) + nDice
|
return min(roll1, roll2)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun normalRoll(r: Random, nDice: Int, range: Int): Int {
|
private fun onNormalRoll(r: Random, nDice: Int, dieSize: Int): Int {
|
||||||
return r.nextInt(range + 1) + nDice
|
val range = (dieSize * nDice)
|
||||||
|
return r.nextInt(range) + nDice
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,20 +102,4 @@ 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) }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class SimpleDice(
|
|
||||||
override val rollString: String,
|
|
||||||
override val rollType: RollType = Normal,
|
|
||||||
override val modifiers: List<Modifier<Int>> = ArrayList()
|
|
||||||
) : Dice {
|
|
||||||
override val nDice: Int
|
|
||||||
override val dieSize: Int
|
|
||||||
|
|
||||||
init {
|
|
||||||
val cleanRollString = rollString.lowercase()
|
|
||||||
val parts = cleanRollString.split('d')
|
|
||||||
nDice = parts[0].toInt()
|
|
||||||
dieSize = parts[1].toInt()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -7,12 +7,12 @@ import java.util.*
|
|||||||
*
|
*
|
||||||
* @param actionRoll 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 damageRoll The dice roll used to determine damage if attack hits.
|
||||||
* @param responseValue The defense value the attack must exceed to hit.
|
* @param passValue The defense value the attack must exceed to hit.
|
||||||
*/
|
*/
|
||||||
class SimpleMeleeAttack(
|
class SimpleMeleeAttack(
|
||||||
override val actionRoll: AttackDice,
|
override val actionRoll: Dice,
|
||||||
val damageRoll: Dice,
|
val damageRoll: Dice,
|
||||||
override val responseValue: Int
|
override val passValue: Int
|
||||||
) : Attack {
|
) : Attack {
|
||||||
|
|
||||||
override fun onNormalHit(r: Random): AttackResult {
|
override fun onNormalHit(r: Random): AttackResult {
|
||||||
|
|||||||
@ -28,10 +28,7 @@ interface Modifier<T> {
|
|||||||
*
|
*
|
||||||
* @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) : Modifier<Int> {
|
class DiceBonus(private val dice: Dice) : Modifier<Int> {
|
||||||
|
|
||||||
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){
|
||||||
dice.roll(r).result + dice.roll(r).result
|
dice.roll(r).result + dice.roll(r).result
|
||||||
@ -50,7 +47,7 @@ class DiceBonusModifier(private val dice: Dice) : Modifier<Int> {
|
|||||||
*
|
*
|
||||||
* @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): Modifier<Int>{
|
class DicePenalty(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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
package simulation
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.test.Test
|
|
||||||
|
|
||||||
class AttackDiceTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testAttackDiceImplementation() {
|
|
||||||
val r = Random()
|
|
||||||
// Test no crit below threshold
|
|
||||||
val dice = AttackDice("1d20")
|
|
||||||
val result = dice.roll(r)
|
|
||||||
if (result.result < 20) {
|
|
||||||
Assertions.assertFalse(dice.isCrit(result))
|
|
||||||
} else {
|
|
||||||
Assertions.assertTrue(dice.isCrit(result))
|
|
||||||
}
|
|
||||||
// Test no crit below threshold
|
|
||||||
val dice2 = AttackDice("1d20c19")
|
|
||||||
val result2 = dice2.roll(r)
|
|
||||||
if (result2.result < 10) {
|
|
||||||
Assertions.assertFalse(dice2.isCrit(result2))
|
|
||||||
} else {
|
|
||||||
Assertions.assertTrue(dice2.isCrit(result2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test crit threshold other than max
|
|
||||||
val dice3 = AttackDice("2d10c8")
|
|
||||||
val result3 = dice3.roll(r)
|
|
||||||
if (result3.result >= 8) {
|
|
||||||
Assertions.assertTrue(dice3.isCrit(result3))
|
|
||||||
} else {
|
|
||||||
Assertions.assertFalse(dice3.isCrit(result3))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun validateCritFunctionality() {
|
|
||||||
val trueCritResult = RollResult(1, 20, 20)
|
|
||||||
val fakeCritResult = RollResult(1, 20, 19)
|
|
||||||
|
|
||||||
val defaultRoll = AttackDice("1d20")
|
|
||||||
val verboseDefaultCrit = AttackDice("1d20c20")
|
|
||||||
val normalModifiedCrit = AttackDice("1d20c19")
|
|
||||||
|
|
||||||
Assertions.assertFalse(defaultRoll.isCrit(fakeCritResult))
|
|
||||||
Assertions.assertFalse(verboseDefaultCrit.isCrit(fakeCritResult))
|
|
||||||
Assertions.assertTrue(normalModifiedCrit.isCrit(fakeCritResult))
|
|
||||||
|
|
||||||
Assertions.assertTrue(defaultRoll.isCrit(trueCritResult))
|
|
||||||
Assertions.assertTrue(verboseDefaultCrit.isCrit(trueCritResult))
|
|
||||||
Assertions.assertTrue(normalModifiedCrit.isCrit(trueCritResult))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -6,13 +6,11 @@ import org.junit.jupiter.api.Test
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
internal class DiceTests {
|
internal class DiceTests {
|
||||||
private val random = Random(1)
|
|
||||||
private val d20 = Dice.makeDice("1d20")
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun roll_normal() {
|
fun roll_normal() {
|
||||||
val dice = Dice.makeDice("2d6")
|
val dice = Dice("2d6")
|
||||||
|
val random = Random(1)
|
||||||
val result = dice.roll(random)
|
val result = dice.roll(random)
|
||||||
|
|
||||||
assertEquals(2, result.min)
|
assertEquals(2, result.min)
|
||||||
@ -22,7 +20,8 @@ internal class DiceTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun roll_advantage() {
|
fun roll_advantage() {
|
||||||
val dice = Dice.makeDice("2d6", RollType.Advantage)
|
val dice = Dice("2d6", RollType.Advantage)
|
||||||
|
val random = Random(1)
|
||||||
val result = dice.roll(random)
|
val result = dice.roll(random)
|
||||||
|
|
||||||
assertEquals(2, result.min)
|
assertEquals(2, result.min)
|
||||||
@ -33,7 +32,8 @@ internal class DiceTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun roll_disadvantage() {
|
fun roll_disadvantage() {
|
||||||
val dice = Dice.makeDice("2d6", RollType.Disadvantage)
|
val dice = Dice("2d6", RollType.Disadvantage)
|
||||||
|
val random = Random(1)
|
||||||
val result = dice.roll(random)
|
val result = dice.roll(random)
|
||||||
|
|
||||||
assertEquals(2, result.min)
|
assertEquals(2, result.min)
|
||||||
@ -45,168 +45,12 @@ 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 = Dice.makeDice("1d20", RollType.Normal, arrayListOf(mod1, mod2))
|
val dice = Dice("1d20", RollType.Normal, mod1, mod2)
|
||||||
|
|
||||||
|
val random = Random(1)
|
||||||
val bonus = dice.evaluateModifiers(random)
|
val bonus = dice.evaluateModifiers(random)
|
||||||
|
|
||||||
assertEquals(3, bonus)
|
assertEquals(3, bonus)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun verifyRollRangeForTypes() {
|
|
||||||
val rollString = "2d6"
|
|
||||||
val iterations = 10_000_000
|
|
||||||
|
|
||||||
RollType.entries.parallelStream()
|
|
||||||
.forEach {
|
|
||||||
val dice = Dice.makeDice(rollString, it)
|
|
||||||
val r = Random(1)
|
|
||||||
repeat(iterations) {
|
|
||||||
val res = dice.roll(r)
|
|
||||||
Assertions.assertTrue(res.min <= res.result)
|
|
||||||
Assertions.assertTrue(res.result <= res.max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun verifyBoundariesForTypes() {
|
|
||||||
verifyBoundariesForTypes(1, 20)
|
|
||||||
verifyBoundariesForTypes(2, 6)
|
|
||||||
verifyBoundariesForTypes(5, 8)
|
|
||||||
verifyBoundariesForTypes(1000, 6)
|
|
||||||
verifyBoundariesForTypes(6, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun verifyBoundariesForTypes(nDice: Int, dieSize: Int) {
|
|
||||||
val rollString = "${nDice}d${dieSize}"
|
|
||||||
val iterations = 10_000_000
|
|
||||||
val max = nDice * dieSize
|
|
||||||
|
|
||||||
var observedMin = false
|
|
||||||
var observedMax = false
|
|
||||||
|
|
||||||
|
|
||||||
RollType.entries.parallelStream()
|
|
||||||
.forEach {
|
|
||||||
val dice = Dice.makeDice(rollString, it)
|
|
||||||
val r = Random(1)
|
|
||||||
for (i in 0..<iterations) {
|
|
||||||
val res = dice.roll(r)
|
|
||||||
if (!observedMin && res.result == nDice) {
|
|
||||||
observedMin = true
|
|
||||||
}
|
|
||||||
if (!observedMax && res.result == max) {
|
|
||||||
observedMax = true
|
|
||||||
}
|
|
||||||
if (observedMin && observedMax) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Assertions.assertTrue(observedMin)
|
|
||||||
Assertions.assertTrue(observedMax)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun verifyNormalRollWithinExpectedBounds() {
|
|
||||||
val n = 2
|
|
||||||
val max = 100
|
|
||||||
val rollString = "${n}d${max}"
|
|
||||||
val tolerance = 0.05 //we expect more than a 25% improvement
|
|
||||||
val iterations = 100_000_000
|
|
||||||
|
|
||||||
val expectedAverageLowerBound = ((n + (n * max)) / 2) * (1 - tolerance)
|
|
||||||
val expectedAverageUpperBound = ((n + (n * max)) / 2) * (1 + tolerance)
|
|
||||||
|
|
||||||
val dice = Dice.makeDice(rollString, RollType.Normal)
|
|
||||||
var total = 0L
|
|
||||||
repeat(iterations) {
|
|
||||||
total += dice.roll(random).result.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
val avg = total.toDouble() / iterations.toDouble()
|
|
||||||
|
|
||||||
//assert that the observed average is greater than the expected lower bound of the normal roll scaled by the
|
|
||||||
//tolerance
|
|
||||||
Assertions.assertTrue(avg > expectedAverageLowerBound)
|
|
||||||
Assertions.assertTrue(avg < expectedAverageUpperBound)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun verifyAdvantageSkew() {
|
|
||||||
val n = 2
|
|
||||||
val max = 100
|
|
||||||
val rollString = "${n}d${max}"
|
|
||||||
val tolerance = 1.25 //we expect more than a 25% improvement
|
|
||||||
val iterations = 10_000_000
|
|
||||||
|
|
||||||
val expectedAverageUpperBound = ((n + (n * max)) / 2) * tolerance
|
|
||||||
|
|
||||||
val dice = Dice.makeDice(rollString, RollType.Advantage)
|
|
||||||
|
|
||||||
var total = 0L
|
|
||||||
repeat(iterations) {
|
|
||||||
total += dice.roll(random).result.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
val avg = total.toDouble() / iterations.toDouble()
|
|
||||||
|
|
||||||
//assert that the observed average is greater than the expected upper bound of the normal roll scaled by the
|
|
||||||
//tolerance
|
|
||||||
Assertions.assertTrue(avg > expectedAverageUpperBound)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun verifyDisadvantageSkew() {
|
|
||||||
val n = 2
|
|
||||||
val max = 100
|
|
||||||
val rollString = "${n}d${max}"
|
|
||||||
val tolerance = 0.75 //we expect more than a 25% penalty
|
|
||||||
val iterations = 10_000_000
|
|
||||||
|
|
||||||
val expectedAverageLowerBound = ((n + (n * max)) / 2) * tolerance
|
|
||||||
|
|
||||||
val dice = Dice.makeDice(rollString, RollType.Disadvantage)
|
|
||||||
|
|
||||||
|
|
||||||
var total = 0L
|
|
||||||
|
|
||||||
repeat(iterations) {
|
|
||||||
total += dice.roll(random).result.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
val avg = total.toDouble() / iterations.toDouble()
|
|
||||||
|
|
||||||
//assert that the observed average is less than the expected lower bound of the normal roll scaled by the
|
|
||||||
//tolerance
|
|
||||||
Assertions.assertTrue(avg < expectedAverageLowerBound)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun verifyDistribution() {
|
|
||||||
val size = 20
|
|
||||||
val dice = Dice.makeDice("1d20")
|
|
||||||
val iterations = 10_000_000
|
|
||||||
val tolerance = 0.05 //5% wiggle on distribution
|
|
||||||
|
|
||||||
val m = HashMap<Int, Int>()
|
|
||||||
|
|
||||||
repeat(iterations) {
|
|
||||||
val rollResult = dice.roll(random)
|
|
||||||
val total = m.getOrDefault(rollResult.result, 0)
|
|
||||||
m[rollResult.result] = (total + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val expected = iterations.toDouble() / size.toDouble()
|
|
||||||
|
|
||||||
m.forEach { (_, count) ->
|
|
||||||
val dCount = count.toDouble()
|
|
||||||
Assertions.assertTrue(dCount > (expected * (1 - tolerance)))
|
|
||||||
Assertions.assertTrue(dCount < (expected * (1 + tolerance)))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
package simulation
|
|
||||||
|
|
||||||
import kotlin.test.Test
|
|
||||||
|
|
||||||
class MeleeAttackTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testAttack() {
|
|
||||||
val itt = 1_000_000
|
|
||||||
val simulator = Simulator.getInstance<AttackResult>(Runtime.getRuntime().availableProcessors())
|
|
||||||
|
|
||||||
val critAttack = SimpleMeleeAttack(
|
|
||||||
actionRoll = AttackDice("1d20c19"),
|
|
||||||
damageRoll = Dice.makeDice("1d8"),
|
|
||||||
10
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val normalAttackModel = AttackSimulatorModel(itt, critAttack)
|
|
||||||
val normalResults = simulator.doSimulation(normalAttackModel)
|
|
||||||
|
|
||||||
|
|
||||||
AttackResult.printSimulationStatistics(normalResults, "Normal Attack")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,6 +14,34 @@ class SimulatorTest {
|
|||||||
val finish = System.nanoTime()
|
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 = 1_000_000
|
||||||
|
val simulator = Simulator.getInstance<AttackResult>(Runtime.getRuntime().availableProcessors())
|
||||||
|
|
||||||
|
val attack = SimpleMeleeAttack(
|
||||||
|
Dice("1d20", RollType.Normal, FlatModifier(5)),
|
||||||
|
Dice("2d6", RollType.Normal, FlatModifier(5)),
|
||||||
|
15
|
||||||
|
)
|
||||||
|
|
||||||
|
val attackWithAdvantageAndBless = SimpleMeleeAttack(
|
||||||
|
Dice("1d20", RollType.Advantage, FlatModifier(5), DiceBonus(Dice("1d4"))),
|
||||||
|
Dice("2d6", RollType.Normal, FlatModifier(5)),
|
||||||
|
15
|
||||||
|
)
|
||||||
|
|
||||||
|
val normalAttackModel = AttackSimulatorModel(itt, attack)
|
||||||
|
val normalResults = simulator.doSimulation(normalAttackModel)
|
||||||
|
|
||||||
|
val buffedAttackModel = AttackSimulatorModel(itt, attackWithAdvantageAndBless)
|
||||||
|
val buffedResults = simulator.doSimulation(buffedAttackModel)
|
||||||
|
|
||||||
|
|
||||||
|
AttackResult.printSimulationStatistics(normalResults, "Normal Attack")
|
||||||
|
AttackResult.printSimulationStatistics(buffedResults, "Buffed Attack")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user