157 lines
6.7 KiB
GDScript
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
|