This commit is contained in:
153
PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs
Normal file
153
PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Static.Moves;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.AI.Explicit;
|
||||
|
||||
public partial class ExplicitAI
|
||||
{
|
||||
private bool TryChooseToSwitchOut(IBattle battle, IPokemon pokemon, bool terribleMoves,
|
||||
[NotNullWhen(true)] out ITurnChoice? choice)
|
||||
{
|
||||
choice = null;
|
||||
if (battle.IsWildBattle)
|
||||
return false;
|
||||
if (TrainerHighSkill)
|
||||
{
|
||||
var opponentSide = battle.Sides.First(x => x != pokemon.BattleData?.BattleSide);
|
||||
var foeCanAct = opponentSide.Pokemon.WhereNotNull().Any(CanAttack);
|
||||
if (!foeCanAct)
|
||||
return false;
|
||||
}
|
||||
var party = battle.Parties.FirstOrDefault(x => x.IsResponsibleForIndex(
|
||||
new ResponsibleIndex(pokemon.BattleData!.SideIndex, pokemon.BattleData.Position)));
|
||||
if (party is null)
|
||||
return false;
|
||||
var usablePokemon = party.GetUsablePokemonNotInField().ToList();
|
||||
if (!terribleMoves)
|
||||
{
|
||||
if (!_skillFlags.ConsiderSwitching)
|
||||
return false;
|
||||
if (!usablePokemon.Any())
|
||||
return false;
|
||||
var shouldSwitch = _handlers.ShouldSwitch(this, pokemon, battle, usablePokemon);
|
||||
if (shouldSwitch && TrainerMediumSkill)
|
||||
{
|
||||
if (_handlers.ShouldNotSwitch(this, pokemon, battle, usablePokemon))
|
||||
{
|
||||
shouldSwitch = false;
|
||||
}
|
||||
}
|
||||
if (!shouldSwitch)
|
||||
return false;
|
||||
}
|
||||
var battleSide = pokemon.BattleData!.BattleSide;
|
||||
var bestReplacement =
|
||||
ChooseBestReplacementPokemon(pokemon.BattleData!.Position, terribleMoves, usablePokemon, battleSide);
|
||||
if (bestReplacement is null)
|
||||
{
|
||||
AILogging.LogInformation(
|
||||
$"ExplicitAI: No suitable replacement Pokemon found for {pokemon} at position {pokemon.BattleData.Position}.");
|
||||
return false;
|
||||
}
|
||||
choice = new SwitchChoice(pokemon, bestReplacement);
|
||||
return true;
|
||||
}
|
||||
|
||||
private IPokemon? ChooseBestReplacementPokemon(byte position, bool terribleMoves,
|
||||
IReadOnlyList<IPokemon> usablePokemon, IBattleSide battleSide)
|
||||
{
|
||||
var options = usablePokemon.Where((pokemon, index) =>
|
||||
{
|
||||
if (_skillFlags.ReserveLastPokemon && index == usablePokemon.Count - 1 && usablePokemon.Count > 1)
|
||||
return false; // Don't switch to the last Pokemon if there are others available.
|
||||
if (_skillFlags.UsePokemonInOrder && index != 0)
|
||||
return false;
|
||||
return true;
|
||||
}).ToList();
|
||||
if (options.Count == 0)
|
||||
return null;
|
||||
var ratedOptions = options
|
||||
.Select(pokemon => new { Pokemon = pokemon, Score = RateReplacementPokemon(pokemon, battleSide) })
|
||||
.OrderBy(x => x.Score).ToList();
|
||||
if (TrainerHighSkill && !terribleMoves)
|
||||
{
|
||||
if (ratedOptions.First().Score < 100)
|
||||
return null;
|
||||
}
|
||||
return ratedOptions.First().Pokemon;
|
||||
}
|
||||
|
||||
private static readonly StringKey HeavyDutyBootsName = "heavy_duty_boots";
|
||||
private static readonly StringKey ToxicSpikesName = "toxic_spikes";
|
||||
private static readonly StringKey StickyWebName = "sticky_web";
|
||||
|
||||
private int RateReplacementPokemon(IPokemon pokemon, IBattleSide battleSide)
|
||||
{
|
||||
var score = 0;
|
||||
var types = pokemon.Types;
|
||||
var entryDamage = CalculateEntryHazardDamage(pokemon, battleSide);
|
||||
if (entryDamage >= pokemon.CurrentHealth)
|
||||
score -= 50;
|
||||
else if (entryDamage > 0)
|
||||
score -= 50 * (int)Math.Round((double)entryDamage / pokemon.MaxHealth, MidpointRounding.AwayFromZero);
|
||||
if (!pokemon.HasHeldItem(HeavyDutyBootsName) && !pokemon.IsFloating)
|
||||
{
|
||||
if (battleSide.VolatileScripts.Contains(ToxicSpikesName) && CanBePoisoned(pokemon, battleSide.Battle))
|
||||
{
|
||||
score -= 20;
|
||||
}
|
||||
if (battleSide.VolatileScripts.Contains(StickyWebName))
|
||||
{
|
||||
score -= 15;
|
||||
}
|
||||
}
|
||||
var opponentSide = battleSide.Battle.Sides.First(x => x != battleSide);
|
||||
foreach (var foe in opponentSide.Pokemon.WhereNotNull())
|
||||
{
|
||||
var lastMoveUsed = foe.BattleData?.LastMoveChoice;
|
||||
if (lastMoveUsed is null || lastMoveUsed.ChosenMove.MoveData.Category == MoveCategory.Status)
|
||||
continue;
|
||||
var moveType = lastMoveUsed.ChosenMove.MoveData.MoveType;
|
||||
var effectiveness = pokemon.Library.StaticLibrary.Types.GetEffectiveness(moveType, types);
|
||||
score -= (int)(lastMoveUsed.ChosenMove.MoveData.BasePower * effectiveness / 5);
|
||||
}
|
||||
foreach (var learnedMove in pokemon.Moves.WhereNotNull())
|
||||
{
|
||||
if (learnedMove.MoveData.BasePower == 0 || learnedMove is { CurrentPp: 0, MaxPp: > 0 })
|
||||
continue;
|
||||
foreach (var foe in opponentSide.Pokemon.WhereNotNull())
|
||||
{
|
||||
if (CanAbsorbMove(foe, learnedMove.MoveData, learnedMove.MoveData.MoveType, battleSide.Battle))
|
||||
continue;
|
||||
var effectiveness =
|
||||
pokemon.Library.StaticLibrary.Types.GetEffectiveness(learnedMove.MoveData.MoveType, foe.Types);
|
||||
score += (int)(learnedMove.MoveData.BasePower * effectiveness / 10);
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
public static uint CalculateEntryHazardDamage(IPokemon pokemon, IBattleSide side)
|
||||
{
|
||||
var damage = 0u;
|
||||
side.RunScriptHook<IAIInfoScriptExpectedEntryDamage>(x => x.ExpectedEntryDamage(pokemon, ref damage));
|
||||
return damage;
|
||||
}
|
||||
|
||||
private static bool CanSwitch(IPokemon pokemon)
|
||||
{
|
||||
var battleData = pokemon.BattleData;
|
||||
if (battleData == null)
|
||||
return false;
|
||||
if (battleData.Battle.IsWildBattle)
|
||||
return false;
|
||||
var partyForIndex = battleData.Battle.Parties.FirstOrDefault(x =>
|
||||
x.IsResponsibleForIndex(new ResponsibleIndex(battleData.SideIndex, battleData.Position)));
|
||||
return partyForIndex != null && partyForIndex.HasUsablePokemonNotInField();
|
||||
}
|
||||
}
|
||||
89
PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs
Normal file
89
PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Static;
|
||||
using PkmnLib.Static.Moves;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.AI.Explicit;
|
||||
|
||||
public partial class ExplicitAI
|
||||
{
|
||||
private static readonly StringKey MistyTerrainName = "misty_terrain";
|
||||
private static readonly StringKey PoisonName = "poison";
|
||||
private static readonly StringKey SteelName = "steel";
|
||||
private static readonly StringKey ImmunityName = "immunity";
|
||||
private static readonly StringKey PastelVeilName = "pastel_veil";
|
||||
private static readonly StringKey FlowerVeilName = "flower_veil";
|
||||
private static readonly StringKey LeafGuardName = "leaf_guard";
|
||||
private static readonly StringKey ComatoseName = "comatose";
|
||||
private static readonly StringKey ShieldsDownName = "shields_down";
|
||||
private static readonly StringKey HarshSunlightName = "harsh_sunlight";
|
||||
private static readonly StringKey DesolateLandsName = "desolate_lands";
|
||||
private static readonly StringKey BulletproofName = "bulletproof";
|
||||
private static readonly StringKey FlashFireName = "flash_fire";
|
||||
private static readonly StringKey LightningRodName = "lightning_rod";
|
||||
private static readonly StringKey MotorDriveName = "motor_drive";
|
||||
private static readonly StringKey VoltAbsorbName = "volt_absorb";
|
||||
private static readonly StringKey SapSipperName = "sap_sipper";
|
||||
private static readonly StringKey SoundproofName = "soundproof";
|
||||
private static readonly StringKey StormDrainName = "storm_drain";
|
||||
private static readonly StringKey WaterAbsorbName = "water_absorb";
|
||||
private static readonly StringKey DrySkinName = "dry_skin";
|
||||
private static readonly StringKey TelepathyName = "telepathy";
|
||||
private static readonly StringKey WonderGuardName = "wonder_guard";
|
||||
private static readonly StringKey FireName = "fire";
|
||||
private static readonly StringKey ElectricName = "electric";
|
||||
private static readonly StringKey WaterName = "water";
|
||||
|
||||
private static bool CanBePoisoned(IPokemon pokemon, IBattle battle)
|
||||
{
|
||||
if (battle.TerrainName == MistyTerrainName)
|
||||
return false;
|
||||
if (pokemon.Types.Any(x => x.Name == PoisonName || x.Name == SteelName))
|
||||
return false;
|
||||
if (pokemon.ActiveAbility?.Name == ImmunityName)
|
||||
return false;
|
||||
if (pokemon.ActiveAbility?.Name == PastelVeilName)
|
||||
return false;
|
||||
if (pokemon.ActiveAbility?.Name == FlowerVeilName && pokemon.Types.Any(x => x.Name == GrassName))
|
||||
return false;
|
||||
if ((pokemon.ActiveAbility?.Name == LeafGuardName && battle.WeatherName == HarshSunlightName) ||
|
||||
battle.WeatherName == DesolateLandsName)
|
||||
return false;
|
||||
if (pokemon.ActiveAbility?.Name == ComatoseName && pokemon.Species.Name == "komala")
|
||||
return false;
|
||||
if (pokemon.ActiveAbility?.Name == ShieldsDownName && pokemon.Species.Name == "minior" &&
|
||||
pokemon.Form.Name.Contains("-meteor"))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CanAbsorbMove(IPokemon pokemon, IMoveData move, TypeIdentifier moveType, IBattle battle)
|
||||
{
|
||||
if (pokemon.ActiveAbility == null)
|
||||
return false;
|
||||
|
||||
var abilityName = pokemon.ActiveAbility.Name;
|
||||
|
||||
if (abilityName == BulletproofName)
|
||||
return move.HasFlag("bomb");
|
||||
if (abilityName == FlashFireName)
|
||||
return moveType.Name == FireName;
|
||||
if (abilityName == LightningRodName || abilityName == MotorDriveName || abilityName == VoltAbsorbName)
|
||||
return moveType.Name == ElectricName;
|
||||
if (abilityName == SapSipperName)
|
||||
return moveType.Name == GrassName;
|
||||
if (abilityName == SoundproofName)
|
||||
return move.HasFlag("sound");
|
||||
if (abilityName == StormDrainName || abilityName == WaterAbsorbName || abilityName == DrySkinName)
|
||||
return moveType.Name == WaterName;
|
||||
if (abilityName == TelepathyName)
|
||||
return false;
|
||||
if (abilityName == WonderGuardName)
|
||||
{
|
||||
var effectiveness = battle.Library.StaticLibrary.Types.GetEffectiveness(moveType, pokemon.Types);
|
||||
return effectiveness <= 1.0f;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using PkmnLib.Dynamic.BattleFlow;
|
||||
using PkmnLib.Dynamic.Libraries;
|
||||
using PkmnLib.Dynamic.Models;
|
||||
@@ -8,21 +9,27 @@ using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.AI.Explicit;
|
||||
|
||||
public interface IExplicitAI
|
||||
{
|
||||
public bool TrainerHighSkill { get; }
|
||||
public bool TrainerMediumSkill { get; }
|
||||
public IRandom Random { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An explicit AI that has explicitly written logic for each Pokémon and move.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is heavily based on the AI used in <a href="https://github.com/Maruno17/pokemon-essentials">Pokémon Essentials</a>
|
||||
/// </remarks>
|
||||
public class ExplicitAI : PokemonAI
|
||||
public partial class ExplicitAI : PokemonAI, IExplicitAI
|
||||
{
|
||||
public const int MoveFailScore = 20;
|
||||
public const int MoveUselessScore = 60;
|
||||
public const int MoveBaseScore = 100;
|
||||
|
||||
private const float TrainerSkill = 100; // TODO: This should be configurable
|
||||
private bool CanPredictMoveFailure => true; // TODO: This should be configurable
|
||||
private bool ScoreMoves => true; // TODO: This should be configurable
|
||||
private SkillFlags _skillFlags = new();
|
||||
|
||||
private float MoveScoreThreshold => (float)(0.6f + 0.35f * Math.Sqrt(Math.Min(TrainerSkill, 100) / 100f));
|
||||
|
||||
@@ -30,17 +37,39 @@ public class ExplicitAI : PokemonAI
|
||||
|
||||
private readonly IRandom _random = new RandomImpl();
|
||||
|
||||
public class SkillFlags
|
||||
{
|
||||
// TODO: Make these configurable
|
||||
public bool CanPredictMoveFailure { get; set; } = true;
|
||||
public bool ScoreMoves { get; set; } = true;
|
||||
public bool ConsiderSwitching { get; set; } = true;
|
||||
public bool ReserveLastPokemon { get; set; } = true;
|
||||
public bool UsePokemonInOrder { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExplicitAI(IDynamicLibrary library) : base("explicit")
|
||||
{
|
||||
_handlers = library.ExplicitAIHandlers;
|
||||
}
|
||||
|
||||
public bool TrainerHighSkill => TrainerSkill >= 45;
|
||||
public bool TrainerMediumSkill => TrainerSkill >= 32;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRandom Random => _random;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
|
||||
{
|
||||
if (battle.HasForcedTurn(pokemon, out var choice))
|
||||
return choice;
|
||||
if (TryChooseToSwitchOut(battle, pokemon, false, out var turnChoice))
|
||||
{
|
||||
AILogging.LogInformation($"{pokemon} is switching out.");
|
||||
return turnChoice;
|
||||
}
|
||||
|
||||
var moveChoices = GetMoveScores(pokemon, battle);
|
||||
if (moveChoices.Count == 0)
|
||||
{
|
||||
@@ -48,7 +77,29 @@ public class ExplicitAI : PokemonAI
|
||||
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position);
|
||||
}
|
||||
var maxScore = moveChoices.Max(x => x.score);
|
||||
// TODO: Consider switching to a different pokémon if the score is too low
|
||||
if (TrainerHighSkill && CanSwitch(pokemon))
|
||||
{
|
||||
var badMoves = false;
|
||||
if (maxScore <= MoveUselessScore)
|
||||
{
|
||||
badMoves = CanAttack(pokemon);
|
||||
if (!badMoves && _random.GetInt(100) < 25)
|
||||
badMoves = true;
|
||||
}
|
||||
else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.BattleData?.TurnsOnField > 2 &&
|
||||
_random.GetInt(100) < 80)
|
||||
{
|
||||
badMoves = true;
|
||||
}
|
||||
if (badMoves)
|
||||
{
|
||||
AILogging.LogInformation($"{pokemon} has no good moves, considering switching.");
|
||||
if (TryChooseToSwitchOut(battle, pokemon, badMoves, out var switchChoice))
|
||||
{
|
||||
return switchChoice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var threshold = (float)Math.Floor(maxScore * MoveScoreThreshold);
|
||||
var considerChoices = moveChoices.Select(x => (x, Math.Max(x.score - threshold, 0))).ToArray();
|
||||
@@ -122,7 +173,7 @@ public class ExplicitAI : PokemonAI
|
||||
}
|
||||
}
|
||||
var aiMove = new AIMoveState(user, moveData);
|
||||
if (CanPredictMoveFailure && PredictMoveFailure(user, battle, aiMove))
|
||||
if (_skillFlags.CanPredictMoveFailure && PredictMoveFailure(user, battle, aiMove))
|
||||
{
|
||||
AILogging.LogInformation($"{user} is considering {aiMove.Move.Name} but it will fail.");
|
||||
AddMoveToChoices(index, MoveFailScore);
|
||||
@@ -210,8 +261,8 @@ public class ExplicitAI : PokemonAI
|
||||
return true;
|
||||
|
||||
// Check if the move will fail based on the handlers
|
||||
return aiMove.Move.SecondaryEffect != null &&
|
||||
_handlers.MoveWillFail(aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, null));
|
||||
return aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFail(this, aiMove.Move.SecondaryEffect.Name,
|
||||
new MoveOption(aiMove, battle, null));
|
||||
}
|
||||
|
||||
private static readonly StringKey PsychicTerrainName = new("psychic_terrain");
|
||||
@@ -226,8 +277,8 @@ public class ExplicitAI : PokemonAI
|
||||
|
||||
private bool PredictMoveFailureAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
|
||||
{
|
||||
if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(aiMove.Move.SecondaryEffect.Name,
|
||||
new MoveOption(aiMove, battle, target)))
|
||||
if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(this,
|
||||
aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, target)))
|
||||
return true;
|
||||
if (aiMove.Move.Priority > 0)
|
||||
{
|
||||
@@ -281,20 +332,20 @@ public class ExplicitAI : PokemonAI
|
||||
score += targetScore;
|
||||
affectedTargets++;
|
||||
}
|
||||
if (affectedTargets == 0 && CanPredictMoveFailure)
|
||||
if (affectedTargets == 0 && _skillFlags.CanPredictMoveFailure)
|
||||
{
|
||||
return MoveFailScore;
|
||||
}
|
||||
if (affectedTargets > 0)
|
||||
score = (int)(score / (float)affectedTargets);
|
||||
}
|
||||
if (ScoreMoves)
|
||||
if (_skillFlags.ScoreMoves)
|
||||
{
|
||||
if (aiMove.Move.SecondaryEffect != null)
|
||||
{
|
||||
_handlers.ApplyMoveEffectScore(aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, null),
|
||||
ref score);
|
||||
_handlers.ApplyGenerateMoveScoreModifiers(new MoveOption(aiMove, battle, null), ref score);
|
||||
_handlers.ApplyMoveEffectScore(this, aiMove.Move.SecondaryEffect.Name,
|
||||
new MoveOption(aiMove, battle, null), ref score);
|
||||
_handlers.ApplyGenerateMoveScoreModifiers(this, new MoveOption(aiMove, battle, null), ref score);
|
||||
}
|
||||
}
|
||||
if (score < 0)
|
||||
@@ -304,18 +355,18 @@ public class ExplicitAI : PokemonAI
|
||||
|
||||
private int GetMoveScoreAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
|
||||
{
|
||||
if (CanPredictMoveFailure && PredictMoveFailureAgainstTarget(user, aiMove, target, battle))
|
||||
if (_skillFlags.CanPredictMoveFailure && PredictMoveFailureAgainstTarget(user, aiMove, target, battle))
|
||||
{
|
||||
AILogging.LogInformation($"{user} is considering {aiMove.Move.Name} against {target} but it will fail.");
|
||||
return -1;
|
||||
}
|
||||
var score = MoveBaseScore;
|
||||
if (ScoreMoves && aiMove.Move.SecondaryEffect != null)
|
||||
if (_skillFlags.ScoreMoves && aiMove.Move.SecondaryEffect != null)
|
||||
{
|
||||
var estimatedDamage = AIHelpers.CalculateDamageEstimation(aiMove.Move, user, target, battle.Library);
|
||||
var moveOption = new MoveOption(aiMove, battle, target, estimatedDamage);
|
||||
_handlers.ApplyMoveEffectAgainstTargetScore(aiMove.Move.SecondaryEffect.Name, moveOption, ref score);
|
||||
_handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(moveOption, ref score);
|
||||
_handlers.ApplyMoveEffectAgainstTargetScore(this, aiMove.Move.SecondaryEffect.Name, moveOption, ref score);
|
||||
_handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(this, moveOption, ref score);
|
||||
}
|
||||
|
||||
if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex &&
|
||||
@@ -343,4 +394,17 @@ public class ExplicitAI : PokemonAI
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CanAttack(IPokemon pokemon)
|
||||
{
|
||||
if (pokemon.Volatile.Contains("requires_recharge"))
|
||||
return false;
|
||||
if (pokemon.HasStatus("frozen") || pokemon.HasStatus("sleep"))
|
||||
return false;
|
||||
if (pokemon.ActiveAbility?.Name == "truant" && pokemon.Volatile.Contains("truant_effect"))
|
||||
return false;
|
||||
if (pokemon.Volatile.Contains("flinch_effect"))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,14 @@ namespace PkmnLib.Dynamic.AI.Explicit;
|
||||
|
||||
public record struct MoveOption(AIMoveState Move, IBattle Battle, IPokemon? Target, uint EstimatedDamage = 0);
|
||||
|
||||
public delegate bool AIBoolHandler(MoveOption option);
|
||||
public delegate bool AIBoolHandler(IExplicitAI ai, MoveOption option);
|
||||
|
||||
public delegate void AIMoveBasePowerHandler(MoveOption option, ref int score);
|
||||
public delegate bool AISwitchBoolHandler(IExplicitAI ai, IPokemon pokemon, IBattle battle,
|
||||
IReadOnlyList<IPokemon> reserves);
|
||||
|
||||
public delegate void AIScoreMoveHandler(MoveOption option, ref int score);
|
||||
public delegate void AIMoveBasePowerHandler(IExplicitAI ai, MoveOption option, ref int score);
|
||||
|
||||
public delegate void AIScoreMoveHandler(IExplicitAI ai, MoveOption option, ref int score);
|
||||
|
||||
public interface IReadOnlyExplicitAIHandlers
|
||||
{
|
||||
@@ -21,7 +24,7 @@ public interface IReadOnlyExplicitAIHandlers
|
||||
/// <summary>
|
||||
/// Checks if a move will fail based on the provided function code and options.
|
||||
/// </summary>
|
||||
bool MoveWillFail(StringKey functionCode, MoveOption option);
|
||||
bool MoveWillFail(IExplicitAI ai, StringKey functionCode, MoveOption option);
|
||||
|
||||
/// <summary>
|
||||
/// A list of checks to determine if a move will fail against a target.
|
||||
@@ -31,7 +34,7 @@ public interface IReadOnlyExplicitAIHandlers
|
||||
/// <summary>
|
||||
/// Checks if a move will fail against a target based on the provided function code and options.
|
||||
/// </summary>
|
||||
bool MoveWillFailAgainstTarget(StringKey functionCode, MoveOption option);
|
||||
bool MoveWillFailAgainstTarget(IExplicitAI ai, StringKey functionCode, MoveOption option);
|
||||
|
||||
/// <summary>
|
||||
/// A list of handlers to apply scores for move effects.
|
||||
@@ -41,7 +44,7 @@ public interface IReadOnlyExplicitAIHandlers
|
||||
/// <summary>
|
||||
/// Applies the score for a move effect based on the provided name and options.
|
||||
/// </summary>
|
||||
void ApplyMoveEffectScore(StringKey name, MoveOption option, ref int score);
|
||||
void ApplyMoveEffectScore(IExplicitAI ai, StringKey name, MoveOption option, ref int score);
|
||||
|
||||
/// <summary>
|
||||
/// A list of handlers to apply scores for move effects against a target.
|
||||
@@ -51,7 +54,7 @@ public interface IReadOnlyExplicitAIHandlers
|
||||
/// <summary>
|
||||
/// Applies the score for a move effect against a target based on the provided name and options.
|
||||
/// </summary>
|
||||
void ApplyMoveEffectAgainstTargetScore(StringKey name, MoveOption option, ref int score);
|
||||
void ApplyMoveEffectAgainstTargetScore(IExplicitAI ai, StringKey name, MoveOption option, ref int score);
|
||||
|
||||
/// <summary>
|
||||
/// A list of handlers to determine the base power of a move.
|
||||
@@ -61,7 +64,7 @@ public interface IReadOnlyExplicitAIHandlers
|
||||
/// <summary>
|
||||
/// Applies the base power for a move based on the provided name and options.
|
||||
/// </summary>
|
||||
void GetBasePower(StringKey name, MoveOption option, ref int power);
|
||||
void GetBasePower(IExplicitAI ai, StringKey name, MoveOption option, ref int power);
|
||||
|
||||
/// <summary>
|
||||
/// A list of handlers to apply scores for general move effectiveness.
|
||||
@@ -71,10 +74,7 @@ public interface IReadOnlyExplicitAIHandlers
|
||||
/// <summary>
|
||||
/// Applies the score for a general move based on the provided option.
|
||||
/// </summary>
|
||||
/// <param name="option"></param>
|
||||
/// <param name="score"></param>
|
||||
/// <returns></returns>
|
||||
void ApplyGenerateMoveScoreModifiers(MoveOption option, ref int score);
|
||||
void ApplyGenerateMoveScoreModifiers(IExplicitAI ai, MoveOption option, ref int score);
|
||||
|
||||
/// <summary>
|
||||
/// A list of handlers to apply scores for general move effectiveness against a target.
|
||||
@@ -84,10 +84,16 @@ public interface IReadOnlyExplicitAIHandlers
|
||||
/// <summary>
|
||||
/// Applies the score for a general move against a target based on the provided option.
|
||||
/// </summary>
|
||||
void ApplyGenerateMoveAgainstTargetScoreModifiers(MoveOption option, ref int score);
|
||||
void ApplyGenerateMoveAgainstTargetScoreModifiers(IExplicitAI ai, MoveOption option, ref int score);
|
||||
|
||||
IReadOnlyDictionary<StringKey, AISwitchBoolHandler> ShouldSwitchFunctions { get; }
|
||||
|
||||
bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves);
|
||||
|
||||
IReadOnlyDictionary<StringKey, AISwitchBoolHandler> ShouldNotSwitchFunctions { get; }
|
||||
|
||||
bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves);
|
||||
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> ShouldSwitch { get; }
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> ShouldNotSwitch { get; }
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> AbilityRanking { get; }
|
||||
}
|
||||
|
||||
@@ -99,8 +105,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
||||
public FunctionHandlerDictionary<AIBoolHandler> MoveFailureCheck { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool MoveWillFail(StringKey functionCode, MoveOption option) =>
|
||||
MoveFailureCheck.TryGetValue(functionCode, out var handler) && handler(option);
|
||||
public bool MoveWillFail(IExplicitAI ai, StringKey functionCode, MoveOption option) =>
|
||||
MoveFailureCheck.TryGetValue(functionCode, out var handler) && handler(ai, option);
|
||||
|
||||
public FunctionHandlerDictionary<AIBoolHandler> MoveFailureAgainstTargetCheck { get; } = new();
|
||||
|
||||
@@ -109,8 +115,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
||||
MoveFailureAgainstTargetCheck;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool MoveWillFailAgainstTarget(StringKey functionCode, MoveOption option) =>
|
||||
MoveFailureAgainstTargetCheck.TryGetValue(functionCode, out var handler) && handler(option);
|
||||
public bool MoveWillFailAgainstTarget(IExplicitAI ai, StringKey functionCode, MoveOption option) =>
|
||||
MoveFailureAgainstTargetCheck.TryGetValue(functionCode, out var handler) && handler(ai, option);
|
||||
|
||||
public FunctionHandlerDictionary<AIScoreMoveHandler> MoveEffectScore { get; } = [];
|
||||
|
||||
@@ -121,10 +127,10 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
||||
public FunctionHandlerDictionary<AIScoreMoveHandler> MoveEffectAgainstTargetScore = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyMoveEffectScore(StringKey name, MoveOption option, ref int score)
|
||||
public void ApplyMoveEffectScore(IExplicitAI ai, StringKey name, MoveOption option, ref int score)
|
||||
{
|
||||
if (MoveEffectScore.TryGetValue(name, out var handler))
|
||||
handler(option, ref score);
|
||||
handler(ai, option, ref score);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -134,10 +140,10 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
||||
public FunctionHandlerDictionary<AIMoveBasePowerHandler> MoveBasePower = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyMoveEffectAgainstTargetScore(StringKey name, MoveOption option, ref int score)
|
||||
public void ApplyMoveEffectAgainstTargetScore(IExplicitAI ai, StringKey name, MoveOption option, ref int score)
|
||||
{
|
||||
if (MoveEffectAgainstTargetScore.TryGetValue(name, out var handler))
|
||||
handler(option, ref score);
|
||||
handler(ai, option, ref score);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -147,10 +153,10 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
||||
public FunctionHandlerDictionary<AIScoreMoveHandler> GeneralMoveScore = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void GetBasePower(StringKey name, MoveOption option, ref int power)
|
||||
public void GetBasePower(IExplicitAI ai, StringKey name, MoveOption option, ref int power)
|
||||
{
|
||||
if (MoveBasePower.TryGetValue(name, out var handler))
|
||||
handler(option, ref power);
|
||||
handler(ai, option, ref power);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -160,11 +166,11 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
||||
public FunctionHandlerDictionary<AIScoreMoveHandler> GeneralMoveAgainstTargetScore = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyGenerateMoveScoreModifiers(MoveOption option, ref int score)
|
||||
public void ApplyGenerateMoveScoreModifiers(IExplicitAI ai, MoveOption option, ref int score)
|
||||
{
|
||||
foreach (var (_, handler) in GeneralMoveScore)
|
||||
{
|
||||
handler(option, ref score);
|
||||
handler(ai, option, ref score);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,27 +178,51 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.GeneralMoveAgainstTargetScore =>
|
||||
GeneralMoveAgainstTargetScore;
|
||||
|
||||
public FunctionHandlerDictionary<AIBoolHandler> ShouldSwitch = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyGenerateMoveAgainstTargetScoreModifiers(MoveOption option, ref int score)
|
||||
public void ApplyGenerateMoveAgainstTargetScoreModifiers(IExplicitAI ai, MoveOption option, ref int score)
|
||||
{
|
||||
foreach (var (_, handler) in GeneralMoveAgainstTargetScore)
|
||||
{
|
||||
handler(option, ref score);
|
||||
handler(ai, option, ref score);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.ShouldSwitch => ShouldSwitch;
|
||||
|
||||
public FunctionHandlerDictionary<AIBoolHandler> ShouldNotSwitch = [];
|
||||
public FunctionHandlerDictionary<AISwitchBoolHandler> ShouldSwitchFunctions = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.ShouldNotSwitch => ShouldNotSwitch;
|
||||
IReadOnlyDictionary<StringKey, AISwitchBoolHandler> IReadOnlyExplicitAIHandlers.ShouldSwitchFunctions =>
|
||||
ShouldSwitchFunctions;
|
||||
|
||||
public bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves)
|
||||
{
|
||||
var shouldSwitch = false;
|
||||
foreach (var (_, handler) in ShouldSwitchFunctions)
|
||||
{
|
||||
shouldSwitch |= handler(ai, pokemon, battle, reserves);
|
||||
}
|
||||
return shouldSwitch;
|
||||
}
|
||||
|
||||
public FunctionHandlerDictionary<AISwitchBoolHandler> ShouldNotSwitchFunctions = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AISwitchBoolHandler> IReadOnlyExplicitAIHandlers.ShouldNotSwitchFunctions =>
|
||||
ShouldNotSwitchFunctions;
|
||||
|
||||
public FunctionHandlerDictionary<AIScoreMoveHandler> AbilityRanking = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves)
|
||||
{
|
||||
var shouldNotSwitch = false;
|
||||
foreach (var (_, handler) in ShouldNotSwitchFunctions)
|
||||
{
|
||||
shouldNotSwitch |= handler(ai, pokemon, battle, reserves);
|
||||
}
|
||||
return shouldNotSwitch;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.AbilityRanking =>
|
||||
AbilityRanking;
|
||||
|
||||
@@ -15,6 +15,7 @@ public static class MoveTurnExecutor
|
||||
{
|
||||
internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice)
|
||||
{
|
||||
moveChoice.User.BattleData!.LastMoveChoice = moveChoice;
|
||||
var chosenMove = moveChoice.ChosenMove;
|
||||
var useMove = chosenMove.MoveData;
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ public class BattleImpl : ScriptSource, IBattle
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanSlotBeFilled(byte side, byte position) => Parties.Any(x =>
|
||||
x.IsResponsibleForIndex(new ResponsibleIndex(side, position)) && x.HasPokemonNotInField());
|
||||
x.IsResponsibleForIndex(new ResponsibleIndex(side, position)) && x.HasUsablePokemonNotInField());
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ValidateBattleState()
|
||||
|
||||
@@ -21,7 +21,12 @@ public interface IBattleParty : IDeepCloneable
|
||||
/// <summary>
|
||||
/// Whether the party has a living Pokemon left that is not in the field.
|
||||
/// </summary>
|
||||
bool HasPokemonNotInField();
|
||||
bool HasUsablePokemonNotInField();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all usable Pokemon that are not currently in the field.
|
||||
/// </summary>
|
||||
IEnumerable<IPokemon> GetUsablePokemonNotInField();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,6 +54,10 @@ public class BattlePartyImpl : IBattleParty
|
||||
public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasPokemonNotInField() =>
|
||||
public bool HasUsablePokemonNotInField() =>
|
||||
Party.WhereNotNull().Any(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IPokemon> GetUsablePokemonNotInField() =>
|
||||
Party.WhereNotNull().Where(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using PkmnLib.Dynamic.Events;
|
||||
using PkmnLib.Dynamic.Libraries;
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.Models.Serialized;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Static;
|
||||
@@ -489,6 +490,8 @@ public interface IPokemonBattleData : IDeepCloneable
|
||||
/// </summary>
|
||||
uint SwitchInTurn { get; internal set; }
|
||||
|
||||
uint TurnsOnField { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The side the Pokémon is on.
|
||||
/// </summary>
|
||||
@@ -503,6 +506,11 @@ public interface IPokemonBattleData : IDeepCloneable
|
||||
/// The form of the Pokémon at the time it was sent out.
|
||||
/// </summary>
|
||||
IForm OriginalForm { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The last move choice executed by the Pokémon.
|
||||
/// </summary>
|
||||
IMoveChoice? LastMoveChoice { get; internal set; }
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IPokemon"/>
|
||||
@@ -1490,6 +1498,9 @@ public class PokemonBattleDataImpl : IPokemonBattleData
|
||||
/// <inheritdoc />
|
||||
public uint SwitchInTurn { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint TurnsOnField => Battle.CurrentTurnNumber - SwitchInTurn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IBattleSide BattleSide => Battle.Sides[SideIndex];
|
||||
|
||||
@@ -1498,4 +1509,7 @@ public class PokemonBattleDataImpl : IPokemonBattleData
|
||||
|
||||
/// <inheritdoc />
|
||||
public IForm OriginalForm { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IMoveChoice? LastMoveChoice { get; set; }
|
||||
}
|
||||
@@ -12,5 +12,10 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="AI\Explicit\ExplicitAI.*.cs">
|
||||
<DependentUpon>ExplicitAI.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -26,4 +26,16 @@ public interface IAIInfoScriptExpectedEndOfTurnDamage
|
||||
/// have an end of turn effect, such as Poison or Burn.
|
||||
/// </summary>
|
||||
void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Script for getting the expected entry damage by the AI.
|
||||
/// </summary>
|
||||
public interface IAIInfoScriptExpectedEntryDamage
|
||||
{
|
||||
/// <summary>
|
||||
/// This function returns the expected entry damage for the script. This is used for scripts that have
|
||||
/// an entry hazard effect, such as Spikes or Stealth Rock.
|
||||
/// </summary>
|
||||
void ExpectedEntryDamage(IPokemon pokemon, ref uint damage);
|
||||
}
|
||||
Reference in New Issue
Block a user