diff --git a/AI references/Marin's AI/AI_BattlerProjection.rb b/AI references/Marin's AI/AI_BattlerProjection.rb new file mode 100644 index 000000000..24806cb2e --- /dev/null +++ b/AI references/Marin's AI/AI_BattlerProjection.rb @@ -0,0 +1,1316 @@ +# TODO: +# Remove Minimize double damage from base damage calculator +# as it is already factored into tramplesMinimize? in the score handlers + +=begin +Console.setup_console + +Events.onTrainerPartyLoad += proc do |sender, e| + if e[0] # Trainer data should exist to be loaded, but may not exist somehow + trainer = e[0][0] # A PokeBattle_Trainer object of the loaded trainer + items = e[0][1] # An array of the trainer's items they can use + e[0][2] = [] + + p1 = pbGenPoke(:RAICHU, 55, trainer) + p1.moves = [ +# PBMove.new(getConst(PBMoves,:SWORDSDANCE)), + PBMove.new(getConst(PBMoves,:CHARGEBEAM)), + PBMove.new(getConst(PBMoves,:CRUNCH)), + PBMove.new(getConst(PBMoves,:DEFENSECURL)), + PBMove.new(getConst(PBMoves,:SLASH)), + ] + p1.abilityflag = 1 +# p1.item = getConst(PBItems,:POWERHERB) +# p1.status = PBStatuses::FROZEN +# p1.hp = p.totalhp.to_f / 10 * 3 + p1.hp = 1 + e[0][2] << p1 + +# p2 = pbGenPoke(:GOLEM, 55, trainer) +# p2.moves = p1.moves.map { |m| m.clone } +# p2.moves = [ +# PBMove.new(getConst(PBMoves,:TACKLE)), +# PBMove.new(getConst(PBMoves,:HEALPULSE)) +# ] +# e[0][2] << p2 + + e[0][2] << pbGenPoke(:LEAVANNY, 50, trainer) +# e[0][2] << pbGenPoke(:MISMAGIUS, 50, trainer) +# e[0][2] << pbGenPoke(:WALREIN, 53, trainer) +# e[0][2] << pbGenPoke(:TYPHLOSION, 50, trainer) +# e[0][2] << pbGenPoke(:GOLEM, 51, trainer) +# e[0][2] << pbGenPoke(:RAICHU, 52, trainer) + + items << PBItems::FULLRESTORE + end +end +=end + +class MKAI + # If true, the AI will always know the enemy's held item, even if it has not + # been revealed in normal gameplay. + AI_KNOWS_HELD_ITEMS = true + + # If true, the AI wil always know the enemy's ability, even if it has not + # been revealed in normal gameplay. + AI_KNOWS_UNSEEN_ABILITIES = true + + # If true, the AI will know the enemy's moves, even if they have not been + # revealed in normal gameplay. + AI_KNOWS_ENEMY_MOVES = true + + class BattlerProjection + attr_accessor :ai_index + attr_accessor :battler + attr_reader :pokemon + attr_reader :side + attr_reader :damage_taken + attr_reader :damage_dealt + attr_accessor :revealed_ability + attr_accessor :revealed_item + attr_accessor :used_moves + attr_reader :flags + + def initialize(side, pokemon, wild_pokemon = false) + @side = side + @pokemon = pokemon + @battler = nil + @ai = @side.ai + @battle = @ai.battle + @damage_taken = [] + @damage_dealt = [] + @ai_index = nil + @used_moves = [] + @revealed_ability = false + @revealed_item = false + @skill = wild_pokemon ? 0 : 200 + @flags = {} + end + + alias original_missing method_missing + def method_missing(name, *args, &block) + if @battler.respond_to?(name) + MKAI.log("WARNING: Deferring method `#{name}` to @battler.") + return @battler.send(name, *args, &block) + else + return original_missing(name, *args, &block) + end + end + + def opposing_side + return @side.opposing_side + end + + def index + return @side.index == 0 ? @ai_index * 2 : @ai_index * 2 + 1 + end + + def hp + return @battler.hp + end + + def fainted? + return @pokemon.fainted? + end + + def totalhp + return @battler.totalhp + end + + def status + return @battler.status + end + + def statusCount + return @battler.statusCount + end + + def burned? + return @battler.burned? + end + + def poisoned? + return @battler.poisoned? + end + + def paralyzed? + return @battler.paralyzed? + end + + def frozen? + return @battler.frozen? + end + + def asleep? + return @battler.asleep? + end + + def confused? + return @battler.effects[PBEffects::Confusion] > 0 + end + + def level + return @battler.level + end + + def active? + return !@battler.nil? + end + + def effective_attack + stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8] + stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2] + stage = @battler.stages[PBStats::ATTACK] + 6 + return (@battler.attack.to_f * stageMul[stage] / stageDiv[stage]).floor + end + + def effective_defense + stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8] + stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2] + stage = @battler.stages[PBStats::DEFENSE] + 6 + return (@battler.defense.to_f * stageMul[stage] / stageDiv[stage]).floor + end + + def effective_spatk + stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8] + stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2] + stage = @battler.stages[PBStats::SPATK] + 6 + return (@battler.spatk.to_f * stageMul[stage] / stageDiv[stage]).floor + end + + def effective_spdef + stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8] + stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2] + stage = @battler.stages[PBStats::SPDEF] + 6 + return (@battler.spdef.to_f * stageMul[stage] / stageDiv[stage]).floor + end + + def effective_speed + stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8] + stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2] + stage = @battler.stages[PBStats::SPEED] + 6 + return (@battler.speed.to_f * stageMul[stage] / stageDiv[stage]).floor + end + + def faster_than?(target) + return self.effective_speed >= target.effective_speed + end + + def has_non_volatile_status? + return burned? || poisoned? || paralyzed? || frozen? || asleep? + end + + # If this is true, this Pokémon will be treated as being a physical attacker. + # This means that the Pokémon will be more likely to try to use attack-boosting and + # defense-lowering status moves, and will be even more likely to use strong physical moves + # if any of these status boosts are active. + def is_physical_attacker? + stats = [effective_attack, effective_spatk] + avg = stats.sum / stats.size.to_f + min = (avg + (stats.max - avg) / 4 * 3).floor + avg = avg.floor + # min is the value the base attack must be above (3/4th avg) in order to for + # attack to be seen as a "high" value. + # Count the number of physical moves + physcount = 0 + attackBoosters = 0 + self.moves.each do |move| + next if move.pp == 0 + physcount += 1 if move.physicalMove? + if move.statUp + for i in 0...move.statUp.size / 2 + attackBoosters += move.statUp[i * 2 + 1] if move.statUp[i * 2] == PBStats::ATTACK + end + end + end + # If the user doesn't have any physical moves, the Pokémon can never be + # a physical attacker. + return false if physcount == 0 + if effective_attack >= min + # Has high attack stat + # All physical moves would be a solid bet since we have a high attack stat. + return true + elsif effective_attack >= avg + # Attack stat is not high, but still above average + # If this Pokémon has any attack-boosting moves, or more than 1 physical move, + # we consider this Pokémon capable of being a physical attacker. + return true if physcount > 1 + return true if attackBoosters >= 1 + end + return false + end + + # If this is true, this Pokémon will be treated as being a special attacker. + # This means that the Pokémon will be more likely to try to use spatk-boosting and + # spdef-lowering status moves, and will be even more likely to use strong special moves + # if any of these status boosts are active. + def is_special_attacker? + stats = [effective_attack, effective_spatk] + avg = stats.sum / stats.size.to_f + min = (avg + (stats.max - avg) / 4 * 3).floor + avg = avg.floor + # min is the value the base attack must be above (3/4th avg) in order to for + # attack to be seen as a "high" value. + # Count the number of physical moves + speccount = 0 + spatkBoosters = 0 + self.moves.each do |move| + next if move.pp == 0 + speccount += 1 if move.specialMove? + if move.statUp + for i in 0...move.statUp.size / 2 + spatkBoosters += move.statUp[i * 2 + 1] if move.statUp[i * 2] == PBStats::SPATK + end + end + end + # If the user doesn't have any physical moves, the Pokémon can never be + # a physical attacker. + return false if speccount == 0 + if effective_spatk >= min + # Has high spatk stat + # All special moves would be a solid bet since we have a high spatk stat. + return true + elsif effective_spatk >= avg + # Spatk stat is not high, but still above average + # If this Pokémon has any spatk-boosting moves, or more than 1 special move, + # we consider this Pokémon capable of being a special attacker. + return true if speccount > 1 + return true if spatkBoosters >= 1 + end + return false + end + + # Whether the pokemon should mega-evolve + def should_mega_evolve?(idx) + # Always mega evolve if the pokemon is able to + return @battle.pbCanMegaEvolve?(@battler.index) + end + + def choose_move + # An array of scores in the format of [move_index, score, target] + scores = [] + + # Calculates whether to use an item + item_score = get_item_score() + # Yields [score, item, target&] + scores << [:ITEM, *item_score] + + # Calculates whether to switch + switch_score = get_switch_score() + # Yields [score, pokemon_index] + scores << [:SWITCH, *switch_score] + + MKAI.log("=" * 10 + " Turn #{@battle.turnCount + 1} " + "=" * 10) + # Gets the battler projections of the opposing side + # Calculate a score for each possible target + + targets = opposing_side.battlers.clone + @side.battlers.each do |proj| + next if proj == self || proj.nil? + targets << proj + end + targets.each do |target| + next if target.nil? + MKAI.log("Moves for #{@battler.pokemon.name} against #{target.pokemon.name}") + # Calculate a score for all the user's moves + for i in 0...4 + move = @battler.moves[i] + if !move.nil? + next if move.pp <= 0 + target_type = move.pbTarget(@battler) + target_index = target.index + if PBTargets.noTargets?(target_type) + # If move has no targets, affects the user, a side or the whole field + target_index = -1 + else + next if !@battle.pbMoveCanTarget?(@battler.index, target.index, target_type) + end + # Get the move score given a user and a target + score = get_move_score(target, move) + next if score.nil? + score = 1 if score < 1 + scores << [i, score.round, target_index, target.pokemon.name] + end + end + end + + # If absolutely no good options exist + if scores.size == 0 + # Then just try to use the very first move with pp + for i in 0...4 + move = @battler.moves[i] + next if move.nil? + if move.pp > 0 + next if @battler.effects[PBEffects::DisableMove] == move.id + scores << [i, 1, 0, "internal"] + end + end + end + + # If we still don't have any options, that means we have no non-disabled moves with pp left, so we use Struggle. + if scores.size == 0 + # Struggle + #scores << [-1, 1000, 0, "internal"] + end + + # Map the numeric skill factor to a -4..1 range (not hard bounds) + skill = @skill / -50.0 + 1 + # Generate a random choice based on the skill factor and the score weights + idx = MKAI.weighted_factored_rand(skill, scores.map { |e| e[1] }) + str = "=" * 30 + "\nSkill: #{@skill}\n" + weights = MKAI.get_weights(skill, scores.map { |e| e[1] }) + total = weights.sum + scores.each_with_index do |e, i| + finalPerc = total == 0 ? 0 : (weights[i] / total.to_f * 100).round + if i == 0 + # Item + name = PBItems.getName(e[2]) + score = e[1] + if score > 0 + str += "ITEM #{name}: #{score} (=> #{finalPerc}%)" + str += " << CHOSEN" if idx == 0 + str += "\n" + end + elsif i == 1 + # Switch + name = @battle.pbParty(@battler.index)[e[2]].name + score = e[1] + if score > 0 + str += "SWITCH #{name}: #{score} (=> #{finalPerc}%)" + str += " << CHOSEN" if idx == 1 + str += "\n" + end + #elsif i == -1 + # str += "STRUGGLE: 100%" + else + move_index, score, target, target_name = e + name = @battler.moves[move_index].name + str += "MOVE(#{target_name}) #{name}: #{score} (=> #{finalPerc}%)" + str += " << CHOSEN" if i == idx + str += "\n" + end + end + str += "=" * 30 + MKAI.log(str) + if idx == 0 + # Index 0 means an item was chosen + ret = [:ITEM, scores[0][2]] + ret << scores[0][3] if scores[0][3] # Optional target + # TODO: Set to-be-healed flag so Heal Pulse doesn't also heal after healing by item + healing_item = scores[0][4] + if healing_item + self.flags[:will_be_healed] + end + return ret + elsif idx == 1 + # Index 1 means switching was chosen + return [:SWITCH, scores[1][2]] + end + # Return [move_index, move_target] + if idx + choice = scores[idx] + move = @battler.moves[choice[0]] + if ["15B", "0D5", "0D6", "0D7", "0D8", "0D9"].include?(move.function) + self.flags[:will_be_healed] = true + elsif move.function == "0DF" + target.flags[:will_be_healed] = true + elsif move.function == "0A1" + @side.flags[:will_luckychant] = true + elsif move.function == "0A2" + @side.flags[:will_reflect] = true + elsif move.function == "0A3" + @side.flags[:will_lightscreen] = true + elsif move.function == "051" + @side.flags[:will_haze] = true + end + return [choice[0], choice[2]] + end + # No choice could be made + # Caller will make sure Struggle is used + end + + def end_of_round + @flags = {} + end + + # Calculates the score of the move against a specific target + def get_move_score(target, move) + # The target variable is a projection of a battler. We know its species and HP, + # but its item, ability, moves and other properties are not known unless they are + # explicitly shown or mentioned. Knowing these properties can change what our AI + # chooses; if we know the item of our target projection, and it's an Air Balloon, + # we won't choose a Ground move, for instance. + if target.side == @side + # The target is an ally + return nil if move.function != "0DF" # Heal Pulse + # Move score calculation will only continue if the target is not an ally, + # or if it is an ally, then the move must be Heal Pulse (0DF). + end + if move.statusMove? + # Start status moves off with a score of 30. + # Since this makes status moves unlikely to be chosen when the other moves + # have a high base power, all status moves should ideally be addressed individually + # in this method, and used in the optimal scenario for each individual move. + score = 30 + MKAI.log("Test move #{move.name} (#{score})...") + # Trigger general score modifier code + score = MKAI::ScoreHandler.trigger_general(score, @ai, self, target, move) + # Trigger status-move score modifier code + score = MKAI::ScoreHandler.trigger_status_moves(score, @ai, self, target, move) + else + # Set the move score to the base power of the move + score = get_move_base_damage(move, target) + MKAI.log("Test move #{move.name} (#{score})...") + # Trigger general score modifier code + score = MKAI::ScoreHandler.trigger_general(score, @ai, self, target, move) + # Trigger damaging-move score modifier code + score = MKAI::ScoreHandler.trigger_damaging_moves(score, @ai, self, target, move) + end + # Trigger move-specific score modifier code + score = MKAI::ScoreHandler.trigger_move(move, score, @ai, self, target) + # Prefer a different move if this move would also hit the user's ally and it is super effective against the ally + # The target is not an ally to begin with (to exclude Heal Pulse and any other good ally-targeting moves) + if target.side != @side + # If the move is a status move, we can assume it has a positive effect and thus would be good for our ally too. + if !move.statusMove? + target_type = move.pbTarget(@battler) + # If the move also targets our ally + if target_type == PBTargets::AllNearOthers || target_type == PBTargets::AllBattlers || target_type == PBTargets::BothSides + # See if we have an ally + if ally = @side.battlers.find { |proj| proj && proj != self && !proj.fainted? } + matchup = ally.calculate_move_matchup(move.id) + # The move would be super effective on our ally + if matchup > 1 + decr = (matchup / 2.0 * 75.0).round + score -= decr + MKAI.log("- #{decr} for super effectiveness on ally battler") + end + end + end + end + end + # Take 10% of the final score if the target is immune to this move. + if !move.statusMove? && target_is_immune?(move, target) + score *= 0.1 + MKAI.log("* 0.1 for the target being immune") + end + # Take 10% of the final score if the move is disabled and thus unusable + if @battler.effects[PBEffects::DisableMove] == move.id + score *= 0.1 + MKAI.log("* 0.1 for the move being disabled") + end + MKAI.log("= #{score}") + return score + end + + # Calculates the best item to use and its score + def get_item_score + # Yields [score, item, optional_target, healing_item] + items = @battle.pbGetOwnerItems(@battler.index) + # Item categories + hpItems = { + PBItems::POTION => 20, + PBItems::SUPERPOTION => 50, + PBItems::HYPERPOTION => 200, + PBItems::MAXPOTION => -1, + PBItems::BERRYJUICE => 20, + PBItems::SWEETHEART => 20, + PBItems::FRESHWATER => 50, + PBItems::SODAPOP => 60, + PBItems::LEMONADE => 80, + PBItems::MOOMOOMILK => 100, + PBItems::ORANBERRY => 10, + PBItems::SITRUSBERRY => self.totalhp / 4, + PBItems::ENERGYPOWDER => 50, + PBItems::ENERGYROOT => 200, + PBItems::FULLRESTORE => -1, + } + hpItems[PBItems::RAGECANDYBAR] = 20 if !NEWEST_BATTLE_MECHANICS + singleStatusCuringItems = { + PBItems::AWAKENING => PBStatuses::SLEEP, + PBItems::CHESTOBERRY => PBStatuses::SLEEP, + PBItems::BLUEFLUTE => PBStatuses::SLEEP, + PBItems::ANTIDOTE => PBStatuses::POISON, + PBItems::PECHABERRY => PBStatuses::POISON, + PBItems::BURNHEAL => PBStatuses::BURN, + PBItems::RAWSTBERRY => PBStatuses::BURN, + PBItems::PARALYZEHEAL => PBStatuses::PARALYSIS, + PBItems::CHERIBERRY => PBStatuses::PARALYSIS, + PBItems::ICEHEAL => PBStatuses::FROZEN, + PBItems::ASPEARBERRY => PBStatuses::FROZEN + } + allStatusCuringItems = [ + PBItems::FULLRESTORE, + PBItems::FULLHEAL, + PBItems::LAVACOOKIE, + PBItems::OLDGATEAU, + PBItems::CASTELIACONE, + PBItems::LUMIOSEGALETTE, + PBItems::SHALOURSABLE, + PBItems::BIGMALASADA, + PBItems::LUMBERRY, + PBItems::HEALPOWDER + ] + xItems = { + PBItems::XATTACK => [PBStats::ATTACK, (NEWEST_BATTLE_MECHANICS) ? 2 : 1], + PBItems::XATTACK2 => [PBStats::ATTACK, 2], + PBItems::XATTACK3 => [PBStats::ATTACK, 3], + PBItems::XATTACK6 => [PBStats::ATTACK, 6], + PBItems::XDEFENSE => [PBStats::DEFENSE, (NEWEST_BATTLE_MECHANICS) ? 2 : 1], + PBItems::XDEFENSE2 => [PBStats::DEFENSE, 2], + PBItems::XDEFENSE3 => [PBStats::DEFENSE, 3], + PBItems::XDEFENSE6 => [PBStats::DEFENSE, 6], + PBItems::XSPATK => [PBStats::SPATK, (NEWEST_BATTLE_MECHANICS) ? 2 : 1], + PBItems::XSPATK2 => [PBStats::SPATK, 2], + PBItems::XSPATK3 => [PBStats::SPATK, 3], + PBItems::XSPATK6 => [PBStats::SPATK, 6], + PBItems::XSPDEF => [PBStats::SPDEF, (NEWEST_BATTLE_MECHANICS) ? 2 : 1], + PBItems::XSPDEF2 => [PBStats::SPDEF, 2], + PBItems::XSPDEF3 => [PBStats::SPDEF, 3], + PBItems::XSPDEF6 => [PBStats::SPDEF, 6], + PBItems::XSPEED => [PBStats::SPEED, (NEWEST_BATTLE_MECHANICS) ? 2 : 1], + PBItems::XSPEED2 => [PBStats::SPEED, 2], + PBItems::XSPEED3 => [PBStats::SPEED, 3], + PBItems::XSPEED6 => [PBStats::SPEED, 6], + PBItems::XACCURACY => [PBStats::ACCURACY, (NEWEST_BATTLE_MECHANICS) ? 2 : 1], + PBItems::XACCURACY2 => [PBStats::ACCURACY, 2], + PBItems::XACCURACY3 => [PBStats::ACCURACY, 3], + PBItems::XACCURACY6 => [PBStats::ACCURACY, 6] + } + scores = items.map do |item| + if item != PBItems::REVIVE && item != PBItems::MAXREVIVE + # Don't try to use the item if we can't use it on this Pokémon (e.g. due to Embargo) + next [0, item] if !@battle.pbCanUseItemOnPokemon?(item, @battler.pokemon, @battler, nil, false) + # Don't try to use the item if it doesn't have any effect, or some other condition that is not met + next [0, item] if !ItemHandlers.triggerCanUseInBattle(item, @battler.pokemon, @battler, nil, false, @battle, nil, false) + end + + score = 0 + # The item is a healing item + if hpToGain = hpItems[item] + hpLost = self.totalhp - self.hp + hpToGain = hpLost if hpToGain == -1 || hpToGain > hpLost + hpFraction = hpToGain / self.totalhp.to_f + # If hpFraction is high, then this item will heal almost all our HP. + # If it is low, then this item will heal very little of our total HP. + # We now factor the effectiveness of using this item into this fraction. + # Because using HP items at full health should not be an option, whereas + # using it at 1 HP should always be preferred. + itemEff = hpToGain / hpLost.to_f + itemEff = 0 if hpLost == 0 + delayEff = 1.0 + if !may_die_next_round? + # If we are likely to survive another hit of the last-used move, + # then we should discourage using healing items this turn because + # we can heal more if we use it later. + delayEff = 0.3 + else + # If we are likely to die next round, we have a choice to make. + # It can occur that the target is also a one-shot from this point, + # which will make move scores skyrocket which can mean we won't use our item. + # So, if we are slower than our opponent, we will likely die first without using + # our item and without using our move. So if this is the case, we dramatically increase + # the score of using our item. + last_dmg = last_damage_taken + if last_dmg && !self.faster_than?(last_dmg[0]) + delayEff = 2.5 + end + end + finalFrac = hpFraction * itemEff * delayEff + score = (finalFrac * 200).round + end + + # Single-status-curing items + if statusToCure = singleStatusCuringItems[item] + if self.status == statusToCure + factor = 1.0 + factor = 0.5 if statusToCure == PBStatuses::PARALYSIS # Paralysis is not that serious + factor = 1.5 if statusToCure == PBStatuses::BURN && self.is_physical_attacker? # Burned while physical attacking + factor = 2.0 if statusToCure == PBStatuses::POISON && self.statusCount > 0 # Toxic + score += (140 * factor).round + end + end + + # All-status-curing items + if allStatusCuringItems.include?(item) + if self.status != PBStatuses::NONE + factor = 1.0 + factor = 0.5 if self.status == PBStatuses::PARALYSIS # Paralysis is not that serious + factor = 1.5 if self.status == PBStatuses::BURN && self.is_physical_attacker? # Burned while physical attacking + factor = 2.0 if self.status == PBStatuses::POISON && self.statusCount > 0 # Toxic + score += (120 * factor).round + end + end + + # X-Items + if xStatus = xItems[item] + stat, increase = xStatus + # Only use X-Items on the battler's first turn + if @battler.turnCount == 0 + factor = 1.0 + factor = 2.0 if stat == PBStats::ATTACK && self.is_physical_attacker? || + stat == PBStats::SPATK && self.is_special_attacker? + score = (80 * factor * increase).round + end + end + + # Revive + if item == PBItems::REVIVE || item == PBItems::MAXREVIVE + party = @battle.pbParty(@battler.index) + candidate = nil + party.each do |pkmn| + if pkmn.fainted? + if candidate + if pkmn.level > candidate.level + candidate = pkmn + end + else + candidate = pkmn + end + end + end + if candidate + if items.include?(PBItems::MAXREVIVE) && item == PBItems::REVIVE + score = 200 + else + score = 400 + end + index = party.index(candidate) + next [score, item, index] + end + end + + next [score, item] + end + max_score = 0 + chosen_item = 0 + chosen_target = nil + scores.each do |score, item, target| + if score >= max_score + max_score = score + chosen_item = item + chosen_target = target + end + end + if chosen_item != 0 + return [max_score, chosen_item, chosen_target, !hpItems[chosen_item].nil?] if chosen_target + return [max_score, chosen_item, nil, !hpItems[chosen_item].nil?] + end + return [0, 0] + end + + # Calculates the best pokemon to switch to and its score + def get_switch_score + # Yields [score, pokemon_index] + switch = false + # Used to render Future Sight useless + switch_to_dark_type = false + # The AI's party + party = @battle.pbParty(@battler.index) + + # If the pokemon is struggling + if !@battle.pbCanChooseAnyMove?(@battler.index) + switch = true + end + # If the pokemon is perish songed and will die next turn + if self.effects[PBEffects::PerishSong] == 1 + switch = true + end + # Encored into bad move + if self.effects[PBEffects::Encore] > 0 + encored_move_index = @battler.pbEncoredMoveIndex + if encored_move_index >= 0 + encored_move = @battler.moves[encored_move_index] + if encored_move.statusMove? + switch = true + else + dmgs = @damage_dealt.select { |e| e[1] == encored_move.id } + if dmgs.size > 0 + last_dmg = dmgs[-1] + # Bad move if it did less than 35% damage + if last_dmg[3] < 0.35 + switch = true + end + else + # No record of dealing damage with this move, + # which probably means the target is immune somehow, + # or the user happened to miss. Don't risk being stuck in + # a bad move in any case, and switch. + switch = true + end + end + end + end + pos = @battle.positions[@battler.index] + # If Future Sight will hit at the end of the round + if pos.effects[PBEffects::FutureSightCounter] == 1 + # And if we have a dark type in our party + if party.any? { |pkmn| pkmn.types.include?(PBTypes::DARK) } + # We should switch to a dark type, + # but not if we're already close to dying anyway. + if !self.may_die_next_round? + switch = true + switch_to_dark_type = true + end + end + end + + # Get the optimal switch choice by type + scores = get_optimal_switch_choice + # If we should switch due to effects in battle + if switch + availscores = scores.select { |e| !e[2].fainted? } + # Switch to a dark type instead of the best type matchup + if switch_to_dark_type + availscores = availscores.select { |e| e[2].pokemon.types.include?(PBTypes::DARK) } + end + while availscores.size > 0 + hi_off_score, hi_def_score, proj = availscores[0] + eligible = true + eligible = false if proj.battler != nil # Already active + eligible = false if proj.pokemon.egg? # Egg + if eligible + score = (150 * hi_off_score * (switch_to_dark_type ? 2.0 : 1.0)).round + index = party.index(proj.pokemon) + return [score, index] + end + availscores.delete_at(0) + end + end + + curr_score = scores.find { |e| e[2] == self }[0] + # If the current battler is not very effective offensively in any of its types, + # then we see if there is a battler that is super effective in at least one of its types. + if curr_score < 1.0 + availscores = scores.select { |e| !e[2].fainted? } + while availscores.size > 0 + hi_off_score, hi_def_score, proj = availscores[0] + eligible = true + eligible = false if proj.battler != nil # Already active + eligible = false if proj.pokemon.egg? # Egg + if eligible && hi_off_score >= 1.0 + # Better choice than the current battler, so let's switch to this pokemon + score = (150 * hi_off_score).round + index = party.index(proj.pokemon) + return [score, index] + end + availscores.delete_at(0) + end + end + return [0, 0] + end + + def get_optimal_switch_choice + party = @battle.pbParty(self.index) + scores = party.map do |pkmn| + proj = @ai.pokemon_to_projection(pkmn) + if !proj + raise "No projection found for party member #{pkmn.name}" + end + offensive_score = 1.0 + defensive_score = 1.0 + self.opposing_side.battlers.each do |target| + next if target.nil? + offensive_score *= proj.get_offense_score(target) + defensive_score *= target.get_offense_score(proj) + end + next [offensive_score, defensive_score, proj] + end + scores.sort! do |a,b| + ret = (b[0] <=> a[0]) + next ret if ret != 0 + # Tie-breaker for pokemon with identical offensive effectiveness + # Prefer the one with the best defense against the targets + # Lower is better, so a <=> b instead of b <=> a to get ascending order + ret = (a[1] <=> b[1]) + next ret if ret != 0 + # Tie-breaker for pokemon with identical defensive effectiveness + next b[2].pokemon.level <=> a[2].pokemon.level + end + #MKAI.log(scores.map { |e| e[2].pokemon.name + ": (#{e[0]}, #{e[1]})" }.join("\n")) + return scores + end + + # Calculates adjusted base power of a move. + # Used as a starting point for a particular move's score against a target. + # Copied from Essentials. + def get_move_base_damage(move, target) + baseDmg = move.baseDamage + baseDmg = 60 if baseDmg == 1 + # Covers all function codes which have their own def pbBaseDamage + case move.function + when "010" # Stomp + baseDmg *= 2 if target.effects[PBEffects::Minimize] + # Sonic Boom, Dragon Rage, Super Fang, Night Shade, Endeavor + when "06A", "06B", "06C", "06D", "06E" + # Multiplied by 2 to favor the idea of guaranteed x damage to the target + baseDmg = move.pbFixedDamage(self,target) * 2 + when "06F" # Psywave + baseDmg = @battler.level + when "070" # OHKO + baseDmg = 200 + when "071", "072", "073" # Counter, Mirror Coat, Metal Burst + baseDmg = 60 + when "075", "076", "0D0", "12D" # Surf, Earthquake, Whirlpool, Shadow Storm + baseDmg = move.pbModifyDamage(baseDmg,@battler,target) + # Gust, Twister, Venoshock, Smelling Salts, Wake-Up Slap, Facade, Hex, Brine, + # Retaliate, Weather Ball, Return, Frustration, Eruption, Crush Grip, + # Stored Power, Punishment, Hidden Power, Fury Cutter, Echoed Voice, + # Trump Card, Flail, Electro Ball, Low Kick, Fling, Spit Up + when "077", "078", "07B", "07C", "07D", "07E", "07F", "080", "085", "087", + "089", "08A", "08B", "08C", "08E", "08F", "090", "091", "092", "097", + "098", "099", "09A", "0F7", "113" + baseDmg = move.pbBaseDamage(baseDmg,@battler,target) + when "086" # Acrobatics + baseDmg *= 2 if @battler.item == 0 || @battler.hasActiveItem?(:FLYINGGEM) + when "08D" # Gyro Ball + targetSpeed = target.effective_speed + userSpeed = self.effective_speed + baseDmg = [[(25 * targetSpeed / userSpeed).floor, 150].min,1].max + when "094" # Present + baseDmg = 50 + when "095" # Magnitude + baseDmg = 71 + baseDmg *= 2 if target.inTwoTurnAttack?("0CA") # Dig + when "096" # Natural Gift + baseDmg = move.pbNaturalGiftBaseDamage(@battler.item) + when "09B" # Heavy Slam + baseDmg = move.pbBaseDamage(baseDmg,@battler,target) + baseDmg *= 2 if NEWEST_BATTLE_MECHANICS && + target.effects[PBEffects::Minimize] + when "0A0", "0BD", "0BE" # Frost Breath, Double Kick, Twineedle + baseDmg *= 2 + when "0BF" # Triple Kick + baseDmg *= 6 # Hits do x1, x2, x3 baseDmg in turn, for x6 in total + when "0C0" # Fury Attack + if @battler.hasActiveAbility?(:SKILLLINK) + baseDmg *= 5 + else + baseDmg = (baseDmg * 19 / 6).floor # Average damage dealt + end + when "0C1" # Beat Up + mult = 0 + @battle.eachInTeamFromBattlerIndex(@battler.index) do |pkmn,_i| + mult += 1 if pkmn && pkmn.able? && pkmn.status == PBStatuses::NONE + end + baseDmg *= mult + when "0C4" # Solar Beam + baseDmg = move.pbBaseDamageMultiplier(baseDmg, @battler, target) + when "0D3" # Rollout + baseDmg *= 2 if @battler.effects[PBEffects::DefenseCurl] + when "0D4" # Bide + baseDmg = 40 + when "0E1" # Final Gambit + baseDmg = @battler.hp + when "144" # Flying Press + # Flying type is handled separately in the move effectiveness score multiplier + baseDmg *= 2 if target.effects[PBEffects::Minimize] + when "166" # Stomping Tantrum + baseDmg *= 2 if @battler.lastRoundMoveFailed + when "175" # Double Iron Bash + baseDmg *= 2 + baseDmg *= 2 if target.effects[PBEffects::Minimize] + end + return baseDmg + end + + # Determines if the target is immune to a move. + # Copied from Essentials. + def target_is_immune?(move, target) + type = move.pbCalcType(@battler) + typeMod = move.pbCalcTypeMod(type,@battler,target) + # Type effectiveness + return true if PBTypes.ineffective?(typeMod) + # Immunity due to ability/item/other effects + if isConst?(move.type, PBTypes,:GROUND) + return true if target.airborne? && !move.hitsFlyingTargets? + elsif isConst?(move.type, PBTypes,:FIRE) + return true if target.hasActiveAbility?(:FLASHFIRE) + elsif isConst?(move.type, PBTypes,:WATER) + return true if target.hasActiveAbility?([:DRYSKIN,:STORMDRAIN,:WATERABSORB]) + elsif isConst?(move.type, PBTypes,:GRASS) + return true if target.hasActiveAbility?(:SAPSIPPER) + elsif isConst?(move.type, PBTypes,:ELECTRIC) + return true if target.hasActiveAbility?([:LIGHTNINGROD,:MOTORDRIVE,:VOLTABSORB]) + end + return true if PBTypes.notVeryEffective?(typeMod) && + target.hasActiveAbility?(:WONDERGUARD) + return true if move.damagingMove? && @battler.index != target.index && !target.opposes?(@battler) && + target.hasActiveAbility?(:TELEPATHY) + return true if move.canMagicCoat? && target.hasActiveAbility?(:MAGICBOUNCE) && + target.opposes?(@battler) + return true if move.soundMove? && target.hasActiveAbility?(:SOUNDPROOF) + return true if move.bombMove? && target.hasActiveAbility?(:BULLETPROOF) + if move.powderMove? + return true if target.pbHasType?(:GRASS) + return true if target.hasActiveAbility?(:OVERCOAT) + return true if target.hasActiveItem?(:SAFETYGOGGLES) + end + return true if target.effects[PBEffects::Substitute]>0 && move.statusMove? && + !move.ignoresSubstitute?(@battler) && @battler.index != target.index + return true if NEWEST_BATTLE_MECHANICS && @battler.hasActiveAbility?(:PRANKSTER) && + target.pbHasType?(:DARK) && target.opposes?(@battler) + return true if move.priority > 0 && @battle.field.terrain == PBBattleTerrains::Psychic && + target.affected_by_terrain? && target.opposes?(@battler) + return false + end + + def get_move_accuracy(move, target) + return 100 if target.effects[PBEffects::Minimize] && move.tramplesMinimize?(1) + return 100 if target.effects[PBEffects::Telekinesis] > 0 + baseAcc = move.pbBaseAccuracy(@battler, target) + return 100 if baseAcc == 0 + return baseAcc + end + + def types(type3 = true) + return @battler.pbTypes(type3) if @battler + return @pokemon.types + end + alias pbTypes types + + def effects + return @battler.effects + end + + def stages + return @battler.stages + end + + def is_species?(species) + return @battler.isSpecies?(species) + end + alias isSpecies? is_species? + + def has_type?(type) + return @battler.pbHasType?(type) + end + alias pbHasType? has_type? + + def ability + return @battler.ability + end + + def has_ability?(ability) + return @battler.hasActiveAbility?(ability) && (AI_KNOWS_UNSEEN_ABILITIES || @revealed_ability) + end + alias hasActiveAbility? has_ability? + + def has_item?(item) + return @battler.hasActiveItem?(item) && (AI_KNOWS_HELD_ITEMS || @revealed_item) + end + alias hasActiveItem? has_item? + + def moves + if @battler.nil? + return @pokemon.moves + elsif AI_KNOWS_ENEMY_MOVES || @side.index == 0 + return @battler.moves + else + return @used_moves + end + end + + def opposes?(projection) + if projection.is_a?(BattlerProjection) + return @side.index != projection.side.move_index + else + return @battler.index % 2 != projection.index % 2 + end + end + + def own_side + return @side + end + alias pbOwnSide own_side + + def affected_by_terrain? + return @battler.affectedByTerrain? + end + alias affectedByTerrain? affected_by_terrain? + + def airborne? + return @battler.airborne? + end + + def semi_invulnerable? + return @battler.semiInvulnerable? + end + alias semiInvulnerable? semi_invulnerable? + + def in_two_turn_attack?(*args) + return @battler.inTwoTurnAttack?(*args) + end + alias inTwoTurnAttack? in_two_turn_attack? + + def can_attract?(target) + return @battler.pbCanAttract?(target) + end + alias pbCanAttract? can_attract? + + def takes_indirect_damage? + return @battler.takesIndirectDamage? + end + alias takesIndirectDamage? takes_indirect_damage? + + def weight + return @battler.pbWeight + end + alias pbWeight weight + + def can_sleep?(inflictor, move, ignore_status = false) + return @battler.pbCanSleep?(inflictor, false, move, ignore_status) + end + + def can_poison?(inflictor, move) + return @battler.pbCanPoison?(inflictor, false, move) + end + + def can_burn?(inflictor, move) + return @battler.pbCanBurn?(inflictor, false, move) + end + + def can_paralyze?(inflictor, move) + return @battler.pbCanParalyze?(inflictor, false, move) + end + + def can_freeze?(inflictor, move) + return @battler.pbCanFreeze?(inflictor, false, move) + end + + def register_damage_dealt(move, target, damage) + move = move.id if move.is_a?(PokeBattle_Move) + @damage_dealt << [target, move, damage, damage / target.totalhp.to_f] + end + + def register_damage_taken(move, user, damage) + user.used_moves << move if !user.used_moves.any? { |m| m.id == move.id } + move = move.id + @damage_taken << [user, move, damage, damage / @battler.totalhp.to_f] + end + + def get_damage_by_user(user) + return @damage_taken.select { |e| e[0] == user } + end + + def get_damage_by_user_and_move(user, move) + move = move.id if move.is_a?(PokeBattle_Move) + return @damage_taken.select { |e| e[0] == user && e[1] == move } + end + + def get_damage_by_move(move) + move = move.id if move.is_a?(PokeBattle_Move) + return @damage_taken.select { |e| e[1] == move } + end + + def last_damage_taken + return @damage_taken[-1] + end + + def last_damage_dealt + return @damage_dealt[-1] + end + + # Estimates how much HP the battler will lose from end-of-round effects, + # such as status conditions or trapping moves + def estimate_hp_difference_at_end_of_round + lost = 0 + # Future Sight + @battle.positions.each_with_index do |pos, idxPos| + next if !pos + # Ignore unless future sight hits at the end of the round + next if pos.effects[PBEffects::FutureSightCounter] != 1 + # And only if its target is this battler + next if @battle.battlers[idxPos] != @battler + # Find the user of the move + moveUser = nil + @battle.eachBattler do |b| + next if b.opposes?(pos.effects[PBEffects::FutureSightUserIndex]) + next if b.pokemonIndex != pos.effects[PBEffects::FutureSightUserPartyIndex] + moveUser = b + break + end + if !moveUser # User isn't in battle, get it from the party + party = @battle.pbParty(pos.effects[PBEffects::FutureSightUserIndex]) + pkmn = party[pos.effects[PBEffects::FutureSightUserPartyIndex]] + if pkmn && pkmn.able? + moveUser = PokeBattle_Battler.new(@battle, pos.effects[PBEffects::FutureSightUserIndex]) + moveUser.pbInitDummyPokemon(pkmn, pos.effects[PBEffects::FutureSightUserPartyIndex]) + end + end + if moveUser && moveUser.pokemon != @battler.pokemon + # We have our move user, and it's not targeting itself + move_id = pos.effects[PBEffects::FutureSightMove] + move = PokeBattle_Move.pbFromPBMove(@battle, PBMove.new(move_id)) + # Calculate how much damage a Future Sight hit will do + calcType = move.pbCalcType(moveUser) + @battler.damageState.typeMod = move.pbCalcTypeMod(calcType, moveUser, @battler) + move.pbCalcDamage(moveUser, @battler) + dmg = @battler.damageState.calcDamage + lost += dmg + end + end + if takes_indirect_damage? + # Sea of Fire (Fire Pledge + Grass Pledge) + weather = @battle.pbWeather + if side.effects[PBEffects::SeaOfFire] != 0 + unless weather == PBWeather::Rain || weather == PBWeather::HeavyRain || + has_type?(:FIRE) + lost += @battler.totalhp / 8.0 + end + end + # Leech Seed + if self.effects[PBEffects::LeechSeed] >= 0 + lost += @battler.totalhp / 8.0 + end + # Poison + if poisoned? && !has_ability?(:POISONHEAL) + dmg = statusCount == 0 ? @battler.totalhp / 8.0 : @battler.totalhp * self.effects[PBEffects::Toxic] / 16.0 + lost += dmg + end + # Burn + if burned? + lost += (NEWEST_BATTLE_MECHANICS ? @battler.totalhp / 16.0 : @battler.totalhp / 8.0) + end + # Sleep + Nightmare + if asleep? && self.effects[PBEffects::Nightmare] + lost += @battler.totalhp / 4.0 + end + # Curse + if self.effects[PBEffects::Curse] + lost += @battler.totalhp / 4.0 + end + # Trapping Effects + if self.effects[PBEffects::Trapping] != 0 + dmg = (NEWEST_BATTLE_MECHANICS ? b.totalhp / 8.0 : b.totalhp / 16.0) + if @battle.battlers[self.effects[PBEffects::TrappingUser]].hasActiveItem?(:BINDINGBAND) + dmg = (NEWEST_BATTLE_MECHANICS ? b.totalhp / 6.0 : b.totalhp / 8.0) + end + lost += dmg + end + end + return lost + end + + def may_die_next_round? + dmg = last_damage_taken + return false if dmg.nil? + # Returns true if the damage from the last move is more than the remaining hp + # This is used in determining if there is a point in using healing moves or items + hplost = dmg[2] + # We will also lose damage from status conditions and end-of-round effects like wrap, + # so we make a rough estimate with those included. + hplost += estimate_hp_difference_at_end_of_round + return hplost >= self.hp + end + + def took_more_than_x_damage?(x) + dmg = last_damage_taken + return false if dmg.nil? + # Returns true if the damage from the last move did more than (x*100)% of the total hp damage + return dmg[3] >= x + end + + # If the battler can survive another hit from the same move the target used last, + # but the battler will die if it does not heal, then healing is considered necessary. + def is_healing_necessary?(x) + return may_die_next_round? && !took_more_than_x_damage?(x) + end + + # Healing is pointless if the target did more damage last round than we can heal + def is_healing_pointless?(x) + return took_more_than_x_damage?(x) + end + + def discourage_making_contact_with?(target) + return false if has_ability?(:LONGREACH) + bad_abilities = [:WEAKARMOR, :STAMINA, :IRONBARBS, :ROUGHSKIN, :PERISHBODY] + return true if bad_abilities.any? { |a| target.has_ability?(a) } + return true if target.has_ability?(:CUTECHARM) && target.can_attract?(self) + return true if (target.has_ability?(:GOOEY) || target.has_ability?(:TANGLINGHAIR)) && faster_than?(target) + return true if target.has_item?(:ROCKYHELMET) + return true if target.has_ability?(:EFFECTSPORE) && !has_type?(:GRASS) && !has_ability?(:OVERCOAT) && !has_item?(:OVERCOAT) + return true if (target.has_ability?(:STATIC) || target.has_ability?(:POISONPOINT) || target.has_ability?(:FLAMEBODY)) && !has_non_volatile_status? + end + + def get_move_damage(target, move) + calcType = move.pbCalcType(@battler) + target.battler.damageState.typeMod = move.pbCalcTypeMod(calcType, @battler, target.battler) + move.pbCalcDamage(@battler, target.battler) + return target.battler.damageState.calcDamage + end + + # Calculates the combined type effectiveness of all user and target types + def calculate_type_matchup(target) + user_types = self.pbTypes(true) + target_types = target.pbTypes(true) + mod = 1.0 + user_types.each do |user_type| + target_types.each do |target_type| + user_eff = PBTypes.getEffectiveness(user_type, target_type) + mod *= user_eff / 2.0 + target_eff = PBTypes.getEffectiveness(target_type, user_type) + mod *= 2.0 / target_eff + end + end + return mod + end + + # Calculates the type effectiveness of a particular move against this user + def calculate_move_matchup(move_id) + move = PokeBattle_Move.pbFromPBMove(@ai.battle, PBMove.new(move_id)) + # Calculate the type this move would be if used by us + types = move.pbCalcType(@battler) + types = [types] if !types.is_a?(Array) + user_types = types + target_types = self.pbTypes(true) + mod = 1.0 + user_types.each do |user_type| + target_types.each do |target_type| + user_eff = PBTypes.getEffectiveness(user_type, target_type) + mod *= user_eff / 2.0 + end + end + return mod + end + + # Whether the type matchup between the user and target is favorable + def bad_against?(target) + return calculate_type_matchup(target) < 1.0 + end + + # Whether the user would be considered an underdog to the target. + # Considers type matchup and level + def underdog?(target) + return true if bad_against?(target) + return true if target.level >= self.level + 5 + return false + end + + def has_usable_move_type?(type) + return self.moves.any? { |m| m.type == type && m.pp > 0 } + end + + def get_offense_score(target) + # Note: self does not have a @battler value as it is a party member, i.e. only a PokeBattle_Pokemon object + # Return 1.0+ value if self is good against the target + user_types = self.pbTypes(true) + target_types = target.pbTypes(true) + max = 0 + user_types.each do |user_type| + next unless self.has_usable_move_type?(user_type) + mod = 1.0 + target_types.each do |target_type| + eff = PBTypes.getEffectiveness(user_type, target_type) / 2.0 + if eff >= 2.0 + mod *= eff + else + mod *= eff + end + end + max = mod if mod > max + end + return max + end + end +end \ No newline at end of file diff --git a/AI references/Marin's AI/AI_Compatibility.rb b/AI references/Marin's AI/AI_Compatibility.rb new file mode 100644 index 000000000..c20f36d73 --- /dev/null +++ b/AI references/Marin's AI/AI_Compatibility.rb @@ -0,0 +1,209 @@ +class PokeBattle_Battle + attr_reader :battleAI + + alias mkai_initialize initialize + def initialize(*args) + mkai_initialize(*args) + @battleAI = MKAI.new(self, self.wildBattle?) + @battleAI.sides[0].set_party(@party1) + @battleAI.sides[0].set_trainers(@player) + @battleAI.sides[1].set_party(@party2) + @battleAI.sides[1].set_trainers(@opponent) + end + + def pbRecallAndReplace(idxBattler, idxParty, batonPass = false) + if !@battlers[idxBattler].fainted? + @scene.pbRecall(idxBattler) + @battleAI.sides[idxBattler % 2].recall(idxBattler) + end + @battlers[idxBattler].pbAbilitiesOnSwitchOut # Inc. primordial weather check + @scene.pbShowPartyLineup(idxBattler & 1) if pbSideSize(idxBattler) == 1 + pbMessagesOnReplace(idxBattler, idxParty) + pbReplace(idxBattler, idxParty, batonPass) + end + + # Bug fix (used b instead of battler) + def pbMessageOnRecall(battler) + if battler.pbOwnedByPlayer? + if battler.hp<=battler.totalhp/4 + pbDisplayBrief(_INTL("Good job, {1}! Come back!",battler.name)) + elsif battler.hp<=battler.totalhp/2 + pbDisplayBrief(_INTL("OK, {1}! Come back!",battler.name)) + elsif battler.turnCount>=5 + pbDisplayBrief(_INTL("{1}, that’s enough! Come back!",battler.name)) + elsif battler.turnCount>=2 + pbDisplayBrief(_INTL("{1}, come back!",battler.name)) + else + pbDisplayBrief(_INTL("{1}, switch out! Come back!",battler.name)) + end + else + owner = pbGetOwnerName(battler.index) + pbDisplayBrief(_INTL("{1} withdrew {2}!",owner,battler.name)) + end + end + + alias mkai_pbEndOfRoundPhase pbEndOfRoundPhase + def pbEndOfRoundPhase + mkai_pbEndOfRoundPhase + @battleAI.end_of_round + end + + alias mkai_pbShowAbilitySplash pbShowAbilitySplash + def pbShowAbilitySplash(battler, delay = false, logTrigger = true) + mkai_pbShowAbilitySplash(battler, delay, logTrigger) + @battleAI.reveal_ability(battler) + end +end + +class PokeBattle_Move + attr_reader :statUp + attr_reader :statDown + + alias mkai_pbReduceDamage pbReduceDamage + def pbReduceDamage(user, target) + mkai_pbReduceDamage(user, target) + @battle.battleAI.register_damage(self, user, target, target.damageState.hpLost) + end + + def pbCouldBeCritical?(user, target) + return false if target.pbOwnSide.effects[PBEffects::LuckyChant] > 0 + # Set up the critical hit ratios + ratios = (NEWEST_BATTLE_MECHANICS) ? [24,8,2,1] : [16,8,4,3,2] + c = 0 + # Ability effects that alter critical hit rate + if c >= 0 && user.abilityActive? + c = BattleHandlers.triggerCriticalCalcUserAbility(user.ability, user, target, c) + end + if c >= 0 && target.abilityActive? && !@battle.moldBreaker + c = BattleHandlers.triggerCriticalCalcTargetAbility(target.ability, user, target, c) + end + # Item effects that alter critical hit rate + if c >= 0 && user.itemActive? + c = BattleHandlers.triggerCriticalCalcUserItem(user.item, user, target, c) + end + if c >= 0 && target.itemActive? + c = BattleHandlers.triggerCriticalCalcTargetItem(target.item, user, target, c) + end + return false if c < 0 + # Move-specific "always/never a critical hit" effects + return false if pbCritialOverride(user,target) == -1 + return true + end +end + +class MKAI + def pbAIRandom(x) + return rand(x) + end + + def pbDefaultChooseEnemyCommand(idxBattler) + sideIndex = idxBattler % 2 + index = MKAI.battler_to_proj_index(idxBattler) + side = @sides[sideIndex] + projection = side.battlers[index] + # Choose move + data = projection.choose_move + if data.nil? + # Struggle + @battle.pbAutoChooseMove(idxBattler) + elsif data[0] == :ITEM + # [:ITEM, item_id, target&] + item = data[1] + # Determine target of item (always the Pokémon choosing the action) + useType = pbGetItemData(item, ITEM_BATTLE_USE) + if data[2] + target_index = data[2] + else + target_index = idxBattler + if useType && (useType == 1 || useType == 6) # Use on Pokémon + target_index = @battle.battlers[target_index].pokemonIndex # Party Pokémon + end + end + # Register our item + @battle.pbRegisterItem(idxBattler, item, target_index) + elsif data[0] == :SWITCH + # [:SWITCH, pokemon_index] + @battle.pbRegisterSwitch(idxBattler, data[1]) + else + # [move_index, move_target] + move_index, move_target = data + # Mega evolve if we determine that we should + @battle.pbRegisterMegaEvolution(idxBattler) if projection.should_mega_evolve?(idxBattler) + # Register our move + @battle.pbRegisterMove(idxBattler, move_index, false) + # Register the move's target + @battle.pbRegisterTarget(idxBattler, move_target) + end + end + + + #============================================================================= + # Choose a replacement Pokémon + #============================================================================= + def pbDefaultChooseNewEnemy(idxBattler, party) + proj = self.battler_to_projection(@battle.battlers[idxBattler]) + scores = proj.get_optimal_switch_choice + scores.each do |_, _, proj| + pkmn = proj.pokemon + index = @battle.pbParty(idxBattler).index(pkmn) + if @battle.pbCanSwitchLax?(idxBattler, index) + return index + end + end + return -1 + end +end + +class PokeBattle_Battler + alias mkai_pbInitialize pbInitialize + def pbInitialize(pkmn, idxParty, batonPass = false) + mkai_pbInitialize(pkmn, idxParty, batonPass) + ai = @battle.battleAI + sideIndex = @index % 2 + ai.sides[sideIndex].send_out(@index, self) + end + + alias mkai_pbFaint pbFaint + def pbFaint(*args) + mkai_pbFaint(*args) + @battle.battleAI.faint_battler(self) + end +end + +class PokeBattle_PoisonMove + attr_reader :toxic +end + +class Array + def sum + n = 0 + self.each { |e| n += e } + n + end +end + +# Overwrite Frisk to show the enemy held item +BattleHandlers::AbilityOnSwitchIn.add(:FRISK, + proc { |ability,battler,battle| + foes = [] + battle.eachOtherSideBattler(battler.index) do |b| + foes.push(b) if b.item > 0 + end + if foes.length > 0 + battle.pbShowAbilitySplash(battler) + if NEWEST_BATTLE_MECHANICS + foes.each do |b| + battle.pbDisplay(_INTL("{1} frisked {2} and found its {3}!", + battler.pbThis, b.pbThis(true), PBItems.getName(b.item))) + battle.battleAI.reveal_item(b) + end + else + foe = foes[battle.pbRandom(foes.length)] + battle.pbDisplay(_INTL("{1} frisked the foe and found one {2}!", + battler.pbThis, PBItems.getName(foe.item))) + battle.battleAI.reveal_item(foe) + end + battle.pbHideAbilitySplash(battler) + end + } +) \ No newline at end of file diff --git a/AI references/Marin's AI/AI_Main.rb b/AI references/Marin's AI/AI_Main.rb new file mode 100644 index 000000000..dd5236eaa --- /dev/null +++ b/AI references/Marin's AI/AI_Main.rb @@ -0,0 +1,133 @@ +class MKAI + attr_reader :battle + attr_reader :sides + + def initialize(battle, wild_battle) + @battle = battle + @sides = [Side.new(self, 0), Side.new(self, 1, wild_battle)] + MKAI.log("AI initialized") + end + + def self.battler_to_proj_index(battlerIndex) + if battlerIndex % 2 == 0 # Player side: 0, 2, 4 -> 0, 1, 2 + return battlerIndex / 2 + else # Opponent side: 1, 3, 5 -> 0, 1, 2 + return (battlerIndex - 1) / 2 + end + end + + def self.weighted_rand(weights) + num = rand(weights.sum) + for i in 0...weights.size + if num < weights[i] + return i + else + num -= weights[i] + end + end + return nil + end + + def self.get_weights(factor, weights) + avg = weights.sum / weights.size.to_f + newweights = weights.map do |e| + diff = e - avg + next [0, ((e - diff * factor) * 100).round].max + end + return newweights + end + + def self.weighted_factored_rand(factor, weights) + avg = weights.sum / weights.size.to_f + newweights = weights.map do |e| + diff = e - avg + next [0, ((e - diff * factor) * 100).round].max + end + return weighted_rand(newweights) + end + + def self.log(msg) + echoln msg + end + + def battler_to_projection(battler) + @sides.each do |side| + side.battlers.each do |projection| + if projection && projection.pokemon == battler.pokemon + return projection + end + end + side.party.each do |projection| + if projection && projection.pokemon == battler.pokemon + return projection + end + end + end + return nil + end + + def pokemon_to_projection(pokemon) + @sides.each do |side| + side.battlers.each do |projection| + if projection && projection.pokemon == pokemon + return projection + end + end + side.party.each do |projection| + if projection && projection.pokemon == pokemon + return projection + end + end + end + return nil + end + + def register_damage(move, user, target, damage) + user = battler_to_projection(user) + target = battler_to_projection(target) + user.register_damage_dealt(move, target, damage) + target.register_damage_taken(move, user, damage) + end + + def faint_battler(battler) + # Remove the battler from the AI's list of the active battlers + @sides.each do |side| + side.battlers.each_with_index do |proj, index| + if proj && proj.battler == battler + # Decouple the projection from the battler + side.recall(battler.index) + side.battlers[index] = nil + break + end + end + end + end + + def end_of_round + @sides.each { |side| side.end_of_round } + end + + def reveal_ability(battler) + @sides.each do |side| + side.battlers.each do |proj| + if proj && proj.battler == battler && !proj.revealed_ability + proj.revealed_ability = true + MKAI.log("#{proj.pokemon.name}'s ability was revealed.") + break + end + end + end + end + + def reveal_item(battler) + @sides.each do |side| + side.battlers.each do |proj| + if proj.battler == battler && !proj.revealed_item + proj.revealed_item = true + MKAI.log("#{proj.pokemon.name}'s item was revealed.") + break + end + end + end + end +end \ No newline at end of file diff --git a/AI references/Marin's AI/AI_ScoreHandler.rb b/AI references/Marin's AI/AI_ScoreHandler.rb new file mode 100644 index 000000000..03b49c3de --- /dev/null +++ b/AI references/Marin's AI/AI_ScoreHandler.rb @@ -0,0 +1,1491 @@ +# Evasion when Foresighted + +# cont 120: self destruct + +class MKAI + class ScoreHandler + @@GeneralCode = [] + @@MoveCode = {} + @@StatusCode = [] + @@DamagingCode = [] + + def self.add_status(&code) + @@StatusCode << code + end + + def self.add_damaging(&code) + @@DamagingCode << code + end + + def self.add(*moves, &code) + if moves.size == 0 + @@GeneralCode << code + else + moves.each do |move| + if move.is_a?(Symbol) # Specific move + id = getConst(PBMoves, move) + raise "Invalid move #{move}" if id.nil? || id == 0 + @@MoveCode[id] = code + elsif move.is_a?(String) # Function code + @@MoveCode[move] = code + end + end + end + end + + def self.trigger(list, score, ai, user, target, move) + return score if list.nil? + list = [list] if !list.is_a?(Array) + list.each do |code| + next if code.nil? + newscore = code.call(score, ai, user, target, move) + score = newscore if newscore.is_a?(Numeric) + end + return score + end + + def self.trigger_general(score, ai, user, target, move) + return self.trigger(@@GeneralCode, score, ai, user, target, move) + end + + def self.trigger_status_moves(score, ai, user, target, move) + return self.trigger(@@StatusCode, score, ai, user, target, move) + end + + def self.trigger_damaging_moves(score, ai, user, target, move) + return self.trigger(@@DamagingCode, score, ai, user, target, move) + end + + def self.trigger_move(move, score, ai, user, target) + id = move.id + id = move.function if !@@MoveCode[id] + return self.trigger(@@MoveCode[id], score, ai, user, target, move) + end + end +end + + + +#=============================================================================# +# # +# Multipliers # +# # +#=============================================================================# + + +# Effectiveness modifier +# For this to have a more dramatic effect, this block could be moved lower down +# so that it factors in more score modifications before multiplying. +MKAI::ScoreHandler.add do |score, ai, user, target, move| + # Effectiveness doesn't add anything for fixed-damage moves. + next if move.is_a?(PokeBattle_FixedDamageMove) || move.statusMove? + # Add half the score times the effectiveness modifiers. Means super effective + # will be a 50% increase in score. + target_types = target.types + mod = move.pbCalcTypeMod(move.type, user, target) / PBTypeEffectiveness::NORMAL_EFFECTIVE.to_f + # If mod is 0, i.e. the target is immune to the move (based on type, at least), + # we do not multiply the score to 0, because immunity is handled as a final multiplier elsewhere. + if mod != 0 && mod != 1 + score *= mod + MKAI.log("* #{mod} for effectiveness") + end + next score +end + + + +#=============================================================================# +# # +# All Moves # +# # +#=============================================================================# + + +# Accuracy modifier to favor high-accuracy moves +MKAI::ScoreHandler.add do |score, ai, user, target, move| + next if user.battler == target.battler + accuracy = user.get_move_accuracy(move, target) + missing = 100 - accuracy + # (High) Jump Kick, a move that damages you when you miss + if move.function == "10B" + # Decrease the score more drastically if it has lower accuracy + missing *= 2.0 + end + if missing > 0 + score -= missing + MKAI.log("- #{missing} for accuracy") + end + next score +end + + +# Increase/decrease score for each positive/negative stat boost the move gives the user +MKAI::ScoreHandler.add do |score, ai, user, target, move| + next if !move.is_a?(PokeBattle_MultiStatUpMove) && !move.is_a?(PokeBattle_StatUpMove) && + !move.is_a?(PokeBattle_StatDownMove) + boosts = 0 + atkBoosts = 0 + spAtkBoosts = 0 + evBoosts = 0 + stats = [] + if move.statUp + for i in 0...move.statUp.size / 2 + stat = move.statUp[i * 2] + incr = move.statUp[i * 2 + 1] + boosts += incr + atkBoosts += incr if stat == PBStats::ATTACK + spAtkBoosts += incr if stat == PBStats::SPATK + evBoosts += incr if stat == PBStats::EVASION + stats << stat + end + end + if move.statDown + for i in 0...move.statDown.size / 2 + stat = move.statDown[i * 2] + decr = move.statDown[i * 2 + 1] + boosts -= decr if + atkBoosts -= decr if stat == PBStats::ATTACK + spAtkBoosts -= decr if stat == PBStats::SPATK + stats << stat if !stats.include?(stat) + end + end + # Increase score by 10 * (net stage differences) + # If attack is boosted and the user is a physical attacker, + # these stage increases are multiplied by 20 instead of 10. + if atkBoosts > 0 && user.is_physical_attacker? + atkIncr = (atkBoosts * 30 * (2 - (user.stages[PBStats::ATTACK] + 6) / 6.0)).round + if atkIncr > 0 + score += atkIncr + MKAI.log("+ #{atkIncr} for attack boost and being a physical attacker") + boosts -= atkBoosts + end + end + # If spatk is boosted and the user is a special attacker, + # these stage increases are multiplied by 20 instead of 10. + if spAtkBoosts > 0 && user.is_special_attacker? + spatkIncr = (spAtkBoosts * 30 * (2 - (user.stages[PBStats::SPATK] + 6) / 6.0)).round + if spatkIncr > 0 + score += spatkIncr + MKAI.log("+ #{spatkIncr} for spatk boost and being a special attacker") + boosts -= spAtkBoosts + end + end + # Boost to evasion + if evBoosts != 0 + evIncr = (evBoosts * 50 * (2 - (user.stages[PBStats::EVASION] + 6) / 6.0)).round + if evIncr > 0 + score += evIncr + MKAI.log("+ #{evIncr} for evasion boost") + boosts -= evBoosts + end + end + # All remaining stat increases (or decreases) are multiplied by 25 and added to the score. + if boosts != 0 + total = 6 * stats.size + eff = total + user.stages.each_with_index do |value, stage| + if stats.include?(stage) + eff -= value + end + end + fact = 1.0 + fact = eff / total.to_f if total != 0 + incr = (boosts * 25 * fact).round + if incr > 0 + score += incr + MKAI.log("+ #{incr} for general user buffs (#{eff}/#{total} effectiveness)") + end + end + next score +end + + +# Increase/decrease score for each positive/negative stat boost the move gives the target +MKAI::ScoreHandler.add do |score, ai, user, target, move| + next if !move.is_a?(PokeBattle_TargetStatDownMove) && !move.is_a?(PokeBattle_TargetMultiStatDownMove) + debuffs = 0 + accDecreases = 0 + stats = [] + if move.statDown + for i in 0...move.statDown.size / 2 + stat = move.statDown[i * 2] + decr = move.statDown[i * 2 + 1] + debuffs += decr + accDecreases += decr if stat == PBStats::ACCURACY + stats << stat if stat != PBStats::EVASION && stat != PBStats::ACCURACY + end + end + if accDecreases != 0 && target.stages[PBStats::ACCURACY] != -6 + accIncr = (accDecreases * 50 * (target.stages[PBStats::ACCURACY] + 6) / 6.0).round + score += accIncr + debuffs -= accIncr + MKAI.log("+ #{accIncr} for target accuracy debuff") + end + # All remaining stat decrases are multiplied by 10 and added to the score. + if debuffs > 0 + total = 6 * stats.size + eff = total + target.stages.each_with_index do |value, stage| + if stats.include?(stage) + eff += value + end + end + fact = 1.0 + fact = eff / total.to_f if total != 0 + incr = (debuffs * 25 * fact).round + score += incr + MKAI.log("+ #{incr} for general target debuffs (#{eff}/#{total} effectiveness)") + end + next score +end + + +# Prefer priority moves that deal enough damage to knock the target out. +# Use previous damage dealt to determine if it deals enough damage now, +# or make a rough estimate. +MKAI::ScoreHandler.add do |score, ai, user, target, move| + # Apply this logic only for priority moves + next if move.priority <= 0 || move.function == "0D4" # Bide + prevDmg = target.get_damage_by_user_and_move(user, move) + if prevDmg.size > 0 + # We have the previous damage this user has done with this move. + # Use the average of the previous damage dealt, and if it's more than the target's hp, + # we can likely use this move to knock out the target. + avg = (prevDmg.map { |e| e[2] }.sum / prevDmg.size.to_f).floor + if avg >= target.battler.hp + MKAI.log("+ 250 for priority move with average damage (#{avg}) >= target hp (#{target.battler.hp})") + score += 250 + end + else + # Calculate the damage this priority move will do. + # The AI kind of cheats here, because this takes all items, berries, abilities, etc. into account. + # It is worth for the effect though; the AI using a priority move to prevent + # you from using one last move before you faint. + dmg = user.get_move_damage(target, move) + if dmg >= target.battler.hp + MKAI.log("+ 250 for priority move with predicted damage (#{dmg}) >= target hp (#{target.battler.hp})") + score += 250 + end + end + next score +end + + +# Encourage using fixed-damage moves if the fixed damage is more than the target has HP +MKAI::ScoreHandler.add do |score, ai, user, target, move| + next if !move.is_a?(PokeBattle_FixedDamageMove) || move.function == "070" || move.function == "0D4" + dmg = move.pbFixedDamage(user, target) + if dmg >= target.hp + score += 175 + MKAI.log("+ 175 for this move's fixed damage being enough to knock out the target") + end + next score +end + + +# See if any moves used in the past did enough damage to now kill the target, +# and if so, give that move slightly more preference. +# There can be more powerful moves that might also take out the user, +# but if this move will also take the user out, this is a safer option. +MKAI::ScoreHandler.add do |score, ai, user, target, move| + next if move.function == "0D4" # Bide + # Get all times this move was used on the target + ary = target.get_damage_by_user_and_move(user, move) + # If this move has been used before, and the move is not a two-turn move + if ary.size > 0 && !move.chargingTurnMove? && move.function != "0C2" # Hyper Beam + # Calculate the average damage of every time this move was used on the target + avg = ary.map { |e| e[2] }.sum / ary.size.to_f + # If the average damage this move dealt is enough to kill the target, increase likelihood of choosing this move + if avg >= target.hp + score += 100 + MKAI.log("+ 100 for this move being likely to take out the target") + end + end + next score +end + + +# Prefer moves that are usable while the user is asleep +MKAI::ScoreHandler.add do |score, ai, user, target, move| + # If the move is usable while asleep, and if the user won't wake up this turn + # Kind of cheating, but insignificant. This way the user can choose a more powerful move instead + if move.usableWhenAsleep? + if user.asleep? && user.statusCount > 1 + score += 200 + MKAI.log("+ 200 for being able to use this move while asleep") + else + score -= 50 + MKAI.log("- 50 for this move will have no effect") + end + end + next score +end + + +# Prefer moves that can thaw the user if the user is frozen +MKAI::ScoreHandler.add do |score, ai, user, target, move| + # If the user is frozen and the move thaws the user + if user.frozen? && move.thawsUser? + score += 80 + MKAI.log("+ 80 for being able to thaw the user") + end + next score +end + + +# Discourage using OHKO moves if the target is higher level or it has sturdy +MKAI::ScoreHandler.add do |score, ai, user, target, move| + if move.function == "070" # OHKO Move + if target.has_ability?(:STURDY) + score -= 100 + MKAI.log("- 100 for the target has Sturdy") + end + if target.level > user.level + score -= 80 + MKAI.log("- 80 for the move will fail due to level difference") + end + score -= 50 + MKAI.log("- 50 for OHKO moves are generally considered bad") + end + next score +end + + +# Encourage using trapping moves, since they're generally weak +MKAI::ScoreHandler.add do |score, ai, user, target, move| + if move.function == "0CF" # Trapping Move + if target.effects[PBEffects::Trapping] == 0 # The target is not yet trapped + score += 60 + MKAI.log("+ 60 for initiating a multi-turn trap") + end + end + next score +end + + +# Encourage using flinching moves if the user is faster +MKAI::ScoreHandler.add do |score, ai, user, target, move| + if move.flinchingMove? && (user.faster_than?(target) || move.priority > 0) + score += 50 + MKAI.log("+ 50 for being able to flinch the target") + end + next score +end + + +# Discourage using a multi-hit physical move if the target has an item or ability +# that will damage the user on each contact. +# Also slightly discourages physical moves if the target has a bad ability in general. +MKAI::ScoreHandler.add do |score, ai, user, target, move| + if move.pbContactMove?(user) + if user.discourage_making_contact_with?(target) + if move.multiHitMove? + score -= 60 + MKAI.log("- 60 for the target has an item or ability that activates on each contact") + else + score -= 30 + MKAI.log("- 30 for the target has an item or ability that activates on contact") + end + end + end + next score +end + + +# Encourage using moves that can cause a burn. +MKAI::ScoreHandler.add do |score, ai, user, target, move| + if move.is_a?(PokeBattle_BurnMove) && !target.burned? && target.can_burn?(user, move) + chance = move.pbAdditionalEffectChance(user, target) + chance = 100 if chance == 0 + if chance > 0 && chance <= 100 + if target.is_physical_attacker? + add = 30 + chance * 2 + score += add + MKAI.log("+ #{add} for being able to burn the physical-attacking target") + else + score += chance + MKAI.log("+ #{chance} for being able to burn the target") + end + end + end + next score +end + + +# Encourage using moves that can cause freezing. +MKAI::ScoreHandler.add do |score, ai, user, target, move| + if move.is_a?(PokeBattle_FreezeMove) && !target.frozen? && target.can_freeze?(user, move) + chance = move.pbAdditionalEffectChance(user, target) + chance = 100 if chance == 0 + if chance > 0 && chance <= 100 + score += chance * 2 + MKAI.log("+ #{chance} for being able to freeze the target") + end + end + next score +end + + +# Encourage using moves that can cause paralysis. +MKAI::ScoreHandler.add do |score, ai, user, target, move| + if move.is_a?(PokeBattle_ParalysisMove) && !target.paralyzed? && target.can_paralyze?(user, move) + chance = move.pbAdditionalEffectChance(user, target) + chance = 100 if chance == 0 + if chance > 0 && chance <= 100 + score += chance + MKAI.log("+ #{chance} for being able to paralyze the target") + end + end + next score +end + + +# Encourage using moves that can cause sleep. +MKAI::ScoreHandler.add do |score, ai, user, target, move| + if move.is_a?(PokeBattle_SleepMove) && !target.asleep? && target.can_sleep?(user, move) + chance = move.pbAdditionalEffectChance(user, target) + chance = 100 if chance == 0 + if chance > 0 && chance <= 100 + score += chance + MKAI.log("+ #{chance} for being able to put the target to sleep") + end + end + next score +end + + +# Encourage using moves that can cause poison. +MKAI::ScoreHandler.add do |score, ai, user, target, move| + if move.is_a?(PokeBattle_PoisonMove) && !target.poisoned? && target.can_poison?(user, move) + chance = move.pbAdditionalEffectChance(user, target) + chance = 100 if chance == 0 + if chance > 0 && chance <= 100 + if move.toxic + add = chance * 1.4 * move.pbNumHits(user, [target]) + score += add + MKAI.log("+ #{add} for being able to badly poison the target") + else + add = chance * move.pbNumHits(user, [target]) + score += add + MKAI.log("+ #{add} for being able to poison the target") + end + end + end + next score +end + + +# Encourage using moves that can cause confusion. +MKAI::ScoreHandler.add do |score, ai, user, target, move| + if move.is_a?(PokeBattle_ConfuseMove) && !target.confused? + chance = move.pbAdditionalEffectChance(user, target) + chance = 100 if chance == 0 + if chance > 0 && chance <= 100 + add = chance * move.pbNumHits(user, [target]) + # The higher the target's attack stats, the more beneficial it is to confuse the target. + stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8] + stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2] + stage = target.stages[PBStats::ATTACK] + 6 + factor = stageMul[stage] / stageDiv[stage].to_f + add *= factor + score += add + MKAI.log("+ #{add} for being able to confuse the target") + end + end + next score +end + + +#=============================================================================# +# # +# Damaging Moves # +# # +#=============================================================================# + + +# STAB modifier +MKAI::ScoreHandler.add_damaging do |score, ai, user, target, move| + # STAB doesn't add anything for fixed-damage moves. + next if move.is_a?(PokeBattle_FixedDamageMove) + calcType = move.pbCalcType(user.battler) + if calcType >= 0 && user.has_type?(calcType) + if user.has_ability?(:ADAPTABILITY) + MKAI.log("+ 90 for STAB with Adaptability") + score += 90 + else + MKAI.log("+ 50 for STAB") + score += 50 + end + end + next score +end + + +# Stat stages and physical/special attacker label +MKAI::ScoreHandler.add_damaging do |score, ai, user, target, move| + # Stat boosts don't add anything for fixed-damage moves. + next if move.is_a?(PokeBattle_FixedDamageMove) + # If the move is physical + if move.physicalMove? + # Increase the score by 25 per stage increase/decrease + if user.stages[PBStats::ATTACK] != 0 + add = user.stages[PBStats::ATTACK] * 25 + score += add + MKAI.log("#{add < 0 ? "-" : "+"} #{add.abs} for attack stages") + end + # Make the move more likely to be chosen if this user is also considered a physical attacker. + if user.is_physical_attacker? + score += 30 + MKAI.log("+ 30 for being a physical attacker") + end + end + + # If the move is special + if move.specialMove? + # Increase the score by 25 per stage increase/decrease + if user.stages[PBStats::SPATK] != 0 + add = user.stages[PBStats::SPATK] * 25 + score += add + MKAI.log("#{add < 0 ? "-" : "+"} #{add.abs} for attack stages") + end + # Make the move more likely to be chosen if this user is also considered a special attacker. + if user.is_special_attacker? + score += 30 + MKAI.log("+ 30 for being a special attacker") + end + end + next score +end + + +# Discourage using damaging moves if the target is semi-invulnerable and slower, +# and encourage using damaging moves if they can break through the semi-invulnerability +# (e.g. prefer earthquake when target is underground) +MKAI::ScoreHandler.add_damaging do |score, ai, user, target, move| + # Target is semi-invulnerable + if target.semiInvulnerable? || target.effects[PBEffects::SkyDrop] >= 0 + encourage = false + discourage = false + # User will hit first while target is still semi-invulnerable. + # If this move will do extra damage because the target is semi-invulnerable, + # encourage using this move. If not, discourage using it. + if user.faster_than?(target) + if target.in_two_turn_attack?("0C9", "0CC", "0CE") # Fly, Bounce, Sky Drop + encourage = move.hitsFlyingTargets? + discourage = !encourage + elsif target.in_two_turn_attack?("0CA") # Dig + # Do not encourage using Fissure, even though it can hit digging targets, because it's an OHKO move + encourage = move.hitsDiggingTargets? && move.function != "070" + discourage = !encourage + elsif target.in_two_turn_attack?("0CB") # Dive + encourage = move.hitsDivingTargets? + discourage = !encourage + else + discourage = true + end + end + # If the user has No Guard + if user.has_ability?(:NOGUARD) + # Then any move would be able to hit the target, meaning this move wouldn't be anything special. + encourage = false + discourage = false + end + if encourage + score += 100 + MKAI.log("+ 100 for being able to hit through a semi-invulnerable state") + elsif discourage + score -= 150 + MKAI.log("- 150 for not being able to hit target because of semi-invulnerability") + end + end + next score +end + + +# Lower the score of multi-turn moves, because they likely have quite high power and thus score. +MKAI::ScoreHandler.add_damaging do |score, ai, user, target, move| + if !user.has_item?(:POWERHERB) && (move.chargingTurnMove? || move.function == "0C2") # Hyper Beam + score -= 70 + MKAI.log("- 70 for requiring a charging turn") + end + next score +end + + +# Prefer using damaging moves based on the level difference between the user and target, +# because if the user will get one-shot, then there's no point in using set-up moves. +# Furthermore, if the target is more than 5 levels higher than the user, priority +# get an additional boost to ensure the user can get a hit in before being potentially one-shot. +# TODO: Make "underdog" method, also for use by moves like perish song or explode and such +MKAI::ScoreHandler.add_damaging do |score, ai, user, target, move| + # Start counting factor this when there's a level difference of greater than 5 + if user.underdog?(target) + add = 5 * (target.level - user.level - 5) + if add > 0 + score += add + MKAI.log("+ #{5 * (target.level - user.level - 5)} for preferring damaging moves due to being a low level") + end + if move.priority > 0 + score += 30 + MKAI.log("+ 30 for being a priority move and being and underdog") + end + end + next score +end + + +# Discourage using physical moves when the user is burned +MKAI::ScoreHandler.add_damaging do |score, ai, user, target, move| + if user.burned? + if move.physicalMove? && move.function != "07E" + score -= 50 + MKAI.log("- 50 for being a physical move and being burned") + end + end + next score +end + + +# Encourage high-critical hit rate moves, or damaging moves in general +# if Laser Focus or Focus Energy has been used +MKAI::ScoreHandler.add_damaging do |score, ai, user, target, move| + next if !move.pbCouldBeCritical?(user.battler, target.battler) + if move.highCriticalRate? || user.effects[PBEffects::LaserFocus] > 0 || + user.effects[PBEffects::FocusEnergy] > 0 + score += 30 + MKAI.log("+ 30 for having a high critical-hit rate") + end + next score +end + + +# Discourage recoil moves if they would knock the user out +MKAI::ScoreHandler.add_damaging do |score, ai, user, target, move| + if move.is_a?(PokeBattle_RecoilMove) + dmg = move.pbRecoilDamage(user.battler, target.battler) + if dmg >= user.hp + score -= 50 + MKAI.log("- 50 for the recoil will knock the user out") + end + end + next score +end + + + +#=============================================================================# +# # +# Move-specific # +# # +#=============================================================================# + + +# Facade +MKAI::ScoreHandler.add("07E") do |score, ai, user, target, move| + if user.burned? || user.poisoned? || user.paralyzed? + score += 50 + MKAI.log("+ 50 for doing more damage with a status condition") + end + next score +end + + +# Aromatherapy, Heal Bell +MKAI::ScoreHandler.add("019") do |score, ai, user, target, move| + count = 0 + user.side.battlers.each do |proj| + next if proj.nil? + # + 80 for each active battler with a status condition + count += 2.0 if proj.has_non_volatile_status? + end + user.side.party.each do |proj| + next if proj.battler # Skip battlers + # Inactive party members do not have a battler attached, + # so we can't use has_non_volatile_status? + count += 1.0 if proj.pokemon.status > 0 + # + 40 for each inactive pokemon with a status condition in the party + end + if count != 0 + add = count * 40.0 + score += add + MKAI.log("+ #{add} for curing status condition(s)") + else + score -= 30 + MKAI.log("- 30 for not curing any status conditions") + end + next score +end + + +# Psycho Shift +MKAI::ScoreHandler.add("01B") do |score, ai, user, target, move| + # If the user has a status condition that is not frozen, + if user.has_non_volatile_status? && !user.frozen? + # And the target doesn't have any status conditions + if !target.has_non_volatile_status? + # Then we can transfer our status condition + transferrable = true + transferrable = false if user.burned? && !target.can_burn?(user, move) + transferrable = false if user.poisoned? && !target.can_poison?(user, move) + transferrable = false if user.paralyzed? && !target.can_paralyze?(user, move) + transferrable = false if user.asleep? && !target.can_sleep?(user, move) + if transferrable + score += 120 + MKAI.log("+ 120 for being able to pass on our status condition") + if user.burned? && target.is_physical_attacker? + score += 50 + MKAI.log("+ 50 for being able to burn the physical-attacking target") + end + end + end + else + score -= 30 + MKAI.log("- 30 for not having a transferrable status condition") + end + next score +end + + +# Purify +MKAI::ScoreHandler.add("15B") do |score, ai, user, target, move| + if target.has_non_volatile_status? + factor = 1 - user.hp / user.totalhp.to_f + # At full hp, factor is 0 (thus not encouraging this move) + # At half hp, factor is 0.5 (thus slightly encouraging this move) + # At 1 hp, factor is about 1.0 (thus encouraging this move) + if user.flags[:will_be_healed] + score -= 30 + MKAI.log("- 30 for the user will already be healed by something") + elsif factor != 0 + if user.is_healing_pointless?(0.5) + score -= 10 + MKAI.log("- 10 for we will take more damage than we can heal if the target repeats their move") + elsif user.is_healing_necessary?(0.5) + add = (factor * 250).round + score += add + MKAI.log("+ #{add} for we will likely die without healing") + else + add = (factor * 125).round + score += add + MKAI.log("+ #{add} for we have lost some hp") + end + end + else + score -= 30 + MKAI.log("- 30 for the move will fail since the target has no status condition") + end + next score +end + + +# Refresh +MKAI::ScoreHandler.add("018") do |score, ai, user, target, move| + if user.burned? || user.poisoned? || user.paralyzed? + score += 70 + MKAI.log("+ 70 for being able to cure our status condition") + end + next score +end + + +# Rest +MKAI::ScoreHandler.add("0D9") do |score, ai, user, target, move| + factor = 1 - user.hp / user.totalhp.to_f + if user.flags[:will_be_healed] + score -= 30 + MKAI.log("- 30 for the user will already be healed by something") + elsif factor != 0 + # Not at full hp + if user.can_sleep?(user, move, true) + add = (factor * 100).round + score += add + MKAI.log("+ #{add} for we have lost some hp") + else + score -= 10 + MKAI.log("- 10 for the move will fail") + end + end + next score +end + + +# Smelling Salts +MKAI::ScoreHandler.add("07C") do |score, ai, user, target, move| + if target.paralyzed? + score += 50 + MKAI.log("+ 50 for doing double damage") + end + next score +end + + +# Wake-Up Slap +MKAI::ScoreHandler.add("07D") do |score, ai, user, target, move| + if target.asleep? + score += 50 + MKAI.log("+ 50 for doing double damage") + end + next score +end + + +# Fire Fang, Flare Blitz +MKAI::ScoreHandler.add("00B", "0FE") do |score, ai, user, target, move| + if !target.burned? && target.can_burn?(user, move) + if target.is_physical_attacker? + score += 40 + MKAI.log("+ 40 for being able to burn the physical-attacking target") + else + score += 10 + MKAI.log("+ 10 for being able to burn the target") + end + end + next score +end + + +# Ice Fang +MKAI::ScoreHandler.add("00E") do |score, ai, user, target, move| + if !target.frozen? && target.can_freeze?(user, move) + score += 20 + MKAI.log("+ 20 for being able to freeze the target") + end + next score +end + + +# Thunder Fang +MKAI::ScoreHandler.add("009") do |score, ai, user, target, move| + if !target.paralyzed? && target.can_paralyze?(user, move) + score += 10 + MKAI.log("+ 10 for being able to paralyze the target") + end + next score +end + + +# Ice Burn +MKAI::ScoreHandler.add("0C6") do |score, ai, user, target, move| + if !target.burned? && target.can_burn?(user, move) + if target.is_physical_attacker? + score += 80 + MKAI.log("+ 80 for being able to burn the physical-attacking target") + else + score += 30 + MKAI.log("+ 30 for being able to burn the target") + end + end + next score +end + + +# Secret Power +MKAI::ScoreHandler.add("0A4") do |score, ai, user, target, move| + score += 40 + MKAI.log("+ 40 for its potential side effects") + next score +end + + +# Tri Attack +MKAI::ScoreHandler.add("017") do |score, ai, user, target, move| + if !target.has_non_volatile_status? + score += 50 + MKAI.log("+ 50 for being able to cause a status condition") + end + next score +end + + +# Freeze Shock, Bounce +MKAI::ScoreHandler.add("0C5", "0CC") do |score, ai, user, target, move| + if !target.paralyzed? && target.can_paralyze?(user, move) + score += 30 + MKAI.log("+ 30 for being able to paralyze the target") + end + next score +end + + +# Volt Tackle +MKAI::ScoreHandler.add("0FD") do |score, ai, user, target, move| + if !target.paralyzed? && target.can_paralyze?(user, move) + score += 10 + MKAI.log("+ 10 for being able to paralyze the target") + end + next score +end + + +# Toxic Thread +MKAI::ScoreHandler.add("159") do |score, ai, user, target, move| + if !target.paralyzed? && target.can_paralyze?(user, move) + score += 50 + MKAI.log("+ 50 for being able to poison the target") + end + if target.battler.pbCanLowerStatStage?(PBStats::SPEED, user, move) && + target.faster_than?(user) + score += 30 + MKAI.log("+ 30 for being able to lower target speed") + end + next score +end + + +# Dark Void +MKAI::ScoreHandler.add(:DARKVOID) do |score, ai, user, target, move| + if user.is_species?(:DARKRAI) + if !target.asleep? && target.can_sleep?(user, move) + score += 120 + MKAI.log("+ 120 for damaging the target with Nightmare if it is asleep") + end + else + score -= 100 + MKAI.log("- 100 for this move will fail") + end + next score +end + + +# Yawn +MKAI::ScoreHandler.add("004") do |score, ai, user, target, move| + if !target.has_non_volatile_status? && target.effects[PBEffects::Yawn] == 0 + score += 60 + MKAI.log("+ 60 for putting the target to sleep") + end + next score +end + + +# Flatter +MKAI::ScoreHandler.add("040") do |score, ai, user, target, move| + if target.confused? + score -= 30 + MKAI.log("- 30 for only raising target stats without being able to confuse it") + else + score += 30 + MKAI.log("+ 30 for confusing the target") + end + next score +end + + +# Swagger +MKAI::ScoreHandler.add("041") do |score, ai, user, target, move| + if target.confused? + score -= 50 + MKAI.log("- 50 for only raising target stats without being able to confuse it") + else + score += 50 + MKAI.log("+ 50 for confusing the target") + if !target.is_physical_attacker? + score += 50 + MKAI.log("+ 50 for the target also is not a physical attacker") + end + end + next score +end + + +# Attract +MKAI::ScoreHandler.add("016") do |score, ai, user, target, move| + # If the target can be attracted by the user + if target.can_attract?(user) + score += 150 + MKAI.log("+ 150 for being able to attract the target") + end + next score +end + + +# Rage +MKAI::ScoreHandler.add("093") do |score, ai, user, target, move| + dmg = user.get_move_damage(target, move) + perc = dmg / target.totalhp.to_f + perc /= 1.5 if user.discourage_making_contact_with?(target) + score += perc * 150 + next score +end + + +# Uproar, Thrash, Petal Dance, Outrage, Ice Ball, Rollout +MKAI::ScoreHandler.add("0D1", "0D2", "0D3") do |score, ai, user, target, move| + dmg = user.get_move_damage(target, move) + perc = dmg / target.totalhp.to_f + perc /= 1.5 if user.discourage_making_contact_with?(target) && move.pbContactMove?(user) + if perc != 0 + add = perc * 80 + score += add + MKAI.log("+ #{add} for dealing about #{(perc * 100).round}% dmg") + end + next score +end + + +# Stealth Rock, Spikes, Toxic Spikes +MKAI::ScoreHandler.add("103", "104", "105") do |score, ai, user, target, move| + if move.function == "103" && user.opposing_side.effects[PBEffects::Spikes] >= 3 || + move.function == "104" && user.opposing_side.effects[PBEffects::ToxicSpikes] >= 2 || + move.function == "105" && user.opposing_side.effects[PBEffects::StealthRock] + score -= 30 + MKAI.log("- 30 for the opposing side already has max spikes") + else + inactive = user.opposing_side.party.size - user.opposing_side.battlers.compact.size + add = inactive * 30 + add *= (3 - user.opposing_side.effects[PBEffects::Spikes]) / 3.0 if move.function == "103" + add *= 3 / 4.0 if user.opposing_side.effects[PBEffects::ToxicSpikes] == 1 && move.function == "104" + score += add + MKAI.log("+ #{add} for there are #{inactive} pokemon to be sent out at some point") + end + next score +end + + +# Disable +MKAI::ScoreHandler.add("0B9") do |score, ai, user, target, move| + # Already disabled one of the target's moves + if target.effects[PBEffects::Disable] > 1 + score -= 30 + MKAI.log("- 30 for the target is already disabled") + else + # Get previous damage done by the target + prevDmg = target.get_damage_by_user(user) + if prevDmg.size > 0 + lastDmg = prevDmg[-1] + # If the last move did more than 50% damage and the target was faster, + # we can't disable the move in time thus using Disable is pointless. + if user.is_healing_pointless?(0.5) && target.faster_than?(user) + score -= 30 + MKAI.log("- 30 for the target move is too strong and the target is faster") + else + add = (lastDmg[3] * 150).round + score += add + MKAI.log("+ #{add} for we disable a strong move") + end + else + # Target hasn't used a damaging move yet + score -= 30 + MKAI.log("- 30 for the target hasn't used a damaging move yet.") + end + end + next score +end + + +# Counter +MKAI::ScoreHandler.add("071") do |score, ai, user, target, move| + expect = false + expect = true if target.is_physical_attacker? && !target.is_healing_necessary?(0.5) + prevDmg = user.get_damage_by_user(target) + if prevDmg.size > 0 + lastDmg = prevDmg[-1] + lastMove = lastDmg[1] + expect = true if lastMove.physicalMove? + end + # If we can reasonably expect the target to use a physical move + if expect + score += 60 + MKAI.log("+ 60 for we can reasonably expect the target to use a physical move") + end + next score +end + + +# Aqua Ring +MKAI::ScoreHandler.add("0DA") do |score, ai, user, target, move| + if !user.effects[PBEffects::AquaRing] + if !user.underdog?(target) + score += 80 + MKAI.log("+ 80 for gaining hp each round") + else + # Underdogs are likely to die fast, so setting up healing for each round + # is likely useless and only a waste of a turn. + score += 40 + MKAI.log("+ 40 for gaining hp each round despite being an underdog") + end + else + score -= 30 + MKAI.log("- 30 for the user already has an aqua ring") + end + next score +end + + +# Ingrain +MKAI::ScoreHandler.add("0DB") do |score, ai, user, target, move| + if !user.effects[PBEffects::Ingrain] + if !user.underdog?(target) + score += 80 + MKAI.log("+ 80 for gaining hp each round") + else + # Underdogs are likely to die fast, so setting up healing for each round + # is likely useless and only a waste of a turn. + score += 40 + MKAI.log("+ 40 for gaining hp each round despite being an underdog") + end + else + score -= 30 + MKAI.log("- 30 for the user is already ingrained") + end + next score +end + + +# Leech Seed +MKAI::ScoreHandler.add("0DC") do |score, ai, user, target, move| + if !user.underdog?(target) && !target.has_type?(:GRASS) && target.effects[PBEffects::LeechSeed] == 0 + score += 60 + MKAI.log("+ 60 for sapping hp from the target") + end + next score +end + + +# Leech Life, Parabolic Charge, Drain Punch, Giga Drain, Horn Leech, Mega Drain, Absorb +MKAI::ScoreHandler.add("0DD") do |score, ai, user, target, move| + dmg = user.get_move_damage(target, move) + add = dmg / 2 + score += add + MKAI.log("+ #{add} for hp gained") + next score +end + + +# Dream Eater +MKAI::ScoreHandler.add("0DE") do |score, ai, user, target, move| + if target.asleep? + dmg = user.get_move_damage(target, move) + add = dmg / 2 + score += add + MKAI.log("+ #{add} for hp gained") + else + score -= 30 + MKAI.log("- 30 for the move will fail") + end + next score +end + + +# Heal Pulse +MKAI::ScoreHandler.add("0DF") do |score, ai, user, target, move| + # If the target is an ally + ally = false + target.battler.eachAlly do |battler| + ally = true if battler == user.battler + end + if ally + factor = 1 - target.hp / target.totalhp.to_f + # At full hp, factor is 0 (thus not encouraging this move) + # At half hp, factor is 0.5 (thus slightly encouraging this move) + # At 1 hp, factor is about 1.0 (thus encouraging this move) + if target.flags[:will_be_healed] + score -= 30 + MKAI.log("- 30 for the target will already be healed by something") + elsif factor != 0 + if target.is_healing_pointless?(0.5) + score -= 10 + MKAI.log("- 10 for the target will take more damage than we can heal if the opponent repeats their move") + elsif target.is_healing_necessary?(0.5) + add = (factor * 250).round + score += add + MKAI.log("+ #{add} for the target will likely die without healing") + else + add = (factor * 125).round + score += add + MKAI.log("+ #{add} for the target has lost some hp") + end + else + score -= 30 + MKAI.log("- 30 for the target is at full hp") + end + else + score -= 30 + MKAI.log("- 30 for the target is not an ally") + end + next score +end + + +# Whirlwind, Roar, Circle Throw, Dragon Tail, U-Turn, Volt Switch +MKAI::ScoreHandler.add("0EB", "0EC", "0EE") do |score, ai, user, target, move| + if user.bad_against?(target) && user.level >= target.level && + !target.has_ability?(:SUCTIONCUPS) && !target.effects[PBEffects::Ingrain] + score += 100 + MKAI.log("+ 100 for forcing our target to switch and we're bad against our target") + end + next score +end + + +# Anchor Shot, Block, Mean Look, Spider Web, Spirit Shackle, Thousand Waves +MKAI::ScoreHandler.add("0EF") do |score, ai, user, target, move| + if target.bad_against?(user) && !target.has_type?(:GHOST) + score += 100 + MKAI.log("+ 100 for locking our target in battle with us and they're bad against us") + end + next score +end + + +# Mimic +MKAI::ScoreHandler.add("05C") do |score, ai, user, target, move| + blacklisted = ["002", "014", "05C", "05D", "0B6"] # Struggle, Chatter, Mimic, Sketch, Metronome + last_move = pbGetMoveData(target.battler.lastRegularMoveUsed) + # Don't mimic if no move has been used or we can't mimic the move + if target.battler.lastRegularMoveUsed <= 0 || blacklisted.include?(last_move[MOVE_FUNCTION_CODE]) + score -= 30 + MKAI.log("- 30 for we can't mimic any move used prior") + else + move_id = last_move[MOVE_ID] + matchup = target.calculate_move_matchup(move_id) + # If our target used a move that would also be super effective against them, + # it would be a good idea to mimic that move now so we can use it against them. + if matchup > 1 + add = (matchup * 75.0).round + score += add + MKAI.log("+ #{add} for we can mimic a move that would be super effective against the target too.") + end + end + next score +end + + +# Recover, Slack Off, Soft-Boiled, Heal Order, Milk Drink, Roost, Wish +MKAI::ScoreHandler.add("0D5", "0D6", "0D7") do |score, ai, user, target, move| + factor = 1 - user.hp / user.totalhp.to_f + # At full hp, factor is 0 (thus not encouraging this move) + # At half hp, factor is 0.5 (thus slightly encouraging this move) + # At 1 hp, factor is about 1.0 (thus encouraging this move) + if user.flags[:will_be_healed] + score -= 30 + MKAI.log("- 30 for the user will already be healed by something") + elsif factor != 0 + if user.is_healing_pointless?(0.5) + score -= 10 + MKAI.log("- 10 for we will take more damage than we can heal if the target repeats their move") + elsif user.is_healing_necessary?(0.5) + add = (factor * 250).round + score += add + MKAI.log("+ #{add} for we will likely die without healing") + else + add = (factor * 125).round + score += add + MKAI.log("+ #{add} for we have lost some hp") + end + else + score -= 30 + MKAI.log("- 30 for we are at full hp") + end + next score +end + + +# Moonlight, Morning Sun, Synthesis +MKAI::ScoreHandler.add("0D8") do |score, ai, user, target, move| + heal_factor = 0.5 + case ai.battle.pbWeather + when PBWeather::Sun, PBWeather::HarshSun + heal_factor = 2.0 / 3.0 + when PBWeather::None, PBWeather::StrongWinds + heal_factor = 0.5 + else + heal_factor = 0.25 + end + effi_factor = 1.0 + effi_factor = 0.5 if heal_factor == 0.25 + factor = 1 - user.hp / user.totalhp.to_f + # At full hp, factor is 0 (thus not encouraging this move) + # At half hp, factor is 0.5 (thus slightly encouraging this move) + # At 1 hp, factor is about 1.0 (thus encouraging this move) + if user.flags[:will_be_healed] + score -= 30 + MKAI.log("- 30 for the user will already be healed by something") + elsif factor != 0 + if user.is_healing_pointless?(heal_factor) + score -= 10 + MKAI.log("- 10 for we will take more damage than we can heal if the target repeats their move") + elsif user.is_healing_necessary?(heal_factor) + add = (factor * 250 * effi_factor).round + score += add + MKAI.log("+ #{add} for we will likely die without healing") + else + add = (factor * 125 * effi_factor).round + score += add + MKAI.log("+ #{add} for we have lost some hp") + end + else + score -= 30 + MKAI.log("- 30 for we are at full hp") + end + next score +end + + +# Minimize +MKAI::ScoreHandler.add("034") do |score, ai, user, target, move| + accuracy = false + double = false + target.side.battlers.any? do |proj| + accuracy = true if proj.moves.any? { |move| move.tramplesMinimize?(1) && !proj.target_is_immune?(move, user) } + double = true if proj.moves.any? { |move| move.tramplesMinimize?(2) && !proj.target_is_immune?(move, user) } + end + if accuracy + score -= 40 + MKAI.log("- 40 for the target has moves that will hit with perfect accuracy against minimized targets") + end + if double + score -= 90 + MKAI.log("- 90 for the target has moves that will deal double damage against minimized targets") + end + next score +end + + +# Lucky Chant +MKAI::ScoreHandler.add("0A1") do |score, ai, user, target, move| + if user.side.effects[PBEffects::LuckyChant] > 0 + score -= 30 + MKAI.log("- 30 for lucky chant is already active") + elsif user.side.flags[:will_luckychant] + score -= 30 + MKAI.log("- 30 for another battler will already use lucky chant") + else + enemies = target.side.battlers.select { |proj| !proj.fainted? }.size + critenemies = target.side.battlers.select { |proj| proj.moves.any? { |m| m.highCriticalRate? } }.size + add = enemies * 20 + critenemies * 30 + score += add + MKAI.log("+ #{add} based on enemy and high-crit-dealing enemy moves count") + end + next score +end + + +# Reflect +MKAI::ScoreHandler.add("0A2") do |score, ai, user, target, move| + if user.side.effects[PBEffects::Reflect] > 0 + score -= 30 + MKAI.log("- 30 for reflect is already active") + elsif user.side.flags[:will_reflect] + score -= 30 + MKAI.log("- 30 for another battler will already use reflect") + else + enemies = target.side.battlers.select { |proj| !proj.fainted? }.size + physenemies = target.side.battlers.select { |proj| proj.is_physical_attacker? }.size + add = enemies * 20 + physenemies * 30 + score += add + MKAI.log("+ #{add} based on enemy and physical enemy count") + end + next score +end + + +# Light Screen +MKAI::ScoreHandler.add("0A3") do |score, ai, user, target, move| + if user.side.effects[PBEffects::LightScreen] > 0 + score -= 30 + MKAI.log("- 30 for light screen is already active") + elsif user.side.flags[:will_lightscreen] + score -= 30 + MKAI.log("- 30 for another battler will already use light screen") + else + enemies = target.side.battlers.select { |proj| !proj.fainted? }.size + specenemies = target.side.battlers.select { |proj| proj.is_special_attacker? }.size + add = enemies * 20 + specenemies * 30 + score += add + MKAI.log("+ #{add} based on enemy and special enemy count") + end + next score +end + + +# Haze +MKAI::ScoreHandler.add("051") do |score, ai, user, target, move| + if user.side.flags[:will_haze] + score -= 30 + MKAI.log("- 30 for another battler will already use haze") + else + net = 0 + # User buffs: net goes up + # User debuffs: net goes down + # Target buffs: net goes down + # Target debuffs: net goes up + # The lower net is, the better Haze is to choose. + user.side.battlers.each do |proj| + PBStats.eachBattleStat { |s| net += proj.stages[s] } + end + target.side.battlers.each do |proj| + PBStats.eachBattleStat { |s| net -= proj.stages[s] } + end + # As long as the target's stat stages are more advantageous than ours (i.e. net < 0), Haze is a good choice + if net < 0 + add = -net * 20 + score += add + MKAI.log("+ #{add} to reset disadvantageous stat stages") + else + score -= 30 + MKAI.log("- 30 for our stat stages are advantageous") + end + end + next score +end + + +# Bide +MKAI::ScoreHandler.add("0D4") do |score, ai, user, target, move| + # If we've been hit at least once, use Bide if we could take two hits of the last attack and survive + prevDmg = target.get_damage_by_user(user) + if prevDmg.size > 0 + lastDmg = prevDmg[-1] + predDmg = lastDmg[2] * 2 + # We would live if we took two hits of the last move + if user.hp - predDmg > 0 + score += 120 + MKAI.log("+ 120 for we can survive two subsequent attacks") + else + score -= 10 + MKAI.log("- 10 for we would not survive two subsequent attacks") + end + else + score -= 10 + MKAI.log("- 10 for we don't know whether we'd survive two subsequent attacks") + end + next score +end + + +# Metronome +MKAI::ScoreHandler.add("0B6") do |score, ai, user, target, move| + score += 20 + MKAI.log("+ 20 to make this move an option if all other choices also have a low score") + next score +end + + +# Mirror Move +MKAI::ScoreHandler.add("0AE") do |score, ai, user, target, move| + if target.battler.lastRegularMoveUsed <= 0 || target.faster_than?(user) + score -= 10 + MKAI.log("- 10 for we don't know what move our target will use") + elsif target.battler.lastRegularMoveUsed <= 0 && user.faster_than?(target) + score -= 30 + MKAI.log("- 30 for our target has not made a move yet") + else + # Can Mirror Move + if pbGetMoveData(target.battler.lastRegularMoveUsed, MOVE_FLAGS)[/e/] + matchup = target.calculate_move_matchup(pbGetMoveData(target.battler.lastRegularMoveUsed, MOVE_ID)) + # Super Effective + if matchup > 1 + add = (matchup * 75.0).round + score += add + MKAI.log("+ #{add} for being able to mirror a super effective move") + else + score -= 30 + MKAI.log("- 30 for we would likely mirror a not very effective move") + end + else + score -= 30 + MKAI.log("- 30 for we would not be able to mirror the move the target will likely use") + end + end + next score +end \ No newline at end of file diff --git a/AI references/Marin's AI/AI_Side.rb b/AI references/Marin's AI/AI_Side.rb new file mode 100644 index 000000000..1f87ffab7 --- /dev/null +++ b/AI references/Marin's AI/AI_Side.rb @@ -0,0 +1,68 @@ +class MKAI + class Side + attr_reader :ai + attr_reader :index + attr_reader :battlers + attr_reader :party + attr_reader :trainers + attr_reader :flags + + def initialize(ai, index, wild_pokemon = false) + @ai = ai + @index = index + @battle = @ai.battle + @wild_pokemon = wild_pokemon + @battlers = [] + @party = [] + @flags = {} + end + + def effects + return @battle.sides[@index].effects + end + + def set_party(party) + @party = party.map { |pokemon| BattlerProjection.new(self, pokemon, @wild_pokemon) } + end + + def set_trainers(trainers) + @trainers = trainers + end + + def opposing_side + return @ai.sides[1 - @index] + end + + def recall(battlerIndex) + index = MKAI.battler_to_proj_index(battlerIndex) + proj = @battlers[index] + if proj.nil? + raise "Battler to be recalled was not found in the active battlers list." + end + if !proj.active? + raise "Battler to be recalled was not active." + end + @battlers[index] = nil + proj.battler = nil + end + + def send_out(battlerIndex, battler) + proj = @party.find { |proj| proj && proj.pokemon == battler.pokemon } + if proj.nil? + raise "Battler to be sent-out was not found in the party list." + end + if proj.active? + raise "Battler to be sent-out was already sent out before." + end + index = MKAI.battler_to_proj_index(battlerIndex) + @battlers[index] = proj + proj.ai_index = index + proj.battler = battler + end + + def end_of_round + @battlers.each { |proj| proj.end_of_round if proj } + @flags = {} + end + end +end \ No newline at end of file