Tales-of-PKMN/game/battle/battle_ai.gd
2026-10-07 17:58:38 +02:00

157 lines
6.7 KiB
GDScript

class_name BattleAI
extends BattleController
#IA simple :
#- attaque : choisit la capacite et la cible qui font le plus de degats attendus
# (achever un adversaire est favorise, taper un allie est penalise)
#- changement volontaire : si son pokemon est tres desavantage (types) et a peu de PV, elle peut
# le remplacer par un pokemon du banc qui a un meilleur rapport de types face aux adversaires
var randomness : float = 0.15 #chance de jouer un coup au hasard, pour ne pas etre previsible
var can_switch : bool = true #false : ne change jamais volontairement (ex. un pokemon sauvage n'a de toute facon pas de banc)
var switch_chance : float = 0.6 #quand les conditions sont reunies, probabilite de vraiment changer
var hp_threshold : float = 0.4 #ne change que si ses PV sont sous ce ratio
var matchup_threshold : float = -1.0 #ne change que si le rapport de types est pire que cette valeur
var min_improvement : float = 1.0 #le remplacant doit etre meilleur d'au moins cette valeur
var min_replacement_hp : float = 0.3 #ne fait pas entrer un pokemon quasi K.O.
var switch_cooldown : int = 2 #tours minimum entre deux changements volontaires d'un meme emplacement
const KILL_SCORE : float = 20.0 #bonus de score d'une attaque qui acheve un adversaire (cf. _score)
var _claimed : Array[PokemonInstance] = [] #remplacants deja choisis ce tour (evite 2 emplacements sur le meme)
var _claimed_turn : int = -1
var _last_switch : Dictionary = {} #id d'emplacement -> dernier tour ou il a change volontairement
func choose_action(battle : Battle, slot : BattleSlot) -> BattleAction:
if _claimed_turn != battle.turn:
_claimed.clear()
_claimed_turn = battle.turn
var usable : Array[MoveSlot] = battle.get_usable_moves(slot.mon)
#options d'attaque = [MoveSlot, cible (BattleSlot ou null), score]
var options : Array = []
for move_slot in usable:
var move : MoveData = move_slot.move
var candidates : Array = [null]
if TargetResolver.needs_selection(battle, slot, move):
candidates = TargetResolver.get_selectable_targets(battle, slot, move)
for candidate in candidates:
options.append([move_slot, candidate, _score(battle, slot, move, candidate) + battle.rng.randf() * 0.01])
var best : Array = []
for option in options:
if best.is_empty() or option[2] > best[2]: best = option
#changement volontaire ?
var best_score : float = best[2] if !best.is_empty() else -1.0
var swap : BattleAction = _consider_switch(battle, slot, best_score, usable.is_empty())
if swap != null: return swap
if options.is_empty(): return BattleAction.use_struggle(slot)
var chosen : Array = options.pick_random() if battle.rng.randf() < randomness else best
return BattleAction.use_move(slot, chosen[0], chosen[1])
func choose_replacement(battle : Battle, slot : BattleSlot) -> PokemonInstance:
#remplacement force (pokemon K.O.) : le meilleur rapport de types face aux adversaires
var options : Array[PokemonInstance] = battle.get_replacement_options(slot)
if options.is_empty(): return null
var opponents : Array[BattleSlot] = battle.get_opponents(slot)
var best : PokemonInstance = options[0]
var best_value : float = -INF
for p in options:
var value : float = _matchup(_types_of(p), opponents)
if value > best_value:
best = p
best_value = value
return best
#---------- changement volontaire ----------
func _consider_switch(battle : Battle, slot : BattleSlot, best_attack_score : float, no_usable_move : bool) -> BattleAction:
if !can_switch: return null
#remplacants possibles : pas deja choisis ce tour ; et, si le pokemon actuel n'a plus de capacite,
#changer n'a de sens que si le remplacant en a une (sinon on tournerait en rond sans jamais attaquer)
var bench : Array[PokemonInstance] = []
for p in battle.get_replacement_options(slot):
if _claimed.has(p): continue
if no_usable_move and !_has_usable_move(p): continue
bench.append(p)
if bench.is_empty(): return null
var opponents : Array[BattleSlot] = battle.get_opponents(slot)
var current : float = _matchup(slot.mon.get_types(), opponents)
if !no_usable_move:
if best_attack_score >= KILL_SCORE: return null #peut achever un adversaire : on attaque
var hp_ratio : float = float(slot.mon.pokemon.current_hp) / float(slot.mon.pokemon.get_max_hp())
if hp_ratio > hp_threshold or current > matchup_threshold: return null
if battle.turn - int(_last_switch.get(slot.id, -999)) < switch_cooldown: return null
if battle.rng.randf() > switch_chance: return null
var best : PokemonInstance = null
var best_value : float = -INF
for p in bench:
if !no_usable_move and float(p.current_hp) / float(p.get_max_hp()) < min_replacement_hp: continue
var value : float = _matchup(_types_of(p), opponents)
if value > best_value:
best = p
best_value = value
if best == null: return null
if !no_usable_move and best_value < current + min_improvement: return null
_claimed.append(best)
_last_switch[slot.id] = battle.turn
return BattleAction.switch_pokemon(slot, best)
func _matchup(my_types : Array[String], opponents : Array[BattleSlot]) -> float:
#rapport de types moyen face aux adversaires : (meilleure efficacite offensive) - (meilleure efficacite defensive subie)
#positif = avantage, negatif = desavantage
if opponents.is_empty(): return 0.0
var total : float = 0.0
for opponent in opponents:
var their_types : Array[String] = opponent.mon.get_types()
var offense : float = 0.0
for t in my_types:
offense = maxf(offense, TypeChart.get_multiplier(t, their_types))
var defense : float = 0.0
for t in their_types:
defense = maxf(defense, TypeChart.get_multiplier(t, my_types))
total += offense - defense
return total / opponents.size()
func _has_usable_move(pokemon : PokemonInstance) -> bool:
for ms in pokemon.active_moves:
if ms.has_pp(): return true
return false
func _types_of(pokemon : PokemonInstance) -> Array[String]:
var result : Array[String] = []
for t in pokemon.species.types:
result.append(TypeChart.type_name(t))
return result
#---------- attaque ----------
func _score(battle : Battle, slot : BattleSlot, move : MoveData, candidate : Variant) -> float:
var targets : Array[BattleSlot] = TargetResolver.resolve(battle, slot, move, candidate)
if targets.is_empty(): return -1.0
if move.power <= 0 or move.category == "status": return 0.1 #pas d'effet implemente : dernier recours
var accuracy : float = 1.0 if move.accuracy <= 0 else move.accuracy / 100.0
var spread : bool = targets.size() > 1
var score : float = 0.0
for t in targets:
var damage : int = DamageCalculator.estimate(slot.mon, t.mon, move, spread)
var remaining : int = t.mon.pokemon.current_hp
var value : float = float(mini(damage, remaining)) * accuracy
if t.side == slot.side:
score -= value
else:
score += value
if damage >= remaining: score += KILL_SCORE
return score