package approach.simulation import kotlin.math.abs import kotlin.math.max import kotlin.math.min import kotlin.random.Random import approach.domain.KromaiaFormat class IndividualBossConfig(override val encoding: KromaiaFormat) : Individual(encoding) { inner class DuelSettings( // Global. val numberOfDuels: Int = 30, val optimalDuelSteps: Int = 60 * (10 + 5 * (CONFIGURATION_ROW_LENGTH - 32) / 32), val maximumDuelSteps: Int = 60 * (20 + 10 * (CONFIGURATION_ROW_LENGTH - 32) / 32), // Steps the player stops on each hull (default: 20). val hullVisitSteps: Int = params[0], // Order of visiting them, i.e., one by one, two by two, ... (default: 1). val hullOrderVisit: Int = params[1], // When the player is hit, modify the number of steps it has left in that hull (default: 0). val hullStepCount: Int = params[2], val bossInitialShields: Int = encoding.vi.count { it == '1' } + 1, val playerInitialShields: Int = 5, val recoveryStepDistance: Int = 10, val killerLifePercentageGap: Float = 0.3f, val hitProbabilityWeapon: Double = 0.014, val hitProbabilityPlayer: Double = 0.25, // Fitness references. val goalPlayerWinPercentage: Double = 0.33, val goalPlayerWinLifePercentage: Double = 0.35, val goalPlayerWinLifeGapPercentage: Double = 0.35, ) internal var params: IntArray = intArrayOf(20, 1, 0) internal val duelSettings: DuelSettings = DuelSettings() data class DuelStats( // Global. var numberOfDuelsWonBySomeone: Int = 0, var numberOfPlayerWins: Int = 0, var playerLifePercentage: Double = 0.0, var bossLifePercentage: Double = 0.0, // Quality. var Q_completion: Double = 0.0, var Q_duration: Double = 0.0, var Q_uncertainty: Double = 0.0, var Q_killerMoves: Double = 0.0, var Q_permanence: Double = 0.0, var Q_leadChange: Double = 0.0, var Q_overall: Double = 0.0, // Fitness. var F_playerWinPercentage: Double = 0.0, var F_playerWinLifePercentage: Double = 0.0, var F_playerWinLifeGapPercentage: Double = 0.0, var F_overall: Double = 0.0, ) internal val duelStats: DuelStats = DuelStats() enum class DuelResult { BOSS_WINS, DRAW, PLAYER_WINS } internal var name: String = encoding.name internal var configuration: Configuration = encoding.configuration internal val trace: MutableList = mutableListOf() private val hullsEnabledIndices: MutableList = mutableListOf() private var isFitnessCalculated: Boolean = false init { for (i in 0 until encoding.hu.count { it == '1' }) { hullsEnabledIndices.add(i) } } override fun crossover(other: Individual): Individual { val crossover = IndividualBossConfig( encoding = KromaiaFormat( if (name.contains("(evolved)")) name else "$name (evolved)", configuration ) ) var newParams = IntArray(2) val otherParams = (other as IndividualBossConfig).params val crossCutIndex = random.nextInt(params.size) for (i in params.indices) { newParams += if (i <= crossCutIndex) params[i] else otherParams[i] } crossover.params = newParams return crossover } override fun mutate(): Individual { val mutant = IndividualBossConfig( encoding = KromaiaFormat( if (name.contains("(evolved)")) name else "$name (evolved)", configuration ) ) mutant.params[0] += (-1..1).random() // hullVisitSteps (default: 20) mutant.params[1] = (1..3).random() // hullOrderVisit (default: 1) mutant.params[2] = (0..2).random() // hullStepCount (default: 0) return mutant } override fun calculateFitness(): Double { if (isFitnessCalculated) { return fitness } val distanceFromEndToLastVitalHullIndex = CONFIGURATION_ROW_LENGTH - 1 - encoding.vi.lastIndexOf('1') repeat(duelSettings.numberOfDuels) { // Set parameters before each duel. var duelResult = DuelResult.DRAW var provisionalLead = DuelResult.DRAW val vitalPointsDestroyed = mutableSetOf() var isForwardRoute = true var isDuelInProgress = true var bossShields = duelSettings.bossInitialShields var playerShields = duelSettings.playerInitialShields var hullStepCount = 0 var duelStepCount = 0 var bossStepsToCritical = 0 var playerStepsToCritical = 0 var highlightMoveCount = 0 var killerMoveCount = 0 var recoveryMoveCount = 0 var lastHighlightStepCount = 0 var leadChangeCount = 0 var bossLifePercentage = 1.0f var playerLifePercentage = 1.0f /** * Duel loop. */ var hullIndex = 0 while (isDuelInProgress && duelStepCount < duelSettings.maximumDuelSteps) { // Collect trace. trace.add(hullIndex) var isHullStudied = true if (hullsEnabledIndices.isNotEmpty() && !hullsEnabledIndices.contains(hullIndex)) { --duelStepCount isHullStudied = false } // Variables used in player attack (later). var isVitalHullAlreadyDestroyed = false var isBossDamaged = false if (isHullStudied) { // Boss attack. val indicesInRange = mutableListOf() var indexInRange = hullIndex - 1 var indicesAdded = 0 while (indicesAdded < 3 && indexInRange >= 0) { if (hullsEnabledIndices.isEmpty() || hullsEnabledIndices.contains(indexInRange)) { indicesInRange.add(0, indexInRange) ++indicesAdded } --indexInRange } indicesInRange.add(hullIndex) indexInRange = hullIndex + 1 indicesAdded = 0 while (indicesAdded < 3 && indexInRange < CONFIGURATION_ROW_LENGTH) { if (hullsEnabledIndices.isEmpty() || hullsEnabledIndices.contains(indexInRange)) { indicesInRange.add(indexInRange) ++indicesAdded } ++indexInRange } var isPlayerDamaged = false var j = 0 while (!isPlayerDamaged && j < indicesInRange.size) { val studiedIndex = indicesInRange[j] val distanceToCurrentHull = abs(j - indicesInRange.indexOf(hullIndex)) isPlayerDamaged = distanceToCurrentHull < 1 && encoding.me[studiedIndex] == '1' && random.nextDouble() <= duelSettings.hitProbabilityWeapon * (4.0 - distanceToCurrentHull.toDouble()) / 4.0 || distanceToCurrentHull < 2 && encoding.bu[studiedIndex] == '1' && random.nextDouble() <= duelSettings.hitProbabilityWeapon * (4.0 - distanceToCurrentHull.toDouble()) / 4.0 || distanceToCurrentHull < 3 && encoding.ho[studiedIndex] == '1' && random.nextDouble() <= duelSettings.hitProbabilityWeapon * (4.0 - distanceToCurrentHull.toDouble()) / 4.0 || distanceToCurrentHull < 4 && encoding.la[studiedIndex] == '1' && random.nextDouble() <= duelSettings.hitProbabilityWeapon * (4.0 - distanceToCurrentHull.toDouble()) / 4.0 ++j } // Player health control. if (isPlayerDamaged) { --playerShields ++highlightMoveCount lastHighlightStepCount = duelStepCount if (playerShields == 1) { playerStepsToCritical = duelStepCount + 1 } else if (playerShields <= 0) { isDuelInProgress = false duelResult = DuelResult.BOSS_WINS } // When the player is hit, he will start to move quickly and try to flee or dodge a possible future attack. // Thus, the remaining steps counter of visiting a hull is reduced to simulate such behavior. hullStepCount += duelSettings.hullStepCount // TODO: Depend on the type of weapon the boss is using? } // Player attack. isVitalHullAlreadyDestroyed = vitalPointsDestroyed.contains(hullIndex) && bossShields > 1 isBossDamaged = encoding.vi[hullIndex] == '1' && !isVitalHullAlreadyDestroyed && random.nextDouble() <= duelSettings.hitProbabilityPlayer // Boss health control. if (isBossDamaged) { vitalPointsDestroyed.add(hullIndex) --bossShields ++highlightMoveCount lastHighlightStepCount = duelStepCount if (bossShields == 1) { bossStepsToCritical = duelStepCount + 1 } if (bossShields <= 0) { isDuelInProgress = false duelResult = DuelResult.PLAYER_WINS } } // Last clash evaluation. val isKillerMovePossible = abs(playerLifePercentage - bossLifePercentage) < duelSettings.killerLifePercentageGap val isRecoveryMovePossible = abs(lastHighlightStepCount - duelStepCount) <= duelSettings.recoveryStepDistance && abs( playerLifePercentage - bossLifePercentage ) >= duelSettings.killerLifePercentageGap val provisionalLeadOld = provisionalLead playerLifePercentage = playerShields.toFloat() / duelSettings.playerInitialShields.toFloat() bossLifePercentage = bossShields.toFloat() / duelSettings.bossInitialShields.toFloat() provisionalLead = if (playerLifePercentage == bossLifePercentage) DuelResult.DRAW else if (playerLifePercentage > bossLifePercentage) DuelResult.PLAYER_WINS else DuelResult.BOSS_WINS if (provisionalLead != provisionalLeadOld) { ++leadChangeCount } if (isKillerMovePossible && abs(playerLifePercentage - bossLifePercentage) >= duelSettings.killerLifePercentageGap) { ++killerMoveCount } if (isRecoveryMovePossible && abs(playerLifePercentage - bossLifePercentage) < duelSettings.killerLifePercentageGap) { ++recoveryMoveCount } } // Route control. ++hullStepCount if (!isHullStudied || isVitalHullAlreadyDestroyed || isBossDamaged || hullStepCount >= if (encoding.vi[hullIndex] == '1') 2 * duelSettings.hullVisitSteps else duelSettings.hullVisitSteps) { hullStepCount = 0 // Direction control. if (isForwardRoute && hullIndex + 1 >= CONFIGURATION_ROW_LENGTH) { if (distanceFromEndToLastVitalHullIndex >= 10) { hullIndex = -1 } else { isForwardRoute = false } } else if (!isForwardRoute && hullIndex - 1 < 0) { isForwardRoute = true } hullIndex += if (isForwardRoute) duelSettings.hullOrderVisit else -duelSettings.hullOrderVisit // Clamp: e.g., no hulls enabled. hullIndex = max(0, min(hullIndex, CONFIGURATION_ROW_LENGTH - 1)) } ++duelStepCount } /** * Update duel scoreboard. */ if (duelResult != DuelResult.DRAW) { ++duelStats.numberOfDuelsWonBySomeone if (duelResult == DuelResult.PLAYER_WINS) { ++duelStats.numberOfPlayerWins } } /** * Duel quality stats. */ duelStats.Q_duration += abs(duelSettings.optimalDuelSteps - duelStepCount).toDouble() / duelSettings.optimalDuelSteps.toDouble() duelStats.Q_uncertainty += (duelStepCount - min(playerStepsToCritical, bossStepsToCritical)).toDouble() / duelStepCount.toDouble() duelStats.Q_killerMoves += killerMoveCount.toDouble() / highlightMoveCount.toDouble() duelStats.Q_permanence += recoveryMoveCount.toDouble() / (highlightMoveCount + killerMoveCount).toDouble() duelStats.Q_leadChange += leadChangeCount.toDouble() / (highlightMoveCount + killerMoveCount).toDouble() /** * Duel fitness stats. */ if (duelResult == DuelResult.PLAYER_WINS) { duelStats.F_playerWinLifePercentage += abs( duelSettings.goalPlayerWinLifePercentage - playerLifePercentage.toDouble() ) / duelSettings.goalPlayerWinLifePercentage duelStats.F_playerWinLifeGapPercentage += abs( duelSettings.goalPlayerWinLifeGapPercentage - abs( playerLifePercentage - bossLifePercentage ).toDouble() ) / duelSettings.goalPlayerWinLifeGapPercentage duelStats.playerLifePercentage += playerLifePercentage.toDouble() duelStats.bossLifePercentage += bossLifePercentage.toDouble() } } /** * Global duel quality stats. */ duelStats.Q_completion = duelStats.numberOfDuelsWonBySomeone.toDouble() / duelSettings.numberOfDuels.toDouble() duelStats.Q_duration = max(0.0, 1.0 - duelStats.Q_duration / duelSettings.numberOfDuels.toDouble()) duelStats.Q_uncertainty = max(0.0, 1.0 - duelStats.Q_uncertainty / duelSettings.numberOfDuels.toDouble()) duelStats.Q_killerMoves = max(0.0, 1.0 - duelStats.Q_killerMoves / duelSettings.numberOfDuels.toDouble()) duelStats.Q_permanence = max(0.0, 1.0 - duelStats.Q_permanence / duelSettings.numberOfDuels.toDouble()) duelStats.Q_leadChange /= duelSettings.numberOfDuels.toDouble() duelStats.Q_overall = (duelStats.Q_completion + duelStats.Q_duration + duelStats.Q_uncertainty + duelStats.Q_killerMoves + duelStats.Q_permanence + duelStats.Q_leadChange) / 6 /** * Global duel fitness stats. */ duelStats.F_playerWinPercentage = max( 0.0, 1.0 - abs( duelSettings.goalPlayerWinPercentage - duelStats.numberOfPlayerWins.toDouble() / duelSettings.numberOfDuels.toDouble() ) / duelSettings.goalPlayerWinPercentage ) duelStats.F_playerWinLifePercentage = if (duelStats.numberOfPlayerWins > 0) max( 0.0, 1.0 - duelStats.F_playerWinLifePercentage / duelStats.numberOfPlayerWins.toDouble() ) else 0.0 duelStats.F_playerWinLifeGapPercentage = if (duelStats.numberOfPlayerWins > 0) max( 0.0, 1.0 - duelStats.F_playerWinLifeGapPercentage / duelStats.numberOfPlayerWins.toDouble() ) else 0.0 duelStats.F_overall = (duelStats.F_playerWinPercentage + duelStats.F_playerWinLifePercentage + duelStats.F_playerWinLifeGapPercentage) / 3 isFitnessCalculated = true return duelStats.F_overall } companion object { private val random: Random = Random(System.nanoTime()) } }