using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Static.Utils; namespace PkmnLib.Dynamic.AI; /// /// PrescientAI is an AI that predicts the best move based on the current state of the battle. /// This is slightly cheaty, as it simulates the battle with each possible move to find the best one. /// public class PrescientAI : PokemonAI { private static readonly PokemonAI OpponentAI = new HighestDamageAI(); /// public PrescientAI() : base("Prescient") { } /// public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon) { var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0; var moves = pokemon.Moves.WhereNotNull().Where(x => battle.CanUse(new MoveChoice(pokemon, x, opponentSide, 0))) .ToList(); var choices = ScoreChoices(battle, moves, pokemon).OrderByDescending(x => x.Score).ToList(); if (choices.Count == 0) { return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, 0); } var bestChoice = choices.First().Choice; return bestChoice; } private static IEnumerable<(ITurnChoice Choice, float Score)> ScoreChoices(IBattle battle, IReadOnlyList moves, IPokemon pokemon) { var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0; foreach (var learnedMoveOriginal in moves.WhereNotNull()) { var battleClone = battle.DeepClone(); var pokemonClone = battleClone.Sides[pokemon.BattleData!.SideIndex].Pokemon[pokemon.BattleData.Position]!; var learnedMove = pokemonClone.Moves.WhereNotNull() .First(m => m.MoveData.Name == learnedMoveOriginal.MoveData.Name); var choice = new MoveChoice(pokemonClone, learnedMove, opponentSide, 0); var opponentChoice = GetOpponentChoice(battleClone, pokemonClone); if (!battleClone.TrySetChoice(opponentChoice)) { var replacementChoice = battleClone.Library.MiscLibrary.ReplacementChoice(pokemonClone, opponentSide, 0); if (!battleClone.TrySetChoice(replacementChoice)) { throw new InvalidOperationException( "Could not set opponent choice or replacement choice in battle clone."); } } if (battleClone.TrySetChoice(choice)) { var score = CalculateScore(battleClone.Parties[pokemon.BattleData.SideIndex], battleClone.Parties[opponentSide]); var realChoice = new MoveChoice(pokemon, learnedMoveOriginal, opponentSide, 0); yield return (realChoice, score); } } } private static ITurnChoice GetOpponentChoice(IBattle battle, IPokemon pokemon) { var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0; var opponent = battle.Sides[opponentSide].Pokemon[0]; if (opponent is null) { throw new InvalidOperationException("Opponent Pokemon is null."); } if (battle.HasForcedTurn(opponent, out var forcedChoice)) { return forcedChoice; } return OpponentAI.GetChoice(battle, opponent); } private static float CalculateScore(IBattleParty ownParty, IBattleParty opponentParty) => ownParty.Party.WhereNotNull().Sum(x => x.CurrentHealth / (float)x.MaxHealth) - opponentParty.Party.WhereNotNull().Sum(x => x.CurrentHealth / (float)x.MaxHealth); }