87 lines
3.6 KiB
C#
87 lines
3.6 KiB
C#
using PkmnLib.Dynamic.Models;
|
|
using PkmnLib.Dynamic.Models.Choices;
|
|
using PkmnLib.Static.Utils;
|
|
|
|
namespace PkmnLib.Dynamic.AI;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class PrescientAI : PokemonAI
|
|
{
|
|
private static readonly PokemonAI OpponentAI = new HighestDamageAI();
|
|
|
|
/// <inheritdoc />
|
|
public PrescientAI() : base("Prescient")
|
|
{
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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<ILearnedMove> 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);
|
|
} |