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

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