Some initial work on prescient AI, AI runner, and some random fixes
All checks were successful
Build / Build (push) Successful in 1m3s
All checks were successful
Build / Build (push) Successful in 1m3s
This commit is contained in:
@@ -5,6 +5,9 @@ using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.AI;
|
||||
|
||||
/// <summary>
|
||||
/// HighestDamageAI is an AI that selects the move that it expects to deal the highest damage.
|
||||
/// </summary>
|
||||
public class HighestDamageAI : PokemonAI
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
87
PkmnLib.Dynamic/AI/PrescientAI.cs
Normal file
87
PkmnLib.Dynamic/AI/PrescientAI.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
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);
|
||||
}
|
||||
@@ -325,7 +325,7 @@ public class BattleImpl : ScriptSource, IBattle
|
||||
// Always allow moves such as Struggle. If we block this, we can run into an infinite loop
|
||||
if (Library.MiscLibrary.IsReplacementChoice(choice))
|
||||
return true;
|
||||
if (HasForcedTurn(choice.User, out var forcedChoice) && !Equals(choice, forcedChoice))
|
||||
if (HasForcedTurn(choice.User, out var forcedChoice) && !IsValidForForcedTurn(forcedChoice, choice))
|
||||
return false;
|
||||
|
||||
if (choice is IMoveChoice moveChoice)
|
||||
@@ -343,6 +343,20 @@ public class BattleImpl : ScriptSource, IBattle
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
bool IsValidForForcedTurn(ITurnChoice forcedChoice, ITurnChoice choiceToCheck)
|
||||
{
|
||||
// If the forced choice is a move choice, we can only use it if the move is the same
|
||||
if (forcedChoice is IMoveChoice forcedMove && choiceToCheck is IMoveChoice moveChoice)
|
||||
{
|
||||
return forcedMove.ChosenMove.MoveData.Name == moveChoice.ChosenMove.MoveData.Name;
|
||||
}
|
||||
if (forcedChoice is IPassChoice && choiceToCheck is IPassChoice)
|
||||
{
|
||||
return true; // Both are pass choices, so they are valid
|
||||
}
|
||||
return forcedChoice.Equals(choiceToCheck);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -155,7 +155,8 @@ public static class ScriptExecution
|
||||
Action<TScriptHook> hook)
|
||||
{
|
||||
List<ScriptCategory>? suppressedCategories = null;
|
||||
foreach (var container in source.SelectMany(x => x))
|
||||
var iterator = new ScriptIterator(source);
|
||||
foreach (var container in iterator)
|
||||
{
|
||||
if (container.IsEmpty)
|
||||
continue;
|
||||
@@ -163,7 +164,7 @@ public static class ScriptExecution
|
||||
if (script is IScriptOnBeforeAnyHookInvoked onBeforeAnyHookInvoked)
|
||||
onBeforeAnyHookInvoked.OnBeforeAnyHookInvoked(ref suppressedCategories);
|
||||
}
|
||||
foreach (var container in source.SelectMany(x => x))
|
||||
foreach (var container in iterator)
|
||||
{
|
||||
if (container.IsEmpty)
|
||||
continue;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace PkmnLib.Dynamic.ScriptHandling;
|
||||
/// We can add, remove, and clear scripts from the set.
|
||||
/// This is generally used for volatile scripts.
|
||||
/// </summary>
|
||||
public interface IScriptSet : IEnumerable<ScriptContainer>
|
||||
public interface IScriptSet : IEnumerable<ScriptContainer>, IDeepCloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a script to the set. If the script with that name already exists in this set, this
|
||||
@@ -97,15 +97,7 @@ public class ScriptSet : IScriptSet
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<ScriptContainer> GetEnumerator()
|
||||
{
|
||||
var currentIndex = 0;
|
||||
while (currentIndex < _scripts.Count)
|
||||
{
|
||||
yield return _scripts[currentIndex];
|
||||
currentIndex++;
|
||||
}
|
||||
}
|
||||
public IEnumerator<ScriptContainer> GetEnumerator() => _scripts.GetEnumerator();
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
Reference in New Issue
Block a user