From: TheSaminator Date: Sun, 5 Jun 2022 19:52:13 +0000 (-0400) Subject: Add histograms to AI training X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=362f46db274bd1749f0eccc0a8cf89ea0cc25654;p=starship-fights Add histograms to AI training --- diff --git a/src/commonMain/kotlin/starshipfights/game/ai/ai_optimization.kt b/src/commonMain/kotlin/starshipfights/game/ai/ai_optimization.kt index 035724c..09695b8 100644 --- a/src/commonMain/kotlin/starshipfights/game/ai/ai_optimization.kt +++ b/src/commonMain/kotlin/starshipfights/game/ai/ai_optimization.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.selects.select import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import starshipfights.data.Id @@ -249,7 +250,7 @@ data class InstinctGamePairing( val guest: Instincts ) -suspend fun performTrials(numTrialsPerPairing: Int, instincts: Set, validBattleSizes: Set = BattleSize.values().toSet(), validFactions: Set = Faction.values().toSet()): Map { +suspend fun performTrials(numTrialsPerPairing: Int, instincts: Set, validBattleSizes: Set = BattleSize.values().toSet(), validFactions: Set = Faction.values().toSet(), cancellationJob: Job = Job(), onProgress: suspend () -> Unit = {}): Map { return coroutineScope { instincts.associateWith { host -> async { @@ -263,11 +264,19 @@ suspend fun performTrials(numTrialsPerPairing: Int, instincts: Set, v val guestFaction = validFactions.random() val gameState = generateOptimizationInitialState(hostFaction, guestFaction, BattleInfo(battleSize, BattleBackground.BLUE_BROWN)) - val winner = withTimeoutOrNull(30_000L * numTrialsPerPairing) { - performTestSession(gameState, host, guest) + val winner = withTimeoutOrNull((20_000L * numTrialsPerPairing) + (400L * numTrialsPerPairing * numTrialsPerPairing)) { + val deferred = async(cancellationJob) { + performTestSession(gameState, host, guest) + } + + select { + cancellationJob.onJoin { null } + deferred.onAwait { it } + } } logInfo("A trial has ended! Winner: ${winner ?: "NEITHER"}") + onProgress() when (winner) { GlobalSide.HOST -> 1 @@ -297,3 +306,17 @@ fun Map.toVictoryMap() = keys.associate { (host, _) -> fun Map.toVictoryPairingMap() = keys.associate { (host, guest) -> InstinctVictoryPairing(host, guest) to ((get(InstinctGamePairing(host, guest)) ?: 0) - (get(InstinctGamePairing(guest, host)) ?: 0)) } + +fun Map.successHistograms(numSegments: Int) = allInstincts.associateWith { instinct -> + val ranges = (0..numSegments).map { it.toDouble() / numSegments }.windowed(2) { (begin, end) -> + val rBegin = instinct.randRange.start + (begin * instinct.randRange.size) + val rEnd = instinct.randRange.start + (end * instinct.randRange.size) + rBegin..rEnd + } + + val perRange = ranges.associateWith { range -> + filterKeys { it[instinct] in range }.values.sum() + } + + perRange +} diff --git a/src/commonMain/kotlin/starshipfights/game/ai/util.kt b/src/commonMain/kotlin/starshipfights/game/ai/util.kt index 07cd069..92bb8a8 100644 --- a/src/commonMain/kotlin/starshipfights/game/ai/util.kt +++ b/src/commonMain/kotlin/starshipfights/game/ai/util.kt @@ -16,6 +16,9 @@ expect fun logError(message: Any?) fun ClosedFloatingPointRange.random(random: Random = Random) = random.nextDouble(start, endInclusive.nextUp()) +val ClosedFloatingPointRange.size: Double + get() = endInclusive.nextUp() - start + fun Map.weightedRandom(random: Random = Random): T { return weightedRandomOrNull(random) ?: error("Cannot take weighted random of effectively-empty collection!") } diff --git a/src/jvmTest/kotlin/starshipfights/game/ai/AITesting.kt b/src/jvmTest/kotlin/starshipfights/game/ai/AITesting.kt index 625279d..8c5a141 100644 --- a/src/jvmTest/kotlin/starshipfights/game/ai/AITesting.kt +++ b/src/jvmTest/kotlin/starshipfights/game/ai/AITesting.kt @@ -1,13 +1,16 @@ package starshipfights.game.ai +import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking import kotlinx.html.* import kotlinx.html.stream.createHTML import starshipfights.game.BattleSize import starshipfights.game.Faction import java.io.File +import java.util.concurrent.atomic.AtomicInteger import javax.swing.JOptionPane import javax.swing.UIManager +import kotlin.concurrent.thread object AITesting { @JvmStatic @@ -26,9 +29,10 @@ object AITesting { if (instinctVectorIndex == JOptionPane.CLOSED_OPTION) return - val instinctVectors = genInstinctCandidates(instinctVectorCounts[instinctVectorIndex]) + val instinctVectorCount = instinctVectorCounts[instinctVectorIndex] + val instinctVectors = genInstinctCandidates(instinctVectorCount) - val numTrialCounts = listOf(3, 5, 7, 10) + val numTrialCounts = listOf(3, 5, 7, 10, 25) val numTrialOptions = numTrialCounts.map { it.toString() }.toTypedArray() val numTrialIndex = JOptionPane.showOptionDialog( @@ -70,14 +74,40 @@ object AITesting { val allowedFactions = allowedFactionChoices[allowedFactionIndex] + val allTrials = numTrials * instinctVectorCount * instinctVectorCount + val doneTrials = AtomicInteger(0) + val cancelJob = Job() + + thread { + while (true) { + val options = arrayOf("Update", "Cancel") + + val option = JOptionPane.showOptionDialog( + null, "Please select an action. ${doneTrials.get()}/$allTrials trials are done.", + "Trials in Progress", JOptionPane.DEFAULT_OPTION, + JOptionPane.INFORMATION_MESSAGE, null, + options, options[0] + ) + + if (option == 1) { + cancelJob.cancel() + break + } + } + } + val instinctPairingSuccessRate = runBlocking { - performTrials(numTrials, instinctVectors, allowedBattleSizes, allowedFactions) + performTrials(numTrials, instinctVectors, allowedBattleSizes, allowedFactions, cancelJob) { + doneTrials.getAndIncrement() + } } val instinctVictories = instinctPairingSuccessRate.toVictoryPairingMap() val instinctSuccessRate = instinctPairingSuccessRate.toVictoryMap() + val instinctHistograms = instinctSuccessRate.successHistograms(instinctVectorCount) + val indexedInstincts = instinctSuccessRate .toList() .sortedBy { (_, v) -> v } @@ -98,7 +128,7 @@ object AITesting { p { +"Battle Sizes Allowed: ${allowedBattleSizes.singleOrNull()?.displayName ?: "All"}" } p { +"Factions Allowed: ${allowedFactions.singleOrNull()?.polityName ?: "All"}" } h2 { +"Instincts Vectors and Battle Results" } - val cellStyle = "border: 1px solid rgba(0, 0, 0, 0.6)" + val cellStyle = "border:1px solid rgba(0, 0, 0, 0.6)" table { thead { tr { @@ -167,6 +197,54 @@ object AITesting { } } } + h2 { +"Instinct Victory Histograms" } + for ((instinct, histogram) in instinctHistograms) { + val sortedHistogram = histogram.toList().sortedBy { (range, _) -> range.start } + val lowestNumber = sortedHistogram.minOf { (_, score) -> score } + val highestNumber = sortedHistogram.maxOf { (_, score) -> score } + + h3 { +"Instinct ${instinct.key}" } + table { + thead { + tr { + th(scope = ThScope.col) { + style = cellStyle + +"Value Range" + } + for (num in lowestNumber..highestNumber) + th(scope = ThScope.col) { + style = cellStyle + +"$num" + } + } + } + tbody { + for ((range, successRate) in sortedHistogram) + tr { + th { + style = cellStyle + +"${range.start}" + br + +"${range.endInclusive}" + } + for (i in lowestNumber until successRate) + td { + style = "$cellStyle;background-color:#AAA;color:#AAA" + +"##" + } + td { + style = "$cellStyle;background-color:#555;color:#555" + +"##" + } + for (i in successRate until highestNumber) + td { + style = "$cellStyle;background-color:#FFF;color:#FFF" + +"##" + } + } + } + } + } } }