diff --git a/AI/AIRunner/AIRunner.csproj b/AI/AIRunner/AIRunner.csproj
new file mode 100644
index 0000000..36b3bc9
--- /dev/null
+++ b/AI/AIRunner/AIRunner.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AI/AIRunner/Program.cs b/AI/AIRunner/Program.cs
new file mode 100644
index 0000000..034a302
--- /dev/null
+++ b/AI/AIRunner/Program.cs
@@ -0,0 +1,89 @@
+using System.CommandLine;
+using System.CommandLine.Parsing;
+using System.Reflection;
+using PkmnLib.Dynamic.AI;
+using Serilog;
+
+namespace AIRunner;
+
+internal static class Program
+{
+ private static List? _availableAIs;
+
+ private static Task Main(string[] args)
+ {
+ Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger();
+ Log.Information("Starting AI Runner...");
+ _availableAIs = AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes())
+ .Where(type => type.IsSubclassOf(typeof(PokemonAI)) && !type.IsAbstract).Select(Activator.CreateInstance)
+ .Cast().ToList();
+
+ var testCommand = new Command("test", "Run two AIs against each other")
+ {
+ new Option("--ai1")
+ {
+ Description = "The name of the first AI script to run against the second AI.",
+ Required = true,
+ Validators = { ValidateAI },
+ },
+ new Option("--ai2")
+ {
+ Description = "The name of the second AI script to run against the first AI.",
+ Required = true,
+ Validators = { ValidateAI },
+ },
+ new Option("--battles")
+ {
+ Description = "The number of battles to run between the two AIs.",
+ Required = false,
+ DefaultValueFactory = _ => 100,
+ Validators =
+ {
+ result =>
+ {
+ if (result.GetValueOrDefault() <= 0)
+ {
+ result.AddError("--battles must be a positive integer.");
+ }
+ },
+ },
+ },
+ };
+ testCommand.SetAction(result =>
+ {
+ var ai1Name = result.GetRequiredValue("--ai1");
+ var ai2Name = result.GetRequiredValue("--ai2");
+ var ai1 = _availableAIs!.First(a =>
+ string.Equals(a.Name, ai1Name, StringComparison.InvariantCultureIgnoreCase));
+ var ai2 = _availableAIs!.First(a =>
+ string.Equals(a.Name, ai2Name, StringComparison.InvariantCultureIgnoreCase));
+
+ return TestCommandRunner.RunTestCommand(ai1, ai2, result.GetRequiredValue("--battles"));
+ });
+
+ var rootCommand = new RootCommand("PkmnLib.NET AI Runner")
+ {
+ testCommand,
+ };
+ rootCommand.Description = "A tool to run AI scripts against each other in Pokémon battles.";
+ var parseResult = rootCommand.Parse(args);
+ return parseResult.InvokeAsync();
+
+ static void ValidateAI(OptionResult result)
+ {
+ var aiName = result.GetValueOrDefault();
+ if (string.IsNullOrWhiteSpace(aiName))
+ {
+ result.AddError("must be a non-empty string.");
+ return;
+ }
+
+ var ai = _availableAIs!.FirstOrDefault(a => a.Name == aiName);
+ if (ai == null)
+ {
+ result.AddError(
+ $"AI '{aiName}' not found. Available AIs: {string.Join(", ", _availableAIs!.Select(a => a.Name))}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AI/AIRunner/TestCommandRunner.cs b/AI/AIRunner/TestCommandRunner.cs
new file mode 100644
index 0000000..7419486
--- /dev/null
+++ b/AI/AIRunner/TestCommandRunner.cs
@@ -0,0 +1,144 @@
+using PkmnLib.Dynamic.AI;
+using PkmnLib.Dynamic.Libraries;
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Plugin.Gen7;
+using PkmnLib.Static.Species;
+using PkmnLib.Static.Utils;
+using Serilog;
+
+namespace AIRunner;
+
+public static class TestCommandRunner
+{
+ internal static async Task RunTestCommand(PokemonAI ai1, PokemonAI ai2, int battles)
+ {
+ var library = DynamicLibraryImpl.Create([
+ new Gen7Plugin(),
+ ]);
+
+ Log.Information("Running {Battles} battles between {AI1} and {AI2}", battles, ai1.Name, ai2.Name);
+ var averageTimePerTurnPerBattle = new List(battles);
+ var results = new List(battles);
+ var random = new RandomImpl();
+ // Here you would implement the logic to run the AI scripts against each other.
+ // This is a placeholder for demonstration purposes.
+ for (var i = 0; i < battles; i++)
+ {
+ Log.Information("Battle {BattleNumber}: {AI1} vs {AI2}", i + 1, ai1.Name, ai2.Name);
+ var battle = GenerateBattle(library, 3, random);
+ var timePerTurn = new List(20);
+ while (!battle.HasEnded)
+ {
+ var res = await GetAndSetChoices(battle, ai1, ai2);
+ timePerTurn.Add(res.MsPerTurn);
+ }
+ var result = battle.Result;
+ Log.Information("Battle {BattleNumber} ended with result: {Result}", i + 1, result);
+ averageTimePerTurnPerBattle.Add(timePerTurn.Average());
+ results.Add(result.Value);
+ }
+ Log.Information("All battles completed");
+ var averageTimePerTurn = averageTimePerTurnPerBattle.Average();
+ Log.Information("Average time per turn: {AverageTimePerTurn} ms", averageTimePerTurn);
+
+ var winCount1 = results.Count(x => x.WinningSide == 0);
+ var winCount2 = results.Count(x => x.WinningSide == 1);
+ var drawCount = results.Count(x => x.WinningSide == null);
+
+ Log.Information("Results: {AI1} wins: {WinCount1}, {AI2} wins: {WinCount2}, Draws: {DrawCount}", ai1.Name,
+ winCount1, ai2.Name, winCount2, drawCount);
+ }
+
+ private static PokemonPartyImpl GenerateParty(IDynamicLibrary library, int length, IRandom random)
+ {
+ var party = new PokemonPartyImpl(6);
+ for (var i = 0; i < length; i++)
+ {
+ var species = library.StaticLibrary.Species.GetRandom(random);
+ var nature = library.StaticLibrary.Natures.GetRandom(random);
+ const byte level = 50;
+ var defaultForm = species.GetDefaultForm();
+ var abilityIndex = (byte)random.GetInt(0, defaultForm.Abilities.Count);
+ var mon = new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
+ {
+ IsHidden = false,
+ Index = abilityIndex,
+ }, level, 0, species.GetRandomGender(random), 0, nature.Name);
+ var moves = defaultForm.Moves.GetDistinctLevelMoves().OrderBy(x => random.GetInt()).Take(4);
+ foreach (var move in moves)
+ mon.LearnMove(move, MoveLearnMethod.LevelUp, 255);
+
+ party.SwapInto(mon, i);
+ }
+ Log.Debug("Generated party: {Party}", party);
+ return party;
+ }
+
+ private static BattleImpl GenerateBattle(IDynamicLibrary library, int partyLength, IRandom random)
+ {
+ var parties = new[]
+ {
+ new BattlePartyImpl(GenerateParty(library, partyLength, random), [
+ new ResponsibleIndex(0, 0),
+ ]),
+ new BattlePartyImpl(GenerateParty(library, partyLength, random), [
+ new ResponsibleIndex(1, 0),
+ ]),
+ };
+ return new BattleImpl(library, parties, false, 2, 1, false, "test");
+ }
+
+ private record struct GetAndSetChoicesResult(double MsPerTurn);
+
+ private static async Task GetAndSetChoices(BattleImpl battle, PokemonAI ai1, PokemonAI ai2)
+ {
+ var pokemon1 = battle.Sides[0].Pokemon[0];
+ if (pokemon1 is null)
+ {
+ pokemon1 = battle.Parties[0].Party.WhereNotNull().FirstOrDefault(x => x.IsUsable);
+ if (pokemon1 is null)
+ throw new InvalidOperationException("No usable Pokémon found in party 1.");
+ battle.Sides[0].SwapPokemon(0, pokemon1);
+ }
+ var pokemon2 = battle.Sides[1].Pokemon[0];
+ if (pokemon2 is null)
+ {
+ pokemon2 = battle.Parties[1].Party.WhereNotNull().FirstOrDefault(x => x.IsUsable);
+ if (pokemon2 is null)
+ throw new InvalidOperationException("No usable Pokémon found in party 2.");
+ battle.Sides[1].SwapPokemon(0, pokemon2);
+ }
+
+ var taskAiOne = !battle.HasForcedTurn(pokemon1, out var choice1)
+ ? Task.Run(() => ai1.GetChoice(battle, pokemon1))
+ : Task.FromResult(choice1);
+ var taskAiTwo = !battle.HasForcedTurn(pokemon2, out var choice2)
+ ? Task.Run(() => ai2.GetChoice(battle, pokemon2))
+ : Task.FromResult(choice2);
+ await Task.WhenAll(taskAiOne, taskAiTwo);
+ choice1 = taskAiOne.Result;
+ choice2 = taskAiTwo.Result;
+ Log.Debug("Turn {Turn}: AI {AI1} choice: {Choice1}, AI {AI2} choice: {Choice2}", battle.CurrentTurnNumber,
+ ai1.Name, choice1, ai2.Name, choice2);
+ var startTime = DateTime.UtcNow;
+ if (!battle.TrySetChoice(choice1))
+ {
+ var replacementChoice = battle.Library.MiscLibrary.ReplacementChoice(pokemon1, 1, 0);
+ if (!battle.TrySetChoice(replacementChoice))
+ {
+ throw new InvalidOperationException($"AI {ai1.Name} failed to set a valid choice: {choice1}");
+ }
+ }
+ if (!battle.TrySetChoice(choice2))
+ {
+ var replacementChoice = battle.Library.MiscLibrary.ReplacementChoice(pokemon2, 0, 0);
+ if (!battle.TrySetChoice(replacementChoice))
+ {
+ throw new InvalidOperationException($"AI {ai2.Name} failed to set a valid choice: {choice2}");
+ }
+ }
+ var endTime = DateTime.UtcNow;
+ var msPerTurn = (endTime - startTime).TotalMilliseconds;
+ return new GetAndSetChoicesResult(msPerTurn);
+ }
+}
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index be140aa..582e4fe 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,8 +3,12 @@
true
+
+
+
+
diff --git a/PkmnLib.Dynamic/AI/RandomAI.cs b/PkmnLib.Dynamic/AI/RandomAI.cs
index f971683..1d9fac5 100644
--- a/PkmnLib.Dynamic/AI/RandomAI.cs
+++ b/PkmnLib.Dynamic/AI/RandomAI.cs
@@ -43,6 +43,7 @@ public class RandomAI : PokemonAI
}
moves.Remove(move);
}
- return new PassChoice(pokemon);
+ return battle.Library.MiscLibrary.ReplacementChoice(pokemon,
+ pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0, pokemon.BattleData.Position);
}
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Libraries/MiscLibrary.cs b/PkmnLib.Dynamic/Libraries/MiscLibrary.cs
index aa433f1..8178190 100644
--- a/PkmnLib.Dynamic/Libraries/MiscLibrary.cs
+++ b/PkmnLib.Dynamic/Libraries/MiscLibrary.cs
@@ -15,6 +15,12 @@ public interface IMiscLibrary
///
ITurnChoice ReplacementChoice(IPokemon user, byte targetSide, byte targetPosition);
+ ///
+ /// Returns whether the given choice is the choice that is used when the user is unable to make a move choice.
+ /// (usually struggle).
+ ///
+ bool IsReplacementChoice(ITurnChoice choice);
+
///
/// Gets the current time of day for the battle.
///
diff --git a/PkmnLib.Dynamic/Models/Battle.cs b/PkmnLib.Dynamic/Models/Battle.cs
index 561158f..977d938 100644
--- a/PkmnLib.Dynamic/Models/Battle.cs
+++ b/PkmnLib.Dynamic/Models/Battle.cs
@@ -66,6 +66,7 @@ public interface IBattle : IScriptSource, IDeepCloneable, IDisposable
///
/// Whether the battle has ended.
///
+ [MemberNotNull(nameof(Result))]
bool HasEnded { get; }
///
@@ -231,6 +232,7 @@ public class BattleImpl : ScriptSource, IBattle
public IBattleRandom Random { get; }
///
+ [MemberNotNull(nameof(Result))]
public bool HasEnded { get; private set; }
///
@@ -320,7 +322,10 @@ public class BattleImpl : ScriptSource, IBattle
{
if (!choice.User.IsUsable)
return false;
- if (HasForcedTurn(choice.User, out var forcedChoice) && choice != forcedChoice)
+ // 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))
return false;
if (choice is IMoveChoice moveChoice)
diff --git a/PkmnLib.Dynamic/Models/BattleParty.cs b/PkmnLib.Dynamic/Models/BattleParty.cs
index 93f2701..b7ee89b 100644
--- a/PkmnLib.Dynamic/Models/BattleParty.cs
+++ b/PkmnLib.Dynamic/Models/BattleParty.cs
@@ -49,5 +49,6 @@ public class BattlePartyImpl : IBattleParty
public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index);
///
- public bool HasPokemonNotInField() => Party.Any(x => x is { IsUsable: true, BattleData.IsOnBattlefield: false });
+ public bool HasPokemonNotInField() =>
+ Party.WhereNotNull().Any(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true);
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs b/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs
index 30d8e09..6562ae7 100644
--- a/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs
@@ -27,4 +27,25 @@ public class FleeTurnChoice : TurnChoice, IFleeChoice
///
public override void CollectScripts(List> scripts) => User.CollectScripts(scripts);
+
+ ///
+ public override string ToString() => $"FleeChoice: {User}";
+
+ protected bool Equals(FleeTurnChoice other) => other.User == User;
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj is null)
+ return false;
+ if (ReferenceEquals(this, obj))
+ return true;
+ if (obj is not FleeTurnChoice other)
+ return false;
+ return Equals(other);
+ }
+
+ ///
+ public override int GetHashCode() =>
+ User?.GetHashCode() ?? 0;
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs b/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs
index e4ff6da..e166833 100644
--- a/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs
@@ -59,4 +59,28 @@ public class ItemChoice : TurnChoice, IItemChoice
{
User.CollectScripts(scripts);
}
+
+ ///
+ public override string ToString() =>
+ $"ItemChoice: User: {User}, Item: {Item.Name}, TargetSide: {TargetSide}, TargetPosition: {TargetPosition}";
+
+ protected bool Equals(ItemChoice other) =>
+ other.User == User && other.Item.Equals(Item) && other.TargetSide == TargetSide &&
+ other.TargetPosition == TargetPosition;
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj is null)
+ return false;
+ if (ReferenceEquals(this, obj))
+ return true;
+ if (obj is not ItemChoice other)
+ return false;
+ return Equals(other);
+ }
+
+ ///
+ public override int GetHashCode() =>
+ HashCode.Combine(User, Item, TargetSide, TargetPosition);
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs b/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs
index 28aba15..431a691 100644
--- a/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs
@@ -105,4 +105,28 @@ public class MoveChoice : TurnChoice, IMoveChoice
GetOwnScripts(scripts);
User.CollectScripts(scripts);
}
+
+ ///
+ public override string ToString() =>
+ $"MoveChoice: {ChosenMove.MoveData.Name}, Target: {TargetSide}:{TargetPosition}, User: {User}";
+
+ protected bool Equals(MoveChoice other) =>
+ other.User == User && other.ChosenMove.Equals(ChosenMove) && other.TargetSide == TargetSide &&
+ other.TargetPosition == TargetPosition;
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj is null)
+ return false;
+ if (ReferenceEquals(this, obj))
+ return true;
+ if (obj is not MoveChoice other)
+ return false;
+ return Equals(other);
+ }
+
+ ///
+ public override int GetHashCode() =>
+ HashCode.Combine(User, ChosenMove, TargetSide, TargetPosition);
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Models/Choices/PassChoice.cs b/PkmnLib.Dynamic/Models/Choices/PassChoice.cs
index 5290fd3..4879e71 100644
--- a/PkmnLib.Dynamic/Models/Choices/PassChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/PassChoice.cs
@@ -30,4 +30,26 @@ public class PassChoice : TurnChoice, IPassChoice
{
User.CollectScripts(scripts);
}
+
+ ///
+ public override string ToString() =>
+ $"PassChoice: {User}";
+
+ protected bool Equals(PassChoice other) => other.User == User;
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj is null)
+ return false;
+ if (ReferenceEquals(this, obj))
+ return true;
+ if (obj is not PassChoice other)
+ return false;
+ return Equals(other);
+ }
+
+ ///
+ public override int GetHashCode() =>
+ User?.GetHashCode() ?? 0;
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs b/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs
index 538aaf0..b2acd2d 100644
--- a/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs
@@ -38,4 +38,27 @@ public class SwitchChoice : TurnChoice, ISwitchChoice
{
User.CollectScripts(scripts);
}
+
+ ///
+ public override string ToString() =>
+ $"SwitchChoice: {User} -> {SwitchTo}";
+
+ protected bool Equals(SwitchChoice other) =>
+ other.User == User && other.SwitchTo == SwitchTo;
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj is null)
+ return false;
+ if (ReferenceEquals(this, obj))
+ return true;
+ if (obj is not SwitchChoice other)
+ return false;
+ return Equals(other);
+ }
+
+ ///
+ public override int GetHashCode() =>
+ User?.GetHashCode() ?? 0 ^ SwitchTo?.GetHashCode() ?? 0;
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Models/LearnedMove.cs b/PkmnLib.Dynamic/Models/LearnedMove.cs
index 2cfb231..490d323 100644
--- a/PkmnLib.Dynamic/Models/LearnedMove.cs
+++ b/PkmnLib.Dynamic/Models/LearnedMove.cs
@@ -138,7 +138,10 @@ public class LearnedMoveImpl : ILearnedMove
public bool TryUse(byte amount = 1)
{
if (CurrentPp < amount)
+ {
+ CurrentPp = 0;
return false;
+ }
CurrentPp -= amount;
return true;
diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs
index 6a3c19c..77bd337 100644
--- a/PkmnLib.Dynamic/Models/Pokemon.cs
+++ b/PkmnLib.Dynamic/Models/Pokemon.cs
@@ -904,7 +904,7 @@ public class PokemonImpl : ScriptSource, IPokemon
changed = change switch
{
> 0 => StatBoost.IncreaseStatistic(stat, change),
- < 0 => StatBoost.DecreaseStatistic(stat, change),
+ < 0 => StatBoost.DecreaseStatistic(stat, (sbyte)-change),
_ => changed,
};
if (!changed)
@@ -1003,7 +1003,21 @@ public class PokemonImpl : ScriptSource, IPokemon
Form = form;
Types = form.Types.ToList();
HeightInMeters = form.Height;
- var newAbility = Form.GetAbility(AbilityIndex);
+ var abilityIndex = AbilityIndex;
+ abilityIndex = AbilityIndex.IsHidden switch
+ {
+ true when form.HiddenAbilities.Count <= abilityIndex.Index => new AbilityIndex
+ {
+ IsHidden = true, Index = (byte)(form.HiddenAbilities.Count - 1),
+ },
+ false when form.Abilities.Count <= abilityIndex.Index => new AbilityIndex
+ {
+ IsHidden = false, Index = (byte)(form.Abilities.Count - 1),
+ },
+ _ => abilityIndex,
+ };
+
+ var newAbility = Form.GetAbility(abilityIndex);
if (OverrideAbility == null && oldAbility != newAbility)
{
@@ -1122,6 +1136,7 @@ public class PokemonImpl : ScriptSource, IPokemon
BattleData.Battle.Sides[BattleData.SideIndex].MarkPositionAsUnfillable(BattleData.Position);
}
BattleData.BattleSide.MarkFaint(BattleData.Position);
+ BattleData.BattleSide.ForceClearPokemonFromField(BattleData.Position);
// Validate the battle state to see if the battle is over.
BattleData.Battle.ValidateBattleState();
diff --git a/PkmnLib.Dynamic/Models/PokemonParty.cs b/PkmnLib.Dynamic/Models/PokemonPartyImpl.cs
similarity index 95%
rename from PkmnLib.Dynamic/Models/PokemonParty.cs
rename to PkmnLib.Dynamic/Models/PokemonPartyImpl.cs
index 13b903b..9bd5101 100644
--- a/PkmnLib.Dynamic/Models/PokemonParty.cs
+++ b/PkmnLib.Dynamic/Models/PokemonPartyImpl.cs
@@ -43,12 +43,12 @@ public interface IPokemonParty : IReadOnlyList, IDeepCloneable
}
///
-public class PokemonParty : IPokemonParty
+public class PokemonPartyImpl : IPokemonParty
{
private readonly IPokemon?[] _pokemon;
- ///
- public PokemonParty(int size)
+ ///
+ public PokemonPartyImpl(int size)
{
_pokemon = new IPokemon[size];
}
diff --git a/PkmnLib.Dynamic/ScriptHandling/Registry/ScriptUtils.cs b/PkmnLib.Dynamic/ScriptHandling/Registry/ScriptUtils.cs
index 6d0bb98..d35cfe2 100644
--- a/PkmnLib.Dynamic/ScriptHandling/Registry/ScriptUtils.cs
+++ b/PkmnLib.Dynamic/ScriptHandling/Registry/ScriptUtils.cs
@@ -1,3 +1,4 @@
+using System.Collections.Concurrent;
using System.Reflection;
using PkmnLib.Static.Utils;
@@ -8,7 +9,7 @@ namespace PkmnLib.Dynamic.ScriptHandling.Registry;
///
public static class ScriptUtils
{
- private static readonly Dictionary Cache = new();
+ private static readonly ConcurrentDictionary Cache = new();
///
/// Resolve name from the of the given script.
diff --git a/PkmnLib.Dynamic/ScriptHandling/ScriptSet.cs b/PkmnLib.Dynamic/ScriptHandling/ScriptSet.cs
index d88a46a..efbbdb8 100644
--- a/PkmnLib.Dynamic/ScriptHandling/ScriptSet.cs
+++ b/PkmnLib.Dynamic/ScriptHandling/ScriptSet.cs
@@ -97,7 +97,15 @@ public class ScriptSet : IScriptSet
}
///
- public IEnumerator GetEnumerator() => _scripts.GetEnumerator();
+ public IEnumerator GetEnumerator()
+ {
+ var currentIndex = 0;
+ while (currentIndex < _scripts.Count)
+ {
+ yield return _scripts[currentIndex];
+ currentIndex++;
+ }
+ }
///
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
@@ -176,7 +184,7 @@ public class ScriptSet : IScriptSet
if (script is null)
return;
script.Script?.OnRemove();
- _scripts.Remove(script);
+ script.Clear();
}
///
diff --git a/PkmnLib.NET.slnx b/PkmnLib.NET.slnx
index 6603061..6c370fa 100644
--- a/PkmnLib.NET.slnx
+++ b/PkmnLib.NET.slnx
@@ -6,6 +6,14 @@
+
+
+
+
+
+
+
+
diff --git a/PkmnLib.Static/Libraries/TypeLibrary.cs b/PkmnLib.Static/Libraries/TypeLibrary.cs
index 85c924d..f9fffcc 100644
--- a/PkmnLib.Static/Libraries/TypeLibrary.cs
+++ b/PkmnLib.Static/Libraries/TypeLibrary.cs
@@ -69,9 +69,9 @@ public class TypeLibrary : IReadOnlyTypeLibrary
public float GetSingleEffectiveness(TypeIdentifier attacking, TypeIdentifier defending)
{
if (attacking.Value < 1 || attacking.Value > _effectiveness.Count)
- throw new ArgumentOutOfRangeException(nameof(attacking));
+ return 1;
if (defending.Value < 1 || defending.Value > _effectiveness.Count)
- throw new ArgumentOutOfRangeException(nameof(defending));
+ return 1;
return _effectiveness[attacking.Value - 1][defending.Value - 1];
}
diff --git a/PkmnLib.Static/StatisticSet.cs b/PkmnLib.Static/StatisticSet.cs
index 7262878..1062172 100644
--- a/PkmnLib.Static/StatisticSet.cs
+++ b/PkmnLib.Static/StatisticSet.cs
@@ -65,7 +65,7 @@ public record ImmutableStatisticSet where T : struct
///
/// Gets a statistic from the set.
///
- public T GetStatistic(Statistic stat)
+ public virtual T GetStatistic(Statistic stat)
{
return stat switch
{
@@ -75,7 +75,7 @@ public record ImmutableStatisticSet where T : struct
Statistic.SpecialAttack => SpecialAttack,
Statistic.SpecialDefense => SpecialDefense,
Statistic.Speed => Speed,
- _ => throw new ArgumentException("Invalid statistic."),
+ _ => throw new ArgumentException($"Invalid statistic {stat}"),
};
}
}
@@ -308,7 +308,7 @@ public abstract record ClampedStatisticSet : StatisticSet where T : struct
var current = GetStatistic(stat);
var newValue = Subtract(current, value);
if (newValue.CompareTo(Min) < 0)
- value = Subtract(current, Min);
+ value = Add(Min, current);
if (value.CompareTo(default) == 0)
return false;
return base.DecreaseStatistic(stat, value);
@@ -400,6 +400,23 @@ public record StatBoostStatisticSet : ClampedStatisticSet
yield return (Statistic.Accuracy, Accuracy);
}
+ ///
+ public override sbyte GetStatistic(Statistic stat)
+ {
+ return stat switch
+ {
+ Statistic.Hp => Hp,
+ Statistic.Attack => Attack,
+ Statistic.Defense => Defense,
+ Statistic.SpecialAttack => SpecialAttack,
+ Statistic.SpecialDefense => SpecialDefense,
+ Statistic.Speed => Speed,
+ Statistic.Evasion => Evasion,
+ Statistic.Accuracy => Accuracy,
+ _ => throw new ArgumentException($"Invalid statistic {stat}"),
+ };
+ }
+
///
/// Resets all statistics to 0.
///
diff --git a/PkmnLib.Tests/Integration/IntegrationTestRunner.cs b/PkmnLib.Tests/Integration/IntegrationTestRunner.cs
index dccd86e..fd2679f 100644
--- a/PkmnLib.Tests/Integration/IntegrationTestRunner.cs
+++ b/PkmnLib.Tests/Integration/IntegrationTestRunner.cs
@@ -66,7 +66,7 @@ public class IntegrationTestRunner
var parties = await test.BattleSetup.Parties.SelectAsync(async Task (x) =>
{
- var party = new PokemonParty(6);
+ var party = new PokemonPartyImpl(6);
for (var index = 0; index < x.Pokemon.Length; index++)
{
var pokemon = x.Pokemon[index];
diff --git a/PkmnLib.Tests/Static/DeepCloneTests.cs b/PkmnLib.Tests/Static/DeepCloneTests.cs
index a357468..570c868 100644
--- a/PkmnLib.Tests/Static/DeepCloneTests.cs
+++ b/PkmnLib.Tests/Static/DeepCloneTests.cs
@@ -90,13 +90,13 @@ public class DeepCloneTests
var library = LibraryHelpers.LoadLibrary();
await Assert.That(library.StaticLibrary.Species.TryGet("bulbasaur", out var bulbasaur)).IsTrue();
await Assert.That(library.StaticLibrary.Species.TryGet("charmander", out var charmander)).IsTrue();
- var party1 = new PokemonParty(6);
+ var party1 = new PokemonPartyImpl(6);
party1.SwapInto(new PokemonImpl(library, bulbasaur!, bulbasaur!.GetDefaultForm(), new AbilityIndex
{
IsHidden = false,
Index = 0,
}, 50, 0, Gender.Male, 0, "hardy"), 0);
- var party2 = new PokemonParty(6);
+ var party2 = new PokemonPartyImpl(6);
party2.SwapInto(new PokemonImpl(library, charmander!, charmander!.GetDefaultForm(), new AbilityIndex
{
IsHidden = false,
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Data/Abilities.jsonc b/Plugins/PkmnLib.Plugin.Gen7/Data/Abilities.jsonc
index 77de6d9..03fdec2 100755
--- a/Plugins/PkmnLib.Plugin.Gen7/Data/Abilities.jsonc
+++ b/Plugins/PkmnLib.Plugin.Gen7/Data/Abilities.jsonc
@@ -684,6 +684,13 @@
"tinted_lens": {
"effect": "tinted_lens"
},
+ "torrent": {
+ "effect": "power_up_type_at_low_health",
+ "parameters": {
+ "type": "water",
+ "threshold": 0.33333
+ }
+ },
"tough_claws": {
"effect": "tough_claws"
},
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Data/Moves.jsonc b/Plugins/PkmnLib.Plugin.Gen7/Data/Moves.jsonc
index 25dc2e1..f88a312 100755
--- a/Plugins/PkmnLib.Plugin.Gen7/Data/Moves.jsonc
+++ b/Plugins/PkmnLib.Plugin.Gen7/Data/Moves.jsonc
@@ -11852,6 +11852,25 @@
}
}
},
+ {
+ "name": "thunder_fang",
+ "type": "electric",
+ "power": 65,
+ "pp": 15,
+ "accuracy": 95,
+ "priority": 0,
+ "target": "Any",
+ "category": "physical",
+ "flags": [
+ "contact",
+ "protect",
+ "mirror",
+ "bite"
+ ],
+ "effect": {
+ "name": "thunder_fang"
+ }
+ },
{
"name": "thunder_punch",
"type": "electric",
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7BattleStatCalculator.cs b/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7BattleStatCalculator.cs
index ffde538..2520c2c 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7BattleStatCalculator.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7BattleStatCalculator.cs
@@ -132,7 +132,7 @@ public class Gen7BattleStatCalculator : IBattleStatCalculator
4 => 6.0f / 2.0f,
5 => 7.0f / 2.0f,
6 => 8.0f / 2.0f,
- _ => throw new ArgumentException("Stat boost was out of expected range of -6 to 6"),
+ _ => throw new ArgumentException($"Stat boost was out of expected range of -6 to 6: {boost}"),
};
}
}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7MiscLibrary.cs b/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7MiscLibrary.cs
index cb5dc5f..c9314f5 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7MiscLibrary.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7MiscLibrary.cs
@@ -14,6 +14,10 @@ public class Gen7MiscLibrary : IMiscLibrary
public ITurnChoice ReplacementChoice(IPokemon user, byte targetSide, byte targetPosition) =>
new MoveChoice(user, new LearnedMoveImpl(_struggleData, MoveLearnMethod.Unknown), targetSide, targetPosition);
+ ///
+ public bool IsReplacementChoice(ITurnChoice choice) =>
+ choice is MoveChoice moveChoice && moveChoice.ChosenMove.MoveData == _struggleData;
+
///
public TimeOfDay GetTimeOfDay()
{
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/WonderGuard.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/WonderGuard.cs
index 65a7e9d..4b116d8 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/WonderGuard.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/WonderGuard.cs
@@ -11,6 +11,9 @@ public class WonderGuard : Script, IScriptBlockIncomingHit
///
public void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block)
{
+ var type = executingMove.GetHitData(target, hitIndex).Type;
+ if (type is null || type.Value.Value == 0)
+ return;
var effectiveness = executingMove.GetHitData(target, hitIndex).Effectiveness;
if (!(effectiveness <= 1.0))
return;
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Encore.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Encore.cs
index 761bb49..dff7faa 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Encore.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Encore.cs
@@ -14,8 +14,8 @@ public class Encore : Script, IScriptOnSecondaryEffect
var currentTurn = battle.ChoiceQueue!.LastRanChoice;
var lastMove = battle.PreviousTurnChoices.SelectMany(x => x).OfType()
- .TakeWhile(x => x != currentTurn).LastOrDefault(x => x.User == target);
- if (lastMove == null)
+ .TakeWhile(x => !Equals(x, currentTurn)).LastOrDefault(x => x.User == target);
+ if (lastMove == null || battle.Library.MiscLibrary.IsReplacementChoice(lastMove))
{
move.GetHitData(target, hit).Fail();
return;
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BideEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BideEffect.cs
index 5c400a1..05c3c40 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BideEffect.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BideEffect.cs
@@ -48,7 +48,6 @@ public class BideEffect : Script, IScriptForceTurnSelection, IScriptOnIncomingHi
choice = _choice;
return;
}
- var opposingSideIndex = (byte)(_owner.BattleData?.SideIndex == 0 ? 1 : 0);
- choice = _choice = TurnChoiceHelper.CreateMoveChoice(_owner, "bide", opposingSideIndex, position);
+ choice = _choice = TurnChoiceHelper.CreateMoveChoice(_owner, "bide", ownerBattleData.SideIndex, position);
}
}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/FreezeShockEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/FreezeShockEffect.cs
index 3dbf6f4..2ad4585 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/FreezeShockEffect.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/FreezeShockEffect.cs
@@ -1,3 +1,4 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
+[Script(ScriptCategory.Pokemon, "require_charge")]
public class RequireChargeEffect(IPokemon owner, StringKey moveName) : BaseChargeEffect(owner, moveName);
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/TruantEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/TruantEffect.cs
index edd270c..db85701 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/TruantEffect.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/TruantEffect.cs
@@ -1,10 +1,17 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
-public class TruantEffect(IPokemon owner) : Script, IScriptForceTurnSelection
+[Script(ScriptCategory.Pokemon, "truant_effect")]
+public class TruantEffect(IPokemon owner) : Script, IScriptForceTurnSelection, IScriptOnEndTurn
{
///
public void ForceTurnSelection(IBattle battle, byte sideIndex, byte position, ref ITurnChoice? choice)
{
choice = new PassChoice(owner);
}
+
+ ///
+ public void OnEndTurn(IScriptSource _, IBattle battle)
+ {
+ RemoveSelf();
+ }
}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/WhirlpoolEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/WhirlpoolEffect.cs
index 4a48c84..02da252 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/WhirlpoolEffect.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/WhirlpoolEffect.cs
@@ -61,6 +61,7 @@ public class WhirlpoolEffect : Script, IScriptOnEndTurn, IScriptPreventOpponentR
if (_user == null)
return;
+ List? pokemonToRemove = null;
foreach (var pokemonTurn in _targetedPokemon.Where(x => x.Pokemon.BattleData?.IsOnBattlefield == true))
{
var pokemon = pokemonTurn.Pokemon;
@@ -68,8 +69,15 @@ public class WhirlpoolEffect : Script, IScriptOnEndTurn, IScriptPreventOpponentR
pokemonTurn.Turns--;
if (pokemonTurn.Turns <= 0)
{
- _targetedPokemon.Remove(pokemonTurn);
+ pokemonToRemove ??= [];
+ pokemonToRemove.Add(pokemonTurn);
}
}
+ if (pokemonToRemove == null)
+ return;
+ foreach (var turn in pokemonToRemove)
+ {
+ _targetedPokemon.Remove(turn);
+ }
}
}
\ No newline at end of file