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 = battle.random_pick(options) 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 _status_score(slot, move, targets) 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 func _status_score(slot : BattleSlot, move : MoveData, targets : Array[BattleSlot]) -> float: #valeur approximative d'une capacite de statut : toujours bien inferieure a une bonne attaque, #mais choisie quand elle apporte vraiment quelque chose (statut sur un adversaire sain, boost utile, soin) var score : float = 0.1 for t in targets: var enemy : bool = t.side != slot.side if move.ailment != "none" and enemy: var status : int = StatusRegistry.from_ailment(move.ailment, move) if status != -1: if t.mon.pokemon.status == PokemonInstance.Status.NONE and !StatusRegistry.is_immune(t.mon.get_types(), status): score += 12.0 elif move.ailment == "confusion" and !t.mon.has_volatile(&"confusion"): score += 8.0 for sc in move.stat_changes: var key : Variant = BattleMon.STAT_KEYS.get(sc.stat) if key == null: continue var stage : int = t.mon.get_stage(key) var helpful : bool = (sc.change > 0 and !enemy) or (sc.change < 0 and enemy) if helpful and absi(stage) < 2: score += 5.0 * absi(sc.change) elif !helpful: score -= 10.0 if move.healing > 0 and !enemy: var missing : float = 1.0 - float(t.mon.pokemon.current_hp) / float(t.mon.pokemon.get_max_hp()) score += 30.0 * missing return score