Some initial work on prescient AI, AI runner, and some random fixes
All checks were successful
Build / Build (push) Successful in 1m3s

This commit is contained in:
2025-07-05 17:48:51 +02:00
parent 7b25161a8d
commit d57076374f
9 changed files with 174 additions and 21 deletions

View File

@@ -8,8 +8,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="iluvadev.ConsoleProgressBar"/>
<PackageReference Include="Serilog"/>
<PackageReference Include="Serilog.Sinks.Console"/>
<PackageReference Include="ShellProgressBar"/>
<PackageReference Include="System.CommandLine"/>
</ItemGroup>

View File

@@ -6,6 +6,8 @@ using PkmnLib.Plugin.Gen7;
using PkmnLib.Static.Species;
using PkmnLib.Static.Utils;
using Serilog;
using Serilog.Events;
using ShellProgressBar;
namespace AIRunner;
@@ -20,6 +22,7 @@ public static class TestCommandRunner
Log.Information("Running {Battles} battles between {AI1} and {AI2}", battles, ai1.Name, ai2.Name);
var averageTimePerTurnPerBattle = new List<double>(battles);
var turnsPerBattle = new ConcurrentBag<uint>();
var results = new ConcurrentBag<BattleResult>();
var rootRandom = new RandomImpl();
@@ -31,27 +34,51 @@ public static class TestCommandRunner
randoms[i] = new RandomImpl(rootRandom.GetInt());
battleTasks[i] = Task.CompletedTask; // Initialize tasks to avoid null references
}
// Here you would implement the logic to run the AI scripts against each other.
// This is a placeholder for demonstration purposes.
// Show a progress bar if debug logging is not enabled.
// This is to avoid weird console output where the progress bar is drawn in the middle of debug logs.
ProgressBar? pb = null;
if (!Log.IsEnabled(LogEventLevel.Debug))
{
pb = new ProgressBar(battles, "Running battles...", new ProgressBarOptions
{
ShowEstimatedDuration = true,
ProgressBarOnBottom = true,
});
pb.EstimatedDuration = TimeSpan.FromMilliseconds(battles);
}
for (var i = 0; i < battles; i++)
{
var taskIndex = i % maxTasks;
var index = i;
var battleTask = Task.Run(async () =>
{
Log.Information("Battle {BattleNumber}: {AI1} vs {AI2}", index + 1, ai1.Name, ai2.Name);
Log.Debug("Battle {BattleNumber}: {AI1} vs {AI2}", index + 1, ai1.Name, ai2.Name);
var random = randoms[taskIndex];
var battle = GenerateBattle(library, 3, random);
var timePerTurn = new List<double>(20);
while (!battle.HasEnded)
{
if (battle.CurrentTurnNumber > 1000)
{
Log.Warning("Battle {BattleNumber} exceeded 1000 turns, ending battle early", index + 1);
battle.ForceEndBattle();
var last10Choices = battle.PreviousTurnChoices.TakeLast(10).ToList();
Log.Warning("Last 10 choices: {Choices}", last10Choices);
return;
}
var res = await GetAndSetChoices(battle, ai1, ai2);
timePerTurn.Add(res.MsPerTurn);
}
var result = battle.Result;
Log.Information("Battle {BattleNumber} ended with result: {Result}", index + 1, result);
Log.Debug("Battle {BattleNumber} ended with result: {Result}", index + 1, result);
averageTimePerTurnPerBattle.Add(timePerTurn.Average());
results.Add(result.Value);
turnsPerBattle.Add(battle.CurrentTurnNumber);
// ReSharper disable once AccessToDisposedClosure
pb?.Tick();
});
battleTasks[taskIndex] = battleTask;
if (i % maxTasks == maxTasks - 1 || i == battles - 1)
@@ -62,11 +89,13 @@ public static class TestCommandRunner
Array.Fill(battleTasks, Task.CompletedTask); // Reset tasks for the next batch
}
}
pb?.Dispose();
var t2 = DateTime.UtcNow;
Log.Information("{Amount} battles completed in {Duration} ms", battles, (t2 - t1).TotalMilliseconds);
var averageTimePerTurn = averageTimePerTurnPerBattle.Average();
Log.Information("Average time per turn: {AverageTimePerTurn} ms", averageTimePerTurn);
Log.Information("Average turns per battle: {AverageTurnsPerBattle}", turnsPerBattle.Average(x => x));
var winCount1 = results.Count(x => x.WinningSide == 0);
var winCount2 = results.Count(x => x.WinningSide == 1);
@@ -123,26 +152,32 @@ public static class TestCommandRunner
private static async Task<GetAndSetChoicesResult> GetAndSetChoices(BattleImpl battle, PokemonAI ai1, PokemonAI ai2)
{
var pokemon1 = battle.Sides[0].Pokemon[0];
if (pokemon1 is null)
while (pokemon1 is null && !battle.HasEnded)
{
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);
pokemon1 = battle.Sides[0].Pokemon[0];
}
var pokemon2 = battle.Sides[1].Pokemon[0];
if (pokemon2 is null)
while (pokemon2 is null && !battle.HasEnded)
{
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);
pokemon2 = battle.Sides[1].Pokemon[0];
}
if (pokemon1 is null || pokemon2 is null)
{
throw new InvalidOperationException("Both Pokémon must be non-null to proceed with the battle.");
}
var taskAiOne = !battle.HasForcedTurn(pokemon1, out var choice1)
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)
var taskAiTwo = !battle.HasForcedTurn(pokemon2!, out var choice2)
? Task.Run(() => ai2.GetChoice(battle, pokemon2))
: Task.FromResult(choice2);
await Task.WhenAll(taskAiOne, taskAiTwo);