broke up reporting into some component pieces

This commit is contained in:
dtookey 2023-09-05 09:53:44 -04:00
parent 5c7d72ea86
commit 28bb5e30e0
10 changed files with 217 additions and 151 deletions

View File

@ -7,6 +7,10 @@ import simulation.fifthEd.AttackResult
import simulation.fifthEd.AttackSimulatorModel
import simulation.fifthEd.MeleeAttackBuilder
import simulation.fifthEd.SimpleMeleeAttackAction
import simulation.reporting.MetricReport
import simulation.reporting.MetricResult
import simulation.reporting.Report
import simulation.reporting.ReportBuilder
fun main() {
val rounds = 1_000_000

View File

@ -1,116 +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 name: String, 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)
.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, it.formatResults(value), 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
}
fun addStdDevMetric(metricName: String, fieldMapFn: (AttackResult) -> Long): ReportBuilder {
metrics.add(StdDevMetric(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

View File

@ -39,7 +39,7 @@ interface CritDice : Dice {
*
* The isCrit() method checks if a roll result meets the crit threshold.
*/
internal class AttackDiceImpl(
internal class CritDiceImpl(
override val rollString: String,
override val modifiers: List<DiceModifier<Int>> = ArrayList()
) : CritDice {

View File

@ -6,7 +6,7 @@ object DiceBag {
}
fun critDice(rollString: String, modifiers: List<DiceModifier<Int>> = ArrayList()): CritDice {
return AttackDiceImpl(rollString, modifiers)
return CritDiceImpl(rollString, modifiers)
}
fun rerollDice(

View File

@ -5,35 +5,3 @@ 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)
}
}

View File

@ -0,0 +1,35 @@
package simulation.fifthEd
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 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)
}
}

View File

@ -0,0 +1,56 @@
package simulation.reporting
import simulation.fifthEd.AttackResult
import kotlin.math.pow
interface Metric<T: Number> {
val metricName: String
fun mapToMetric(results: List<AttackResult>): T
fun formatResults(result: T): String
fun toResult(results: T): MetricResult {
return MetricResult(this.metricName, this.formatResults(results), results.toDouble())
}
}
private fun Boolean.toInt(): Int = if (this) 1 else 0
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(Boolean::toInt).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)
}
}
data class MetricResult(val name: String, val label: String, val metricValue: Double)

View File

@ -0,0 +1,63 @@
package simulation.reporting
import simulation.fifthEd.AttackResult
/**
* Interface for generating metric reports.
*
* @property name The name of the report.
* @property metrics The list of metrics to compute for the report.
*
* @see Metric
*/
interface Report {
val name: String
val metrics: List<Metric<Double>>
companion object {
/**
* Formats the given report results into a string.
*
* @param name The name of the report.
* @param results The computed metric results.
* @return The formatted report string.
*/
fun formatReport(name: String, results: List<MetricResult>): String {
val builder = StringBuilder("[$name]:\n")
results.forEach {
builder.append(it.label)
.append('\t')
}
return builder.toString()
}
}
/**
* Computes the metric results for this report.
*
* @param results The list of attack results to compute metrics over.
* @return A MetricReport containing the computed metric results.
*/
fun computeResults(results: List<AttackResult>): MetricReport {
val m = ArrayList<MetricResult>(metrics.size)
metrics.forEach {
val value = it.mapToMetric(results)
m.add(it.toResult(value))
}
return MetricReport(name, m)
}
}
/**
* Internal implementation of the [Report] interface.
*
* @param name The name of the report.
* @param metrics The list of metrics to compute for this report.
*
* @constructor Creates a new [ReportImpl] instance.
*/
internal class ReportImpl(override val name: String, override val metrics: List<Metric<Double>>) : Report
data class MetricReport(val name: String, val results: List<MetricResult>)

View File

@ -0,0 +1,54 @@
package simulation.reporting
import simulation.fifthEd.AttackResult
/**
* Builder class to construct [Report] instances.
*
* This class allows you to configure the metrics that will be included in the report.
*
* Use the companion object's [getInstance] method to get an instance of [ReportBuilder],
* then call methods like [addAverageMetric] to add metrics.
*
* Call [build] and pass the name of the report to construct the [Report] instance with
* the configured metrics.
*/
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
}
fun addStdDevMetric(metricName: String, fieldMapFn: (AttackResult) -> Long): ReportBuilder {
metrics.add(StdDevMetric(metricName, fieldMapFn))
return this
}
}
/**
* Internal implementation of the [ReportBuilder] interface.
*
* This class holds the list of metrics and allows adding new metrics.
*
* @param metrics The mutable list of metrics to include in the report.
*
* @constructor Creates a new [ReportBuilderImpl] instance with an empty metrics list.
*/
internal class ReportBuilderImpl(override val metrics: MutableList<Metric<Double>> = ArrayList()) : ReportBuilder

View File

@ -6,6 +6,8 @@ import simulation.fifthEd.ActionRollInfo
import simulation.fifthEd.AttackResult
import simulation.fifthEd.AttackSimulatorModel
import simulation.fifthEd.SimpleMeleeAttackAction
import simulation.reporting.Report
import simulation.reporting.ReportBuilder
import kotlin.test.Test
class MeleeAttackTest {