ConsoleLauncher: Add binomial test output (#12496)

* Add binomial test

* Fix Major Civ count
This commit is contained in:
itanasi
2024-11-20 03:30:17 -08:00
committed by GitHub
parent e4f5d65684
commit 6bf3de0549

View File

@ -10,6 +10,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.math.max
import kotlin.math.sqrt
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime
@ -23,9 +24,10 @@ class Simulation(
) {
private val maxSimulations = threadsNumber * simulationsPerThread
val civilizations = newGameInfo.civilizations.filter { it.civName != Constants.spectator }.map { it.civName }
private val majorCivs = newGameInfo.civilizations.filter { it.civName != Constants.spectator }.filter { it.isMajorCiv() }.size
private var startTime: Long = 0
var steps = ArrayList<SimulationStep>()
var winRate = mutableMapOf<String, MutableInt>()
var numWins = mutableMapOf<String, MutableInt>()
private var winRateByVictory = HashMap<String, MutableMap<String, MutableInt>>()
private var avgSpeed = 0f
private var avgDuration: Duration = Duration.ZERO
@ -36,7 +38,7 @@ class Simulation(
init{
for (civ in civilizations) {
this.winRate[civ] = MutableInt(0)
this.numWins[civ] = MutableInt(0)
winRateByVictory[civ] = mutableMapOf()
for (victory in UncivGame.Current.gameInfo!!.ruleset.victories.keys)
winRateByVictory[civ]!![victory] = MutableInt(0)
@ -94,11 +96,11 @@ class Simulation(
fun getStats() {
// win Rate
winRate.values.forEach { it.value = 0 }
numWins.values.forEach { it.value = 0 }
winRateByVictory.flatMap { it.value.values }.forEach { it.value = 0 }
steps.forEach {
if (it.winner != null) {
winRate[it.winner!!]!!.inc()
numWins[it.winner!!]!!.inc()
winRateByVictory[it.winner!!]!![it.victoryType]!!.inc()
}
}
@ -112,16 +114,29 @@ class Simulation(
var outString = ""
for (civ in civilizations) {
val wins = winRate[civ]!!.value * 100 / max(steps.size, 1)
if (wins == 0) continue
val numSteps = max(steps.size, 1)
val expWinRate = 1f / majorCivs
val winRate = numWins[civ]!!.value * 100 / numSteps
if (winRate == 0) continue
outString += "\n$civ:\n"
outString += "$wins% total win rate \n"
outString += "$winRate% total win rate \n"
if (numSteps * expWinRate >= 10 && numSteps * (1 - expWinRate) >= 10) {
// large enough sample size, binomial distribution approximates the Normal Curve
val pval = binomialTest(
numWins[civ]!!.value.toDouble(),
numSteps.toDouble(),
expWinRate.toDouble(),
"greater"
)
outString += "one-tail binomial pval = $pval\n"
}
for (victory in UncivGame.Current.gameInfo!!.ruleset.victories.keys) {
val winsVictory = winRateByVictory[civ]!![victory]!!.value * 100 / max(winRate[civ]!!.value, 1)
val winsVictory =
winRateByVictory[civ]!![victory]!!.value * 100 / max(numWins[civ]!!.value, 1)
outString += "$victory: $winsVictory% "
}
outString += "\n"
outString += "\n"
}
outString += "\nAverage speed: %.1f turns/s \n".format(avgSpeed)
outString += "Average game duration: $avgDuration\n"
@ -129,5 +144,36 @@ class Simulation(
return outString
}
private fun binomialTest(successes: Double, trials: Double, p: Double, alternative: String): Double {
val q = 1 - p
val mean = trials * p
val variance = trials * p * q
val stdDev = sqrt(variance)
val z = (successes - mean) / stdDev
val pValue = 1 - normalCdf(z)
return when (alternative) {
"greater" -> pValue
"less" -> 1 - pValue
else -> throw IllegalArgumentException("Alternative must be 'greater' or 'less'")
}
}
private fun normalCdf(z: Double): Double {
return 0.5 * (1 + erf(z / sqrt(2.0)))
}
// Approximation of the error function
private fun erf(x: Double): Double {
val t = 1.0 / (1.0 + 0.5 * kotlin.math.abs(x))
val tau =
t * kotlin.math.exp(
-x * x - 1.26551223 +
t * (1.00002368 + t * (0.37409196 + t * (0.09678418
+ t * (-0.18628806 + t * (0.27886807 + t * (-1.13520398 +
t * (1.48851587 + t * (-0.82215223 + t * 0.17087277))))))))
)
return if (x >= 0) 1 - tau else tau - 1
}
}