Initial setup for testing AI performance, random fixes
All checks were successful
Build / Build (push) Successful in 54s
All checks were successful
Build / Build (push) Successful in 54s
This commit is contained in:
parent
4499927551
commit
32aaa5150a
21
AI/AIRunner/AIRunner.csproj
Normal file
21
AI/AIRunner/AIRunner.csproj
Normal file
@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog"/>
|
||||
<PackageReference Include="Serilog.Sinks.Console"/>
|
||||
<PackageReference Include="System.CommandLine"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\PkmnLib.Dynamic\PkmnLib.Dynamic.csproj"/>
|
||||
<ProjectReference Include="..\..\Plugins\PkmnLib.Plugin.Gen7\PkmnLib.Plugin.Gen7.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
89
AI/AIRunner/Program.cs
Normal file
89
AI/AIRunner/Program.cs
Normal file
@ -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<PokemonAI>? _availableAIs;
|
||||
|
||||
private static Task<int> 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<PokemonAI>().ToList();
|
||||
|
||||
var testCommand = new Command("test", "Run two AIs against each other")
|
||||
{
|
||||
new Option<string>("--ai1")
|
||||
{
|
||||
Description = "The name of the first AI script to run against the second AI.",
|
||||
Required = true,
|
||||
Validators = { ValidateAI },
|
||||
},
|
||||
new Option<string>("--ai2")
|
||||
{
|
||||
Description = "The name of the second AI script to run against the first AI.",
|
||||
Required = true,
|
||||
Validators = { ValidateAI },
|
||||
},
|
||||
new Option<int>("--battles")
|
||||
{
|
||||
Description = "The number of battles to run between the two AIs.",
|
||||
Required = false,
|
||||
DefaultValueFactory = _ => 100,
|
||||
Validators =
|
||||
{
|
||||
result =>
|
||||
{
|
||||
if (result.GetValueOrDefault<int>() <= 0)
|
||||
{
|
||||
result.AddError("--battles must be a positive integer.");
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
testCommand.SetAction(result =>
|
||||
{
|
||||
var ai1Name = result.GetRequiredValue<string>("--ai1");
|
||||
var ai2Name = result.GetRequiredValue<string>("--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<int>("--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<string>();
|
||||
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))}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
144
AI/AIRunner/TestCommandRunner.cs
Normal file
144
AI/AIRunner/TestCommandRunner.cs
Normal file
@ -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<double>(battles);
|
||||
var results = new List<BattleResult>(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<double>(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<GetAndSetChoicesResult> 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);
|
||||
}
|
||||
}
|
@ -3,8 +3,12 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1"/>
|
||||
<PackageVersion Include="PcgRandom" Version="1.2.0"/>
|
||||
<PackageVersion Include="Serilog" Version="4.3.0"/>
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="8.0.0"/>
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta5.25306.1"/>
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.5"/>
|
||||
<PackageVersion Include="CSPath" Version="0.0.4"/>
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4"/>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -15,6 +15,12 @@ public interface IMiscLibrary
|
||||
/// </summary>
|
||||
ITurnChoice ReplacementChoice(IPokemon user, byte targetSide, byte targetPosition);
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the given choice is the choice that is used when the user is unable to make a move choice.
|
||||
/// (usually struggle).
|
||||
/// </summary>
|
||||
bool IsReplacementChoice(ITurnChoice choice);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current time of day for the battle.
|
||||
/// </summary>
|
||||
|
@ -66,6 +66,7 @@ public interface IBattle : IScriptSource, IDeepCloneable, IDisposable
|
||||
/// <summary>
|
||||
/// Whether the battle has ended.
|
||||
/// </summary>
|
||||
[MemberNotNull(nameof(Result))]
|
||||
bool HasEnded { get; }
|
||||
|
||||
/// <summary>
|
||||
@ -231,6 +232,7 @@ public class BattleImpl : ScriptSource, IBattle
|
||||
public IBattleRandom Random { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MemberNotNull(nameof(Result))]
|
||||
public bool HasEnded { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -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)
|
||||
|
@ -49,5 +49,6 @@ public class BattlePartyImpl : IBattleParty
|
||||
public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index);
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
@ -27,4 +27,25 @@ public class FleeTurnChoice : TurnChoice, IFleeChoice
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void CollectScripts(List<IEnumerable<ScriptContainer>> scripts) => User.CollectScripts(scripts);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"FleeChoice: {User}";
|
||||
|
||||
protected bool Equals(FleeTurnChoice other) => other.User == User;
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() =>
|
||||
User?.GetHashCode() ?? 0;
|
||||
}
|
@ -59,4 +59,28 @@ public class ItemChoice : TurnChoice, IItemChoice
|
||||
{
|
||||
User.CollectScripts(scripts);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(User, Item, TargetSide, TargetPosition);
|
||||
}
|
@ -105,4 +105,28 @@ public class MoveChoice : TurnChoice, IMoveChoice
|
||||
GetOwnScripts(scripts);
|
||||
User.CollectScripts(scripts);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(User, ChosenMove, TargetSide, TargetPosition);
|
||||
}
|
@ -30,4 +30,26 @@ public class PassChoice : TurnChoice, IPassChoice
|
||||
{
|
||||
User.CollectScripts(scripts);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() =>
|
||||
$"PassChoice: {User}";
|
||||
|
||||
protected bool Equals(PassChoice other) => other.User == User;
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() =>
|
||||
User?.GetHashCode() ?? 0;
|
||||
}
|
@ -38,4 +38,27 @@ public class SwitchChoice : TurnChoice, ISwitchChoice
|
||||
{
|
||||
User.CollectScripts(scripts);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() =>
|
||||
$"SwitchChoice: {User} -> {SwitchTo}";
|
||||
|
||||
protected bool Equals(SwitchChoice other) =>
|
||||
other.User == User && other.SwitchTo == SwitchTo;
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() =>
|
||||
User?.GetHashCode() ?? 0 ^ SwitchTo?.GetHashCode() ?? 0;
|
||||
}
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -43,12 +43,12 @@ public interface IPokemonParty : IReadOnlyList<IPokemon?>, IDeepCloneable
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class PokemonParty : IPokemonParty
|
||||
public class PokemonPartyImpl : IPokemonParty
|
||||
{
|
||||
private readonly IPokemon?[] _pokemon;
|
||||
|
||||
/// <inheritdoc cref="PokemonParty" />
|
||||
public PokemonParty(int size)
|
||||
/// <inheritdoc cref="PokemonPartyImpl" />
|
||||
public PokemonPartyImpl(int size)
|
||||
{
|
||||
_pokemon = new IPokemon[size];
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
@ -8,7 +9,7 @@ namespace PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
/// </summary>
|
||||
public static class ScriptUtils
|
||||
{
|
||||
private static readonly Dictionary<Type, (ScriptCategory category, StringKey name)> Cache = new();
|
||||
private static readonly ConcurrentDictionary<Type, (ScriptCategory category, StringKey name)> Cache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Resolve name from the <see cref="ScriptAttribute"/> of the given script.
|
||||
|
@ -97,7 +97,15 @@ public class ScriptSet : IScriptSet
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<ScriptContainer> GetEnumerator() => _scripts.GetEnumerator();
|
||||
public IEnumerator<ScriptContainer> GetEnumerator()
|
||||
{
|
||||
var currentIndex = 0;
|
||||
while (currentIndex < _scripts.Count)
|
||||
{
|
||||
yield return _scripts[currentIndex];
|
||||
currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -6,6 +6,14 @@
|
||||
<Platform Name="x64" />
|
||||
<Platform Name="x86" />
|
||||
</Configurations>
|
||||
<Folder Name="/AI/">
|
||||
<Project Path="AI\AIRunner\AIRunner.csproj" Type="Classic C#">
|
||||
<Configuration Solution="Debug|x64" Project="Debug|Any CPU" />
|
||||
<Configuration Solution="Debug|x86" Project="Debug|Any CPU" />
|
||||
<Configuration Solution="Release|x64" Project="Release|Any CPU" />
|
||||
<Configuration Solution="Release|x86" Project="Release|Any CPU" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/Plugins/">
|
||||
<Project Path="Plugins\PkmnLib.Plugin.Gen7.Tests\PkmnLib.Plugin.Gen7.Tests.csproj" Type="Classic C#" />
|
||||
<Project Path="Plugins\PkmnLib.Plugin.Gen7\PkmnLib.Plugin.Gen7.csproj" Type="Classic C#" />
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ public record ImmutableStatisticSet<T> where T : struct
|
||||
/// <summary>
|
||||
/// Gets a statistic from the set.
|
||||
/// </summary>
|
||||
public T GetStatistic(Statistic stat)
|
||||
public virtual T GetStatistic(Statistic stat)
|
||||
{
|
||||
return stat switch
|
||||
{
|
||||
@ -75,7 +75,7 @@ public record ImmutableStatisticSet<T> 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<T> : StatisticSet<T> 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<sbyte>
|
||||
yield return (Statistic.Accuracy, Accuracy);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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}"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all statistics to 0.
|
||||
/// </summary>
|
||||
|
@ -66,7 +66,7 @@ public class IntegrationTestRunner
|
||||
|
||||
var parties = await test.BattleSetup.Parties.SelectAsync(async Task<IBattleParty> (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];
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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}"),
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReplacementChoice(ITurnChoice choice) =>
|
||||
choice is MoveChoice moveChoice && moveChoice.ChosenMove.MoveData == _struggleData;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeOfDay GetTimeOfDay()
|
||||
{
|
||||
|
@ -11,6 +11,9 @@ public class WonderGuard : Script, IScriptBlockIncomingHit
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
|
@ -14,8 +14,8 @@ public class Encore : Script, IScriptOnSecondaryEffect
|
||||
|
||||
var currentTurn = battle.ChoiceQueue!.LastRanChoice;
|
||||
var lastMove = battle.PreviousTurnChoices.SelectMany(x => x).OfType<IMoveChoice>()
|
||||
.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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void ForceTurnSelection(IBattle battle, byte sideIndex, byte position, ref ITurnChoice? choice)
|
||||
{
|
||||
choice = new PassChoice(owner);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnEndTurn(IScriptSource _, IBattle battle)
|
||||
{
|
||||
RemoveSelf();
|
||||
}
|
||||
}
|
@ -61,6 +61,7 @@ public class WhirlpoolEffect : Script, IScriptOnEndTurn, IScriptPreventOpponentR
|
||||
if (_user == null)
|
||||
return;
|
||||
|
||||
List<PokemonTurn>? 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user