Implements AI Switching
All checks were successful
Build / Build (push) Successful in 58s

This commit is contained in:
2025-07-12 13:03:00 +02:00
parent 364d4b9080
commit bf83b25238
34 changed files with 903 additions and 226 deletions

View 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();
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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()

View File

@@ -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);
}

View File

@@ -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; }
}

View File

@@ -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>

View File

@@ -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);
}