Implements critical capture, tweaks for integration tests.
All checks were successful
Build / Build (push) Successful in 48s

This commit is contained in:
Deukhoofd 2025-05-18 17:07:46 +02:00
parent cbd4340b13
commit 377c1a1c68
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
11 changed files with 158 additions and 52 deletions

View File

@ -50,7 +50,7 @@ public class DynamicLibraryImpl : IDynamicLibrary
/// Initializes a new instance of the <see cref="DynamicLibraryImpl"/> class, with the given /// Initializes a new instance of the <see cref="DynamicLibraryImpl"/> class, with the given
/// plugins and static library. /// plugins and static library.
/// </summary> /// </summary>
public static IDynamicLibrary Create(IEnumerable<Plugin> plugins) public static IDynamicLibrary Create(IEnumerable<IPlugin> plugins)
{ {
var load = LibraryLoader.LoadPlugins(plugins); var load = LibraryLoader.LoadPlugins(plugins);

View File

@ -18,7 +18,7 @@ public static class LibraryLoader
/// <summary> /// <summary>
/// Loads plugins and creates a static library from them. /// Loads plugins and creates a static library from them.
/// </summary> /// </summary>
public static LoadResult LoadPlugins(IEnumerable<Plugin> plugins) public static LoadResult LoadPlugins(IEnumerable<IPlugin> plugins)
{ {
var registry = new ScriptRegistry(); var registry = new ScriptRegistry();
var orderedPlugins = plugins.OrderBy(x => x.LoadOrder).ToList(); var orderedPlugins = plugins.OrderBy(x => x.LoadOrder).ToList();
@ -39,7 +39,7 @@ public static class LibraryLoader
return new LoadResult(registry, scriptResolver, staticLibrary); return new LoadResult(registry, scriptResolver, staticLibrary);
} }
private static StaticLibraryImpl CreateStaticLibrary(IReadOnlyList<Plugin> plugins) private static StaticLibraryImpl CreateStaticLibrary(IReadOnlyList<IPlugin> plugins)
{ {
var resourceProviders = plugins.OfType<IResourceProvider>().ToList(); var resourceProviders = plugins.OfType<IResourceProvider>().ToList();
var settings = resourceProviders.Select(x => x.Settings).LastOrDefault(x => x != null); var settings = resourceProviders.Select(x => x.Settings).LastOrDefault(x => x != null);

View File

@ -173,4 +173,7 @@ public class LearnedMoveImpl : ILearnedMove
{ {
CurrentPp = Math.Min(uses, MaxPp); CurrentPp = Math.Min(uses, MaxPp);
} }
/// <inheritdoc />
public override string ToString() => MoveData.Name;
} }

View File

@ -1261,6 +1261,14 @@ public class PokemonImpl : ScriptSource, IPokemon
side.CollectScripts(scripts); side.CollectScripts(scripts);
} }
} }
/// <inheritdoc />
public override string ToString()
{
if (!string.IsNullOrEmpty(Nickname))
return $"{Nickname} ({Species.Name})";
return Species.Name;
}
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -2,20 +2,37 @@ using JetBrains.Annotations;
namespace PkmnLib.Dynamic.ScriptHandling.Registry; namespace PkmnLib.Dynamic.ScriptHandling.Registry;
public interface IPlugin
{
/// <summary>
/// The name of the plugin. Mostly used for debugging purposes.
/// </summary>
string Name { get; }
/// <summary>
/// When the plugin should be loaded. Lower values are loaded first.
/// 0 should be reserved for the core battle scripts.
/// </summary>
uint LoadOrder { get; }
/// <summary>
/// Run the registration of the plugin when we're building the library.
/// </summary>
void Register(ScriptRegistry registry);
}
/// <summary> /// <summary>
/// A plugin is a way to register scripts and other dynamic components to the script registry. /// A plugin is a way to register scripts and other dynamic components to the script registry.
/// </summary> /// </summary>
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
public abstract class Plugin public abstract class Plugin<TConfiguration> : IPlugin where TConfiguration : IPluginConfiguration
{ {
/// <inheritdoc cref="Plugin"/> public TConfiguration Configuration { get; }
protected Plugin()
{
}
/// <inheritdoc cref="Plugin"/> /// <inheritdoc cref="Plugin{TConfiguration}"/>
protected Plugin(PluginConfiguration configuration) protected Plugin(TConfiguration configuration)
{ {
Configuration = configuration;
} }
/// <summary> /// <summary>
@ -38,4 +55,4 @@ public abstract class Plugin
/// <summary> /// <summary>
/// Base class for plugin configuration. /// Base class for plugin configuration.
/// </summary> /// </summary>
public abstract class PluginConfiguration; public interface IPluginConfiguration;

View File

@ -1,6 +1,8 @@
using System.Text.Json; using System.Text.Json;
using EnumerableAsyncProcessor.Extensions; using EnumerableAsyncProcessor.Extensions;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models;
using PkmnLib.Plugin.Gen7;
using PkmnLib.Static.Species; using PkmnLib.Static.Species;
using PkmnLib.Tests.Integration.Models; using PkmnLib.Tests.Integration.Models;
@ -10,7 +12,7 @@ public class IntegrationTestRunner
{ {
public static IEnumerable<Func<IntegrationTestModel>> TestCases() public static IEnumerable<Func<IntegrationTestModel>> TestCases()
{ {
var files = Directory.GetFiles("Integration/Tests", "*.json"); var files = Directory.GetFiles("../../../../PkmnLib.Tests/Integration/Tests", "*.json");
var serializerOptions = new JsonSerializerOptions var serializerOptions = new JsonSerializerOptions
{ {
PropertyNameCaseInsensitive = true, PropertyNameCaseInsensitive = true,
@ -32,7 +34,13 @@ public class IntegrationTestRunner
[Test, MethodDataSource(nameof(TestCases))] [Test, MethodDataSource(nameof(TestCases))]
public async Task RunIntegrationTest(IntegrationTestModel test) public async Task RunIntegrationTest(IntegrationTestModel test)
{ {
var library = LibraryHelpers.LoadLibrary(); var library = DynamicLibraryImpl.Create([
new Gen7Plugin(new Gen7PluginConfiguration
{
DamageCalculatorHasRandomness = true,
}),
]);
await TestContext.Current!.OutputWriter.WriteLineAsync("File: " + $"file://{test.FileName}"); await TestContext.Current!.OutputWriter.WriteLineAsync("File: " + $"file://{test.FileName}");
TestContext.Current.AddArtifact(new Artifact TestContext.Current.AddArtifact(new Artifact
{ {

View File

@ -3,6 +3,7 @@ using System.Text.Json.Serialization;
using CSPath; using CSPath;
using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.Models.Choices;
using TUnit.Core.Logging;
using JsonSerializer = System.Text.Json.JsonSerializer; using JsonSerializer = System.Text.Json.JsonSerializer;
namespace PkmnLib.Tests.Integration.Models; namespace PkmnLib.Tests.Integration.Models;
@ -23,6 +24,7 @@ public class SetPokemonAction : IntegrationTestAction
{ {
var mon = battle.Parties[FromParty[0]].Party[FromParty[1]]; var mon = battle.Parties[FromParty[0]].Party[FromParty[1]];
battle.Sides[Place[0]].SwapPokemon(Place[1], mon); battle.Sides[Place[0]].SwapPokemon(Place[1], mon);
Console.WriteLine($"Set: {mon} to place {Place[0]}:{Place[1]}");
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
@ -42,6 +44,8 @@ public class SetMoveChoiceAction : IntegrationTestAction
await Assert.That(move).IsNotNull(); await Assert.That(move).IsNotNull();
var res = battle.TrySetChoice(new MoveChoice(user, move!, Target[0], Target[1])); var res = battle.TrySetChoice(new MoveChoice(user, move!, Target[0], Target[1]));
await Assert.That(res).IsTrue(); await Assert.That(res).IsTrue();
var target = battle.Sides[Target[0]].Pokemon[Target[1]];
Console.WriteLine($"Choice: {user} used {move} on {target} ({Target[0]}:{Target[1]})");
} }
} }
@ -56,13 +60,14 @@ public class SetPassChoiceAction : IntegrationTestAction
await Assert.That(user).IsNotNull(); await Assert.That(user).IsNotNull();
var res = battle.TrySetChoice(new PassChoice(user!)); var res = battle.TrySetChoice(new PassChoice(user!));
await Assert.That(res).IsTrue(); await Assert.That(res).IsTrue();
Console.WriteLine($"Choice: {user} Pass");
} }
} }
public class AssertAction : IntegrationTestAction public class AssertAction : IntegrationTestAction
{ {
public string Value { get; set; } = null!; public string Value { get; init; } = null!;
public JsonNode Expected { get; set; } = null!; public JsonNode Expected { get; init; } = null!;
/// <inheritdoc /> /// <inheritdoc />
public override async Task Execute(IBattle battle) public override async Task Execute(IBattle battle)
@ -71,6 +76,9 @@ public class AssertAction : IntegrationTestAction
var value = list.Count == 1 ? list[0] : list; var value = list.Count == 1 ? list[0] : list;
var serialized = JsonSerializer.Serialize(value); var serialized = JsonSerializer.Serialize(value);
await Assert.That(serialized).IsEqualTo(Expected.ToJsonString()); #pragma warning disable TUnitAssertions0003
await Assert.That(serialized, Value).IsEqualTo(Expected.ToJsonString());
#pragma warning restore TUnitAssertions0003
Console.WriteLine($"Assert: {Value} = {serialized}");
} }
} }

View File

@ -8,22 +8,36 @@
"positionsPerSide": 1, "positionsPerSide": 1,
"parties": [ "parties": [
{ {
"indices": [[0, 0]], "indices": [
[
0,
0
]
],
"pokemon": [ "pokemon": [
{ {
"species": "charizard", "species": "charizard",
"level": 50, "level": 50,
"moves": ["ember"] "moves": [
"ember"
]
} }
] ]
}, },
{ {
"indices": [[1, 0]], "indices": [
[
1,
0
]
],
"pokemon": [ "pokemon": [
{ {
"species": "venusaur", "species": "venusaur",
"level": 50, "level": 50,
"moves": ["vine_whip"] "moves": [
"vine_whip"
]
} }
] ]
} }
@ -32,13 +46,25 @@
"actions": [ "actions": [
{ {
"$type": "setPokemon", "$type": "setPokemon",
"place": [0, 0], "place": [
"fromParty": [0, 0] 0,
0
],
"fromParty": [
0,
0
]
}, },
{ {
"$type": "setPokemon", "$type": "setPokemon",
"place": [1, 0], "place": [
"fromParty": [1, 0] 1,
0
],
"fromParty": [
1,
0
]
}, },
{ {
"$type": "assert", "$type": "assert",
@ -47,18 +73,27 @@
}, },
{ {
"$type": "setMoveChoice", "$type": "setMoveChoice",
"place": [0, 0], "place": [
0,
0
],
"move": "ember", "move": "ember",
"target": [1, 0] "target": [
1,
0
]
}, },
{ {
"$type": "setPassChoice", "$type": "setPassChoice",
"place": [1, 0] "place": [
1,
0
]
}, },
{ {
"$type": "assert", "$type": "assert",
"value": ".Sides[1].Pokemon[0].CurrentHealth", "value": ".Sides[1].Pokemon[0].CurrentHealth",
"expected": 78 "expected": 84
} }
] ]
} }

View File

@ -30,16 +30,7 @@
<ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj"/> <ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj"/>
<ProjectReference Include="..\Plugins\PkmnLib.Plugin.Gen7\PkmnLib.Plugin.Gen7.csproj"/> <ProjectReference Include="..\Plugins\PkmnLib.Plugin.Gen7\PkmnLib.Plugin.Gen7.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="Data\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Integration\Tests\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="WorkaroundRider117732" AfterTargets="Build" Condition="!$([MSBuild]::IsOSPlatform('Windows'))"> <Target Name="WorkaroundRider117732" AfterTargets="Build" Condition="!$([MSBuild]::IsOSPlatform('Windows'))">
<Copy Condition="Exists('$(OutputPath)$(AssemblyName)')" SourceFiles="$(OutputPath)$(AssemblyName)" DestinationFiles="$(OutputPath)$(AssemblyName).exe"/> <Copy Condition="Exists('$(OutputPath)$(AssemblyName)')" SourceFiles="$(OutputPath)$(AssemblyName)" DestinationFiles="$(OutputPath)$(AssemblyName).exe"/>
</Target> </Target>

View File

@ -1,27 +1,37 @@
using System; using System;
using PkmnLib.Plugin.Gen7.Libraries.Battling; using PkmnLib.Plugin.Gen7.Libraries.Battling;
using PkmnLib.Static.Libraries; using PkmnLib.Static.Libraries;
using PkmnLib.Static.Species;
namespace PkmnLib.Plugin.Gen7; namespace PkmnLib.Plugin.Gen7;
public class Gen7PluginConfiguration : PluginConfiguration public class Gen7PluginConfiguration : IPluginConfiguration
{ {
public bool DamageCalculatorHasRandomness { get; set; } = true; /// <summary>
/// Whether the damage calculator has randomness or not. If set to false, the damage calculator will always return
/// the same value for the same inputs. If set to true, the damage calculator will randomize the damage output
/// between 0.85 and 1.00 of the calculated damage.
///
/// This should be set to true for most cases, as it simulates the actual damage calculation in the games. Only
/// set to false for testing purposes.
/// </summary>
public bool DamageCalculatorHasRandomness { get; init; } = true;
/// <summary>
/// The number of times a species has been caught. This is used for critical capture calculations.
/// </summary>
public Func<ISpecies, int> TimesSpeciesCaught { get; init; } = _ => 0;
} }
public class Gen7Plugin : Dynamic.ScriptHandling.Registry.Plugin, IResourceProvider public class Gen7Plugin : Plugin<Gen7PluginConfiguration>, IResourceProvider
{ {
private readonly Gen7PluginConfiguration _configuration; public Gen7Plugin() : base(new Gen7PluginConfiguration())
public Gen7Plugin()
{ {
_configuration = new Gen7PluginConfiguration();
} }
/// <inheritdoc /> /// <inheritdoc />
public Gen7Plugin(PluginConfiguration configuration) : base(configuration) public Gen7Plugin(Gen7PluginConfiguration configuration) : base(configuration)
{ {
_configuration = (Gen7PluginConfiguration)configuration;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -35,9 +45,9 @@ public class Gen7Plugin : Dynamic.ScriptHandling.Registry.Plugin, IResourceProvi
{ {
registry.RegisterAssemblyScripts(typeof(Gen7Plugin).Assembly); registry.RegisterAssemblyScripts(typeof(Gen7Plugin).Assembly);
registry.RegisterBattleStatCalculator(new Gen7BattleStatCalculator()); registry.RegisterBattleStatCalculator(new Gen7BattleStatCalculator());
registry.RegisterDamageCalculator(new Gen7DamageCalculator(_configuration.DamageCalculatorHasRandomness)); registry.RegisterDamageCalculator(new Gen7DamageCalculator(Configuration.DamageCalculatorHasRandomness));
registry.RegisterMiscLibrary(new Gen7MiscLibrary()); registry.RegisterMiscLibrary(new Gen7MiscLibrary());
registry.RegisterCaptureLibrary(new Gen7CaptureLibrary()); registry.RegisterCaptureLibrary(new Gen7CaptureLibrary(Configuration));
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -5,6 +5,13 @@ namespace PkmnLib.Plugin.Gen7.Libraries.Battling;
public class Gen7CaptureLibrary : ICaptureLibrary public class Gen7CaptureLibrary : ICaptureLibrary
{ {
private readonly Gen7PluginConfiguration _configuration;
public Gen7CaptureLibrary(Gen7PluginConfiguration configuration)
{
_configuration = configuration;
}
/// <inheritdoc /> /// <inheritdoc />
public CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random) public CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random)
{ {
@ -22,10 +29,10 @@ public class Gen7CaptureLibrary : ICaptureLibrary
byte bonusStatus = 1; byte bonusStatus = 1;
target.RunScriptHook(x => x.ChangeCatchRateBonus(target, captureItem, ref bonusStatus)); target.RunScriptHook(x => x.ChangeCatchRateBonus(target, captureItem, ref bonusStatus));
var modifiedCatchRate = (3.0 * maxHealth - 2.0 * currentHealth) * catchRate * bonusBall / (3.0 * maxHealth); var modifiedCatchRate = (3.0f * maxHealth - 2.0f * currentHealth) * catchRate * bonusBall / (3.0f * maxHealth);
modifiedCatchRate *= bonusStatus; modifiedCatchRate *= bonusStatus;
var shakeProbability = 65536 / Math.Pow(255 / modifiedCatchRate, 0.1875); var shakeProbability = 65536 / Math.Pow(255 / modifiedCatchRate, 0.1875f);
byte shakes = 0; byte shakes = 0;
if (modifiedCatchRate >= 255) if (modifiedCatchRate >= 255)
{ {
@ -33,7 +40,26 @@ public class Gen7CaptureLibrary : ICaptureLibrary
} }
else else
{ {
// FIXME: Implement critical capture var timesCaught = _configuration.TimesSpeciesCaught(target.Species);
if (timesCaught > 30)
{
var criticalCaptureModifier = timesCaught switch
{
> 600 => 2.5f,
> 450 => 2.0f,
> 300 => 1.5f,
> 150 => 1.0f,
> 30 => 0.5f,
// Default arm, should be heuristically unreachable (due to the timesCaught > 30 check above)
_ => throw new ArgumentOutOfRangeException(),
};
var criticalCaptureChance = (int)(modifiedCatchRate * criticalCaptureModifier / 6);
if (random.GetInt(0, 256) < criticalCaptureChance)
{
return new CaptureResult(true, 1, true);
}
}
for (var i = 0; i < 4; i++) for (var i = 0; i < 4; i++)
{ {
if (random.GetInt(0, 65536) < shakeProbability) if (random.GetInt(0, 65536) < shakeProbability)