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:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user