forked from mirrors/cobblemon
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
767fc05387 | ||
|
|
1941636113 |
@@ -0,0 +1,671 @@
|
||||
package com.cobblemon.mod.common.battles.ai
|
||||
|
||||
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.battles.model.ai.BattleAI
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.battles.*
|
||||
import com.cobblemon.mod.common.battles.ai.competitive.*
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
|
||||
import kotlin.random.Random
|
||||
|
||||
class CompetitiveBattleAI(private val skill: Int) : BattleAI {
|
||||
|
||||
companion object {
|
||||
private const val DEBUG_TOP_MOVES = 4
|
||||
}
|
||||
|
||||
private val skillLevel = skill.coerceIn(0, 5)
|
||||
|
||||
private val aiDebugEnabled = isAiDebugEnabled()
|
||||
|
||||
private val battleTracker = EnhancedActiveTracker()
|
||||
private val opponentPredictor = OpponentPredictor()
|
||||
private val strategyAnalyzer = StrategyAnalyzer()
|
||||
private lateinit var damageCalculator: DamageCalculator
|
||||
private lateinit var moveEvaluator: MoveEvaluator
|
||||
private lateinit var switchDecisionMaker: SwitchDecisionMaker
|
||||
private lateinit var doublesDecisionMaker: DoublesDecisionMaker
|
||||
|
||||
private var currentBattle: PokemonBattle? = null
|
||||
private var currentGamePlan: GamePlan? = null
|
||||
private var battleState: BattleState? = null
|
||||
private var lastMoveEvaluation: MoveEvaluator.MoveEvaluation? = null
|
||||
|
||||
private var isDoublesMode: Boolean = false
|
||||
private var currentSlot: Int = 0
|
||||
private var currentTurn: Int = -1
|
||||
private val turnDecisions: MutableMap<Int, DoublesDecisionMaker.MoveTargetPair> = mutableMapOf()
|
||||
private var firstSlotDecided: Boolean = false
|
||||
|
||||
override fun choose(
|
||||
activeBattlePokemon: ActiveBattlePokemon,
|
||||
battle: PokemonBattle,
|
||||
aiSide: BattleSide,
|
||||
moveset: ShowdownMoveset?,
|
||||
forceSwitch: Boolean
|
||||
): ShowdownActionResponse {
|
||||
try {
|
||||
initializeComponents(battle)
|
||||
updateBattleState(activeBattlePokemon, battle)
|
||||
|
||||
if (currentGamePlan == null) {
|
||||
val npcTeam = battle.side2.actors.first().pokemonList.map { it.effectedPokemon }
|
||||
currentGamePlan = strategyAnalyzer.analyzeTeamComposition(npcTeam)
|
||||
}
|
||||
|
||||
if (battle.turn != currentTurn) {
|
||||
currentTurn = battle.turn
|
||||
turnDecisions.clear()
|
||||
firstSlotDecided = false
|
||||
}
|
||||
|
||||
if (forceSwitch || activeBattlePokemon.isGone()) {
|
||||
return handleForcedSwitch(activeBattlePokemon, battle)
|
||||
}
|
||||
|
||||
if (!passesSkillCheck()) {
|
||||
debugLog("Skill check failed; selecting random move.")
|
||||
return makeRandomMove(moveset, activeBattlePokemon)
|
||||
}
|
||||
|
||||
updateOpponentModel(battle)
|
||||
|
||||
if (isDoublesMode) {
|
||||
return chooseForDoubles(activeBattlePokemon, battle, moveset)
|
||||
}
|
||||
|
||||
if (shouldAttemptSwitch() && shouldSwitch(activeBattlePokemon, battle)) {
|
||||
val switchTarget = findBestSwitch(activeBattlePokemon, battle)
|
||||
if (switchTarget != null) {
|
||||
switchTarget.willBeSwitchedIn = true
|
||||
logSwitchDecision("voluntary_switch", battle, listOf(switchTarget), switchTarget)
|
||||
return SwitchActionResponse(switchTarget.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
val bestMove = chooseBestMove(moveset, activeBattlePokemon, battle)
|
||||
logMoveDecision("move_select", bestMove, activeBattlePokemon)
|
||||
return executeMove(bestMove, activeBattlePokemon)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return PassActionResponse
|
||||
}
|
||||
}
|
||||
|
||||
private fun chooseForDoubles(
|
||||
activeBattlePokemon: ActiveBattlePokemon,
|
||||
battle: PokemonBattle,
|
||||
moveset: ShowdownMoveset?
|
||||
): ShowdownActionResponse {
|
||||
val state = battleState ?: return PassActionResponse
|
||||
val plan = currentGamePlan ?: return PassActionResponse
|
||||
val myPokemon = battleTracker.p2Active.getActiveAt(currentSlot) ?: return PassActionResponse
|
||||
val partnerSlot = 1 - currentSlot
|
||||
val partner = battleTracker.p2Active.getActiveAt(partnerSlot)
|
||||
val opponents = battleTracker.p1Active.getAllActive()
|
||||
|
||||
val decision = doublesDecisionMaker.findBestMoveAndTarget(
|
||||
moveset,
|
||||
activeBattlePokemon,
|
||||
myPokemon,
|
||||
opponents,
|
||||
partner,
|
||||
state,
|
||||
plan
|
||||
)
|
||||
|
||||
if (decision == null) {
|
||||
debugLog("Doubles: no evaluated option; selecting random move.")
|
||||
return makeRandomMove(moveset, activeBattlePokemon)
|
||||
}
|
||||
|
||||
turnDecisions[currentSlot] = decision
|
||||
firstSlotDecided = true
|
||||
|
||||
debugLog(
|
||||
"Doubles: chose move=${decision.move.id} target=${decision.targetPNX ?: "auto"} " +
|
||||
"score=${"%.2f".format(decision.evaluation.totalScore)} " +
|
||||
"expectedDamage=${"%.1f".format(decision.evaluation.expectedDamage)} " +
|
||||
"strategic=${"%.1f".format(decision.evaluation.strategicValue)} " +
|
||||
"risk=${"%.2f".format(decision.evaluation.riskFactor)}"
|
||||
)
|
||||
return executeDoublesMove(decision, activeBattlePokemon)
|
||||
}
|
||||
|
||||
private fun executeDoublesMove(
|
||||
decision: DoublesDecisionMaker.MoveTargetPair,
|
||||
activeBattlePokemon: ActiveBattlePokemon
|
||||
): ShowdownActionResponse {
|
||||
return if (decision.targetPNX != null) {
|
||||
MoveActionResponse(decision.move.id, decision.targetPNX)
|
||||
} else {
|
||||
MoveActionResponse(decision.move.id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeComponents(battle: PokemonBattle) {
|
||||
if (currentBattle != battle) {
|
||||
currentBattle = battle
|
||||
isDoublesMode = battle.format.battleType.pokemonPerSide > 1
|
||||
damageCalculator = DamageCalculator(battle)
|
||||
moveEvaluator = MoveEvaluator(damageCalculator, opponentPredictor, isDoublesMode)
|
||||
switchDecisionMaker = SwitchDecisionMaker(damageCalculator, opponentPredictor, strategyAnalyzer)
|
||||
doublesDecisionMaker = DoublesDecisionMaker(moveEvaluator, damageCalculator)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBattleState(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle) {
|
||||
val request = activeBattlePokemon.actor.request ?: return
|
||||
|
||||
// Determine current slot from PNX (e.g., "p2a" = slot 0, "p2b" = slot 1)
|
||||
val pnx = activeBattlePokemon.getPNX()
|
||||
currentSlot = if (pnx.length >= 3) pnx[2] - 'a' else 0
|
||||
|
||||
if (isDoublesMode) {
|
||||
battle.side1.activePokemon.forEachIndexed { index, active ->
|
||||
active.battlePokemon?.let { pokemon ->
|
||||
battleTracker.updatePokemonSlot(battleTracker.p1Active, index, pokemon)
|
||||
}
|
||||
}
|
||||
|
||||
battle.side2.activePokemon.forEachIndexed { index, active ->
|
||||
active.battlePokemon?.let { pokemon ->
|
||||
battleTracker.updatePokemonSlot(battleTracker.p2Active, index, pokemon)
|
||||
}
|
||||
}
|
||||
|
||||
battleTracker.p1Active.activePokemon =
|
||||
battleTracker.p1Active.getActiveAt(currentSlot) ?: battleTracker.p1Active.activePokemon
|
||||
battleTracker.p2Active.activePokemon =
|
||||
battleTracker.p2Active.getActiveAt(currentSlot) ?: battleTracker.p2Active.activePokemon
|
||||
} else {
|
||||
battle.side1.activePokemon.firstOrNull()?.battlePokemon?.let { playerPokemon ->
|
||||
battleTracker.updatePokemon(battleTracker.p1Active.activePokemon, playerPokemon)
|
||||
battleTracker.updateParty(battleTracker.p1Active, battleTracker.p1Active.activePokemon)
|
||||
}
|
||||
|
||||
activeBattlePokemon.battlePokemon?.let { aiPokemon ->
|
||||
battleTracker.updatePokemon(battleTracker.p2Active.activePokemon, aiPokemon)
|
||||
battleTracker.updateParty(battleTracker.p2Active, battleTracker.p2Active.activePokemon)
|
||||
}
|
||||
}
|
||||
|
||||
updateRevealedMoves(battle)
|
||||
battleState = createBattleState(battle, activeBattlePokemon)
|
||||
}
|
||||
|
||||
private fun createBattleState(battle: PokemonBattle, activeBattlePokemon: ActiveBattlePokemon): BattleState? {
|
||||
val p1Actor = battle.side1.actors.firstOrNull() ?: return null
|
||||
val p2Actor = battle.side2.actors.firstOrNull() ?: return null
|
||||
|
||||
val playerActiveTracker = if (isDoublesMode) {
|
||||
battleTracker.p1Active.getActiveAt(currentSlot) ?: battleTracker.p1Active.activePokemon
|
||||
} else {
|
||||
battleTracker.p1Active.activePokemon
|
||||
}
|
||||
val npcActiveTracker = if (isDoublesMode) {
|
||||
battleTracker.p2Active.getActiveAt(currentSlot) ?: battleTracker.p2Active.activePokemon
|
||||
} else {
|
||||
battleTracker.p2Active.activePokemon
|
||||
}
|
||||
|
||||
val playerActive = playerActiveTracker.pokemon ?: return null
|
||||
val npcActive = npcActiveTracker.pokemon ?: return null
|
||||
val playerActiveSlots = battleTracker.p1Active.getAllActive()
|
||||
.mapNotNull { it.pokemon }
|
||||
.ifEmpty { listOf(playerActive) }
|
||||
|
||||
val npcActiveSlots = battleTracker.p2Active.getAllActive()
|
||||
.mapNotNull { it.pokemon }
|
||||
.ifEmpty { listOf(npcActive) }
|
||||
|
||||
val partnerSlot = if (isDoublesMode) {
|
||||
val mySlot = currentSlot
|
||||
1 - mySlot
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return BattleState(
|
||||
turn = battle.turn,
|
||||
playerTeam = p1Actor.pokemonList.map { it.effectedPokemon },
|
||||
npcTeam = p2Actor.pokemonList.map { it.effectedPokemon },
|
||||
playerActive = playerActive,
|
||||
npcActive = npcActive,
|
||||
weather = getWeather(battle),
|
||||
terrain = getTerrain(battle),
|
||||
playerHazards = getHazards(battle.side1),
|
||||
npcHazards = getHazards(battle.side2),
|
||||
playerBoosts = battleTracker.p1Active.activePokemon.boosts,
|
||||
npcBoosts = battleTracker.p2Active.activePokemon.boosts,
|
||||
battleContext = battle.contextManager,
|
||||
isDoubles = isDoublesMode,
|
||||
playerActiveSlots = playerActiveSlots,
|
||||
npcActiveSlots = npcActiveSlots,
|
||||
mySlotIndex = currentSlot,
|
||||
partnerSlotIndex = partnerSlot
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleForcedSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): ShowdownActionResponse {
|
||||
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
|
||||
|
||||
if (availableSwitches.isEmpty()) {
|
||||
return ForfeitActionResponse()
|
||||
}
|
||||
|
||||
val state = battleState
|
||||
val plan = currentGamePlan
|
||||
|
||||
val bestSwitch = if (state != null && plan != null) {
|
||||
switchDecisionMaker.findBestSwitch(
|
||||
availableSwitches,
|
||||
battleTracker.p1Active.activePokemon,
|
||||
state,
|
||||
plan
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val switchTarget = bestSwitch ?: availableSwitches.first()
|
||||
switchTarget.willBeSwitchedIn = true
|
||||
logSwitchDecision("forced_switch", battle, availableSwitches, switchTarget)
|
||||
return SwitchActionResponse(switchTarget.uuid)
|
||||
}
|
||||
|
||||
private fun shouldSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): Boolean {
|
||||
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
|
||||
if (availableSwitches.isEmpty()) return false
|
||||
|
||||
val state = battleState ?: return false
|
||||
val plan = currentGamePlan ?: return false
|
||||
|
||||
return switchDecisionMaker.shouldVoluntarilySwitch(
|
||||
battleTracker.p2Active.activePokemon,
|
||||
battleTracker.p1Active.activePokemon,
|
||||
state,
|
||||
plan,
|
||||
availableSwitches
|
||||
)
|
||||
}
|
||||
|
||||
private fun findBestSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): BattlePokemon? {
|
||||
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
|
||||
|
||||
val state = battleState ?: return null
|
||||
val plan = currentGamePlan ?: return null
|
||||
|
||||
return switchDecisionMaker.findBestSwitch(
|
||||
availableSwitches,
|
||||
battleTracker.p1Active.activePokemon,
|
||||
state,
|
||||
plan
|
||||
)
|
||||
}
|
||||
|
||||
private fun chooseBestMove(
|
||||
moveset: ShowdownMoveset?,
|
||||
activeBattlePokemon: ActiveBattlePokemon,
|
||||
battle: PokemonBattle
|
||||
): InBattleMove? {
|
||||
if (moveset == null) return null
|
||||
|
||||
val usableMoves = moveset.moves.filter { it.canBeUsed() }
|
||||
if (usableMoves.isEmpty()) return null
|
||||
|
||||
val state = battleState ?: return null
|
||||
val plan = currentGamePlan ?: return null
|
||||
|
||||
val evaluations = usableMoves.map { move ->
|
||||
moveEvaluator.evaluateMove(
|
||||
move,
|
||||
battleTracker.p2Active.activePokemon,
|
||||
battleTracker.p1Active.activePokemon,
|
||||
state,
|
||||
plan
|
||||
)
|
||||
}
|
||||
|
||||
val bestEvaluation = evaluations.maxByOrNull { it.totalScore }
|
||||
logMoveRanking(evaluations)
|
||||
lastMoveEvaluation = bestEvaluation
|
||||
return bestEvaluation?.move
|
||||
}
|
||||
|
||||
private fun executeMove(move: InBattleMove?, activeBattlePokemon: ActiveBattlePokemon): ShowdownActionResponse {
|
||||
if (move == null) {
|
||||
return makeRandomMove(null, activeBattlePokemon)
|
||||
}
|
||||
|
||||
val target = if (move.mustBeUsed()) {
|
||||
null
|
||||
} else {
|
||||
move.target.targetList(activeBattlePokemon)
|
||||
}
|
||||
|
||||
return if (target == null) {
|
||||
MoveActionResponse(move.id)
|
||||
} else {
|
||||
val chosenTarget = target.filter { !it.isAllied(activeBattlePokemon) }.randomOrNull()
|
||||
?: target.random()
|
||||
MoveActionResponse(move.id, (chosenTarget as ActiveBattlePokemon).getPNX())
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeRandomMove(moveset: ShowdownMoveset?, activeBattlePokemon: ActiveBattlePokemon): ShowdownActionResponse {
|
||||
if (moveset == null) return PassActionResponse
|
||||
|
||||
val usableMoves = moveset.moves.filter { it.canBeUsed() }
|
||||
val move = usableMoves.randomOrNull() ?: return PassActionResponse
|
||||
|
||||
val target = if (move.mustBeUsed()) null else move.target.targetList(activeBattlePokemon)
|
||||
|
||||
return if (target == null) {
|
||||
MoveActionResponse(move.id)
|
||||
} else {
|
||||
val chosenTarget = target.filter { !it.isAllied(activeBattlePokemon) }.randomOrNull()
|
||||
?: target.random()
|
||||
MoveActionResponse(move.id, (chosenTarget as ActiveBattlePokemon).getPNX())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOpponentModel(battle: PokemonBattle) {
|
||||
val situation = categorizeSituation(battleState ?: return)
|
||||
|
||||
val opponentActives = if (isDoublesMode) {
|
||||
battleTracker.p1Active.getAllActive()
|
||||
} else {
|
||||
listOf(battleTracker.p1Active.activePokemon)
|
||||
}
|
||||
|
||||
for (opponentActive in opponentActives) {
|
||||
for (move in opponentActive.revealedMoves) {
|
||||
opponentPredictor.updateMoveHistory(
|
||||
opponentActive.species ?: "unknown",
|
||||
move,
|
||||
situation.name.lowercase()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHealthChange(packet: BattleHealthChangePacket) {
|
||||
try {
|
||||
val battle = currentBattle ?: return
|
||||
val pnx = packet.pnx
|
||||
|
||||
val activePokemon = battle.activePokemon.find { it.getPNX() == pnx } ?: return
|
||||
val newHealth = packet.newHealth.toInt()
|
||||
val isPlayerPokemon = activePokemon.actor == battle.side1.actors.firstOrNull()
|
||||
val teamTracker = if (isPlayerPokemon) {
|
||||
battleTracker.p1Active
|
||||
} else {
|
||||
battleTracker.p2Active
|
||||
}
|
||||
val tracker = battleTracker.getTrackedPokemonByPNX(pnx, teamTracker) ?: teamTracker.activePokemon
|
||||
|
||||
val oldHealth = tracker.currentHp
|
||||
val hpChange = newHealth - oldHealth
|
||||
|
||||
tracker.previousHp = oldHealth
|
||||
tracker.currentHp = newHealth
|
||||
tracker.lastHpChange = hpChange
|
||||
tracker.currentHpPercent = if (tracker.maxHp > 0) newHealth.toDouble() / tracker.maxHp else 0.0
|
||||
|
||||
updateMomentum(hpChange, isPlayerPokemon)
|
||||
|
||||
detectCriticalThresholds(tracker, oldHealth, newHealth, isPlayerPokemon)
|
||||
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateMomentum(hpChange: Int, isPlayerPokemon: Boolean) {
|
||||
val momentumChange = when {
|
||||
hpChange > 0 -> -5.0 // Opponent healing shifts momentum toward them
|
||||
hpChange < 0 -> kotlin.math.abs(hpChange) / 10.0 // Damage shifts momentum
|
||||
else -> 0.0
|
||||
}
|
||||
|
||||
if (isPlayerPokemon) {
|
||||
// Player took damage = AI momentum increases
|
||||
battleTracker.p2Active.momentum += momentumChange
|
||||
battleTracker.p1Active.momentum -= momentumChange
|
||||
} else {
|
||||
// AI took damage = Player momentum increases
|
||||
battleTracker.p2Active.momentum -= momentumChange
|
||||
battleTracker.p1Active.momentum += momentumChange
|
||||
}
|
||||
|
||||
// Clamp momentum to -100 to 100
|
||||
battleTracker.p1Active.momentum = battleTracker.p1Active.momentum.coerceIn(-100.0, 100.0)
|
||||
battleTracker.p2Active.momentum = battleTracker.p2Active.momentum.coerceIn(-100.0, 100.0)
|
||||
}
|
||||
|
||||
private fun detectCriticalThresholds(
|
||||
tracker: TrackedPokemon,
|
||||
oldHealth: Int,
|
||||
newHealth: Int,
|
||||
isPlayerPokemon: Boolean
|
||||
) {
|
||||
if (newHealth <= 0 && oldHealth > 0) {
|
||||
if (isPlayerPokemon) {
|
||||
battleTracker.p2Active.momentum += 20.0
|
||||
} else {
|
||||
battleTracker.p1Active.momentum += 20.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun categorizeSituation(battleState: BattleState): BattleSituation {
|
||||
val myHP = battleState.npcActive.currentHealth.toDouble() / battleState.npcActive.maxHealth
|
||||
val oppHP = battleState.playerActive.currentHealth.toDouble() / battleState.playerActive.maxHealth
|
||||
|
||||
return when {
|
||||
myHP < 0.25 -> BattleSituation.DESPERATE
|
||||
oppHP < 0.25 -> BattleSituation.WINNING
|
||||
myHP > 0.8 && oppHP > 0.8 -> BattleSituation.EARLY_GAME
|
||||
battleState.npcTeam.count { it.currentHealth > 0 } <= 2 -> BattleSituation.ENDGAME
|
||||
battleState.playerBoosts.values.any { it >= 2 } -> BattleSituation.OPPONENT_SETUP
|
||||
battleState.npcBoosts.values.any { it >= 2 } -> BattleSituation.MY_SETUP
|
||||
battleState.weather != null -> BattleSituation.WEATHER_CONTROL
|
||||
else -> BattleSituation.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAvailableSwitches(activeBattlePokemon: ActiveBattlePokemon): List<BattlePokemon> {
|
||||
val currentUuid = activeBattlePokemon.battlePokemon?.uuid
|
||||
|
||||
return activeBattlePokemon.actor.pokemonList.filter {
|
||||
it.health > 0 &&
|
||||
it.uuid != currentUuid &&
|
||||
try { it.canBeSentOut() } catch (e: Exception) { true }
|
||||
}
|
||||
}
|
||||
|
||||
private fun passesSkillCheck(): Boolean {
|
||||
if (skillLevel == 5) return true
|
||||
if (skillLevel == 0) return false
|
||||
|
||||
val baseChance = skillLevel * 20
|
||||
val turnBonus = if ((battleState?.turn ?: 0) > 10) 10 else 0
|
||||
|
||||
return Random.nextInt(100) < (baseChance + turnBonus)
|
||||
}
|
||||
|
||||
private fun shouldAttemptSwitch(): Boolean {
|
||||
return when (skillLevel) {
|
||||
in 0..2 -> false
|
||||
3 -> Random.nextDouble() < 0.25
|
||||
4 -> Random.nextDouble() < 0.70
|
||||
5 -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWeather(battle: PokemonBattle): String? {
|
||||
return try {
|
||||
battle.contextManager.get(BattleContext.Type.WEATHER)
|
||||
?.firstOrNull()?.id
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTerrain(battle: PokemonBattle): String? {
|
||||
return try {
|
||||
battle.contextManager.get(BattleContext.Type.TERRAIN)
|
||||
?.firstOrNull()?.id
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHazards(side: BattleSide): List<String> {
|
||||
return try {
|
||||
side.contextManager.get(BattleContext.Type.HAZARD)
|
||||
?.map { it.id } ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRevealedMoves(battle: PokemonBattle) {
|
||||
updateTeamRevealedMoves(battle, battleTracker.p1Active)
|
||||
updateTeamRevealedMoves(battle, battleTracker.p2Active)
|
||||
}
|
||||
|
||||
private fun updateTeamRevealedMoves(battle: PokemonBattle, team: TeamTracker) {
|
||||
val active = team.getAllActive().ifEmpty { listOf(team.activePokemon) }
|
||||
for (tracked in active) {
|
||||
val pokemon = tracked.pokemon ?: continue
|
||||
val majorMessage = battle.majorBattleActions[pokemon.uuid]
|
||||
val minorMessage = battle.minorBattleActions[pokemon.uuid]
|
||||
val candidates = listOfNotNull(majorMessage, minorMessage)
|
||||
for (message in candidates) {
|
||||
val id = message.id.lowercase()
|
||||
if (id != "move" && id != "-singlemove" && id != "singlemove") continue
|
||||
val moveId = message.effectAt(1)?.id?.lowercase() ?: continue
|
||||
tracked.revealedMoves.add(moveId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logMoveDecision(
|
||||
reason: String,
|
||||
move: InBattleMove?,
|
||||
activeBattlePokemon: ActiveBattlePokemon
|
||||
) {
|
||||
if (!aiDebugEnabled || move == null) return
|
||||
val eval = lastMoveEvaluation
|
||||
val self = activeBattlePokemon.battlePokemon?.effectedPokemon?.species?.name ?: "unknown"
|
||||
val opp = battleTracker.p1Active.activePokemon.species ?: "unknown"
|
||||
val plan = currentGamePlan?.winCondition?.name ?: "unknown"
|
||||
if (eval == null) {
|
||||
debugLog("Decision=$reason self=$self opp=$opp move=${move.id} plan=$plan")
|
||||
return
|
||||
}
|
||||
debugLog(
|
||||
"Decision=$reason self=$self opp=$opp move=${move.id} plan=$plan " +
|
||||
"score=${"%.2f".format(eval.totalScore)} " +
|
||||
"expectedDamage=${"%.1f".format(eval.expectedDamage)} " +
|
||||
"strategic=${"%.1f".format(eval.strategicValue)} " +
|
||||
"risk=${"%.2f".format(eval.riskFactor)}"
|
||||
)
|
||||
}
|
||||
|
||||
private fun logMoveRanking(evaluations: List<MoveEvaluator.MoveEvaluation>) {
|
||||
if (!aiDebugEnabled || evaluations.isEmpty()) return
|
||||
val plan = currentGamePlan?.winCondition?.name ?: "unknown"
|
||||
val situation = battleState?.let { categorizeSituation(it).name } ?: "unknown"
|
||||
val top = evaluations.sortedByDescending { it.totalScore }.take(DEBUG_TOP_MOVES)
|
||||
val state = battleState
|
||||
val planData = currentGamePlan
|
||||
val attacker = battleTracker.p2Active.activePokemon
|
||||
val defender = battleTracker.p1Active.activePokemon
|
||||
val details = top.joinToString(", ") { eval ->
|
||||
val debug = if (state != null && planData != null) {
|
||||
moveEvaluator.buildDebugDetails(eval.move, attacker, defender, state, planData)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val flags = if (debug != null) {
|
||||
listOf(
|
||||
"stab=${debug.hasStab}",
|
||||
"eff=${"%.2f".format(debug.effectiveness)}",
|
||||
"acc=${"%.2f".format(debug.accuracy)}",
|
||||
"prio=${debug.priority}",
|
||||
"status=${debug.isStatusMove}",
|
||||
"setup=${debug.isSetup}",
|
||||
"hazardSet=${debug.setsHazards}",
|
||||
"hazardClear=${debug.removesHazards}",
|
||||
"weather=${debug.setsWeather}",
|
||||
"recovery=${debug.isRecovery}",
|
||||
"pivot=${debug.isPivot}",
|
||||
"risky=${debug.isRisky}"
|
||||
).joinToString(" ")
|
||||
} else {
|
||||
"n/a"
|
||||
}
|
||||
"${eval.move.id}:" +
|
||||
"score=${"%.2f".format(eval.totalScore)} " +
|
||||
"raw=${"%.1f".format(eval.rawDamage)} " +
|
||||
"dmg=${"%.1f".format(eval.expectedDamage)} " +
|
||||
"strat=${"%.1f".format(eval.strategicValue)} " +
|
||||
"align=${"%.1f".format(eval.gamePlanAlignment)} " +
|
||||
"turn=${"%.1f".format(eval.turnAdvantage)} " +
|
||||
"risk=${"%.2f".format(eval.riskFactor)} " +
|
||||
flags
|
||||
}
|
||||
debugLog("MoveRank plan=$plan situation=$situation top=[$details]")
|
||||
}
|
||||
|
||||
private fun logSwitchDecision(
|
||||
reason: String,
|
||||
battle: PokemonBattle,
|
||||
availableSwitches: List<BattlePokemon>,
|
||||
chosen: BattlePokemon?
|
||||
) {
|
||||
if (!aiDebugEnabled) return
|
||||
val state = battleState
|
||||
val plan = currentGamePlan
|
||||
val opponent = battleTracker.p1Active.activePokemon
|
||||
val ranked = if (state != null && plan != null) {
|
||||
availableSwitches.map { candidate ->
|
||||
val score = switchDecisionMaker.evaluateSwitchOption(
|
||||
candidate.effectedPokemon,
|
||||
opponent,
|
||||
state,
|
||||
plan
|
||||
)
|
||||
candidate to score
|
||||
}.sortedByDescending { it.second }
|
||||
.take(3)
|
||||
.joinToString(", ") { (candidate, score) ->
|
||||
"${candidate.effectedPokemon.species.name}:${"%.1f".format(score)}"
|
||||
}
|
||||
} else {
|
||||
"n/a"
|
||||
}
|
||||
|
||||
val chosenName = chosen?.effectedPokemon?.species?.name ?: "none"
|
||||
debugLog("Decision=$reason chosen=$chosenName options=[$ranked]")
|
||||
}
|
||||
|
||||
private fun debugLog(message: String) {
|
||||
if (aiDebugEnabled) {
|
||||
Cobblemon.LOGGER.info("[CompetitiveBattleAI] $message")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAiDebugEnabled(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
object AiBattleUtils {
|
||||
fun normalizeWeatherId(weather: String?): String? {
|
||||
val id = weather?.lowercase() ?: return null
|
||||
return when (id) {
|
||||
"sun", "sunny", "sunnyday", "harshsunlight" -> "sunnyday"
|
||||
"rain", "raindance", "heavyrain" -> "raindance"
|
||||
"sand", "sandstorm" -> "sandstorm"
|
||||
"hail" -> "hail"
|
||||
"snow", "snowscape" -> "snow"
|
||||
else -> id
|
||||
}
|
||||
}
|
||||
|
||||
fun hasStab(moveTypeName: String, pokemonTypes: List<String>): Boolean {
|
||||
val moveType = moveTypeName.lowercase()
|
||||
return pokemonTypes.any { it.lowercase() == moveType }
|
||||
}
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.Move
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stat
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.battles.interpreter.ContextManager
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
|
||||
data class BattleState(
|
||||
val turn: Int,
|
||||
val playerTeam: List<Pokemon>,
|
||||
val npcTeam: List<Pokemon>,
|
||||
val playerActive: Pokemon,
|
||||
val npcActive: Pokemon,
|
||||
val weather: String?,
|
||||
val terrain: String?,
|
||||
val playerHazards: List<String>,
|
||||
val npcHazards: List<String>,
|
||||
val playerBoosts: Map<Stat, Int>,
|
||||
val npcBoosts: Map<Stat, Int>,
|
||||
val battleContext: ContextManager,
|
||||
val gamePhase: GamePhase = determineGamePhase(turn, npcTeam, playerTeam),
|
||||
val isDoubles: Boolean = false,
|
||||
val playerActiveSlots: List<Pokemon> = listOf(playerActive),
|
||||
val npcActiveSlots: List<Pokemon> = listOf(npcActive),
|
||||
val mySlotIndex: Int = 0,
|
||||
val partnerSlotIndex: Int? = null
|
||||
) {
|
||||
companion object {
|
||||
private fun determineGamePhase(turn: Int, npcTeam: List<Pokemon>, playerTeam: List<Pokemon>): GamePhase {
|
||||
val npcAlive = npcTeam.count { it.currentHealth > 0 }
|
||||
val playerAlive = playerTeam.count { it.currentHealth > 0 }
|
||||
|
||||
return when {
|
||||
turn <= 3 -> GamePhase.EARLY_GAME
|
||||
npcAlive <= 2 || playerAlive <= 2 -> GamePhase.ENDGAME
|
||||
turn >= 15 -> GamePhase.LATE_GAME
|
||||
else -> GamePhase.MID_GAME
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class GamePhase {
|
||||
EARLY_GAME,
|
||||
MID_GAME,
|
||||
LATE_GAME,
|
||||
ENDGAME
|
||||
}
|
||||
|
||||
data class TrackedPokemon(
|
||||
var pokemon: Pokemon? = null,
|
||||
var originalPokemon: Pokemon? = null,
|
||||
var species: String? = null,
|
||||
|
||||
var currentHp: Int = 0,
|
||||
var currentHpPercent: Double = 0.0,
|
||||
var maxHp: Int = 0,
|
||||
var previousHp: Int = 0,
|
||||
var lastHpChange: Int = 0,
|
||||
|
||||
var boosts: Map<Stat, Int> = mapOf(),
|
||||
var atkBoost: Int = 0,
|
||||
var defBoost: Int = 0,
|
||||
var spaBoost: Int = 0,
|
||||
var spdBoost: Int = 0,
|
||||
var speBoost: Int = 0,
|
||||
|
||||
var currentAbility: String? = null,
|
||||
var currentTypes: MutableList<String>? = null,
|
||||
|
||||
var stats: Map<Stat, Int> = mapOf(),
|
||||
var moves: List<Move> = listOf(),
|
||||
var revealedMoves: MutableSet<String> = mutableSetOf(),
|
||||
|
||||
var cachedAttack: Double = 0.0,
|
||||
var cachedDefense: Double = 0.0,
|
||||
var cachedSpAttack: Double = 0.0,
|
||||
var cachedSpDefense: Double = 0.0,
|
||||
var cachedSpeed: Double = 0.0,
|
||||
|
||||
var sideConditions: Map<String, Any> = mapOf(),
|
||||
var firstTurn: Int = 0,
|
||||
var protectCount: Int = 0,
|
||||
var lastMoveDamage: Int = 0,
|
||||
var statusTurns: Int = 0,
|
||||
var switchInTurn: Int = 0,
|
||||
|
||||
var predictedRole: PokemonRole = PokemonRole.UNKNOWN,
|
||||
var threatLevel: Int = 0
|
||||
)
|
||||
|
||||
|
||||
enum class PokemonRole {
|
||||
UNKNOWN,
|
||||
SWEEPER,
|
||||
SETUP_SWEEPER,
|
||||
WALLBREAKER,
|
||||
REVENGE_KILLER,
|
||||
TANK,
|
||||
SUPPORT,
|
||||
PIVOT,
|
||||
BALANCED
|
||||
}
|
||||
|
||||
data class TeamTracker(
|
||||
var party: MutableList<TrackedPokemon> = mutableListOf(),
|
||||
var uuid: String? = null,
|
||||
var activePokemon: TrackedPokemon = TrackedPokemon(),
|
||||
var activeSlots: MutableList<TrackedPokemon> = mutableListOf(),
|
||||
var nRemainingMons: Int = 0,
|
||||
var hazardDamage: Int = 0,
|
||||
var teamSynergy: Double = 0.0,
|
||||
var momentum: Double = 0.0
|
||||
) {
|
||||
fun getActiveAt(slotIndex: Int): TrackedPokemon? = activeSlots.getOrNull(slotIndex)
|
||||
|
||||
fun getAllActive(): List<TrackedPokemon> = activeSlots.filter { it.currentHp > 0 }
|
||||
|
||||
fun syncLegacyActivePokemon() {
|
||||
if (activeSlots.isNotEmpty()) {
|
||||
activePokemon = activeSlots[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EnhancedActiveTracker {
|
||||
var p1Active: TeamTracker = TeamTracker()
|
||||
var p2Active: TeamTracker = TeamTracker()
|
||||
|
||||
private var lastStateUpdateTurn: Int = -1
|
||||
|
||||
fun updatePokemonSlot(team: TeamTracker, slotIndex: Int, battlePokemon: BattlePokemon) {
|
||||
while (team.activeSlots.size <= slotIndex) {
|
||||
team.activeSlots.add(TrackedPokemon())
|
||||
}
|
||||
|
||||
updatePokemon(team.activeSlots[slotIndex], battlePokemon)
|
||||
team.syncLegacyActivePokemon()
|
||||
}
|
||||
|
||||
fun getTrackedPokemonByPNX(pnx: String, team: TeamTracker): TrackedPokemon? {
|
||||
if (pnx.length < 3) return null
|
||||
val slotIndex = pnx[2] - 'a'
|
||||
return team.getActiveAt(slotIndex)
|
||||
}
|
||||
|
||||
fun updatePokemon(tracker: TrackedPokemon, battlePokemon: BattlePokemon) {
|
||||
val pokemon = battlePokemon.effectedPokemon
|
||||
|
||||
tracker.pokemon = pokemon
|
||||
tracker.originalPokemon = battlePokemon.originalPokemon
|
||||
tracker.species = pokemon.species.name
|
||||
tracker.currentHp = pokemon.currentHealth
|
||||
tracker.maxHp = pokemon.maxHealth
|
||||
tracker.currentHpPercent = pokemon.currentHealth.toDouble() / pokemon.maxHealth
|
||||
tracker.currentTypes = pokemon.types.map { it.name.lowercase() }.toMutableList()
|
||||
tracker.currentAbility = pokemon.ability.name
|
||||
tracker.moves = pokemon.moveSet.getMoves()
|
||||
|
||||
tracker.boosts = battlePokemon.statChanges
|
||||
tracker.atkBoost = battlePokemon.statChanges[Stats.ATTACK] ?: 0
|
||||
tracker.defBoost = battlePokemon.statChanges[Stats.DEFENCE] ?: 0
|
||||
tracker.spaBoost = battlePokemon.statChanges[Stats.SPECIAL_ATTACK] ?: 0
|
||||
tracker.spdBoost = battlePokemon.statChanges[Stats.SPECIAL_DEFENCE] ?: 0
|
||||
tracker.speBoost = battlePokemon.statChanges[Stats.SPEED] ?: 0
|
||||
|
||||
cacheStats(tracker)
|
||||
}
|
||||
|
||||
private fun cacheStats(tracker: TrackedPokemon) {
|
||||
val pokemon = tracker.pokemon ?: return
|
||||
|
||||
tracker.cachedAttack = calculateStat(pokemon, Stats.ATTACK)
|
||||
tracker.cachedDefense = calculateStat(pokemon, Stats.DEFENCE)
|
||||
tracker.cachedSpAttack = calculateStat(pokemon, Stats.SPECIAL_ATTACK)
|
||||
tracker.cachedSpDefense = calculateStat(pokemon, Stats.SPECIAL_DEFENCE)
|
||||
tracker.cachedSpeed = calculateStat(pokemon, Stats.SPEED)
|
||||
}
|
||||
|
||||
private fun calculateStat(pokemon: Pokemon, stat: Stat): Double {
|
||||
val base = pokemon.species.baseStats[stat] ?: 0
|
||||
val iv = pokemon.ivs[stat] ?: 31
|
||||
val ev = pokemon.evs[stat] ?: 0
|
||||
val level = pokemon.level
|
||||
|
||||
return ((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
|
||||
}
|
||||
|
||||
fun updateParty(team: TeamTracker, pokemon: TrackedPokemon) {
|
||||
val existingIndex = team.party.indexOfFirst { it.pokemon?.uuid == pokemon.pokemon?.uuid }
|
||||
if (existingIndex == -1) {
|
||||
team.party.add(pokemon)
|
||||
} else {
|
||||
team.party[existingIndex] = pokemon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class FearThreat(
|
||||
val pokemon: Pokemon,
|
||||
val hasEndeavor: Boolean,
|
||||
val hasPriorityMove: Boolean,
|
||||
val hasFocusItem: Boolean,
|
||||
val threatLevel: Int
|
||||
)
|
||||
|
||||
enum class BattleSituation {
|
||||
DESPERATE,
|
||||
WINNING,
|
||||
EARLY_GAME,
|
||||
ENDGAME,
|
||||
OPPONENT_SETUP,
|
||||
MY_SETUP,
|
||||
NEUTRAL,
|
||||
HAZARD_ADVANTAGE,
|
||||
WEATHER_CONTROL
|
||||
}
|
||||
|
||||
data class MatchupEvaluation(
|
||||
val offensiveAdvantage: Double,
|
||||
val defensiveAdvantage: Double,
|
||||
val speedAdvantage: Double,
|
||||
val overallScore: Double,
|
||||
val recommendation: MatchupRecommendation
|
||||
)
|
||||
|
||||
enum class MatchupRecommendation {
|
||||
STAY_IN_AND_ATTACK,
|
||||
STAY_IN_AND_SETUP,
|
||||
STAY_IN_AND_STATUS,
|
||||
CONSIDER_SWITCHING,
|
||||
SWITCH_OUT,
|
||||
PIVOT_OUT
|
||||
}
|
||||
+483
@@ -0,0 +1,483 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stat
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.api.types.ElementalType
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.battles.InBattleMove
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
|
||||
class DamageCalculator(
|
||||
private val battle: PokemonBattle?,
|
||||
private val consumedItems: MutableMap<UUID, MutableList<String>> = mutableMapOf()
|
||||
) {
|
||||
|
||||
fun calculateDamage(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
weather: String? = null,
|
||||
criticalHit: Boolean = false,
|
||||
isSpreadMove: Boolean = false,
|
||||
targetCount: Int = 1
|
||||
): Double {
|
||||
val moveData = Moves.getByName(move.id) ?: return 0.0
|
||||
var power = moveData.power.toDouble()
|
||||
|
||||
if (power <= 0) return 0.0
|
||||
|
||||
if (isSpreadMove && targetCount > 1) {
|
||||
power *= 0.75
|
||||
}
|
||||
|
||||
val immunityMultiplier = checkAllImmunities(moveData, attacker, defender)
|
||||
if (immunityMultiplier == 0.0) return 0.0
|
||||
|
||||
val level = attacker.pokemon?.level ?: 50
|
||||
val attackStat = getAttackStat(moveData, attacker)
|
||||
val defenseStat = getDefenseStat(moveData, defender)
|
||||
|
||||
var damage = ((2.0 * level / 5.0 + 2.0) * power * (attackStat / defenseStat)) / 50.0 + 2.0
|
||||
|
||||
damage *= calculateCriticalMultiplier(criticalHit)
|
||||
damage *= calculateSTABMultiplier(attacker, moveData)
|
||||
damage *= TypeEffectiveness.calculateTypeEffectiveness(moveData.elementalType, defender.currentTypes ?: listOf())
|
||||
damage *= calculateWeatherMultiplier(moveData, AiBattleUtils.normalizeWeatherId(weather))
|
||||
damage *= calculateAbilityMultiplier(attacker, defender, moveData)
|
||||
damage *= calculateItemMultiplier(attacker, defender, moveData)
|
||||
damage *= calculateBurnMultiplier(attacker, moveData)
|
||||
damage *= calculateScreenMultiplier(attacker, defender, moveData)
|
||||
damage *= calculateRandomFactor()
|
||||
damage = calculateMultiHitDamage(moveData, damage)
|
||||
|
||||
val finalDamage = max(1.0, damage)
|
||||
|
||||
if (finalDamage > 0) {
|
||||
trackConsumedItems(defender, moveData)
|
||||
}
|
||||
|
||||
return finalDamage
|
||||
}
|
||||
|
||||
private fun getAttackStat(moveData: MoveTemplate, attacker: TrackedPokemon): Double {
|
||||
val pokemon = attacker.pokemon ?: return 100.0
|
||||
|
||||
val baseStat = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
|
||||
getEffectiveStat(pokemon, Stats.ATTACK)
|
||||
} else {
|
||||
getEffectiveStat(pokemon, Stats.SPECIAL_ATTACK)
|
||||
}
|
||||
|
||||
val boost = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
|
||||
attacker.atkBoost
|
||||
} else {
|
||||
attacker.spaBoost
|
||||
}
|
||||
|
||||
return applyStatBoosts(baseStat, boost)
|
||||
}
|
||||
|
||||
private fun getDefenseStat(moveData: MoveTemplate, defender: TrackedPokemon): Double {
|
||||
val pokemon = defender.pokemon ?: return 100.0
|
||||
|
||||
val baseStat = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
|
||||
getEffectiveStat(pokemon, Stats.DEFENCE)
|
||||
} else {
|
||||
getEffectiveStat(pokemon, Stats.SPECIAL_DEFENCE)
|
||||
}
|
||||
|
||||
val boost = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
|
||||
defender.defBoost
|
||||
} else {
|
||||
defender.spdBoost
|
||||
}
|
||||
|
||||
return applyStatBoosts(baseStat, boost)
|
||||
}
|
||||
|
||||
|
||||
private fun applyStatBoosts(baseStat: Double, boost: Int): Double {
|
||||
val clampedBoost = boost.coerceIn(-6, 6)
|
||||
return if (clampedBoost >= 0) {
|
||||
baseStat * (2.0 + clampedBoost) / 2.0
|
||||
} else {
|
||||
baseStat * 2.0 / (2.0 - clampedBoost)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getEffectiveStat(pokemon: Pokemon, stat: Stat): Double {
|
||||
val base = pokemon.species.baseStats[stat] ?: 0
|
||||
val iv = pokemon.ivs[stat] ?: 31
|
||||
val ev = pokemon.evs[stat] ?: 0
|
||||
val level = pokemon.level
|
||||
|
||||
return if (stat == Stats.HP) {
|
||||
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
|
||||
} else {
|
||||
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAllImmunities(
|
||||
moveData: MoveTemplate,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon
|
||||
): Double {
|
||||
if (checkTypeImmunity(moveData, defender)) return 0.0
|
||||
if (checkAbilityImmunity(moveData, attacker, defender)) return 0.0
|
||||
if (checkItemImmunity(moveData, defender)) return 0.0
|
||||
if (checkMoveSpecificImmunity(moveData, defender)) return 0.0
|
||||
return 1.0
|
||||
}
|
||||
|
||||
private fun checkTypeImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
|
||||
val defenderTypes = (defender.currentTypes ?: defender.pokemon?.types?.map { it.name } ?: listOf())
|
||||
.map { it.lowercase() }
|
||||
|
||||
return when (moveData.elementalType) {
|
||||
ElementalTypes.GROUND -> "flying" in defenderTypes
|
||||
ElementalTypes.ELECTRIC -> "ground" in defenderTypes
|
||||
ElementalTypes.NORMAL, ElementalTypes.FIGHTING -> "ghost" in defenderTypes
|
||||
ElementalTypes.PSYCHIC -> "dark" in defenderTypes
|
||||
ElementalTypes.POISON -> "steel" in defenderTypes
|
||||
ElementalTypes.DRAGON -> "fairy" in defenderTypes
|
||||
ElementalTypes.GHOST -> "normal" in defenderTypes
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAbilityImmunity(
|
||||
moveData: MoveTemplate,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon
|
||||
): Boolean {
|
||||
val defenderAbility = defender.currentAbility?.lowercase()
|
||||
?: defender.pokemon?.ability?.name?.lowercase()
|
||||
?: return false
|
||||
|
||||
val attackerAbility = attacker.currentAbility?.lowercase()
|
||||
?: attacker.pokemon?.ability?.name?.lowercase()
|
||||
if (attackerAbility in listOf("moldbreaker", "turboblaze", "teravolt")) {
|
||||
if (defenderAbility != "wonderguard") return false
|
||||
}
|
||||
|
||||
return when (defenderAbility) {
|
||||
"levitate" -> moveData.elementalType == ElementalTypes.GROUND
|
||||
"voltabsorb", "lightningrod", "motordrive" -> moveData.elementalType == ElementalTypes.ELECTRIC
|
||||
"flashfire", "wellbakedbody" -> moveData.elementalType == ElementalTypes.FIRE
|
||||
"waterabsorb", "stormdrain", "dryskin" -> moveData.elementalType == ElementalTypes.WATER
|
||||
"sapsipper" -> moveData.elementalType == ElementalTypes.GRASS
|
||||
"eartheater" -> moveData.elementalType == ElementalTypes.GROUND
|
||||
"soundproof" -> isSoundMove(moveData)
|
||||
"bulletproof" -> isBallOrBombMove(moveData)
|
||||
"queenlymajesty", "dazzling" -> moveData.priority > 0
|
||||
"wonderguard" -> {
|
||||
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
|
||||
moveData.elementalType,
|
||||
defender.currentTypes ?: listOf()
|
||||
)
|
||||
effectiveness <= 1.0
|
||||
}
|
||||
"goodasgold" -> moveData.power.toInt() == 0
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkItemImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
|
||||
val defenderItem = defender.pokemon?.heldItem()?.item?.let {
|
||||
BuiltInRegistries.ITEM.getKey(it).toString()
|
||||
} ?: return false
|
||||
|
||||
return when (defenderItem) {
|
||||
"cobblemon:air_balloon" -> {
|
||||
val notConsumed = consumedItems[defender.pokemon?.uuid]?.contains("airballoon") != true
|
||||
notConsumed && moveData.elementalType == ElementalTypes.GROUND
|
||||
}
|
||||
"cobblemon:safety_goggles" -> isPowderMove(moveData) || isWeatherDamageMove(moveData)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkMoveSpecificImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
|
||||
val defenderTypes = (defender.currentTypes ?: defender.pokemon?.types?.map { it.name } ?: listOf())
|
||||
.map { it.lowercase() }
|
||||
val defenderAbility = defender.currentAbility?.lowercase()
|
||||
?: defender.pokemon?.ability?.name?.lowercase()
|
||||
|
||||
return when (moveData.name.lowercase()) {
|
||||
"thunderwave" -> {
|
||||
"electric" in defenderTypes || "ground" in defenderTypes ||
|
||||
defenderAbility in listOf("limber", "volt absorb", "lightningrod", "motordrive")
|
||||
}
|
||||
"willowisp" -> {
|
||||
"fire" in defenderTypes ||
|
||||
defenderAbility in listOf("waterveil", "waterbubble", "flashfire")
|
||||
}
|
||||
"toxic", "poisonpowder", "poisongas" -> {
|
||||
"poison" in defenderTypes || "steel" in defenderTypes ||
|
||||
defenderAbility in listOf("immunity", "poisonheal", "pastelveil")
|
||||
}
|
||||
"sleeppowder", "spore", "hypnosis", "sing" -> {
|
||||
defenderAbility in listOf("insomnia", "vitalspirit", "comatose", "sweetveil")
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateCriticalMultiplier(criticalHit: Boolean): Double {
|
||||
return if (criticalHit) 1.5 else 1.0
|
||||
}
|
||||
|
||||
private fun calculateSTABMultiplier(attacker: TrackedPokemon, moveData: MoveTemplate): Double {
|
||||
val pokemonTypes = attacker.currentTypes ?: attacker.pokemon?.types?.map { it.name } ?: return 1.0
|
||||
return if (AiBattleUtils.hasStab(moveData.elementalType.name, pokemonTypes)) {
|
||||
if (attacker.currentAbility?.lowercase() == "adaptability") 2.0 else 1.5
|
||||
} else 1.0
|
||||
}
|
||||
|
||||
private fun calculateWeatherMultiplier(moveData: MoveTemplate, weather: String?): Double {
|
||||
return when (weather) {
|
||||
"sunnyday" -> when {
|
||||
moveData.elementalType == ElementalTypes.FIRE -> 1.5
|
||||
moveData.elementalType == ElementalTypes.WATER && moveData.name != "hydrosteam" -> 0.5
|
||||
moveData.name == "hydrosteam" -> 1.5
|
||||
else -> 1.0
|
||||
}
|
||||
"raindance" -> when {
|
||||
moveData.elementalType == ElementalTypes.WATER -> 1.5
|
||||
moveData.elementalType == ElementalTypes.FIRE -> 0.5
|
||||
else -> 1.0
|
||||
}
|
||||
"sandstorm", "hail", "snow" -> if (moveData.name == "weatherball") 2.0 else 1.0
|
||||
else -> 1.0
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateAbilityMultiplier(
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
moveData: MoveTemplate
|
||||
): Double {
|
||||
var multiplier = 1.0
|
||||
|
||||
when (attacker.currentAbility?.lowercase()) {
|
||||
"hugepower", "purepower" -> {
|
||||
if (moveData.damageCategory == DamageCategories.PHYSICAL) multiplier *= 2.0
|
||||
}
|
||||
"guts" -> {
|
||||
if (attacker.pokemon?.status != null && moveData.damageCategory == DamageCategories.PHYSICAL) {
|
||||
multiplier *= 1.5
|
||||
}
|
||||
}
|
||||
"overgrow" -> {
|
||||
if (moveData.elementalType == ElementalTypes.GRASS && attacker.currentHpPercent <= 0.33) {
|
||||
multiplier *= 1.5
|
||||
}
|
||||
}
|
||||
"blaze" -> {
|
||||
if (moveData.elementalType == ElementalTypes.FIRE && attacker.currentHpPercent <= 0.33) {
|
||||
multiplier *= 1.5
|
||||
}
|
||||
}
|
||||
"torrent" -> {
|
||||
if (moveData.elementalType == ElementalTypes.WATER && attacker.currentHpPercent <= 0.33) {
|
||||
multiplier *= 1.5
|
||||
}
|
||||
}
|
||||
"swarm" -> {
|
||||
if (moveData.elementalType == ElementalTypes.BUG && attacker.currentHpPercent <= 0.33) {
|
||||
multiplier *= 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (defender.currentAbility?.lowercase()) {
|
||||
"thickfat" -> {
|
||||
if (moveData.elementalType == ElementalTypes.FIRE || moveData.elementalType == ElementalTypes.ICE) {
|
||||
multiplier *= 0.5
|
||||
}
|
||||
}
|
||||
"filter", "solidrock" -> {
|
||||
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
|
||||
moveData.elementalType,
|
||||
defender.currentTypes ?: listOf()
|
||||
)
|
||||
if (effectiveness > 1.0) multiplier *= 0.75
|
||||
}
|
||||
}
|
||||
|
||||
return multiplier
|
||||
}
|
||||
|
||||
private fun calculateItemMultiplier(
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
moveData: MoveTemplate
|
||||
): Double {
|
||||
var multiplier = 1.0
|
||||
|
||||
val attackerItem = attacker.pokemon?.heldItem()?.item?.let {
|
||||
BuiltInRegistries.ITEM.getKey(it).toString()
|
||||
}
|
||||
|
||||
when (attackerItem) {
|
||||
"cobblemon:choice_band" -> {
|
||||
if (moveData.damageCategory == DamageCategories.PHYSICAL) multiplier *= 1.5
|
||||
}
|
||||
"cobblemon:choice_specs" -> {
|
||||
if (moveData.damageCategory == DamageCategories.SPECIAL) multiplier *= 1.5
|
||||
}
|
||||
"cobblemon:life_orb" -> multiplier *= 1.3
|
||||
}
|
||||
|
||||
return multiplier
|
||||
}
|
||||
|
||||
private fun calculateBurnMultiplier(attacker: TrackedPokemon, moveData: MoveTemplate): Double {
|
||||
return if (attacker.pokemon?.status?.status?.showdownName == "burn" &&
|
||||
moveData.damageCategory == DamageCategories.PHYSICAL &&
|
||||
attacker.currentAbility != "guts") {
|
||||
0.5
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateScreenMultiplier(
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
moveData: MoveTemplate
|
||||
): Double {
|
||||
val attackerAbility = attacker.currentAbility?.lowercase() ?: attacker.pokemon?.ability?.name?.lowercase()
|
||||
if (attackerAbility == "infiltrator") {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
val sideConditions = defender.sideConditions.keys.map { it.lowercase() }
|
||||
|
||||
val hasReflect = "reflect" in sideConditions
|
||||
val hasLightScreen = "lightscreen" in sideConditions || "light screen" in sideConditions
|
||||
val hasAuroraVeil = "auroraveil" in sideConditions || "aurora veil" in sideConditions
|
||||
|
||||
val multiplier = when (moveData.damageCategory) {
|
||||
DamageCategories.PHYSICAL -> {
|
||||
when {
|
||||
hasAuroraVeil -> 0.5
|
||||
hasReflect -> 0.5
|
||||
else -> 1.0
|
||||
}
|
||||
}
|
||||
DamageCategories.SPECIAL -> {
|
||||
when {
|
||||
hasAuroraVeil -> 0.5
|
||||
hasLightScreen -> 0.5
|
||||
else -> 1.0
|
||||
}
|
||||
}
|
||||
else -> 1.0
|
||||
}
|
||||
|
||||
return multiplier
|
||||
}
|
||||
|
||||
private fun calculateRandomFactor(): Double = 0.925
|
||||
|
||||
private fun calculateMultiHitDamage(moveData: MoveTemplate, baseDamage: Double): Double {
|
||||
val moveName = moveData.name.lowercase()
|
||||
|
||||
return when {
|
||||
moveName == "populationbomb" -> {
|
||||
val accuracy = moveData.accuracy / 100.0
|
||||
var expectedDamage = 0.0
|
||||
var probability = 1.0
|
||||
|
||||
for (hit in 1..10) {
|
||||
expectedDamage += baseDamage * probability
|
||||
probability *= accuracy
|
||||
}
|
||||
expectedDamage
|
||||
}
|
||||
|
||||
moveName in listOf("triplekick", "tripleaxel") -> {
|
||||
val accuracy = moveData.accuracy / 100.0
|
||||
baseDamage * 1.0 +
|
||||
baseDamage * 2.0 * accuracy +
|
||||
baseDamage * 3.0 * accuracy * accuracy
|
||||
}
|
||||
|
||||
moveName in MoveData.multiHitMoves.keys -> {
|
||||
val hitRange = MoveData.multiHitMoves[moveName]!!
|
||||
val minHits = hitRange.keys.minOrNull() ?: 1
|
||||
val maxHits = hitRange.values.maxOrNull() ?: 1
|
||||
|
||||
when {
|
||||
minHits == 2 && maxHits == 5 -> baseDamage * 3.0
|
||||
minHits == 2 && maxHits == 2 -> baseDamage * 2.0
|
||||
minHits == 3 && maxHits == 3 -> baseDamage * 3.0
|
||||
else -> baseDamage * ((minHits + maxHits) / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
else -> baseDamage
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSoundMove(moveData: MoveTemplate): Boolean {
|
||||
return moveData.name.lowercase() in listOf(
|
||||
"boomburst", "bugbuzz", "chatter", "clangoroussoul", "disarmingvoice",
|
||||
"echoedvoice", "grasswhistle", "growl", "hypervoice", "metalsound",
|
||||
"nobleroar", "overdrive", "perishsong", "relicsong", "roar", "round",
|
||||
"screech", "sing", "snore", "sparklingaria", "supersonic", "uproar"
|
||||
)
|
||||
}
|
||||
|
||||
private fun isBallOrBombMove(moveData: MoveTemplate): Boolean {
|
||||
return moveData.name.lowercase() in listOf(
|
||||
"acidspray", "aurasphere", "barrage", "bulletseed", "eggbomb",
|
||||
"electroball", "energyball", "focusblast", "gyroball", "iceball",
|
||||
"magnetbomb", "mistball", "mudbomb", "octazooka", "pollenpuff",
|
||||
"rockblast", "searingshot", "seedbomb", "shadowball", "sludgebomb",
|
||||
"weatherball", "zapcannon"
|
||||
)
|
||||
}
|
||||
|
||||
private fun isPowderMove(moveData: MoveTemplate): Boolean {
|
||||
return moveData.name.lowercase() in listOf(
|
||||
"sleeppowder", "stunspore", "poisonpowder", "cottonspore",
|
||||
"ragepowder", "spore"
|
||||
)
|
||||
}
|
||||
|
||||
private fun isWeatherDamageMove(moveData: MoveTemplate): Boolean {
|
||||
return moveData.name.lowercase() in listOf("sandstorm", "hail")
|
||||
}
|
||||
|
||||
private fun trackConsumedItems(defender: TrackedPokemon, moveData: MoveTemplate) {
|
||||
val pokemon = defender.pokemon ?: return
|
||||
val defenderItem = pokemon.heldItem()?.item?.let {
|
||||
BuiltInRegistries.ITEM.getKey(it).toString()
|
||||
} ?: return
|
||||
|
||||
if (defenderItem == "cobblemon:air_balloon") {
|
||||
val alreadyConsumed = consumedItems[pokemon.uuid]?.contains("airballoon") == true
|
||||
if (!alreadyConsumed) {
|
||||
addConsumedItem(pokemon.uuid, "airballoon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addConsumedItem(pokemonUuid: UUID, item: String) {
|
||||
val items = consumedItems.getOrPut(pokemonUuid) { mutableListOf() }
|
||||
items.add(item)
|
||||
}
|
||||
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
|
||||
import com.cobblemon.mod.common.battles.InBattleMove
|
||||
import com.cobblemon.mod.common.battles.ShowdownMoveset
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
|
||||
class DoublesDecisionMaker(
|
||||
private val moveEvaluator: MoveEvaluator,
|
||||
private val damageCalculator: DamageCalculator
|
||||
) {
|
||||
|
||||
data class MoveTargetPair(
|
||||
val move: InBattleMove,
|
||||
val targetPNX: String?,
|
||||
val evaluation: MoveEvaluator.MoveEvaluation
|
||||
)
|
||||
|
||||
fun findBestMoveAndTarget(
|
||||
moveset: ShowdownMoveset?,
|
||||
activeBattlePokemon: ActiveBattlePokemon,
|
||||
myPokemon: TrackedPokemon,
|
||||
opponents: List<TrackedPokemon>,
|
||||
partner: TrackedPokemon?,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): MoveTargetPair? {
|
||||
if (moveset == null) return null
|
||||
|
||||
val usableMoves = moveset.moves.filter { it.canBeUsed() }
|
||||
if (usableMoves.isEmpty()) return null
|
||||
|
||||
val allOptions = mutableListOf<MoveTargetPair>()
|
||||
|
||||
for (move in usableMoves) {
|
||||
val moveData = Moves.getByName(move.id) ?: continue
|
||||
val targetList = move.target.targetList(activeBattlePokemon)
|
||||
|
||||
if (targetList == null) {
|
||||
val evaluation = moveEvaluator.evaluateMoveForDoubles(
|
||||
move, myPokemon, opponents.firstOrNull() ?: TrackedPokemon(),
|
||||
opponents, partner, battleState, gamePlan
|
||||
)
|
||||
allOptions.add(MoveTargetPair(move, null, evaluation))
|
||||
} else {
|
||||
val opponentTargets = targetList.filterIsInstance<ActiveBattlePokemon>()
|
||||
.filter { !it.isAllied(activeBattlePokemon) }
|
||||
|
||||
for (target in opponentTargets) {
|
||||
val targetPokemon = opponents.find {
|
||||
it.pokemon?.uuid == target.battlePokemon?.uuid
|
||||
} ?: continue
|
||||
|
||||
val evaluation = moveEvaluator.evaluateMoveForDoubles(
|
||||
move, myPokemon, targetPokemon,
|
||||
opponents, partner, battleState, gamePlan
|
||||
)
|
||||
|
||||
allOptions.add(MoveTargetPair(move, target.getPNX(), evaluation))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allOptions.maxByOrNull { it.evaluation.totalScore }
|
||||
}
|
||||
|
||||
fun scoreTarget(
|
||||
target: TrackedPokemon,
|
||||
myPokemon: TrackedPokemon,
|
||||
allOpponents: List<TrackedPokemon>,
|
||||
battleState: BattleState
|
||||
): Double {
|
||||
var score = 0.0
|
||||
|
||||
score += target.threatLevel * 20.0
|
||||
|
||||
val mySpeed = myPokemon.cachedSpeed
|
||||
val targetSpeed = target.cachedSpeed
|
||||
if (targetSpeed > mySpeed && target.currentHpPercent < 0.5) {
|
||||
score += 100.0
|
||||
}
|
||||
|
||||
if (target.boosts.values.any { it >= 2 }) {
|
||||
score += 120.0
|
||||
}
|
||||
|
||||
if (target.cachedAttack > 100 || target.cachedSpAttack > 100) {
|
||||
score += 40.0
|
||||
}
|
||||
|
||||
if (target.currentHpPercent < 0.3) {
|
||||
score += 80.0
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
fun selectBestTarget(
|
||||
move: InBattleMove,
|
||||
activeBattlePokemon: ActiveBattlePokemon,
|
||||
myPokemon: TrackedPokemon,
|
||||
opponents: List<TrackedPokemon>,
|
||||
battleState: BattleState
|
||||
): String? {
|
||||
val targetList = move.target.targetList(activeBattlePokemon) ?: return null
|
||||
|
||||
val opponentTargets = targetList.filterIsInstance<ActiveBattlePokemon>()
|
||||
.filter { !it.isAllied(activeBattlePokemon) }
|
||||
|
||||
if (opponentTargets.isEmpty()) return null
|
||||
|
||||
val targetScores = opponentTargets.mapNotNull { target ->
|
||||
val targetPokemon = opponents.find {
|
||||
it.pokemon?.uuid == target.battlePokemon?.uuid
|
||||
}
|
||||
|
||||
if (targetPokemon != null) {
|
||||
val damage = damageCalculator.calculateDamage(
|
||||
move, myPokemon, targetPokemon, battleState.weather
|
||||
)
|
||||
|
||||
val targetScore = scoreTarget(targetPokemon, myPokemon, opponents, battleState)
|
||||
val totalScore = damage + targetScore
|
||||
|
||||
target to totalScore
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return targetScores.maxByOrNull { it.second }?.first?.getPNX()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stat
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.api.pokemon.status.Statuses
|
||||
|
||||
|
||||
object MoveData {
|
||||
val multiHitMoves: Map<String, Map<Int, Int>> = mapOf(
|
||||
"armthrust" to mapOf(2 to 5),
|
||||
"barrage" to mapOf(2 to 5),
|
||||
"bonerush" to mapOf(2 to 5),
|
||||
"bulletseed" to mapOf(2 to 5),
|
||||
"cometpunch" to mapOf(2 to 5),
|
||||
"doubleslap" to mapOf(2 to 5),
|
||||
"furyattack" to mapOf(2 to 5),
|
||||
"furyswipes" to mapOf(2 to 5),
|
||||
"iciclespear" to mapOf(2 to 5),
|
||||
"pinmissile" to mapOf(2 to 5),
|
||||
"rockblast" to mapOf(2 to 5),
|
||||
"scaleshot" to mapOf(2 to 5),
|
||||
"spikecannon" to mapOf(2 to 5),
|
||||
"tailslap" to mapOf(2 to 5),
|
||||
"watershuriken" to mapOf(2 to 5),
|
||||
"bonemerang" to mapOf(2 to 2),
|
||||
"doublehit" to mapOf(2 to 2),
|
||||
"doubleironbash" to mapOf(2 to 2),
|
||||
"doublekick" to mapOf(2 to 2),
|
||||
"dragondarts" to mapOf(2 to 2),
|
||||
"dualchop" to mapOf(2 to 2),
|
||||
"dualwingbeat" to mapOf(2 to 2),
|
||||
"geargrind" to mapOf(2 to 2),
|
||||
"twinbeam" to mapOf(2 to 2),
|
||||
"twineedle" to mapOf(2 to 2),
|
||||
"surgingstrikes" to mapOf(3 to 3),
|
||||
"tripledive" to mapOf(3 to 3),
|
||||
"tripleaxel" to mapOf(1 to 3),
|
||||
"triplekick" to mapOf(1 to 3),
|
||||
"populationbomb" to mapOf(1 to 10)
|
||||
)
|
||||
|
||||
val statusMoves: Map<MoveTemplate?, String> = mapOf(
|
||||
Moves.getByName("willowisp") to Statuses.BURN.showdownName,
|
||||
Moves.getByName("scald") to Statuses.BURN.showdownName,
|
||||
Moves.getByName("scorchingsands") to Statuses.BURN.showdownName,
|
||||
Moves.getByName("flamethrower") to Statuses.BURN.showdownName,
|
||||
Moves.getByName("sacredfire") to Statuses.BURN.showdownName,
|
||||
Moves.getByName("glare") to Statuses.PARALYSIS.showdownName,
|
||||
Moves.getByName("nuzzle") to Statuses.PARALYSIS.showdownName,
|
||||
Moves.getByName("stunspore") to Statuses.PARALYSIS.showdownName,
|
||||
Moves.getByName("thunderwave") to Statuses.PARALYSIS.showdownName,
|
||||
Moves.getByName("bodyslam") to Statuses.PARALYSIS.showdownName,
|
||||
Moves.getByName("discharge") to Statuses.PARALYSIS.showdownName,
|
||||
Moves.getByName("spark") to Statuses.PARALYSIS.showdownName,
|
||||
Moves.getByName("darkvoid") to Statuses.SLEEP.showdownName,
|
||||
Moves.getByName("hypnosis") to Statuses.SLEEP.showdownName,
|
||||
Moves.getByName("lovelykiss") to Statuses.SLEEP.showdownName,
|
||||
Moves.getByName("relicsong") to Statuses.SLEEP.showdownName,
|
||||
Moves.getByName("sing") to Statuses.SLEEP.showdownName,
|
||||
Moves.getByName("sleeppowder") to Statuses.SLEEP.showdownName,
|
||||
Moves.getByName("spore") to Statuses.SLEEP.showdownName,
|
||||
Moves.getByName("yawn") to Statuses.SLEEP.showdownName,
|
||||
Moves.getByName("chatter") to "confusion",
|
||||
Moves.getByName("confuseray") to "confusion",
|
||||
Moves.getByName("dynamicpunch") to "confusion",
|
||||
Moves.getByName("flatter") to "confusion",
|
||||
Moves.getByName("supersonic") to "confusion",
|
||||
Moves.getByName("swagger") to "confusion",
|
||||
Moves.getByName("sweetkiss") to "confusion",
|
||||
Moves.getByName("teeterdance") to "confusion",
|
||||
Moves.getByName("poisongas") to Statuses.POISON.showdownName,
|
||||
Moves.getByName("poisonpowder") to Statuses.POISON.showdownName,
|
||||
Moves.getByName("poisonjab") to Statuses.POISON.showdownName,
|
||||
Moves.getByName("sludgebomb") to Statuses.POISON.showdownName,
|
||||
Moves.getByName("crosspoison") to Statuses.POISON.showdownName,
|
||||
Moves.getByName("toxic") to Statuses.POISON_BADLY.showdownName,
|
||||
Moves.getByName("toxicthread") to Statuses.POISON.showdownName,
|
||||
Moves.getByName("curse") to "cursed",
|
||||
Moves.getByName("leechseed") to "leech"
|
||||
)
|
||||
|
||||
val boostFromMoves: Map<String, Map<Stat, Int>> = mapOf(
|
||||
"bellydrum" to mapOf(Stats.ATTACK to 6),
|
||||
"swordsdance" to mapOf(Stats.ATTACK to 2),
|
||||
"honeclaws" to mapOf(Stats.ATTACK to 1, Stats.ACCURACY to 1),
|
||||
"bulkup" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1),
|
||||
"curse" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPEED to -1),
|
||||
"howl" to mapOf(Stats.ATTACK to 1),
|
||||
"meditate" to mapOf(Stats.ATTACK to 1),
|
||||
"sharpen" to mapOf(Stats.ATTACK to 1),
|
||||
"workup" to mapOf(Stats.ATTACK to 1, Stats.SPECIAL_ATTACK to 1),
|
||||
"acidarmor" to mapOf(Stats.DEFENCE to 2),
|
||||
"barrier" to mapOf(Stats.DEFENCE to 2),
|
||||
"cottonguard" to mapOf(Stats.DEFENCE to 3),
|
||||
"defensecurl" to mapOf(Stats.DEFENCE to 1),
|
||||
"irondefense" to mapOf(Stats.DEFENCE to 2),
|
||||
"stockpile" to mapOf(Stats.DEFENCE to 1, Stats.SPECIAL_DEFENCE to 1),
|
||||
"stuffcheeks" to mapOf(Stats.DEFENCE to 2),
|
||||
"shelter" to mapOf(Stats.DEFENCE to 2, Stats.EVASION to 1),
|
||||
"withdraw" to mapOf(Stats.DEFENCE to 1),
|
||||
"harden" to mapOf(Stats.DEFENCE to 1),
|
||||
"nastyplot" to mapOf(Stats.SPECIAL_ATTACK to 2),
|
||||
"calmmind" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1),
|
||||
"quiverdance" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
|
||||
"tailglow" to mapOf(Stats.SPECIAL_ATTACK to 3),
|
||||
"takeheart" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1),
|
||||
"geomancy" to mapOf(Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to 2, Stats.SPEED to 2),
|
||||
"growth" to mapOf(Stats.SPECIAL_ATTACK to 1),
|
||||
"amnesia" to mapOf(Stats.SPECIAL_DEFENCE to 2),
|
||||
"agility" to mapOf(Stats.SPEED to 2),
|
||||
"autotomize" to mapOf(Stats.SPEED to 2),
|
||||
"rockpolish" to mapOf(Stats.SPEED to 2),
|
||||
"dragondance" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 1),
|
||||
"shiftgear" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 2),
|
||||
"clangoroussoul" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
|
||||
"extremeevoboost" to mapOf(Stats.ATTACK to 2, Stats.DEFENCE to 2, Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to 2, Stats.SPEED to 2),
|
||||
"clangoroussoulblaze" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
|
||||
"filletaway" to mapOf(Stats.ATTACK to 2, Stats.SPECIAL_ATTACK to 2, Stats.SPEED to 2),
|
||||
"noretreat" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
|
||||
"shellsmash" to mapOf(Stats.ATTACK to 2, Stats.DEFENCE to -1, Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to -1, Stats.SPEED to 2),
|
||||
"tidyup" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 1),
|
||||
"victorydance" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPEED to 1),
|
||||
"coil" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.ACCURACY to 1),
|
||||
"minimize" to mapOf(Stats.EVASION to 2),
|
||||
"doubleteam" to mapOf(Stats.EVASION to 1)
|
||||
)
|
||||
|
||||
val weatherMoves: Map<String, String> = mapOf(
|
||||
"raindance" to "rain",
|
||||
"sunnyday" to "sun",
|
||||
"sandstorm" to "sand",
|
||||
"hail" to "hail",
|
||||
"snowscape" to "snow"
|
||||
)
|
||||
|
||||
val weatherDependentMoves: Map<String, String> = mapOf(
|
||||
"thunder" to "rain",
|
||||
"hurricane" to "rain",
|
||||
"blizzard" to "hail",
|
||||
"solarbeam" to "sun",
|
||||
"hydrosteam" to "sun",
|
||||
"weatherball" to "any"
|
||||
)
|
||||
|
||||
val weatherAbilities: Map<String, String> = mapOf(
|
||||
"swiftswim" to "rain",
|
||||
"raindish" to "rain",
|
||||
"dryskin" to "rain",
|
||||
"chlorophyll" to "sun",
|
||||
"leafguard" to "sun",
|
||||
"flowergift" to "sun",
|
||||
"solarpower" to "sun",
|
||||
"sandrush" to "sand",
|
||||
"sandveil" to "sand",
|
||||
"sandforce" to "sand",
|
||||
"icebody" to "hail",
|
||||
"snowcloak" to "hail"
|
||||
)
|
||||
|
||||
val entryHazards = listOf("spikes", "stealthrock", "stickyweb", "toxicspikes")
|
||||
val antiHazardsMoves = listOf("rapidspin", "defog", "tidyup")
|
||||
val antiBoostMoves = listOf("clearsmog", "haze", "roar", "whirlwind")
|
||||
val pivotMoves = listOf("uturn", "flipturn", "partingshot", "batonpass", "voltswitch", "teleport")
|
||||
val setupMoves = setOf("tailwind", "trickroom", "auroraveil", "lightscreen", "reflect")
|
||||
val selfRecoveryMoves = listOf("healorder", "milkdrink", "recover", "rest", "roost", "slackoff", "softboiled")
|
||||
val priorityMoves = listOf(
|
||||
"quickattack", "extremespeed", "bulletpunch", "machpunch",
|
||||
"aquajet", "shadowsneak", "accelerock", "iceshard",
|
||||
"suckerpunch", "firstimpression", "fakeout"
|
||||
)
|
||||
|
||||
val recoilMoves = listOf(
|
||||
"doubleedge", "bravebird", "closecombat", "superpower",
|
||||
"overheat", "dracometeor", "leafstorm", "flareblitz"
|
||||
)
|
||||
}
|
||||
+737
@@ -0,0 +1,737 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.battles.InBattleMove
|
||||
import com.cobblemon.mod.common.battles.MoveTarget
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
class MoveEvaluator(
|
||||
private val damageCalculator: DamageCalculator,
|
||||
private val opponentPredictor: OpponentPredictor,
|
||||
private val isDoublesMode: Boolean = false
|
||||
) {
|
||||
|
||||
|
||||
data class MoveEvaluation(
|
||||
val move: InBattleMove,
|
||||
val rawDamage: Double,
|
||||
val expectedDamage: Double,
|
||||
val strategicValue: Double,
|
||||
val riskFactor: Double,
|
||||
val priority: Int,
|
||||
val turnAdvantage: Double,
|
||||
val gamePlanAlignment: Double
|
||||
) {
|
||||
|
||||
val totalScore: Double = (expectedDamage + strategicValue + gamePlanAlignment + turnAdvantage) / riskFactor
|
||||
}
|
||||
|
||||
data class MoveDebugDetails(
|
||||
val moveId: String,
|
||||
val moveType: String,
|
||||
val accuracy: Double,
|
||||
val rawDamage: Double,
|
||||
val expectedDamage: Double,
|
||||
val effectiveness: Double,
|
||||
val hasStab: Boolean,
|
||||
val priority: Int,
|
||||
val isStatusMove: Boolean,
|
||||
val setsWeather: Boolean,
|
||||
val setsHazards: Boolean,
|
||||
val removesHazards: Boolean,
|
||||
val isRecovery: Boolean,
|
||||
val isPivot: Boolean,
|
||||
val isSetup: Boolean,
|
||||
val isRisky: Boolean
|
||||
)
|
||||
|
||||
fun buildDebugDetails(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): MoveDebugDetails? {
|
||||
val moveData = Moves.getByName(move.id) ?: return null
|
||||
val rawDamage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
|
||||
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
|
||||
val expectedDamage = rawDamage * accuracy
|
||||
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
|
||||
moveData.elementalType,
|
||||
defender.currentTypes ?: listOf()
|
||||
)
|
||||
val attackerTypes = attacker.currentTypes ?: attacker.pokemon?.types?.map { it.name } ?: listOf()
|
||||
val hasStab = AiBattleUtils.hasStab(moveData.elementalType.name, attackerTypes)
|
||||
val moveId = move.id.lowercase()
|
||||
val isStatusMove = MoveData.statusMoves.keys.any { it?.name?.lowercase() == moveId }
|
||||
val setsWeather = moveId in MoveData.weatherMoves.keys
|
||||
val setsHazards = moveId in MoveData.entryHazards
|
||||
val removesHazards = moveId in MoveData.antiHazardsMoves
|
||||
val isRecovery = moveId in MoveData.selfRecoveryMoves
|
||||
val isPivot = moveId in MoveData.pivotMoves
|
||||
val isSetup = moveId in MoveData.boostFromMoves.keys
|
||||
val isRisky = moveId in gamePlan.riskyMoves
|
||||
|
||||
return MoveDebugDetails(
|
||||
moveId = move.id,
|
||||
moveType = moveData.elementalType.name,
|
||||
accuracy = accuracy,
|
||||
rawDamage = rawDamage,
|
||||
expectedDamage = expectedDamage,
|
||||
effectiveness = effectiveness,
|
||||
hasStab = hasStab,
|
||||
priority = moveData.priority,
|
||||
isStatusMove = isStatusMove,
|
||||
setsWeather = setsWeather,
|
||||
setsHazards = setsHazards,
|
||||
removesHazards = removesHazards,
|
||||
isRecovery = isRecovery,
|
||||
isPivot = isPivot,
|
||||
isSetup = isSetup,
|
||||
isRisky = isRisky
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun evaluateMove(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): MoveEvaluation {
|
||||
val moveData = Moves.getByName(move.id)
|
||||
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
|
||||
|
||||
|
||||
val rawDamage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
|
||||
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
|
||||
val expectedDamage = rawDamage * accuracy
|
||||
|
||||
|
||||
val strategicValue = calculateStrategicValue(move, attacker, defender, battleState, gamePlan)
|
||||
val riskFactor = calculateRiskFactor(move, attacker, defender, battleState, gamePlan)
|
||||
val turnAdvantage = calculateTurnAdvantage(move, attacker, defender, battleState)
|
||||
val gamePlanAlignment = calculateGamePlanAlignment(move, attacker, defender, battleState, gamePlan)
|
||||
|
||||
return MoveEvaluation(
|
||||
move = move,
|
||||
rawDamage = rawDamage,
|
||||
expectedDamage = expectedDamage,
|
||||
strategicValue = strategicValue,
|
||||
riskFactor = riskFactor,
|
||||
priority = moveData.priority,
|
||||
turnAdvantage = turnAdvantage,
|
||||
gamePlanAlignment = gamePlanAlignment
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun getEffectiveAccuracy(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
weather: String?
|
||||
): Double {
|
||||
val moveData = Moves.getByName(move.id) ?: return 0.0
|
||||
var accuracy = if (moveData.accuracy <= 0) 1.0 else moveData.accuracy / 100.0
|
||||
val normalizedWeather = AiBattleUtils.normalizeWeatherId(weather)
|
||||
|
||||
|
||||
when (normalizedWeather) {
|
||||
"raindance" -> {
|
||||
if (move.id.lowercase() in listOf("thunder", "hurricane")) accuracy = 1.0
|
||||
}
|
||||
|
||||
"hail", "snow" -> {
|
||||
if (move.id.lowercase() == "blizzard") accuracy = 1.0
|
||||
}
|
||||
|
||||
"sunnyday" -> {
|
||||
if (move.id.lowercase() in listOf("thunder", "hurricane")) accuracy *= 0.5
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
when (attacker.currentAbility?.lowercase()) {
|
||||
"compoundeyes" -> accuracy *= 1.3
|
||||
"noguard" -> accuracy = 1.0
|
||||
}
|
||||
|
||||
when (defender.currentAbility?.lowercase()) {
|
||||
"noguard" -> accuracy = 1.0
|
||||
"sandveil" -> if (normalizedWeather == "sandstorm") accuracy *= 0.8
|
||||
"snowcloak" -> if (normalizedWeather in listOf("hail", "snow")) accuracy *= 0.8
|
||||
}
|
||||
|
||||
|
||||
val accuracyBoost = attacker.boosts.getOrDefault(Stats.ACCURACY, 0)
|
||||
val evasionBoost = defender.boosts.getOrDefault(Stats.EVASION, 0)
|
||||
val netBoost = accuracyBoost - evasionBoost
|
||||
|
||||
if (netBoost != 0) {
|
||||
accuracy *= if (netBoost > 0) {
|
||||
(3.0 + netBoost) / 3.0
|
||||
} else {
|
||||
3.0 / (3.0 - netBoost)
|
||||
}
|
||||
}
|
||||
|
||||
return accuracy.coerceIn(0.0, 1.0)
|
||||
}
|
||||
|
||||
|
||||
private fun calculateStrategicValue(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): Double {
|
||||
var value = 0.0
|
||||
|
||||
|
||||
if (move.id.lowercase() in MoveData.weatherMoves.keys) {
|
||||
value += evaluateWeatherSetting(move, gamePlan, battleState)
|
||||
}
|
||||
|
||||
|
||||
if (move.id.lowercase() in MoveData.boostFromMoves.keys) {
|
||||
value += evaluateSetupMove(move, attacker, defender, battleState, gamePlan)
|
||||
}
|
||||
|
||||
|
||||
val moveId = move.id.lowercase()
|
||||
val isStatusMove = MoveData.statusMoves.keys.any { it?.name?.lowercase() == moveId }
|
||||
if (isStatusMove) {
|
||||
value += evaluateStatusMove(move, attacker, defender, battleState, gamePlan)
|
||||
}
|
||||
|
||||
|
||||
if (move.id.lowercase() in MoveData.entryHazards) {
|
||||
value += evaluateHazardSetting(battleState, gamePlan)
|
||||
}
|
||||
|
||||
|
||||
if (move.id.lowercase() in MoveData.antiHazardsMoves) {
|
||||
value += evaluateHazardRemoval(battleState, gamePlan)
|
||||
}
|
||||
|
||||
|
||||
val moveData = Moves.getByName(move.id)
|
||||
if (moveData != null && moveData.priority > 0) {
|
||||
value += evaluatePriorityMove(move, attacker, defender, battleState)
|
||||
}
|
||||
|
||||
|
||||
if (move.id.lowercase() in MoveData.selfRecoveryMoves) {
|
||||
value += evaluateRecoveryMove(attacker, battleState, gamePlan)
|
||||
}
|
||||
|
||||
|
||||
if (move.id.lowercase() in MoveData.pivotMoves) {
|
||||
value += evaluatePivotMove(move, attacker, defender, battleState)
|
||||
}
|
||||
|
||||
|
||||
if (battleState.gamePhase == GamePhase.ENDGAME) {
|
||||
value += evaluateEndgameMove(move, battleState)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
private fun calculateGamePlanAlignment(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): Double {
|
||||
val moveData = Moves.getByName(move.id) ?: return 0.0
|
||||
var alignment = 0.0
|
||||
val normalizedWeather = AiBattleUtils.normalizeWeatherId(battleState.weather)
|
||||
|
||||
|
||||
if (move.id.lowercase() in gamePlan.riskyMoves) {
|
||||
alignment -= 100.0
|
||||
}
|
||||
|
||||
|
||||
alignment += when (gamePlan.winCondition) {
|
||||
WinCondition.RAIN_OFFENSE -> {
|
||||
var score = 0.0
|
||||
if (move.id.lowercase() == "raindance") score += 200.0
|
||||
if (normalizedWeather == "raindance") {
|
||||
if (moveData.elementalType.name.lowercase() == "water") score += 50.0
|
||||
if (move.id.lowercase() in listOf("thunder", "hurricane")) score += 100.0
|
||||
}
|
||||
score
|
||||
}
|
||||
|
||||
WinCondition.SUN_OFFENSE -> {
|
||||
var score = 0.0
|
||||
if (move.id.lowercase() == "sunnyday") score += 200.0
|
||||
if (normalizedWeather == "sunnyday") {
|
||||
if (moveData.elementalType.name.lowercase() == "fire") score += 50.0
|
||||
if (move.id.lowercase() == "solarbeam") score += 100.0
|
||||
}
|
||||
score
|
||||
}
|
||||
|
||||
WinCondition.SETUP_SWEEP -> {
|
||||
var score = 0.0
|
||||
if (move.id.lowercase() in MoveData.boostFromMoves.keys &&
|
||||
attacker.species in gamePlan.keyPokemon
|
||||
) {
|
||||
score += 150.0
|
||||
}
|
||||
if (attacker.boosts.values.any { it >= 2 } && moveData.power > 80) {
|
||||
score += 100.0
|
||||
}
|
||||
score
|
||||
}
|
||||
|
||||
WinCondition.AGGRESSIVE_SWEEP -> {
|
||||
var score = 0.0
|
||||
if (moveData.power >= 90) score += 60.0
|
||||
if (moveData.priority > 0 && defender.currentHpPercent < 0.5) score += 80.0
|
||||
score
|
||||
}
|
||||
|
||||
WinCondition.DEFENSIVE_STALL -> {
|
||||
var score = 0.0
|
||||
if (MoveData.statusMoves.keys.any { it?.name?.lowercase() == move.id.lowercase() }) score += 70.0
|
||||
if (move.id.lowercase() in MoveData.selfRecoveryMoves) score += 60.0
|
||||
if (moveData.power > 80) score -= 30.0
|
||||
score
|
||||
}
|
||||
|
||||
WinCondition.HAZARD_CONTROL -> {
|
||||
var score = 0.0
|
||||
if (move.id.lowercase() == "stealthrock" && "stealthrock" !in battleState.playerHazards) {
|
||||
score += 250.0
|
||||
}
|
||||
if (move.id.lowercase() in MoveData.entryHazards) score += 100.0
|
||||
score
|
||||
}
|
||||
|
||||
WinCondition.REVENGE_KILLING -> {
|
||||
var score = 0.0
|
||||
if (moveData.priority > 0) score += 120.0
|
||||
if (defender.currentHp > 50 && moveData.power >= 40 && moveData.power <= 80) score += 40.0
|
||||
score
|
||||
}
|
||||
|
||||
else -> 0.0
|
||||
}
|
||||
|
||||
|
||||
if (moveData.power > 0) {
|
||||
alignment += moveData.power * gamePlan.aggressionLevel * 0.3
|
||||
} else {
|
||||
alignment += (1.0 - gamePlan.aggressionLevel) * 30.0
|
||||
}
|
||||
|
||||
return alignment
|
||||
}
|
||||
|
||||
|
||||
private fun calculateRiskFactor(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): Double {
|
||||
val moveData = Moves.getByName(move.id) ?: return 1.0
|
||||
var risk = 1.0
|
||||
|
||||
|
||||
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
|
||||
risk += (1.0 - accuracy) * 0.5
|
||||
|
||||
|
||||
if (move.id.lowercase() in MoveData.recoilMoves) {
|
||||
risk += if (attacker.currentHpPercent < 0.5) 0.5 else 0.2
|
||||
}
|
||||
|
||||
|
||||
if (move.id.lowercase() in listOf("explosion", "selfdestruct", "memento", "healingwish")) {
|
||||
|
||||
if (attacker.species in gamePlan.keyPokemon) {
|
||||
risk += 2.0
|
||||
} else if (attacker.currentHpPercent < 0.3) {
|
||||
risk += 0.2
|
||||
} else {
|
||||
risk += 0.8
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (move.pp <= 1) {
|
||||
risk += 0.3
|
||||
}
|
||||
|
||||
|
||||
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
|
||||
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
|
||||
if (abs(mySpeed - oppSpeed) < 5) {
|
||||
risk += 0.1
|
||||
}
|
||||
|
||||
|
||||
val teamAdvantage = calculateTeamAdvantage(battleState)
|
||||
if (teamAdvantage > 0) {
|
||||
|
||||
risk *= 0.8
|
||||
} else if (teamAdvantage < -1) {
|
||||
|
||||
risk *= 1.2
|
||||
}
|
||||
|
||||
return max(0.1, risk)
|
||||
}
|
||||
|
||||
|
||||
private fun calculateTurnAdvantage(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState
|
||||
): Double {
|
||||
val moveData = Moves.getByName(move.id) ?: return 0.0
|
||||
var advantage = 0.0
|
||||
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
|
||||
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
|
||||
if (mySpeed > oppSpeed) advantage += 15.0
|
||||
if (moveData.priority > 0) advantage += moveData.priority * 25.0
|
||||
if (battleState.gamePhase == GamePhase.ENDGAME && moveData.power > 90) {
|
||||
advantage += 20.0
|
||||
}
|
||||
return advantage
|
||||
}
|
||||
|
||||
|
||||
private fun evaluateWeatherSetting(move: InBattleMove, gamePlan: GamePlan, battleState: BattleState): Double {
|
||||
val moveName = move.id.lowercase()
|
||||
val weatherType = MoveData.weatherMoves[moveName] ?: return 0.0
|
||||
|
||||
return when (gamePlan.winCondition) {
|
||||
WinCondition.RAIN_OFFENSE -> if (weatherType == "rain") 300.0 else 0.0
|
||||
WinCondition.SUN_OFFENSE -> if (weatherType == "sun") 300.0 else 0.0
|
||||
WinCondition.SAND_OFFENSE -> if (weatherType == "sand") 300.0 else 0.0
|
||||
WinCondition.WEATHER_CONTROL -> 150.0
|
||||
else -> 30.0
|
||||
}
|
||||
}
|
||||
|
||||
private fun evaluateSetupMove(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): Double {
|
||||
val boosts = MoveData.boostFromMoves[move.id.lowercase()] ?: return 0.0
|
||||
var value = 0.0
|
||||
val totalBoosts = boosts.values.sum()
|
||||
value += totalBoosts * 40.0 * gamePlan.setupPriority
|
||||
val relevantBoosts = boosts.keys.mapNotNull { stat -> attacker.boosts[stat] }
|
||||
if (relevantBoosts.any { it >= 3 }) value *= 0.2
|
||||
val predictedMoves = opponentPredictor.predictMoveHeuristically(defender, attacker)
|
||||
val dangerLevel = predictedMoves.count { (moveName, _) ->
|
||||
(Moves.getByName(moveName)?.power ?: 0.0) > 80
|
||||
}
|
||||
if (dangerLevel > 0 && attacker.currentHpPercent < 0.7) {
|
||||
value *= 0.4
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private fun evaluateStatusMove(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): Double {
|
||||
|
||||
if (defender.pokemon?.status != null) return 0.0
|
||||
|
||||
return when (move.id.lowercase()) {
|
||||
"thunderwave", "stunspore", "glare" -> {
|
||||
|
||||
if (defender.predictedRole == PokemonRole.SWEEPER) 90.0 else 50.0
|
||||
}
|
||||
|
||||
"willowisp" -> {
|
||||
|
||||
if (defender.predictedRole in listOf(PokemonRole.WALLBREAKER, PokemonRole.SWEEPER)) {
|
||||
80.0
|
||||
} else 40.0
|
||||
}
|
||||
|
||||
"toxic" -> {
|
||||
|
||||
if (gamePlan.winCondition == WinCondition.DEFENSIVE_STALL) 100.0 else 60.0
|
||||
}
|
||||
|
||||
"spore", "sleeppowder" -> 120.0
|
||||
else -> 30.0
|
||||
}
|
||||
}
|
||||
|
||||
private fun evaluateHazardSetting(battleState: BattleState, gamePlan: GamePlan): Double {
|
||||
val remainingOpponents = battleState.playerTeam.count { it.currentHealth > 0 }
|
||||
if (remainingOpponents < 2) return 15.0
|
||||
|
||||
var value = remainingOpponents * 20.0
|
||||
if (gamePlan.winCondition == WinCondition.HAZARD_CONTROL) value *= 1.8
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private fun evaluateHazardRemoval(battleState: BattleState, gamePlan: GamePlan): Double {
|
||||
val myHazards = battleState.npcHazards.size
|
||||
if (myHazards == 0) return 5.0
|
||||
|
||||
var value = myHazards * 40.0
|
||||
val remainingTeam = battleState.npcTeam.count { it.currentHealth > 0 }
|
||||
if (remainingTeam >= 3) value *= 1.3
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private fun evaluatePriorityMove(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState
|
||||
): Double {
|
||||
val moveData = Moves.getByName(move.id) ?: return 0.0
|
||||
var value = moveData.priority * 20.0
|
||||
|
||||
|
||||
if (defender.currentHpPercent < 0.5) {
|
||||
val damage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
|
||||
if (damage >= defender.currentHp) value += 150.0
|
||||
}
|
||||
|
||||
|
||||
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
|
||||
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
|
||||
if (abs(mySpeed - oppSpeed) < 10) value += 50.0
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private fun evaluateRecoveryMove(
|
||||
attacker: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): Double {
|
||||
if (attacker.currentHpPercent > 0.8) return 5.0
|
||||
|
||||
var value = (1.0 - attacker.currentHpPercent) * 140.0
|
||||
|
||||
|
||||
if (gamePlan.winCondition == WinCondition.DEFENSIVE_STALL) value *= 1.5
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private fun evaluatePivotMove(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState
|
||||
): Double {
|
||||
var value = 40.0
|
||||
|
||||
|
||||
val damage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
|
||||
value += damage * 0.3
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private fun evaluateEndgameMove(move: InBattleMove, battleState: BattleState): Double {
|
||||
val moveData = Moves.getByName(move.id) ?: return 0.0
|
||||
var bonus = 0.0
|
||||
|
||||
|
||||
if (moveData.power > 0 && moveData.accuracy >= 90) {
|
||||
bonus += 35.0
|
||||
}
|
||||
|
||||
|
||||
if (moveData.accuracy < 80) {
|
||||
bonus -= 30.0
|
||||
}
|
||||
|
||||
|
||||
if (moveData.priority > 0) {
|
||||
bonus += 50.0
|
||||
}
|
||||
|
||||
return bonus
|
||||
}
|
||||
|
||||
private fun calculateTeamAdvantage(battleState: BattleState): Int {
|
||||
val npcAlive = battleState.npcTeam.count { it.currentHealth > 0 }
|
||||
val playerAlive = battleState.playerTeam.count { it.currentHealth > 0 }
|
||||
return npcAlive - playerAlive
|
||||
}
|
||||
|
||||
private fun getEffectiveStat(pokemon: Pokemon, stat: Stats): Double {
|
||||
val base = pokemon.species.baseStats[stat] ?: 0
|
||||
val iv = pokemon.ivs[stat] ?: 31
|
||||
val ev = pokemon.evs[stat] ?: 0
|
||||
val level = pokemon.level
|
||||
|
||||
return if (stat == Stats.HP) {
|
||||
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
|
||||
} else {
|
||||
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
|
||||
}
|
||||
}
|
||||
|
||||
fun evaluateMoveForDoubles(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
primaryTarget: TrackedPokemon,
|
||||
allOpponents: List<TrackedPokemon>,
|
||||
partner: TrackedPokemon?,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): MoveEvaluation {
|
||||
val moveData = Moves.getByName(move.id)
|
||||
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
|
||||
|
||||
if (isSpreadMove(moveData.target)) {
|
||||
return evaluateSpreadMove(move, attacker, allOpponents, partner, battleState, gamePlan)
|
||||
}
|
||||
|
||||
val baseEvaluation = evaluateMove(move, attacker, primaryTarget, battleState, gamePlan)
|
||||
|
||||
var targetingBonus = 0.0
|
||||
if (primaryTarget.threatLevel > 5) {
|
||||
targetingBonus += 20.0
|
||||
}
|
||||
|
||||
return MoveEvaluation(
|
||||
move = baseEvaluation.move,
|
||||
rawDamage = baseEvaluation.rawDamage,
|
||||
expectedDamage = baseEvaluation.expectedDamage,
|
||||
strategicValue = baseEvaluation.strategicValue + targetingBonus,
|
||||
riskFactor = baseEvaluation.riskFactor,
|
||||
priority = baseEvaluation.priority,
|
||||
turnAdvantage = baseEvaluation.turnAdvantage,
|
||||
gamePlanAlignment = baseEvaluation.gamePlanAlignment
|
||||
)
|
||||
}
|
||||
|
||||
private fun evaluateSpreadMove(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
allOpponents: List<TrackedPokemon>,
|
||||
partner: TrackedPokemon?,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): MoveEvaluation {
|
||||
val moveData = Moves.getByName(move.id)
|
||||
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
|
||||
|
||||
var totalDamage = 0.0
|
||||
var totalStrategicValue = 0.0
|
||||
val activeOpponents = allOpponents.filter { it.currentHp > 0 }
|
||||
val targetCount = activeOpponents.size + (if (hitsPartner(moveData.target) && partner != null) 1 else 0)
|
||||
|
||||
|
||||
for (opponent in activeOpponents) {
|
||||
val damage = damageCalculator.calculateDamage(
|
||||
move, attacker, opponent, battleState.weather,
|
||||
isSpreadMove = true,
|
||||
targetCount = targetCount
|
||||
)
|
||||
totalDamage += damage
|
||||
|
||||
|
||||
if (damage >= opponent.currentHp) {
|
||||
totalStrategicValue += 150.0
|
||||
}
|
||||
}
|
||||
|
||||
val multiTargetBonus = activeOpponents.size * 40.0
|
||||
totalStrategicValue += multiTargetBonus
|
||||
|
||||
var partnerPenalty = 0.0
|
||||
if (hitsPartner(moveData.target) && partner != null && partner.currentHp > 0) {
|
||||
val partnerDamage = damageCalculator.calculateDamage(
|
||||
move, attacker, partner, battleState.weather,
|
||||
isSpreadMove = true,
|
||||
targetCount = targetCount
|
||||
)
|
||||
|
||||
partnerPenalty = when {
|
||||
partnerDamage >= partner.currentHp -> -999999.0
|
||||
partner.species in gamePlan.keyPokemon -> -partnerDamage * 5.0
|
||||
partner.currentHpPercent < 0.3 -> -partnerDamage * 4.0
|
||||
else -> -partnerDamage * 2.0
|
||||
}
|
||||
|
||||
totalStrategicValue += partnerPenalty
|
||||
}
|
||||
|
||||
val accuracy =
|
||||
getEffectiveAccuracy(move, attacker, activeOpponents.firstOrNull() ?: TrackedPokemon(), battleState.weather)
|
||||
val expectedDamage = totalDamage * accuracy
|
||||
|
||||
val riskFactor = if (partnerPenalty < -1000) {
|
||||
999.0
|
||||
} else if (partnerPenalty < 0) {
|
||||
max(1.5, 1.0 + abs(partnerPenalty) / 100.0)
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
|
||||
val gamePlanAlignment = if (activeOpponents.isNotEmpty()) {
|
||||
calculateGamePlanAlignment(move, attacker, activeOpponents.first(), battleState, gamePlan)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
|
||||
return MoveEvaluation(
|
||||
move = move,
|
||||
rawDamage = totalDamage,
|
||||
expectedDamage = expectedDamage,
|
||||
strategicValue = totalStrategicValue,
|
||||
riskFactor = riskFactor,
|
||||
priority = moveData.priority,
|
||||
turnAdvantage = 0.0,
|
||||
gamePlanAlignment = gamePlanAlignment
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun isSpreadMove(target: MoveTarget): Boolean {
|
||||
return target in listOf(
|
||||
MoveTarget.allAdjacent,
|
||||
MoveTarget.allAdjacentFoes,
|
||||
MoveTarget.all
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun hitsPartner(target: MoveTarget): Boolean {
|
||||
return target == MoveTarget.allAdjacent
|
||||
}
|
||||
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
class OpponentPredictor {
|
||||
private val moveHistory = mutableMapOf<String, MutableList<String>>()
|
||||
private val switchHistory = mutableMapOf<String, MutableList<String>>()
|
||||
private val situationResponses = mutableMapOf<String, MutableMap<String, Int>>()
|
||||
private val aggressionLevel = mutableMapOf<String, Double>()
|
||||
private val setupTendency = mutableMapOf<String, Double>()
|
||||
private val switchFrequency = mutableMapOf<String, Double>()
|
||||
|
||||
|
||||
fun updateMoveHistory(pokemon: String, move: String, situation: String) {
|
||||
moveHistory.getOrPut(pokemon) { mutableListOf() }.add(move)
|
||||
situationResponses.getOrPut(situation) { mutableMapOf() }.merge(move, 1, Int::plus)
|
||||
val moveData = Moves.getByName(move)
|
||||
if (moveData != null) {
|
||||
val currentAggression = aggressionLevel[pokemon] ?: 0.5
|
||||
val newAggression = if (moveData.power > 0) {
|
||||
|
||||
currentAggression * 0.9 + 0.8 * 0.1
|
||||
} else {
|
||||
|
||||
currentAggression * 0.9 + 0.2 * 0.1
|
||||
}
|
||||
aggressionLevel[pokemon] = newAggression
|
||||
if (move in MoveData.boostFromMoves.keys) {
|
||||
val currentSetup = setupTendency[pokemon] ?: 0.3
|
||||
setupTendency[pokemon] = min(1.0, currentSetup + 0.15)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSwitchPattern(fromPokemon: String, toPokemon: String, reason: String) {
|
||||
switchHistory.getOrPut(fromPokemon) { mutableListOf() }.add(toPokemon)
|
||||
situationResponses.getOrPut("switch_$reason") { mutableMapOf() }.merge(toPokemon, 1, Int::plus)
|
||||
val totalActions = (moveHistory[fromPokemon]?.size ?: 0) + (switchHistory[fromPokemon]?.size ?: 0)
|
||||
if (totalActions > 0) {
|
||||
switchFrequency[fromPokemon] = (switchHistory[fromPokemon]?.size ?: 0).toDouble() / totalActions
|
||||
}
|
||||
}
|
||||
|
||||
fun predictNextMove(pokemon: String, situation: String): Map<String, Double> {
|
||||
val responses = situationResponses[situation] ?: return emptyMap()
|
||||
val total = responses.values.sum().toDouble()
|
||||
|
||||
return if (total > 0) {
|
||||
responses.mapValues { it.value / total }
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSwitchProbability(pokemon: String, situation: String): Double {
|
||||
val switches = switchHistory[pokemon]?.size ?: 0
|
||||
val totalActions = (moveHistory[pokemon]?.size ?: 0) + switches
|
||||
|
||||
|
||||
if (totalActions < 5) return 0.2
|
||||
|
||||
return min(0.8, switches.toDouble() / totalActions + 0.1)
|
||||
}
|
||||
|
||||
|
||||
private fun getAggressionLevel(pokemon: String): Double = aggressionLevel[pokemon] ?: 0.5
|
||||
private fun getSetupTendency(pokemon: String): Double = setupTendency[pokemon] ?: 0.3
|
||||
private fun getSwitchFrequency(pokemon: String): Double = switchFrequency[pokemon] ?: 0.2
|
||||
|
||||
fun predictMoveHeuristically(
|
||||
opponent: TrackedPokemon,
|
||||
myPokemon: TrackedPokemon
|
||||
): Map<String, Double> {
|
||||
val predictions = mutableMapOf<String, Double>()
|
||||
val aggression = getAggressionLevel(opponent.species ?: "unknown")
|
||||
for (moveName in opponent.revealedMoves) {
|
||||
val moveData = Moves.getByName(moveName) ?: continue
|
||||
var probability = 0.25
|
||||
|
||||
probability *= if (moveData.power > 0) {
|
||||
aggression + 0.3
|
||||
} else {
|
||||
(1.0 - aggression) + 0.3
|
||||
}
|
||||
|
||||
val myTypes = myPokemon.currentTypes ?: listOf()
|
||||
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(moveData.elementalType, myTypes)
|
||||
probability *= (0.5 + effectiveness)
|
||||
|
||||
predictions[moveName] = probability
|
||||
}
|
||||
|
||||
val total = predictions.values.sum()
|
||||
return if (total > 0) {
|
||||
predictions.mapValues { it.value / total }
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
fun predictSwitchTarget(
|
||||
fromPokemon: String,
|
||||
myActivePokemon: TrackedPokemon
|
||||
): Map<String, Double> {
|
||||
val pastSwitches = switchHistory[fromPokemon] ?: return emptyMap()
|
||||
|
||||
|
||||
val switchCounts = mutableMapOf<String, Int>()
|
||||
for (target in pastSwitches) {
|
||||
switchCounts.merge(target, 1, Int::plus)
|
||||
}
|
||||
|
||||
val total = switchCounts.values.sum().toDouble()
|
||||
return if (total > 0) {
|
||||
switchCounts.mapValues { it.value / total }
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun categorizePlaystyle(pokemon: String): OpponentPlaystyle {
|
||||
val aggression = getAggressionLevel(pokemon)
|
||||
val setupTend = getSetupTendency(pokemon)
|
||||
val switchFreq = getSwitchFrequency(pokemon)
|
||||
|
||||
return when {
|
||||
setupTend > 0.5 -> OpponentPlaystyle.SETUP_ORIENTED
|
||||
aggression > 0.7 && switchFreq < 0.3 -> OpponentPlaystyle.HYPER_OFFENSIVE
|
||||
switchFreq > 0.5 -> OpponentPlaystyle.MOMENTUM_BASED
|
||||
aggression < 0.4 -> OpponentPlaystyle.DEFENSIVE_STALL
|
||||
else -> OpponentPlaystyle.BALANCED
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun reset() {
|
||||
moveHistory.clear()
|
||||
switchHistory.clear()
|
||||
situationResponses.clear()
|
||||
aggressionLevel.clear()
|
||||
setupTendency.clear()
|
||||
switchFrequency.clear()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum class OpponentPlaystyle {
|
||||
HYPER_OFFENSIVE,
|
||||
SETUP_ORIENTED,
|
||||
MOMENTUM_BASED,
|
||||
DEFENSIVE_STALL,
|
||||
BALANCED
|
||||
}
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
class StrategyAnalyzer {
|
||||
fun analyzeTeamComposition(team: List<Pokemon>): GamePlan {
|
||||
val roles = team.map { identifyPokemonRole(it) }
|
||||
val sweepers = roles.count { it == PokemonRole.SWEEPER }
|
||||
val setupSweepers = roles.count { it == PokemonRole.SETUP_SWEEPER }
|
||||
val wallbreakers = roles.count { it == PokemonRole.WALLBREAKER }
|
||||
val supports = roles.count { it in listOf(PokemonRole.SUPPORT, PokemonRole.TANK) }
|
||||
val revengeKillers = roles.count { it == PokemonRole.REVENGE_KILLER }
|
||||
val weatherStrategy = identifyWeatherStrategy(team)
|
||||
if (weatherStrategy != null) {
|
||||
return createWeatherGamePlan(weatherStrategy, team)
|
||||
}
|
||||
val hazardStrategy = identifyHazardStrategy(team)
|
||||
if (hazardStrategy != null) {
|
||||
return createHazardGamePlan(team)
|
||||
}
|
||||
val winCondition = when {
|
||||
setupSweepers >= 2 && supports >= 2 -> WinCondition.SETUP_SWEEP
|
||||
sweepers + wallbreakers >= 4 -> WinCondition.AGGRESSIVE_SWEEP
|
||||
supports >= 3 -> WinCondition.DEFENSIVE_STALL
|
||||
revengeKillers >= 2 -> WinCondition.REVENGE_KILLING
|
||||
else -> WinCondition.BALANCED
|
||||
}
|
||||
return createGamePlan(winCondition, team, roles)
|
||||
}
|
||||
|
||||
|
||||
private fun identifyWeatherStrategy(team: List<Pokemon>): WinCondition? {
|
||||
val rainSetters = team.count { hasWeatherSettingMove(it, "rain") || hasWeatherSettingAbility(it, "rain") }
|
||||
val rainAbusers = team.count { hasWeatherAbility(it, "rain") || hasWeatherMoves(it, "rain") }
|
||||
val sunSetters = team.count { hasWeatherSettingMove(it, "sun") || hasWeatherSettingAbility(it, "sun") }
|
||||
val sunAbusers = team.count { hasWeatherAbility(it, "sun") || hasWeatherMoves(it, "sun") }
|
||||
val sandSetters = team.count { hasWeatherSettingMove(it, "sand") || hasWeatherSettingAbility(it, "sand") }
|
||||
val sandAbusers = team.count { hasWeatherAbility(it, "sand") }
|
||||
return when {
|
||||
rainSetters >= 1 && rainAbusers >= 2 -> WinCondition.RAIN_OFFENSE
|
||||
sunSetters >= 1 && sunAbusers >= 2 -> WinCondition.SUN_OFFENSE
|
||||
sandSetters >= 1 && sandAbusers >= 2 -> WinCondition.SAND_OFFENSE
|
||||
rainSetters >= 1 || sunSetters >= 1 || sandSetters >= 1 -> WinCondition.WEATHER_CONTROL
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun identifyHazardStrategy(team: List<Pokemon>): Boolean {
|
||||
val hazardSetters = team.count { pokemon ->
|
||||
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.entryHazards }
|
||||
}
|
||||
val hazardRemovers = team.count { pokemon ->
|
||||
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.antiHazardsMoves }
|
||||
}
|
||||
return hazardSetters >= 2 && hazardRemovers >= 1
|
||||
}
|
||||
private fun createWeatherGamePlan(winCondition: WinCondition, team: List<Pokemon>): GamePlan {
|
||||
val weatherType = when (winCondition) {
|
||||
WinCondition.RAIN_OFFENSE -> "rain"
|
||||
WinCondition.SUN_OFFENSE -> "sun"
|
||||
WinCondition.SAND_OFFENSE -> "sand"
|
||||
else -> "any"
|
||||
}
|
||||
val setters =
|
||||
team.filter { hasWeatherSettingMove(it, weatherType) || hasWeatherSettingAbility(it, weatherType) }
|
||||
val abusers = team.filter { hasWeatherAbility(it, weatherType) || hasWeatherMoves(it, weatherType) }
|
||||
val keyPokemon = (setters + abusers).distinct().map { it.species.name }
|
||||
val priorities = when (winCondition) {
|
||||
WinCondition.RAIN_OFFENSE -> listOf(
|
||||
"set_rain_immediately",
|
||||
"preserve_rain_setter",
|
||||
"sweep_with_rain_abusers",
|
||||
"use_perfect_accuracy_moves",
|
||||
"prevent_weather_changes"
|
||||
)
|
||||
WinCondition.SUN_OFFENSE -> listOf(
|
||||
"set_sun_immediately",
|
||||
"preserve_sun_setter",
|
||||
"sweep_with_chlorophyll",
|
||||
"use_solar_beam",
|
||||
"prevent_weather_changes"
|
||||
)
|
||||
WinCondition.SAND_OFFENSE -> listOf(
|
||||
"set_sandstorm",
|
||||
"abuse_sand_rush",
|
||||
"chip_damage_from_sand",
|
||||
"prevent_weather_changes"
|
||||
)
|
||||
else -> listOf("maintain_weather", "use_weather_abusers")
|
||||
}
|
||||
|
||||
val riskyMoves = listOf(
|
||||
"explosion",
|
||||
"selfdestruct",
|
||||
"memento"
|
||||
)
|
||||
|
||||
return GamePlan(
|
||||
winCondition = winCondition,
|
||||
keyPokemon = keyPokemon,
|
||||
priorities = priorities,
|
||||
riskyMoves = riskyMoves,
|
||||
aggressionLevel = 0.8,
|
||||
setupPriority = 0.9
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun createHazardGamePlan(team: List<Pokemon>): GamePlan {
|
||||
val hazardSetters = team.filter { pokemon ->
|
||||
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.entryHazards }
|
||||
}.map { it.species.name }
|
||||
|
||||
return GamePlan(
|
||||
winCondition = WinCondition.HAZARD_CONTROL,
|
||||
keyPokemon = hazardSetters,
|
||||
priorities = listOf(
|
||||
"set_stealth_rock_early",
|
||||
"stack_hazards_if_safe",
|
||||
"remove_opponent_hazards",
|
||||
"force_switches",
|
||||
"preserve_hazard_setters"
|
||||
),
|
||||
riskyMoves = listOf("explosion"),
|
||||
aggressionLevel = 0.6,
|
||||
setupPriority = 0.7
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun createGamePlan(winCondition: WinCondition, team: List<Pokemon>, roles: List<PokemonRole>): GamePlan {
|
||||
val keyPokemon = identifyKeyPokemon(winCondition, team, roles)
|
||||
|
||||
val priorities = when (winCondition) {
|
||||
WinCondition.SETUP_SWEEP -> listOf(
|
||||
"create_setup_opportunity",
|
||||
"execute_setup_moves",
|
||||
"sweep_after_setup",
|
||||
"preserve_setup_sweeper",
|
||||
"remove_priority_threats"
|
||||
)
|
||||
|
||||
WinCondition.AGGRESSIVE_SWEEP -> listOf(
|
||||
"maintain_offensive_pressure",
|
||||
"use_coverage_moves",
|
||||
"preserve_sweepers",
|
||||
"use_priority_to_finish",
|
||||
"momentum_over_setup"
|
||||
)
|
||||
|
||||
WinCondition.DEFENSIVE_STALL -> listOf(
|
||||
"spread_status_conditions",
|
||||
"use_recovery_moves",
|
||||
"set_up_hazards",
|
||||
"stall_out_threats",
|
||||
"remove_opponent_hazards"
|
||||
)
|
||||
|
||||
WinCondition.REVENGE_KILLING -> listOf(
|
||||
"preserve_revenge_killer",
|
||||
"use_priority_strategically",
|
||||
"chip_into_priority_range",
|
||||
"scout_with_protect",
|
||||
"maintain_momentum"
|
||||
)
|
||||
|
||||
WinCondition.HAZARD_CONTROL -> listOf(
|
||||
"set_stealth_rock_early",
|
||||
"stack_hazards_safely",
|
||||
"remove_opponent_hazards",
|
||||
"force_switches"
|
||||
)
|
||||
|
||||
else -> listOf(
|
||||
"maintain_balance",
|
||||
"adapt_to_opponent",
|
||||
"resource_management",
|
||||
"take_calculated_risks"
|
||||
)
|
||||
}
|
||||
|
||||
val riskyMoves = when (winCondition) {
|
||||
WinCondition.SETUP_SWEEP -> listOf("explosion", "memento", "healingwish", "lunardance")
|
||||
WinCondition.REVENGE_KILLING -> listOf("doubleedge", "bravebird", "flareblitz")
|
||||
WinCondition.DEFENSIVE_STALL -> MoveData.recoilMoves
|
||||
else -> listOf("explosion", "memento")
|
||||
}
|
||||
|
||||
val aggressionLevel = when (winCondition) {
|
||||
WinCondition.AGGRESSIVE_SWEEP -> 0.9
|
||||
WinCondition.SETUP_SWEEP -> 0.7
|
||||
WinCondition.REVENGE_KILLING -> 0.75
|
||||
WinCondition.DEFENSIVE_STALL -> 0.3
|
||||
else -> 0.6
|
||||
}
|
||||
|
||||
val setupPriority = when (winCondition) {
|
||||
WinCondition.SETUP_SWEEP -> 0.95
|
||||
WinCondition.DEFENSIVE_STALL -> 0.6
|
||||
else -> 0.5
|
||||
}
|
||||
|
||||
return GamePlan(
|
||||
winCondition = winCondition,
|
||||
keyPokemon = keyPokemon,
|
||||
priorities = priorities,
|
||||
riskyMoves = riskyMoves,
|
||||
aggressionLevel = aggressionLevel,
|
||||
setupPriority = setupPriority
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun identifyKeyPokemon(
|
||||
winCondition: WinCondition,
|
||||
team: List<Pokemon>,
|
||||
roles: List<PokemonRole>
|
||||
): List<String> {
|
||||
return when (winCondition) {
|
||||
WinCondition.SETUP_SWEEP -> {
|
||||
team.filterIndexed { index, _ -> roles[index] == PokemonRole.SETUP_SWEEPER }
|
||||
.map { it.species.name }
|
||||
}
|
||||
|
||||
WinCondition.AGGRESSIVE_SWEEP -> {
|
||||
team.filterIndexed { index, _ -> roles[index] in listOf(PokemonRole.SWEEPER, PokemonRole.WALLBREAKER) }
|
||||
.map { it.species.name }
|
||||
}
|
||||
|
||||
WinCondition.REVENGE_KILLING -> {
|
||||
team.filterIndexed { index, _ -> roles[index] == PokemonRole.REVENGE_KILLER }
|
||||
.map { it.species.name }
|
||||
}
|
||||
|
||||
WinCondition.DEFENSIVE_STALL -> {
|
||||
team.filterIndexed { index, _ -> roles[index] in listOf(PokemonRole.TANK, PokemonRole.SUPPORT) }
|
||||
.map { it.species.name }
|
||||
}
|
||||
|
||||
else -> team.take(2).map { it.species.name }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun identifyPokemonRole(pokemon: Pokemon): PokemonRole {
|
||||
val attack = pokemon.species.baseStats[Stats.ATTACK] ?: 0
|
||||
val spAttack = pokemon.species.baseStats[Stats.SPECIAL_ATTACK] ?: 0
|
||||
val defense = pokemon.species.baseStats[Stats.DEFENCE] ?: 0
|
||||
val spDefense = pokemon.species.baseStats[Stats.SPECIAL_DEFENCE] ?: 0
|
||||
val speed = pokemon.species.baseStats[Stats.SPEED] ?: 0
|
||||
val hp = pokemon.species.baseStats[Stats.HP] ?: 0
|
||||
|
||||
val maxAttack = max(attack, spAttack)
|
||||
val avgDefense = (defense + spDefense) / 2.0
|
||||
val bulk = hp + avgDefense
|
||||
|
||||
val hasSetupMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.boostFromMoves.keys }
|
||||
val hasPriorityMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.priorityMoves }
|
||||
val hasPivotMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.pivotMoves }
|
||||
val hasRecoveryMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.selfRecoveryMoves }
|
||||
|
||||
return when {
|
||||
hasPivotMoves -> PokemonRole.PIVOT
|
||||
hasSetupMoves && speed >= 90 && maxAttack >= 90 -> PokemonRole.SETUP_SWEEPER
|
||||
speed >= 110 && hasPriorityMoves -> PokemonRole.REVENGE_KILLER
|
||||
speed >= 100 && maxAttack >= 100 -> PokemonRole.SWEEPER
|
||||
maxAttack >= 120 -> PokemonRole.WALLBREAKER
|
||||
bulk >= 180 && maxAttack < 80 -> PokemonRole.TANK
|
||||
hasRecoveryMoves || hasSetupMoves -> PokemonRole.SUPPORT
|
||||
else -> PokemonRole.BALANCED
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun hasWeatherSettingMove(pokemon: Pokemon, weather: String): Boolean {
|
||||
val moves = pokemon.moveSet.getMoves().map { it.name.lowercase() }
|
||||
return when (weather) {
|
||||
"rain" -> "raindance" in moves
|
||||
"sun" -> "sunnyday" in moves
|
||||
"sand" -> "sandstorm" in moves
|
||||
else -> moves.any { it in MoveData.weatherMoves.keys }
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasWeatherSettingAbility(pokemon: Pokemon, weather: String): Boolean {
|
||||
val ability = pokemon.ability.name.lowercase()
|
||||
return when (weather) {
|
||||
"rain" -> ability == "drizzle"
|
||||
"sun" -> ability == "drought"
|
||||
"sand" -> ability == "sandstream"
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasWeatherAbility(pokemon: Pokemon, weather: String): Boolean {
|
||||
val ability = pokemon.ability.name.lowercase()
|
||||
return when (weather) {
|
||||
"rain" -> ability in listOf("swiftswim", "raindish", "dryskin")
|
||||
"sun" -> ability in listOf("chlorophyll", "leafguard", "flowergift", "solarpower")
|
||||
"sand" -> ability in listOf("sandrush", "sandveil", "sandforce")
|
||||
else -> MoveData.weatherAbilities[ability] == weather
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasWeatherMoves(pokemon: Pokemon, weather: String): Boolean {
|
||||
val moves = pokemon.moveSet.getMoves().map { it.name.lowercase() }
|
||||
return when (weather) {
|
||||
"rain" -> moves.any { it in listOf("thunder", "hurricane") }
|
||||
"sun" -> moves.any { it in listOf("solarbeam", "hydrosteam") }
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum class WinCondition {
|
||||
AGGRESSIVE_SWEEP,
|
||||
SETUP_SWEEP,
|
||||
DEFENSIVE_STALL,
|
||||
HAZARD_CONTROL,
|
||||
REVENGE_KILLING,
|
||||
BALANCED,
|
||||
RAIN_OFFENSE,
|
||||
SUN_OFFENSE,
|
||||
SAND_OFFENSE,
|
||||
WEATHER_CONTROL
|
||||
}
|
||||
|
||||
|
||||
data class GamePlan(
|
||||
val winCondition: WinCondition,
|
||||
val keyPokemon: List<String>,
|
||||
val priorities: List<String>,
|
||||
val riskyMoves: List<String>,
|
||||
val aggressionLevel: Double = 0.6,
|
||||
val setupPriority: Double = 0.5
|
||||
)
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
class SwitchDecisionMaker(
|
||||
private val damageCalculator: DamageCalculator,
|
||||
private val opponentPredictor: OpponentPredictor,
|
||||
private val strategyAnalyzer: StrategyAnalyzer
|
||||
) {
|
||||
|
||||
|
||||
fun shouldVoluntarilySwitch(
|
||||
myPokemon: TrackedPokemon,
|
||||
opponentPokemon: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan,
|
||||
availableSwitches: List<BattlePokemon>
|
||||
): Boolean {
|
||||
if (availableSwitches.isEmpty()) return false
|
||||
|
||||
|
||||
val currentMatchup = evaluateMatchup(myPokemon, opponentPokemon, battleState)
|
||||
|
||||
|
||||
val bestSwitchScore = availableSwitches.maxOfOrNull { switchOption ->
|
||||
evaluateSwitchOption(
|
||||
switchOption.effectedPokemon,
|
||||
opponentPokemon,
|
||||
battleState,
|
||||
gamePlan
|
||||
)
|
||||
} ?: 0.0
|
||||
|
||||
|
||||
val switchThreshold = 40.0
|
||||
if (bestSwitchScore > currentMatchup.overallScore + switchThreshold) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
val shouldSwitch = checkSwitchConditions(
|
||||
myPokemon,
|
||||
opponentPokemon,
|
||||
battleState,
|
||||
gamePlan,
|
||||
currentMatchup
|
||||
)
|
||||
|
||||
return shouldSwitch
|
||||
}
|
||||
|
||||
|
||||
fun findBestSwitch(
|
||||
availableSwitches: List<BattlePokemon>,
|
||||
opponentPokemon: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): BattlePokemon? {
|
||||
if (availableSwitches.isEmpty()) return null
|
||||
|
||||
return availableSwitches.maxByOrNull { switchOption ->
|
||||
evaluateSwitchOption(
|
||||
switchOption.effectedPokemon,
|
||||
opponentPokemon,
|
||||
battleState,
|
||||
gamePlan
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun evaluateSwitchOption(
|
||||
candidate: Pokemon,
|
||||
opponent: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): Double {
|
||||
var score = 0.0
|
||||
|
||||
|
||||
val candidateTracker = createTrackerPokemon(candidate)
|
||||
|
||||
|
||||
val matchup = evaluateMatchup(candidateTracker, opponent, battleState)
|
||||
score += matchup.overallScore
|
||||
|
||||
|
||||
val hpFactor = candidateTracker.currentHpPercent
|
||||
if (hpFactor < 0.5) {
|
||||
score -= (1.0 - hpFactor) * 40.0
|
||||
}
|
||||
|
||||
|
||||
if (gamePlan.keyPokemon.contains(candidate.species.name)) {
|
||||
score += 20.0
|
||||
}
|
||||
|
||||
|
||||
val mySpeed = getEffectiveStat(candidate, Stats.SPEED)
|
||||
val oppSpeed = opponent.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
|
||||
if (mySpeed > oppSpeed) score += 25.0
|
||||
|
||||
|
||||
val hazardDamage = calculateEntryHazardDamage(candidate, battleState.npcHazards)
|
||||
score -= hazardDamage * 0.5
|
||||
|
||||
|
||||
val role = strategyAnalyzer.identifyPokemonRole(candidate)
|
||||
score += evaluateRoleSynergy(role, gamePlan, battleState)
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
|
||||
private fun evaluateMatchup(
|
||||
myPokemon: TrackedPokemon,
|
||||
opponent: TrackedPokemon,
|
||||
battleState: BattleState
|
||||
): MatchupEvaluation {
|
||||
|
||||
val ourBestEffectiveness = calculateBestMoveEffectiveness(myPokemon, opponent)
|
||||
val offensiveAdvantage = (ourBestEffectiveness - 1.0) * 40.0
|
||||
|
||||
|
||||
val theirBestEffectiveness = calculateBestMoveEffectiveness(opponent, myPokemon)
|
||||
val defensiveAdvantage = -(theirBestEffectiveness - 1.0) * 35.0
|
||||
|
||||
|
||||
val mySpeed = myPokemon.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
|
||||
val oppSpeed = opponent.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
|
||||
val speedAdvantage = if (mySpeed > oppSpeed) 20.0 else -15.0
|
||||
|
||||
|
||||
val overallScore = offensiveAdvantage + defensiveAdvantage + speedAdvantage
|
||||
|
||||
|
||||
val recommendation = when {
|
||||
overallScore > 60 && myPokemon.currentHpPercent > 0.7 -> MatchupRecommendation.STAY_IN_AND_SETUP
|
||||
overallScore > 30 -> MatchupRecommendation.STAY_IN_AND_ATTACK
|
||||
overallScore > 0 -> MatchupRecommendation.CONSIDER_SWITCHING
|
||||
overallScore > -30 -> MatchupRecommendation.PIVOT_OUT
|
||||
else -> MatchupRecommendation.SWITCH_OUT
|
||||
}
|
||||
|
||||
return MatchupEvaluation(
|
||||
offensiveAdvantage = offensiveAdvantage,
|
||||
defensiveAdvantage = defensiveAdvantage,
|
||||
speedAdvantage = speedAdvantage,
|
||||
overallScore = overallScore,
|
||||
recommendation = recommendation
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun checkSwitchConditions(
|
||||
myPokemon: TrackedPokemon,
|
||||
opponent: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan,
|
||||
matchup: MatchupEvaluation
|
||||
): Boolean {
|
||||
|
||||
val theirBestEffectiveness = calculateBestMoveEffectiveness(opponent, myPokemon)
|
||||
if (theirBestEffectiveness >= 2.0 && myPokemon.currentHpPercent > 0.3) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
if (myPokemon.currentHpPercent < 0.3 && matchup.overallScore < 30) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
if (opponent.boosts.values.any { it >= 2 } && matchup.overallScore < 20) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
if (myPokemon.species in gamePlan.keyPokemon &&
|
||||
myPokemon.currentHpPercent < 0.5 &&
|
||||
theirBestEffectiveness > 1.0
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
private fun calculateBestMoveEffectiveness(
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon
|
||||
): Double {
|
||||
val moves = attacker.pokemon?.moveSet?.getMoves() ?: return 1.0
|
||||
return moves.maxOfOrNull { move ->
|
||||
TypeEffectiveness.calculateTypeEffectiveness(
|
||||
move.type,
|
||||
defender.currentTypes ?: listOf()
|
||||
)
|
||||
} ?: 1.0
|
||||
}
|
||||
|
||||
|
||||
private fun calculateEntryHazardDamage(pokemon: Pokemon, hazards: List<String>): Double {
|
||||
var damage = 0.0
|
||||
val maxHP = getEffectiveStat(pokemon, Stats.HP)
|
||||
|
||||
for (hazard in hazards) {
|
||||
when (hazard.lowercase()) {
|
||||
"stealthrock" -> {
|
||||
|
||||
val rockEffectiveness = pokemon.types.fold(1.0) { acc, type ->
|
||||
acc * TypeEffectiveness.getDamageMultiplier(
|
||||
com.cobblemon.mod.common.api.types.ElementalTypes.ROCK,
|
||||
type
|
||||
)
|
||||
}
|
||||
damage += maxHP * rockEffectiveness * 0.125
|
||||
}
|
||||
|
||||
"spikes" -> damage += maxHP * 0.125
|
||||
"toxicspikes" -> {
|
||||
|
||||
if (!pokemon.types.any { it.name.lowercase() in listOf("poison", "steel") }) {
|
||||
damage += maxHP * 0.15
|
||||
}
|
||||
}
|
||||
|
||||
"stickyweb" -> damage += 10.0
|
||||
}
|
||||
}
|
||||
|
||||
return damage
|
||||
}
|
||||
|
||||
|
||||
private fun evaluateRoleSynergy(
|
||||
role: PokemonRole,
|
||||
gamePlan: GamePlan,
|
||||
battleState: BattleState
|
||||
): Double {
|
||||
return when (gamePlan.winCondition) {
|
||||
WinCondition.SETUP_SWEEP -> {
|
||||
when (role) {
|
||||
PokemonRole.SETUP_SWEEPER -> 50.0
|
||||
PokemonRole.SUPPORT -> if (battleState.gamePhase == GamePhase.EARLY_GAME) 30.0 else 10.0
|
||||
else -> 0.0
|
||||
}
|
||||
}
|
||||
|
||||
WinCondition.AGGRESSIVE_SWEEP -> {
|
||||
when (role) {
|
||||
PokemonRole.SWEEPER, PokemonRole.WALLBREAKER -> 40.0
|
||||
PokemonRole.REVENGE_KILLER -> 30.0
|
||||
else -> 0.0
|
||||
}
|
||||
}
|
||||
|
||||
WinCondition.DEFENSIVE_STALL -> {
|
||||
when (role) {
|
||||
PokemonRole.TANK, PokemonRole.SUPPORT -> 45.0
|
||||
else -> -10.0
|
||||
}
|
||||
}
|
||||
|
||||
WinCondition.REVENGE_KILLING -> {
|
||||
when (role) {
|
||||
PokemonRole.REVENGE_KILLER -> 60.0
|
||||
PokemonRole.PIVOT -> 25.0
|
||||
else -> 0.0
|
||||
}
|
||||
}
|
||||
|
||||
WinCondition.RAIN_OFFENSE, WinCondition.SUN_OFFENSE, WinCondition.SAND_OFFENSE -> {
|
||||
|
||||
when (role) {
|
||||
PokemonRole.SWEEPER -> 35.0
|
||||
else -> 0.0
|
||||
}
|
||||
}
|
||||
|
||||
else -> 0.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun createTrackerPokemon(pokemon: Pokemon): TrackedPokemon {
|
||||
val tracker = TrackedPokemon(
|
||||
pokemon = pokemon,
|
||||
originalPokemon = pokemon,
|
||||
species = pokemon.species.name,
|
||||
currentHp = pokemon.currentHealth,
|
||||
maxHp = pokemon.maxHealth,
|
||||
currentHpPercent = pokemon.currentHealth.toDouble() / pokemon.maxHealth,
|
||||
currentTypes = pokemon.types.map { it.name.lowercase() }.toMutableList(),
|
||||
currentAbility = pokemon.ability.name,
|
||||
moves = pokemon.moveSet.getMoves(),
|
||||
predictedRole = strategyAnalyzer.identifyPokemonRole(pokemon),
|
||||
cachedAttack = getEffectiveStat(pokemon, Stats.ATTACK),
|
||||
cachedDefense = getEffectiveStat(pokemon, Stats.DEFENCE),
|
||||
cachedSpAttack = getEffectiveStat(pokemon, Stats.SPECIAL_ATTACK),
|
||||
cachedSpDefense = getEffectiveStat(pokemon, Stats.SPECIAL_DEFENCE),
|
||||
cachedSpeed = getEffectiveStat(pokemon, Stats.SPEED)
|
||||
)
|
||||
return tracker
|
||||
}
|
||||
|
||||
private fun getEffectiveStat(pokemon: Pokemon, stat: Stats): Double {
|
||||
val base = pokemon.species.baseStats[stat] ?: 0
|
||||
val iv = pokemon.ivs[stat] ?: 31
|
||||
val ev = pokemon.evs[stat] ?: 0
|
||||
val level = pokemon.level
|
||||
|
||||
return if (stat == Stats.HP) {
|
||||
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
|
||||
} else {
|
||||
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.types.ElementalType
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
|
||||
|
||||
object TypeEffectiveness {
|
||||
|
||||
|
||||
val typeEffectivenessMap: Map<ElementalType, Map<ElementalType, Double>> = mapOf(
|
||||
ElementalTypes.NORMAL to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 1.0,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 1.0,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 0.5,
|
||||
ElementalTypes.GHOST to 0.0,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 0.5,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.FIRE to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 0.5,
|
||||
ElementalTypes.WATER to 0.5,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 2.0,
|
||||
ElementalTypes.ICE to 2.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 2.0,
|
||||
ElementalTypes.ROCK to 0.5,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 0.5,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 2.0,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.WATER to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 2.0,
|
||||
ElementalTypes.WATER to 0.5,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 0.5,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 2.0,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 2.0,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 0.5,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 1.0,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.ELECTRIC to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 1.0,
|
||||
ElementalTypes.WATER to 2.0,
|
||||
ElementalTypes.ELECTRIC to 0.5,
|
||||
ElementalTypes.GRASS to 0.5,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 0.0,
|
||||
ElementalTypes.FLYING to 2.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 1.0,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 0.5,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 1.0,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.GRASS to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 0.5,
|
||||
ElementalTypes.WATER to 2.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 0.5,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 0.5,
|
||||
ElementalTypes.GROUND to 2.0,
|
||||
ElementalTypes.FLYING to 0.5,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 0.5,
|
||||
ElementalTypes.ROCK to 2.0,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 0.5,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 0.5,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.ICE to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 0.5,
|
||||
ElementalTypes.WATER to 0.5,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 2.0,
|
||||
ElementalTypes.ICE to 0.5,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 2.0,
|
||||
ElementalTypes.FLYING to 2.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 1.0,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 2.0,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 0.5,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.FIGHTING to mapOf(
|
||||
ElementalTypes.NORMAL to 2.0,
|
||||
ElementalTypes.FIRE to 1.0,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 1.0,
|
||||
ElementalTypes.ICE to 2.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 0.5,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 0.5,
|
||||
ElementalTypes.PSYCHIC to 0.5,
|
||||
ElementalTypes.BUG to 0.5,
|
||||
ElementalTypes.ROCK to 2.0,
|
||||
ElementalTypes.GHOST to 0.0,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 2.0,
|
||||
ElementalTypes.STEEL to 2.0,
|
||||
ElementalTypes.FAIRY to 0.5
|
||||
),
|
||||
ElementalTypes.POISON to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 1.0,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 2.0,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 0.5,
|
||||
ElementalTypes.GROUND to 0.5,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 0.5,
|
||||
ElementalTypes.GHOST to 0.5,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 0.0,
|
||||
ElementalTypes.FAIRY to 2.0
|
||||
),
|
||||
ElementalTypes.GROUND to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 2.0,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 2.0,
|
||||
ElementalTypes.GRASS to 0.5,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 2.0,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 0.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 0.5,
|
||||
ElementalTypes.ROCK to 2.0,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 2.0,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.FLYING to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 1.0,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 0.5,
|
||||
ElementalTypes.GRASS to 2.0,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 2.0,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 2.0,
|
||||
ElementalTypes.ROCK to 0.5,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 0.5,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.PSYCHIC to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 1.0,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 1.0,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 2.0,
|
||||
ElementalTypes.POISON to 2.0,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 0.5,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 1.0,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 0.0,
|
||||
ElementalTypes.STEEL to 0.5,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.BUG to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 0.5,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 2.0,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 0.5,
|
||||
ElementalTypes.POISON to 0.5,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 0.5,
|
||||
ElementalTypes.PSYCHIC to 2.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 1.0,
|
||||
ElementalTypes.GHOST to 0.5,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 2.0,
|
||||
ElementalTypes.STEEL to 0.5,
|
||||
ElementalTypes.FAIRY to 0.5
|
||||
),
|
||||
ElementalTypes.ROCK to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 2.0,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 1.0,
|
||||
ElementalTypes.ICE to 2.0,
|
||||
ElementalTypes.FIGHTING to 0.5,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 0.5,
|
||||
ElementalTypes.FLYING to 2.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 2.0,
|
||||
ElementalTypes.ROCK to 1.0,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 0.5,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.GHOST to mapOf(
|
||||
ElementalTypes.NORMAL to 0.0,
|
||||
ElementalTypes.FIRE to 1.0,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 1.0,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 2.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 1.0,
|
||||
ElementalTypes.GHOST to 2.0,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 0.5,
|
||||
ElementalTypes.STEEL to 1.0,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
),
|
||||
ElementalTypes.DRAGON to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 1.0,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 1.0,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 1.0,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 2.0,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 0.5,
|
||||
ElementalTypes.FAIRY to 0.0
|
||||
),
|
||||
ElementalTypes.DARK to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 1.0,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 1.0,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 0.5,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 2.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 1.0,
|
||||
ElementalTypes.GHOST to 2.0,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 0.5,
|
||||
ElementalTypes.STEEL to 1.0,
|
||||
ElementalTypes.FAIRY to 0.5
|
||||
),
|
||||
ElementalTypes.STEEL to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 0.5,
|
||||
ElementalTypes.WATER to 0.5,
|
||||
ElementalTypes.ELECTRIC to 0.5,
|
||||
ElementalTypes.GRASS to 1.0,
|
||||
ElementalTypes.ICE to 2.0,
|
||||
ElementalTypes.FIGHTING to 1.0,
|
||||
ElementalTypes.POISON to 1.0,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 2.0,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 1.0,
|
||||
ElementalTypes.DARK to 1.0,
|
||||
ElementalTypes.STEEL to 0.5,
|
||||
ElementalTypes.FAIRY to 2.0
|
||||
),
|
||||
ElementalTypes.FAIRY to mapOf(
|
||||
ElementalTypes.NORMAL to 1.0,
|
||||
ElementalTypes.FIRE to 0.5,
|
||||
ElementalTypes.WATER to 1.0,
|
||||
ElementalTypes.ELECTRIC to 1.0,
|
||||
ElementalTypes.GRASS to 1.0,
|
||||
ElementalTypes.ICE to 1.0,
|
||||
ElementalTypes.FIGHTING to 2.0,
|
||||
ElementalTypes.POISON to 0.5,
|
||||
ElementalTypes.GROUND to 1.0,
|
||||
ElementalTypes.FLYING to 1.0,
|
||||
ElementalTypes.PSYCHIC to 1.0,
|
||||
ElementalTypes.BUG to 1.0,
|
||||
ElementalTypes.ROCK to 1.0,
|
||||
ElementalTypes.GHOST to 1.0,
|
||||
ElementalTypes.DRAGON to 2.0,
|
||||
ElementalTypes.DARK to 2.0,
|
||||
ElementalTypes.STEEL to 0.5,
|
||||
ElementalTypes.FAIRY to 1.0
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
fun getDamageMultiplier(attackerType: ElementalType, defenderType: ElementalType): Double {
|
||||
return typeEffectivenessMap[attackerType]?.get(defenderType) ?: 1.0
|
||||
}
|
||||
|
||||
|
||||
fun calculateTypeEffectiveness(moveType: ElementalType, defenderTypes: List<String>): Double {
|
||||
var multiplier = 1.0
|
||||
for (typeString in defenderTypes) {
|
||||
val defenderType = ElementalTypes.get(typeString.lowercase())
|
||||
if (defenderType != null) {
|
||||
multiplier *= getDamageMultiplier(moveType, defenderType)
|
||||
}
|
||||
}
|
||||
return multiplier
|
||||
}
|
||||
|
||||
|
||||
fun getEffectivenessCategory(effectiveness: Double): EffectivenessCategory {
|
||||
return when {
|
||||
effectiveness == 0.0 -> EffectivenessCategory.IMMUNE
|
||||
effectiveness < 1.0 -> EffectivenessCategory.NOT_VERY_EFFECTIVE
|
||||
effectiveness == 1.0 -> EffectivenessCategory.NEUTRAL
|
||||
else -> EffectivenessCategory.SUPER_EFFECTIVE
|
||||
}
|
||||
}
|
||||
|
||||
enum class EffectivenessCategory {
|
||||
IMMUNE, NOT_VERY_EFFECTIVE, NEUTRAL, SUPER_EFFECTIVE
|
||||
}
|
||||
}
|
||||
-1
@@ -31,7 +31,6 @@ class WeatherInstruction(val message: BattleMessage): InterpreterInstruction {
|
||||
val weather = message.effectAt(0)?.id ?: return
|
||||
val source = message.battlePokemonFromOptional(battle)
|
||||
source?.let { ShowdownInterpreter.broadcastOptionalAbility(battle, message.effect(), source) }
|
||||
|
||||
battle.dispatchWaiting(1.5F) {
|
||||
val lang = when {
|
||||
message.hasOptionalArgument("upkeep") -> battleLang("weather.$weather.upkeep")
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.api.storage.party.PartyStore
|
||||
import com.cobblemon.mod.common.battles.ai.CompetitiveBattleAI
|
||||
import com.cobblemon.mod.common.battles.ai.StrongBattleAI
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
|
||||
@@ -30,7 +31,7 @@ class NPCBattleActor(
|
||||
) : AIBattleActor(
|
||||
gameId = npc.uuid,
|
||||
pokemonList = pokemonList.let { if (npc.npc.randomizePartyOrder) it.shuffled() else it },
|
||||
battleAI = StrongBattleAI(skill)
|
||||
battleAI = CompetitiveBattleAI(skill)
|
||||
), EntityBackedBattleActor<NPCEntity> {
|
||||
override val entity = npc
|
||||
override val type = ActorType.NPC
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AiBattleUtilsTest {
|
||||
@Test
|
||||
fun normalizesWeatherIdsToShowdownNames() {
|
||||
assertEquals("sunnyday", AiBattleUtils.normalizeWeatherId("sun"))
|
||||
assertEquals("sunnyday", AiBattleUtils.normalizeWeatherId("sunnyday"))
|
||||
assertEquals("raindance", AiBattleUtils.normalizeWeatherId("rain"))
|
||||
assertEquals("raindance", AiBattleUtils.normalizeWeatherId("raindance"))
|
||||
assertEquals("sandstorm", AiBattleUtils.normalizeWeatherId("sand"))
|
||||
assertEquals("snow", AiBattleUtils.normalizeWeatherId("snowscape"))
|
||||
assertEquals("hail", AiBattleUtils.normalizeWeatherId("hail"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun detectsStabCaseInsensitively() {
|
||||
val types = listOf("Fire", "Flying")
|
||||
assertTrue(AiBattleUtils.hasStab("fire", types))
|
||||
assertTrue(AiBattleUtils.hasStab("Flying", types))
|
||||
assertFalse(AiBattleUtils.hasStab("water", types))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user