Getting started with implementing an explicit AI, based on the Essentials one.
All checks were successful
Build / Build (push) Successful in 1m2s

This commit is contained in:
2025-07-11 17:03:08 +02:00
parent 084ae84130
commit a3a4993407
56 changed files with 2687 additions and 1274 deletions

View File

@@ -0,0 +1,59 @@
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Static;
using PkmnLib.Static.Moves;
using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.AI;
public static class AIHelpers
{
public static uint CalculateDamageEstimation(IMoveData move, IPokemon user, IPokemon target,
IDynamicLibrary library)
{
var hitData = new CustomHitData
{
BasePower = move.BasePower,
Effectiveness = library.StaticLibrary.Types.GetEffectiveness(move.MoveType, target.Types),
Type = move.MoveType,
};
return library.DamageCalculator.GetDamage(null, move.Category, user, target, 1, 0, hitData);
}
private class CustomHitData : IHitData
{
/// <inheritdoc />
public bool IsCritical => false;
/// <inheritdoc />
public ushort BasePower { get; init; }
/// <inheritdoc />
public float Effectiveness { get; init; }
/// <inheritdoc />
public uint Damage => 0;
/// <inheritdoc />
public TypeIdentifier? Type { get; init; }
/// <inheritdoc />
public bool IsContact => false;
/// <inheritdoc />
public bool HasFailed => false;
/// <inheritdoc />
public void Fail()
{
}
/// <inheritdoc />
public void SetFlag(StringKey flag)
{
}
/// <inheritdoc />
public bool HasFlag(StringKey flag) => false;
}
}

View File

@@ -0,0 +1,8 @@
namespace PkmnLib.Dynamic.AI;
public static class AILogging
{
public static Action<string> LogHandler { get; set; } = message => { };
public static void LogInformation(string message) => LogHandler(message);
}

View File

@@ -0,0 +1,16 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Static.Moves;
namespace PkmnLib.Dynamic.AI.Explicit;
public class AIMoveState
{
public AIMoveState(IPokemon user, IMoveData move)
{
User = user;
Move = move;
}
public IPokemon User { get; }
public IMoveData Move { get; }
}

View File

@@ -0,0 +1,346 @@
using PkmnLib.Dynamic.BattleFlow;
using PkmnLib.Dynamic.Libraries;
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;
/// <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 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 float MoveScoreThreshold => (float)(0.6f + 0.35f * Math.Sqrt(Math.Min(TrainerSkill, 100) / 100f));
private readonly IReadOnlyExplicitAIHandlers _handlers;
private readonly IRandom _random = new RandomImpl();
/// <inheritdoc />
public ExplicitAI(IDynamicLibrary library) : base("explicit")
{
_handlers = library.ExplicitAIHandlers;
}
/// <inheritdoc />
public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
{
if (battle.HasForcedTurn(pokemon, out var choice))
return choice;
var moveChoices = GetMoveScores(pokemon, battle);
if (moveChoices.Count == 0)
{
var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
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
var threshold = (float)Math.Floor(maxScore * MoveScoreThreshold);
var considerChoices = moveChoices.Select(x => (x, Math.Max(x.score - threshold, 0))).ToArray();
var totalScore = considerChoices.Sum(x => x.Item2);
if (totalScore == 0)
{
var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position);
}
var initialRandomValue = _random.GetFloat(0, totalScore);
var randomValue = initialRandomValue;
for (var i = 0; i < considerChoices.Length; i++)
{
randomValue -= considerChoices[i].Item2;
if (randomValue >= 0)
continue;
var (index, _, targetIndex) = considerChoices[i].x;
var learnedMove = pokemon.Moves[index];
var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
if (targetIndex == -1)
targetIndex = pokemon.BattleData.Position;
return new MoveChoice(pokemon, learnedMove!, opponentSide, (byte)targetIndex);
}
throw new InvalidOperationException("No valid move choice found. This should not happen.");
}
private List<(int index, int score, int targetIndex)> GetMoveScores(IPokemon user, IBattle battle)
{
var choices = new List<(int index, int score, int targetIndex)>();
foreach (var (learnedMove, index) in user.Moves.Select((x, i) => (x, i)).Where(x => x.x != null))
{
var moveChoice = new MoveChoice(user, learnedMove!, 0, 0);
if (!battle.CanUse(moveChoice))
{
if (learnedMove!.CurrentPp == 0 && learnedMove.MaxPp > 0)
{
AILogging.LogInformation($"{user} cannot use {learnedMove} because it has no PP left.");
}
else
{
AILogging.LogInformation($"{user} cannot use {learnedMove} because it is not a valid choice.");
}
continue;
}
var moveData = learnedMove!.MoveData;
var moveName = moveData.Name;
moveChoice.RunScriptHook<IScriptChangeMove>(x => x.ChangeMove(moveChoice, ref moveName));
if (moveName != learnedMove.MoveData.Name)
{
AILogging.LogInformation($"{user} changed {learnedMove} to {moveName}.");
if (!battle.Library.StaticLibrary.Moves.TryGet(moveName, out moveData))
throw new InvalidOperationException($"Move {moveName} not found in the move library.");
var secondaryEffect = moveData.SecondaryEffect;
if (secondaryEffect != null)
{
if (moveChoice.User.Library.ScriptResolver.TryResolve(ScriptCategory.Move, secondaryEffect.Name,
secondaryEffect.Parameters, out var script))
{
moveChoice.Script.Set(script);
script.OnAddedToParent(moveChoice);
}
else
{
moveChoice.Script.Clear();
}
}
else
{
moveChoice.Script.Clear();
}
}
var aiMove = new AIMoveState(user, moveData);
if (CanPredictMoveFailure && PredictMoveFailure(user, battle, aiMove))
{
AILogging.LogInformation($"{user} is considering {aiMove.Move.Name} but it will fail.");
AddMoveToChoices(index, MoveFailScore);
}
var target = moveData.Target;
if (target is MoveTarget.All or MoveTarget.AllAlly or MoveTarget.AllOpponent or MoveTarget.SelfUse)
{
var score = GetMoveScore(user, aiMove, battle);
AddMoveToChoices(index, score);
}
else if (target is MoveTarget.AdjacentOpponent or MoveTarget.Any or MoveTarget.Adjacent
or MoveTarget.AdjacentAllySelf)
{
// TODO: get redirected target
foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
{
var battleData = pokemon.BattleData;
if (battleData == null)
continue;
if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user))
continue;
if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
{
continue;
}
var score = GetMoveScoreAgainstTarget(user, aiMove, pokemon, battle);
AddMoveToChoices(index, score, battleData.Position);
}
}
else
{
var targets = new List<IPokemon>();
foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
{
var battleData = pokemon.BattleData;
if (battleData == null)
continue;
if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user))
continue;
if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
{
continue;
}
targets.Add(pokemon);
}
var score = GetMoveScore(user, aiMove, battle, targets);
AddMoveToChoices(index, score);
}
}
return choices;
void AddMoveToChoices(int index, int score, int targetIndex = -1)
{
choices.Add((index, score, targetIndex));
// If the user is a wild Pokémon, doubly prefer a random move.
if (battle.IsWildBattle && user.PersonalityValue % user.Moves.Count == index)
{
choices.Add((index, score, targetIndex));
}
}
}
private bool PredictMoveFailure(IPokemon user, IBattle battle, AIMoveState aiMove)
{
if (user.HasStatus("sleep"))
{
// User is asleep, and will not wake up, and the move is not usable while asleep.
if (user.GetStatusTurnsLeft != 0 && !aiMove.Move.HasFlag("usable_while_asleep"))
{
AILogging.LogInformation($"{user} is asleep and cannot use {aiMove.Move.Name}.");
return true;
}
}
// The move is only usable while asleep, but the user is not asleep
else if (aiMove.Move.HasFlag("usable_while_asleep"))
{
return true;
}
// Primal weather
if (battle.WeatherName == "primordial_sea" && aiMove.Move.MoveType.Name == "fire")
return true;
if (battle.WeatherName == "desolate_lands" && aiMove.Move.MoveType.Name == "water")
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));
}
private static readonly StringKey PsychicTerrainName = new("psychic_terrain");
private static readonly StringKey DazzlingName = new("dazzling");
private static readonly StringKey QueenlyMajestyName = new("queenly_majesty");
private static readonly StringKey PranksterName = new("prankster");
private static readonly StringKey DarkName = new("dark");
private static readonly StringKey GroundName = new("ground");
private static readonly StringKey PowderName = new("powder");
private static readonly StringKey SubstituteName = new("substitute");
private static readonly StringKey InfiltratorName = new("infiltrator");
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)))
return true;
if (aiMove.Move.Priority > 0)
{
if (target.BattleData?.SideIndex != user.BattleData?.SideIndex)
{
// Psychic Terrain makes all priority moves fail if the target is affected
if (battle.TerrainName == PsychicTerrainName && !target.IsFloating)
{
return true;
}
// Dazzling and Queenly Majesty prevent priority moves from being used against the Pokémon with those abilities
if (target.BattleData?.BattleSide.Pokemon.WhereNotNull().Any(x =>
x.ActiveAbility?.Name == DazzlingName || x.ActiveAbility?.Name == QueenlyMajestyName) == true)
{
return true;
}
}
}
// TODO: Check immunity because of ability
var moveType = aiMove.Move.MoveType;
var typeEffectiveness = battle.Library.StaticLibrary.Types.GetEffectiveness(moveType, target.Types);
if (aiMove.Move.Category != MoveCategory.Status && typeEffectiveness == 0)
return true;
if (user.ActiveAbility?.Name == PranksterName && aiMove.Move.Category == MoveCategory.Status &&
target.Types.Any(x => x.Name == DarkName) && target.BattleData?.SideIndex != user.BattleData?.SideIndex)
return true;
if (aiMove.Move.Category != MoveCategory.Status && moveType.Name == GroundName && target.IsFloating)
return true;
if (aiMove.Move.HasFlag(PowderName) && !AffectedByPowder(target))
return true;
if (target.Volatile.Contains(SubstituteName) && aiMove.Move.Category == MoveCategory.Status &&
user.ActiveAbility?.Name != InfiltratorName)
return true;
return false;
}
private int GetMoveScore(IPokemon user, AIMoveState aiMove, IBattle battle, IReadOnlyList<IPokemon>? targets = null)
{
var score = MoveBaseScore;
if (targets != null)
{
score = 0;
var affectedTargets = 0;
foreach (var target in targets)
{
var targetScore = GetMoveScoreAgainstTarget(user, aiMove, target, battle);
if (targetScore < 0)
continue;
score += targetScore;
affectedTargets++;
}
if (affectedTargets == 0 && CanPredictMoveFailure)
{
return MoveFailScore;
}
if (affectedTargets > 0)
score = (int)(score / (float)affectedTargets);
}
if (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);
}
}
if (score < 0)
score = 0;
return score;
}
private int GetMoveScoreAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
{
if (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)
{
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);
}
if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex &&
target.BattleData?.Position != user.BattleData?.Position)
{
if (score == MoveUselessScore)
return -1;
score = (int)(1.85f * MoveBaseScore - score);
}
return score;
}
private static readonly StringKey GrassName = new("grass");
private static readonly StringKey OvercoatName = new("overcoat");
private static readonly StringKey SafetyGogglesName = new("safety_goggles");
private static bool AffectedByPowder(IPokemon pokemon)
{
if (pokemon.Types.Any(x => x.Name == GrassName))
return false;
if (pokemon.ActiveAbility?.Name == OvercoatName)
return false;
if (pokemon.HasHeldItem(SafetyGogglesName))
return false;
return true;
}
}

View File

@@ -0,0 +1,199 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Static.Utils;
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 void AIMoveBasePowerHandler(MoveOption option, ref int score);
public delegate void AIScoreMoveHandler(MoveOption option, ref int score);
public interface IReadOnlyExplicitAIHandlers
{
/// <summary>
/// A list of checks to determine if a move will fail.
/// </summary>
IReadOnlyDictionary<StringKey, AIBoolHandler> MoveFailureCheck { get; }
/// <summary>
/// Checks if a move will fail based on the provided function code and options.
/// </summary>
bool MoveWillFail(StringKey functionCode, MoveOption option);
/// <summary>
/// A list of checks to determine if a move will fail against a target.
/// </summary>
IReadOnlyDictionary<StringKey, AIBoolHandler> MoveFailureAgainstTargetCheck { get; }
/// <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);
/// <summary>
/// A list of handlers to apply scores for move effects.
/// </summary>
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> MoveEffectScore { get; }
/// <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);
/// <summary>
/// A list of handlers to apply scores for move effects against a target.
/// </summary>
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> MoveEffectAgainstTargetScore { get; }
/// <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);
/// <summary>
/// A list of handlers to determine the base power of a move.
/// </summary>
IReadOnlyDictionary<StringKey, AIMoveBasePowerHandler> MoveBasePower { get; }
/// <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);
/// <summary>
/// A list of handlers to apply scores for general move effectiveness.
/// </summary>
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> GeneralMoveScore { get; }
/// <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);
/// <summary>
/// A list of handlers to apply scores for general move effectiveness against a target.
/// </summary>
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> GeneralMoveAgainstTargetScore { get; }
/// <summary>
/// Applies the score for a general move against a target based on the provided option.
/// </summary>
void ApplyGenerateMoveAgainstTargetScoreModifiers(MoveOption option, ref int score);
IReadOnlyDictionary<StringKey, AIBoolHandler> ShouldSwitch { get; }
IReadOnlyDictionary<StringKey, AIBoolHandler> ShouldNotSwitch { get; }
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> AbilityRanking { get; }
}
public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
{
/// <inheritdoc />
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.MoveFailureCheck => MoveFailureCheck;
public FunctionHandlerDictionary<AIBoolHandler> MoveFailureCheck { get; } = new();
/// <inheritdoc />
public bool MoveWillFail(StringKey functionCode, MoveOption option) =>
MoveFailureCheck.TryGetValue(functionCode, out var handler) && handler(option);
public FunctionHandlerDictionary<AIBoolHandler> MoveFailureAgainstTargetCheck { get; } = new();
/// <inheritdoc />
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.MoveFailureAgainstTargetCheck =>
MoveFailureAgainstTargetCheck;
/// <inheritdoc />
public bool MoveWillFailAgainstTarget(StringKey functionCode, MoveOption option) =>
MoveFailureAgainstTargetCheck.TryGetValue(functionCode, out var handler) && handler(option);
public FunctionHandlerDictionary<AIScoreMoveHandler> MoveEffectScore { get; } = [];
/// <inheritdoc />
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.MoveEffectScore =>
MoveEffectScore;
public FunctionHandlerDictionary<AIScoreMoveHandler> MoveEffectAgainstTargetScore = [];
/// <inheritdoc />
public void ApplyMoveEffectScore(StringKey name, MoveOption option, ref int score)
{
if (MoveEffectScore.TryGetValue(name, out var handler))
handler(option, ref score);
}
/// <inheritdoc />
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.MoveEffectAgainstTargetScore =>
MoveEffectAgainstTargetScore;
public FunctionHandlerDictionary<AIMoveBasePowerHandler> MoveBasePower = [];
/// <inheritdoc />
public void ApplyMoveEffectAgainstTargetScore(StringKey name, MoveOption option, ref int score)
{
if (MoveEffectAgainstTargetScore.TryGetValue(name, out var handler))
handler(option, ref score);
}
/// <inheritdoc />
IReadOnlyDictionary<StringKey, AIMoveBasePowerHandler> IReadOnlyExplicitAIHandlers.MoveBasePower =>
MoveBasePower;
public FunctionHandlerDictionary<AIScoreMoveHandler> GeneralMoveScore = [];
/// <inheritdoc />
public void GetBasePower(StringKey name, MoveOption option, ref int power)
{
if (MoveBasePower.TryGetValue(name, out var handler))
handler(option, ref power);
}
/// <inheritdoc />
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.GeneralMoveScore =>
GeneralMoveScore;
public FunctionHandlerDictionary<AIScoreMoveHandler> GeneralMoveAgainstTargetScore = [];
/// <inheritdoc />
public void ApplyGenerateMoveScoreModifiers(MoveOption option, ref int score)
{
foreach (var (_, handler) in GeneralMoveScore)
{
handler(option, ref score);
}
}
/// <inheritdoc />
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.GeneralMoveAgainstTargetScore =>
GeneralMoveAgainstTargetScore;
public FunctionHandlerDictionary<AIBoolHandler> ShouldSwitch = [];
/// <inheritdoc />
public void ApplyGenerateMoveAgainstTargetScoreModifiers(MoveOption option, ref int score)
{
foreach (var (_, handler) in GeneralMoveAgainstTargetScore)
{
handler(option, ref score);
}
}
/// <inheritdoc />
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.ShouldSwitch => ShouldSwitch;
public FunctionHandlerDictionary<AIBoolHandler> ShouldNotSwitch = [];
/// <inheritdoc />
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.ShouldNotSwitch => ShouldNotSwitch;
public FunctionHandlerDictionary<AIScoreMoveHandler> AbilityRanking = [];
/// <inheritdoc />
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.AbilityRanking =>
AbilityRanking;
}

View File

@@ -0,0 +1,117 @@
using System.Collections;
using System.Collections.Specialized;
using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.AI.Explicit;
public class FunctionHandlerDictionary<TValue> : IDictionary<StringKey, TValue>, IReadOnlyDictionary<StringKey, TValue>
where TValue : class
{
private readonly OrderedDictionary _backingDictionary = new();
/// <inheritdoc />
public IEnumerator<KeyValuePair<StringKey, TValue>> GetEnumerator() => _backingDictionary.Cast<DictionaryEntry>()
.Select(entry => new KeyValuePair<StringKey, TValue>((StringKey)entry.Key, (TValue)entry.Value))
.GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <inheritdoc />
public void Add(KeyValuePair<StringKey, TValue> item) => _backingDictionary.Add(item.Key, item.Value);
/// <inheritdoc />
public void Clear() => _backingDictionary.Clear();
/// <inheritdoc />
public bool Contains(KeyValuePair<StringKey, TValue> item)
{
if (_backingDictionary.Contains(item.Key))
{
var value = _backingDictionary[item.Key];
return EqualityComparer<TValue>.Default.Equals((TValue)value, item.Value);
}
return false;
}
/// <inheritdoc />
public void CopyTo(KeyValuePair<StringKey, TValue>[] array, int arrayIndex)
{
if (array == null)
throw new ArgumentNullException(nameof(array));
if (arrayIndex < 0 || arrayIndex + Count > array.Length)
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
foreach (var item in this)
{
array[arrayIndex++] = item;
}
}
/// <inheritdoc />
public bool Remove(KeyValuePair<StringKey, TValue> item)
{
if (_backingDictionary.Contains(item.Key))
{
var value = _backingDictionary[item.Key];
if (EqualityComparer<TValue>.Default.Equals((TValue)value, item.Value))
{
_backingDictionary.Remove(item.Key);
return true;
}
}
return false;
}
/// <inheritdoc cref="IReadOnlyDictionary{StringKey, TValue}.Count" />
public int Count => _backingDictionary.Count;
/// <inheritdoc />
public bool IsReadOnly => false;
/// <inheritdoc />
public void Add(StringKey key, TValue value) => _backingDictionary.Add(key, value);
/// <inheritdoc cref="IReadOnlyDictionary{StringKey, TValue}.ContainsKey" />
public bool ContainsKey(StringKey key) => _backingDictionary.Contains(key);
/// <inheritdoc />
public bool Remove(StringKey key)
{
if (!_backingDictionary.Contains(key))
return false;
_backingDictionary.Remove(key);
return true;
}
/// <inheritdoc />
public bool TryGetValue(StringKey key, out TValue value)
{
if (_backingDictionary.Contains(key))
{
value = (TValue)_backingDictionary[key];
return true;
}
value = null!;
return false;
}
/// <inheritdoc />
public TValue this[StringKey key]
{
get => (TValue)_backingDictionary[key];
set => _backingDictionary[key] = value;
}
/// <inheritdoc />
IEnumerable<StringKey> IReadOnlyDictionary<StringKey, TValue>.Keys => Keys;
/// <inheritdoc />
IEnumerable<TValue> IReadOnlyDictionary<StringKey, TValue>.Values => Values;
/// <inheritdoc />
public ICollection<StringKey> Keys => _backingDictionary.Keys.Cast<StringKey>().ToList();
/// <inheritdoc />
public ICollection<TValue> Values => _backingDictionary.Values.Cast<TValue>().ToList();
}

View File

@@ -30,21 +30,10 @@ public class HighestDamageAI : PokemonAI
: battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, 0);
}
var movesWithDamage = moves.Select(move =>
var movesWithDamage = moves.Select(move => new
{
var hitData = new CustomHitData
{
BasePower = move.MoveData.BasePower,
Effectiveness =
battle.Library.StaticLibrary.Types.GetEffectiveness(move.MoveData.MoveType, opponent.Types),
Type = move.MoveData.MoveType,
};
return new
{
Move = move,
Damage = battle.Library.DamageCalculator.GetDamage(null, move.MoveData.Category, pokemon, opponent, 1,
0, hitData),
};
Move = move,
Damage = AIHelpers.CalculateDamageEstimation(move.MoveData, pokemon, opponent, battle.Library),
}).OrderByDescending(x => x.Damage).FirstOrDefault();
if (movesWithDamage is null)
{
@@ -53,41 +42,4 @@ public class HighestDamageAI : PokemonAI
var bestMove = movesWithDamage.Move;
return new MoveChoice(pokemon, bestMove, opponentSide, 0);
}
private class CustomHitData : IHitData
{
/// <inheritdoc />
public bool IsCritical => false;
/// <inheritdoc />
public ushort BasePower { get; init; }
/// <inheritdoc />
public float Effectiveness { get; init; }
/// <inheritdoc />
public uint Damage => 0;
/// <inheritdoc />
public TypeIdentifier? Type { get; init; }
/// <inheritdoc />
public bool IsContact => false;
/// <inheritdoc />
public bool HasFailed => false;
/// <inheritdoc />
public void Fail()
{
}
/// <inheritdoc />
public void SetFlag(StringKey flag)
{
}
/// <inheritdoc />
public bool HasFlag(StringKey flag) => false;
}
}

View File

@@ -1,3 +1,4 @@
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Static.Moves;
@@ -96,4 +97,20 @@ public abstract class PokemonAI
yield break;
byte GetOppositeSide(byte side) => side == 0 ? (byte)1 : (byte)0;
}
public static List<PokemonAI> InstantiateAis(IDynamicLibrary library)
{
return AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes())
.Where(type => type.IsSubclassOf(typeof(PokemonAI)) && !type.IsAbstract).Select(x =>
{
var ctorWithLibrary = x.GetConstructor([typeof(IDynamicLibrary)]);
if (ctorWithLibrary != null)
return Activator.CreateInstance(x, library);
var defaultCtor = x.GetConstructor(Type.EmptyTypes);
if (defaultCtor != null)
return Activator.CreateInstance(x);
throw new InvalidOperationException($"No suitable constructor found for {x.Name}. " +
"Ensure it has a constructor with IDynamicLibrary parameter or a default constructor.");
}).Cast<PokemonAI>().ToList();
}
}

View File

@@ -1,3 +1,5 @@
using PkmnLib.Dynamic.AI.Explicit;
using PkmnLib.Dynamic.Plugins;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Static.Libraries;
@@ -41,6 +43,12 @@ public interface IDynamicLibrary
/// A holder of the script types that can be resolved by this library.
/// </summary>
ScriptResolver ScriptResolver { get; }
/// <summary>
/// The deterministic AI handlers provide a way to access the various handlers used by the
/// <see cref="ExplicitAI"/> to make decisions.
/// </summary>
IReadOnlyExplicitAIHandlers ExplicitAIHandlers { get; }
}
/// <inheritdoc />
@@ -55,18 +63,20 @@ public class DynamicLibraryImpl : IDynamicLibrary
var load = LibraryLoader.LoadPlugins(plugins);
return new DynamicLibraryImpl(load.StaticLibrary, load.Registry.BattleStatCalculator!,
load.Registry.DamageCalculator!, load.Registry.MiscLibrary!, load.Registry.CaptureLibrary!, load.Resolver);
load.Registry.DamageCalculator!, load.Registry.MiscLibrary!, load.Registry.CaptureLibrary!, load.Resolver,
load.Registry.ExplicitAIHandlers);
}
private DynamicLibraryImpl(IStaticLibrary staticLibrary, IBattleStatCalculator statCalculator,
IDamageCalculator damageCalculator, IMiscLibrary miscLibrary, ICaptureLibrary captureLibrary,
ScriptResolver scriptResolver)
ScriptResolver scriptResolver, IReadOnlyExplicitAIHandlers explicitAIHandlers)
{
StaticLibrary = staticLibrary;
StatCalculator = statCalculator;
DamageCalculator = damageCalculator;
MiscLibrary = miscLibrary;
ScriptResolver = scriptResolver;
ExplicitAIHandlers = explicitAIHandlers;
CaptureLibrary = captureLibrary;
}
@@ -87,4 +97,7 @@ public class DynamicLibraryImpl : IDynamicLibrary
/// <inheritdoc />
public ScriptResolver ScriptResolver { get; }
/// <inheritdoc />
public IReadOnlyExplicitAIHandlers ExplicitAIHandlers { get; }
}

View File

@@ -1,4 +1,5 @@
using PkmnLib.Dynamic.Libraries.DataLoaders;
using PkmnLib.Dynamic.Plugins;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Static.Libraries;

View File

@@ -220,6 +220,11 @@ public interface IPokemon : IScriptSource, IDeepCloneable
/// </summary>
ScriptContainer StatusScript { get; }
/// <summary>
/// The number of turns left for the current non-volatile status.
/// </summary>
int? GetStatusTurnsLeft { get; }
/// <summary>
/// The volatile status scripts of the Pokemon.
/// </summary>
@@ -782,6 +787,9 @@ public class PokemonImpl : ScriptSource, IPokemon
/// <inheritdoc />
public ScriptContainer StatusScript { get; } = new();
/// <inheritdoc />
public int? GetStatusTurnsLeft => (StatusScript.Script as IAIInfoScriptNumberTurnsLeft)?.TurnsLeft();
/// <inheritdoc />
public IScriptSet Volatile { get; }

View File

@@ -1,4 +1,6 @@
namespace PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Dynamic.ScriptHandling.Registry;
namespace PkmnLib.Dynamic.Plugins;
/// <summary>
/// A plugin is a way to register scripts and other components to the script registry.

View File

@@ -1,7 +1,7 @@
using PkmnLib.Dynamic.Libraries.DataLoaders.Models;
using PkmnLib.Static;
namespace PkmnLib.Dynamic.ScriptHandling.Registry;
namespace PkmnLib.Dynamic.Plugins;
/// <summary>
/// Interface for plugins that can mutate data.

View File

@@ -1,7 +1,7 @@
using System.Reflection;
using PkmnLib.Static.Libraries;
namespace PkmnLib.Dynamic.ScriptHandling.Registry;
namespace PkmnLib.Dynamic.Plugins;
/// <summary>
/// Interface for plugins that provide resources.

View File

@@ -1,5 +1,6 @@
using System.Linq.Expressions;
using System.Reflection;
using PkmnLib.Dynamic.AI.Explicit;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Static;
using PkmnLib.Static.Utils;
@@ -115,4 +116,5 @@ public class ScriptRegistry
internal IDamageCalculator? DamageCalculator => _damageCalculator;
internal IMiscLibrary? MiscLibrary => _miscLibrary;
internal ICaptureLibrary? CaptureLibrary => _captureLibrary;
public ExplicitAIHandlers ExplicitAIHandlers { get; } = new();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using PkmnLib.Dynamic.Models;
namespace PkmnLib.Dynamic.ScriptHandling;
// These interfaces are used for the AI to determine contextual information about the battle.
/// <summary>
/// Script for getting the number of turns left on a script by the AI.
/// </summary>
public interface IAIInfoScriptNumberTurnsLeft
{
/// <summary>
/// This function returns the number of turns left on the script. This is used for scripts that have a
/// limited number of turns, such as Sleep or Confusion.
/// </summary>
int TurnsLeft();
}
/// <summary>
/// Script for getting the expected end of turn damage by the AI.
/// </summary>
public interface IAIInfoScriptExpectedEndOfTurnDamage
{
/// <summary>
/// This function returns the expected end of turn damage for the script. This is used for scripts that
/// have an end of turn effect, such as Poison or Burn.
/// </summary>
void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage);
}

File diff suppressed because it is too large Load Diff