diff --git a/src/main/kotlin/entries/BGSimulation.kt b/src/main/kotlin/entries/BGSimulation.kt index b1cd35d..656f81d 100644 --- a/src/main/kotlin/entries/BGSimulation.kt +++ b/src/main/kotlin/entries/BGSimulation.kt @@ -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 @@ -124,7 +128,7 @@ fun generateCombatSuggestions( } } -fun defaultReport(): ReportBuilder{ +fun defaultReport(): ReportBuilder { return ReportBuilder.getInstance() .addRateMetric("Accuracy") { it.rollSucceeded } .addRateMetric("Crit Rate") { it.rollWasCritical } diff --git a/src/main/kotlin/simulation/Reporting.kt b/src/main/kotlin/simulation/Reporting.kt deleted file mode 100644 index 26951fe..0000000 --- a/src/main/kotlin/simulation/Reporting.kt +++ /dev/null @@ -1,116 +0,0 @@ -package simulation - -import simulation.fifthEd.AttackResult -import kotlin.math.pow - -data class MetricReport(val name: String, val results: List) -data class MetricResult(val name: String, val label: String, val metricValue: Double) - -interface Report { - val name: String - val metrics: List> - - companion object { - fun formatReport(name: String, results: List): String { - val builder = StringBuilder() - results.forEach { - builder.append(it.label) - .append('\t') - - } - return builder.toString() - } - } - - - fun computeResults(results: List): MetricReport { - val m = ArrayList(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>) : Report - -interface ReportBuilder { - val metrics: MutableList> - - 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> = ArrayList() - -} - -interface Metric { - val metricName: String - fun mapToMetric(results: List): T - - fun formatResults(result: T): String - -} - -class AverageMetric(override val metricName: String, private val fieldMapFn: (AttackResult) -> Long) : Metric { - override fun mapToMetric(results: List): 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 { - override fun mapToMetric(results: List): 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 { - override fun mapToMetric(results: List): 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 \ No newline at end of file diff --git a/src/main/kotlin/simulation/dice/CritDice.kt b/src/main/kotlin/simulation/dice/CritDice.kt index f965a8e..7640807 100644 --- a/src/main/kotlin/simulation/dice/CritDice.kt +++ b/src/main/kotlin/simulation/dice/CritDice.kt @@ -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> = ArrayList() ) : CritDice { diff --git a/src/main/kotlin/simulation/dice/DiceBag.kt b/src/main/kotlin/simulation/dice/DiceBag.kt index 8f0fad5..052a2f5 100644 --- a/src/main/kotlin/simulation/dice/DiceBag.kt +++ b/src/main/kotlin/simulation/dice/DiceBag.kt @@ -6,7 +6,7 @@ object DiceBag { } fun critDice(rollString: String, modifiers: List> = ArrayList()): CritDice { - return AttackDiceImpl(rollString, modifiers) + return CritDiceImpl(rollString, modifiers) } fun rerollDice( diff --git a/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt b/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt index 0d5b489..1049486 100644 --- a/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt +++ b/src/main/kotlin/simulation/fifthEd/ActionRollInfo.kt @@ -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) - } - -} diff --git a/src/main/kotlin/simulation/fifthEd/SimpleMeleeAttackAction.kt b/src/main/kotlin/simulation/fifthEd/SimpleMeleeAttackAction.kt new file mode 100644 index 0000000..8c6a2a1 --- /dev/null +++ b/src/main/kotlin/simulation/fifthEd/SimpleMeleeAttackAction.kt @@ -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) + } + +} diff --git a/src/main/kotlin/simulation/reporting/Metric.kt b/src/main/kotlin/simulation/reporting/Metric.kt new file mode 100644 index 0000000..92d0d77 --- /dev/null +++ b/src/main/kotlin/simulation/reporting/Metric.kt @@ -0,0 +1,56 @@ +package simulation.reporting + +import simulation.fifthEd.AttackResult +import kotlin.math.pow + + +interface Metric { + val metricName: String + fun mapToMetric(results: List): 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 { + override fun mapToMetric(results: List): 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 { + override fun mapToMetric(results: List): 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 { + override fun mapToMetric(results: List): 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) + + diff --git a/src/main/kotlin/simulation/reporting/Report.kt b/src/main/kotlin/simulation/reporting/Report.kt new file mode 100644 index 0000000..95fe4d5 --- /dev/null +++ b/src/main/kotlin/simulation/reporting/Report.kt @@ -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> + + 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): 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): MetricReport { + val m = ArrayList(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>) : Report + + +data class MetricReport(val name: String, val results: List) \ No newline at end of file diff --git a/src/main/kotlin/simulation/reporting/ReportBuilder.kt b/src/main/kotlin/simulation/reporting/ReportBuilder.kt new file mode 100644 index 0000000..ace0059 --- /dev/null +++ b/src/main/kotlin/simulation/reporting/ReportBuilder.kt @@ -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> + + 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> = ArrayList()) : ReportBuilder diff --git a/src/test/kotlin/simulation/MeleeAttackTest.kt b/src/test/kotlin/simulation/MeleeAttackTest.kt index 92dd92a..a1b66aa 100644 --- a/src/test/kotlin/simulation/MeleeAttackTest.kt +++ b/src/test/kotlin/simulation/MeleeAttackTest.kt @@ -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 {