Initial commit

This commit is contained in:
Deukhoofd 2024-07-20 13:51:52 +02:00
commit 3845f91601
Signed by: Deukhoofd
GPG Key ID: AC7AE601ABF7DC43
26 changed files with 1822 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea
*.DotSettings.user

22
PkmnLib.NET.sln Normal file
View File

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PkmnLib.Static", "PkmnLib.Static\PkmnLib.Static.csproj", "{312782DA-1066-4490-BD0E-DF4DF8713B4A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PkmnLib.Tests", "PkmnLib.Tests\PkmnLib.Tests.csproj", "{42DE3095-0468-4827-AF5C-691C94BA7F92}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{312782DA-1066-4490-BD0E-DF4DF8713B4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{312782DA-1066-4490-BD0E-DF4DF8713B4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{312782DA-1066-4490-BD0E-DF4DF8713B4A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{312782DA-1066-4490-BD0E-DF4DF8713B4A}.Release|Any CPU.Build.0 = Release|Any CPU
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1 @@
global using LevelInt = byte;

View File

@ -0,0 +1,81 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Static;
/// <summary>
/// A growth rate defines how much experience is required per level.
/// </summary>
public interface IGrowthRate
{
/// <summary>
/// The name of the growth rate.
/// </summary>
public StringKey Name { get; }
///<summary>
/// Calculate the level something with this growth rate would have at a certain experience.
/// </summary>
/// <param name="experience">The experience to calculate the level for.</param>
/// <returns>The level at the given experience.</returns>
LevelInt CalculateLevel(uint experience);
///<summary>
/// Calculate the experience something with this growth rate would have at a certain level.
/// </summary>
/// <param name="level">The level to calculate the experience for.</param>
/// <returns>The starting experience at the given level.</returns>
uint CalculateExperience(LevelInt level);
}
/// <summary>
/// An implementation of the growth rate that uses a lookup table for experience.
/// </summary>
public class LookupGrowthRate : IGrowthRate
{
/// <inheritdoc />
public StringKey Name { get; }
private readonly uint[] _experienceTable;
/// <inheritdoc cref="LookupGrowthRate" />
public LookupGrowthRate(StringKey name, IEnumerable<uint> experienceTable)
{
Name = name;
_experienceTable = experienceTable.ToArray();
if (_experienceTable.Length < 1)
{
throw new ArgumentException("Experience table must have at least one entry.");
}
if (_experienceTable[0] != 0)
{
throw new ArgumentException("Experience table must start at 0.");
}
if (_experienceTable.Length > LevelInt.MaxValue)
{
throw new ArgumentException($"Experience table may have at most {LevelInt.MaxValue} entries.");
}
}
/// <inheritdoc />
public LevelInt CalculateLevel(uint experience)
{
for (LevelInt level = 0; level < _experienceTable.Length; level++)
{
if (_experienceTable[level] > experience)
{
return level;
}
}
return (LevelInt)(_experienceTable.Length);
}
/// <inheritdoc />
public uint CalculateExperience(LevelInt level)
{
if (level < 1) level = 1;
return level >= _experienceTable.Length ? _experienceTable[^1] : _experienceTable[level - 1];
}
}

155
PkmnLib.Static/Item.cs Normal file
View File

@ -0,0 +1,155 @@
using System.Collections.Immutable;
using PkmnLib.Static.Utils;
namespace PkmnLib.Static;
/// <summary>
/// An item category defines which bag slot items are stored in.
/// </summary>
public enum ItemCategory
{
/// <summary>
/// This is where most items should go.
/// </summary>
MiscItem,
/// <summary>
/// Pokeballs are used for capturing Pokemons.
/// </summary>
Pokeball,
/// <summary>
/// Medicine is used for healing HP, PP, and status effects.
/// </summary>
Medicine,
/// <summary>
/// Berry is used for all berries.
/// </summary>
Berry,
/// <summary>
/// TMHM is used for Technical and Hidden Machines.
/// </summary>
TmHm,
/// <summary>
/// Form Changer is used for items that change forms, such as mega stones.
/// </summary>
FormChanger,
/// <summary>
/// Key Items are single stored items, generally used for story progression.
/// </summary>
KeyItem,
/// <summary>
/// Mail is used for mail items.
/// </summary>
Mail,
}
/// <summary>
/// A battle item category defines how the item is categorized when in battle.
/// </summary>
public enum BattleItemCategory
{
/// <summary>
/// This item can't be used in battle.
/// </summary>
None,
/// <summary>
/// This item is used for healing Pokemon.
/// </summary>
Healing,
/// <summary>
/// This item is used for healing Pokemon from a status.
/// </summary>
StatusHealing,
/// <summary>
/// This item is used for capturing Pokemon.
/// </summary>
Pokeball,
/// <summary>
/// This item does not belong in above categories, but is still a battle item.
/// </summary>
MiscBattleItem,
}
/// <summary>
/// An item is an object which the player can pick up, keep in their Bag, and use in some manner.
/// </summary>
public interface IItem
{
/// <summary>
/// The name of the item.
/// </summary>
StringKey Name { get; }
/// <summary>
/// Which bag slot items are stored in.
/// </summary>
ItemCategory Category { get; }
/// <summary>
/// How the item is categorized when in battle.
/// </summary>
BattleItemCategory BattleCategory { get; }
/// <summary>
/// The buying value of the item.
/// </summary>
int Price { get; }
/// <summary>
/// A set of arbitrary flags that can be set on the item.
/// </summary>
ImmutableHashSet<StringKey> Flags { get; }
/// <summary>
/// Checks whether the item has a specific flag.
/// </summary>
/// <param name="key">The flag to check for.</param>
/// <returns>True if the item has the flag, false otherwise.</returns>
bool HasFlag(string key);
}
/// <inheritdoc />
public class ItemImpl : IItem
{
/// <inheritdoc cref="ItemImpl"/>
public ItemImpl(StringKey name, ItemCategory category, BattleItemCategory battleCategory, int price,
IEnumerable<StringKey> flags)
{
Name = name;
Category = category;
BattleCategory = battleCategory;
Price = price;
Flags = [..flags];
}
/// <inheritdoc />
public StringKey Name { get; }
/// <inheritdoc />
public ItemCategory Category { get; }
/// <inheritdoc />
public BattleItemCategory BattleCategory { get; }
/// <inheritdoc />
public int Price { get; }
/// <inheritdoc />
public ImmutableHashSet<StringKey> Flags { get; }
/// <inheritdoc />
public bool HasFlag(string key)
{
return Flags.Contains(key);
}
}

View File

@ -0,0 +1,204 @@
using System.Collections.Immutable;
using PkmnLib.Static.Utils;
namespace PkmnLib.Static.Moves;
/// <summary>
/// The move category defines what global kind of move this move is.
/// </summary>
public enum MoveCategory
{
/// <summary>
/// A physical move uses the physical attack stats and physical defense stats to calculate damage.
/// </summary>
Physical = 0,
/// <summary>
/// A special move uses the special attack stats and special defense stats to calculate damage.
/// </summary>
Special = 1,
/// <summary>
/// A status move does not do damage, and only runs a secondary effect.
/// </summary>
Status = 2,
}
/// <summary>
/// The move target defines what kind of targets the move can touch.
/// </summary>
public enum MoveTarget
{
/// <summary>
/// Adjacent allows a move to target any Pokemon that is either directly to the left or right of
/// the user, opposed to the user, or left or right of the slot that is opposing the user.
/// </summary>
Adjacent = 0,
/// <summary>
/// AdjacentAlly allows a move to target any Pokemon that is directly to the left or right of
/// the user.
/// </summary>
AdjacentAlly,
/// <summary>
/// AdjacentAllySelf allows a move to target any Pokemon that is either directly to the left or
/// right of the user, or the user itself.
/// </summary>
AdjacentAllySelf,
/// <summary>
/// AdjacentOpponent allows a move to target any Pokemon that is either the opponent, or directly
/// to the left or right of it.
/// </summary>
AdjacentOpponent,
/// <summary>
/// All makes the move target everything on the field.
/// </summary>
All,
/// <summary>
/// AllAdjacent makes the move target everything adjacent on the field.
/// </summary>
AllAdjacent,
/// <summary>
/// AllAdjacentOpponent makes the move target everything adjacent to the opponent, and the opponent.
/// </summary>
AllAdjacentOpponent,
/// <summary>
/// AllAlly targets all Pokemon on the same side as the user.
/// </summary>
AllAlly,
/// <summary>
/// AllOpponent targets all Pokemon on an opposing side from the user.
/// </summary>
AllOpponent,
/// <summary>
/// Any allows a move to target a single Pokemon, in any position.
/// </summary>
Any,
/// <summary>
/// RandomOpponent allows a move to target a single Pokemon, in a random position.
/// </summary>
RandomOpponent,
/// <summary>
/// SelfUse makes the move target the user itself.
/// </summary>
SelfUse,
}
/// <summary>
/// A move is the skill Pokémon primarily use in battle. This is the data related to that.
/// </summary>
public interface IMoveData
{
/// <summary>
/// The name of the move.
/// </summary>
string Name { get; }
/// <summary>
/// The attacking type of the move.
/// </summary>
TypeIdentifier MoveType { get; }
/// <summary>
/// The category of the move.
/// </summary>
MoveCategory Category { get; }
/// <summary>
/// The base power, not considering any modifiers, the move has.
/// </summary>
byte BasePower { get; }
/// <summary>
/// The accuracy of the move in percentage. Should be 255 for moves that always hit.
/// </summary>
byte Accuracy { get; }
/// <summary>
/// The number of times the move can be used. This can be modified on actually learned moves using PP-Ups
/// </summary>
byte BaseUsages { get; }
/// <summary>
/// How the move handles targets.
/// </summary>
MoveTarget Target { get; }
/// <summary>
/// The priority of the move. A higher priority means the move should go before other moves.
/// </summary>
sbyte Priority { get; }
/// <summary>
/// The optional secondary effect the move has.
/// </summary>
ISecondaryEffect? SecondaryEffect { get; }
/// <summary>
/// Arbitrary flags that can be applied to the move.
/// </summary>
bool HasFlag(string key);
}
/// <inheritdoc />
public class MoveDataImpl : IMoveData
{
/// <inheritdoc cref="MoveDataImpl" />
public MoveDataImpl(string name, TypeIdentifier moveType, MoveCategory category, byte basePower, byte accuracy,
byte baseUsages, MoveTarget target, sbyte priority, ISecondaryEffect? secondaryEffect,
IEnumerable<StringKey> flags)
{
Name = name;
MoveType = moveType;
Category = category;
BasePower = basePower;
Accuracy = accuracy;
BaseUsages = baseUsages;
Target = target;
Priority = priority;
SecondaryEffect = secondaryEffect;
_flags = [..flags];
}
/// <inheritdoc />
public string Name { get; }
/// <inheritdoc />
public TypeIdentifier MoveType { get; }
/// <inheritdoc />
public MoveCategory Category { get; }
/// <inheritdoc />
public byte BasePower { get; }
/// <inheritdoc />
public byte Accuracy { get; }
/// <inheritdoc />
public byte BaseUsages { get; }
/// <inheritdoc />
public MoveTarget Target { get; }
/// <inheritdoc />
public sbyte Priority { get; }
/// <inheritdoc />
public ISecondaryEffect? SecondaryEffect { get; }
private readonly ImmutableHashSet<StringKey> _flags;
/// <inheritdoc />
public bool HasFlag(string key) => _flags.Contains(key);
}

View File

@ -0,0 +1,45 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Static.Moves;
/// <summary>
/// A secondary effect is an effect on a move that happens after it hits.
/// </summary>
public interface ISecondaryEffect
{
/// <summary>
/// The chance in percentages that the effect triggers. When less than 0, the effect is always active.
/// </summary>
public float Chance { get; }
/// <summary>
/// The name of the effect.
/// </summary>
public StringKey Name { get; }
/// <summary>
/// Parameters for the effect.
/// </summary>
public IReadOnlyDictionary<StringKey, object> Parameters { get; }
}
/// <inheritdoc />
public class SecondaryEffectImpl : ISecondaryEffect
{
/// <inheritdoc cref="SecondaryEffectImpl" />
public SecondaryEffectImpl(float chance, StringKey name, IReadOnlyDictionary<StringKey, object> parameters)
{
Chance = chance;
Name = name;
Parameters = parameters;
}
/// <inheritdoc />
public float Chance { get; }
/// <inheritdoc />
public StringKey Name { get; }
/// <inheritdoc />
public IReadOnlyDictionary<StringKey, object> Parameters { get; }
}

91
PkmnLib.Static/Nature.cs Normal file
View File

@ -0,0 +1,91 @@
using System;
namespace PkmnLib.Static;
/// <summary>
/// A nature is an attribute on a Pokemon that modifies the effective base stats on a Pokemon. They
/// can have an increased statistic and a decreased statistic, or be neutral.
/// </summary>
public interface INature
{
/// <summary>
/// The name of the nature.
/// </summary>
string Name { get; }
/// <summary>
/// The stat that should receive the increased modifier.
/// </summary>
Statistic IncreasedStat { get; }
/// <summary>
/// The stat that should receive the decreased modifier.
/// </summary>
Statistic DecreasedStat { get; }
/// <summary>
/// The amount that the increased stat gets modified by.
/// </summary>
float IncreasedModifier { get; }
/// <summary>
/// The amount that the decreased stat gets modified by.
/// </summary>
float DecreasedModifier { get; }
/// <summary>
/// Calculates the modifier for a given stat. If it's the increased stat, returns the increased
/// modifier, if it's the decreased stat, returns the decreased modifier. Otherwise returns 1.0.
/// </summary>
/// <param name="stat">The stat to calculate the modifier for.</param>
/// <returns>The calculated modifier.</returns>
float GetStatModifier(Statistic stat);
/// <summary>
/// Checks if two natures are equal.
/// </summary>
/// <param name="other">The other nature to compare to.</param>
/// <returns>True if the natures are equal, false otherwise.</returns>
bool Equals(INature other);
}
/// <inheritdoc />
public class Nature(
string name,
Statistic increaseStat,
Statistic decreaseStat,
float increaseModifier,
float decreaseModifier)
: INature
{
/// <inheritdoc />
public string Name { get; } = name;
/// <inheritdoc />
public Statistic IncreasedStat { get; } = increaseStat;
/// <inheritdoc />
public Statistic DecreasedStat { get; } = decreaseStat;
/// <inheritdoc />
public float IncreasedModifier { get; } = increaseModifier;
/// <inheritdoc />
public float DecreasedModifier { get; } = decreaseModifier;
/// <inheritdoc />
public float GetStatModifier(Statistic stat)
{
if (stat == IncreasedStat && stat != DecreasedStat)
return IncreasedModifier;
if (stat == DecreasedStat && stat != IncreasedStat)
return DecreasedModifier;
return 1.0f;
}
/// <inheritdoc />
public bool Equals(INature? other)
{
return other is not null && StringComparer.InvariantCultureIgnoreCase.Equals(Name, other.Name);
}
}

View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\netstandard2.1\PkmnLib.Static.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\netstandard2.1\PkmnLib.Static.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentResults" Version="3.16.0" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Libraries\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,63 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Static.Species;
/// <summary>
/// An ability is a passive effect in battle that is attached to a Pokemon.
/// </summary>
public interface IAbility
{
/// <summary>
/// The name of the ability.
/// </summary>
StringKey Name { get; }
/// <summary>
/// The name of the script effect of the ability. This should refer to the name of the script that will be executed
/// when the ability is triggered.
/// </summary>
StringKey Effect { get; }
/// <summary>
/// The parameters for the script effect of the ability.
/// </summary>
IReadOnlyDictionary<StringKey, object> Parameters { get; }
}
/// <inheritdoc />
public class AbilityImpl : IAbility
{
/// <inheritdoc cref="AbilityImpl" />
public AbilityImpl(StringKey name, StringKey effect, IReadOnlyDictionary<StringKey, object> parameters)
{
Name = name;
Effect = effect;
Parameters = parameters;
}
/// <inheritdoc />
public StringKey Name { get; }
/// <inheritdoc />
public StringKey Effect { get; }
/// <inheritdoc />
public IReadOnlyDictionary<StringKey, object> Parameters { get; }
}
/// <summary>
/// An ability index allows us to find an ability on a form. It combines a bool for whether the
/// ability is hidden or not, and then an index of the ability.
/// </summary>
public readonly record struct AbilityIndex
{
/// <summary>
/// Whether the ability we're referring to is a hidden ability.
/// </summary>
public required bool IsHidden { get; init; }
/// <summary>
/// The index of the ability.
/// </summary>
public required byte Index { get; init; }
}

View File

@ -0,0 +1,234 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Static.Species;
/// <summary>
/// Data about how and into which Pokemon a species can evolve.
/// </summary>
public interface IEvolution
{
/// <summary>
/// The species that the Pokemon evolves into.
/// </summary>
StringKey ToSpecies { get; }
}
/// <summary>
/// Evolves when a certain level is reached.
/// </summary>
public record LevelEvolution : IEvolution
{
/// <summary>
/// The level at which the Pokemon evolves.
/// </summary>
public required uint Level { get; init; }
/// <inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when a certain level is reached, and the Pokemon is a specific gender
/// </summary>
public record LevelGenderEvolution : IEvolution
{
/// <summary>
/// The level at which the Pokemon evolves.
/// </summary>
public required uint Level { get; init; }
/// <summary>
/// The gender the Pokemon needs to have to evolve
/// </summary>
public required Gender Gender { get; init; }
/// <inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when an item is used on the Pokemon.
/// </summary>
public record ItemUseEvolution : IEvolution
{
/// <summary>
/// The item that needs to be used.
/// </summary>
public required StringKey Item { get; init; }
/// <inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when an item is used on the Pokemon, and the Pokemon is a specific gender
/// </summary>
public record ItemGenderEvolution : IEvolution
{
/// <summary>
/// The item that needs to be used.
/// </summary>
public required StringKey Item { get; init; }
/// <summary>
/// The gender the Pokemon needs to have to evolve
/// </summary>
public Gender Gender { get; init; }
/// <inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when an item is held by the Pokemon, and the Pokemon levels up.
/// </summary>
public record HoldItemEvolution : IEvolution
{
/// <summary>
/// The item that needs to be held.
/// </summary>
public required StringKey Item { get; init; }
/// <inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when an item is held by the Pokemon, and the Pokemon levels up, and it's day.
/// </summary>
public record DayHoldItemEvolution : IEvolution
{
/// <summary>
/// The item that needs to be held.
/// </summary>
public required StringKey Item { get; init; }
/// < inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when an item is held by the Pokemon, and the Pokemon levels up, and it's night.
/// </summary>
public record NightHoldItemEvolution : IEvolution
{
/// <summary>
/// The item that needs to be held.
/// </summary>
public required StringKey Item { get; init; }
/// < inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when the Pokemon knows a certain move, and the Pokemon levels up.
/// </summary>
public record HasMoveEvolution : IEvolution
{
/// <summary>
/// The name of the move that needs to be known.
/// </summary>
public required StringKey MoveName { get; init; }
/// < inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when above a certain happiness level, and the Pokemon levels up.
/// </summary>
public record HappinessEvolution : IEvolution
{
/// <summary>
/// The happiness level that needs to be reached.
/// </summary>
public required byte Happiness { get; init; }
/// < inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when above a certain happiness level, and the Pokemon levels up, and it's day.
/// </summary>
public record HappinessDayEvolution : IEvolution
{
/// <summary>
/// The happiness level that needs to be reached.
/// </summary>
public required byte Happiness { get; init; }
/// < inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when above a certain happiness level, and the Pokemon levels up, and it's night.
/// </summary>
public record HappinessNightEvolution : IEvolution
{
/// <summary>
/// The happiness level that needs to be reached.
/// </summary>
public required byte Happiness { get; init; }
/// < inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when traded.
/// </summary>
public record TradeEvolution : IEvolution
{
/// < inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when traded with a certain species.
/// </summary>
public record TradeSpeciesEvolution : IEvolution
{
/// <summary>
/// The species that needs to be traded with.
/// </summary>
public required StringKey WithSpecies { get; init; }
/// < inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Evolves when traded while it's holding a certain item.
/// </summary>
public record TradeItemEvolution : IEvolution
{
/// <summary>
/// The item that needs to be held.
/// </summary>
public required StringKey Item { get; init; }
/// < inheritdoc />
public required StringKey ToSpecies { get; init; }
}
/// <summary>
/// Custom evolution method, implemented by the user.
/// </summary>
public record CustomEvolution : IEvolution
{
/// <summary>
/// The name of the custom evolution method. This should refer to the name of the script that will be executed.
/// </summary>
public required StringKey Name { get; init; }
/// <summary>
/// The parameters of the custom evolution method.
/// </summary>
public required IReadOnlyDictionary<StringKey, object> Parameters { get; init; }
/// < inheritdoc />
public required StringKey ToSpecies { get; init; }
}

View File

@ -0,0 +1,218 @@
using System.Collections.Immutable;
using FluentResults;
using PkmnLib.Static.Utils;
using PkmnLib.Static.Utils.Errors;
namespace PkmnLib.Static.Species;
/// <summary>
/// A form is a variant of a specific species. A species always has at least one form, but can have
/// many more.
/// </summary>
public interface IForm
{
/// <summary>
/// The name of the form.
/// </summary>
StringKey Name { get; }
/// <summary>
/// The height of the form in meters.
/// </summary>
float Height { get; }
/// <summary>
/// The weight of the form in kilograms.
/// </summary>
float Weight { get; }
/// <summary>
/// The base amount of experience that is gained when beating a Pokemon with this form.
/// </summary>
uint BaseExperience { get; }
/// <summary>
/// The normal types a Pokemon with this form has.
/// </summary>
IReadOnlyList<TypeIdentifier> Types { get; }
/// <summary>
/// The inherent values of a form of species that are used for the stats of a Pokemon.
/// </summary>
StaticStatisticSet<ushort> BaseStats { get; }
/// <summary>
/// The possible abilities a Pokemon with this form can have.
/// </summary>
IReadOnlyList<StringKey> Abilities { get; }
/// <summary>
/// The possible hidden abilities a Pokemon with this form can have.
/// </summary>
IReadOnlyList<StringKey> HiddenAbilities { get; }
/// <summary>
/// The moves a Pokemon with this form can learn.
/// </summary>
ILearnableMoves Moves { get; }
/// <summary>
/// Arbitrary flags can be set on a form for scripting use.
/// </summary>
ImmutableHashSet<StringKey> Flags { get; }
/// <summary>
/// Get a type of the form at a certain index.
/// </summary>
Result<TypeIdentifier> GetType(int index);
/// <summary>
/// Gets a single base stat value.
/// </summary>
ushort GetBaseStat(Statistic stat);
/// <summary>
/// Find the index of an ability that can be on this form.
/// </summary>
AbilityIndex? FindAbilityIndex(IAbility ability);
/// <summary>
/// Gets an ability from the form.
/// </summary>
Result<StringKey> GetAbility(AbilityIndex index);
/// <summary>
/// Gets a random ability from the form.
/// </summary>
StringKey GetRandomAbility(IRandom rand);
/// <summary>
/// Gets a random hidden ability from the form.
/// </summary>
StringKey GetRandomHiddenAbility(IRandom rand);
/// <summary>
/// Check if the form has a specific flag set.
/// </summary>
bool HasFlag(string key);
}
/// <inheritdoc />
public class FormImpl : IForm
{
/// <inheritdoc cref="FormImpl" />
public FormImpl(StringKey name, float height, float weight, uint baseExperience,
IEnumerable<TypeIdentifier> types, StaticStatisticSet<ushort> baseStats, IEnumerable<StringKey> abilities,
IEnumerable<StringKey> hiddenAbilities, ILearnableMoves moves, ImmutableHashSet<StringKey> flags)
{
Name = name;
Height = height;
Weight = weight;
BaseExperience = baseExperience;
Types = [..types];
BaseStats = baseStats;
Abilities = [..abilities];
HiddenAbilities = [..hiddenAbilities];
Moves = moves;
Flags = flags;
if (Types.Count == 0)
throw new ArgumentException("A form must have at least one type.");
if (Abilities.Count == 0)
throw new ArgumentException("A form must have at least one ability.");
if (HiddenAbilities.Count == 0)
throw new ArgumentException("A form must have at least one hidden ability.");
}
/// <inheritdoc />
public StringKey Name { get; }
/// <inheritdoc />
public float Height { get; }
/// <inheritdoc />
public float Weight { get; }
/// <inheritdoc />
public uint BaseExperience { get; }
/// <inheritdoc />
public IReadOnlyList<TypeIdentifier> Types { get; }
/// <inheritdoc />
public StaticStatisticSet<ushort> BaseStats { get; }
/// <inheritdoc />
public IReadOnlyList<StringKey> Abilities { get; }
/// <inheritdoc />
public IReadOnlyList<StringKey> HiddenAbilities { get; }
/// <inheritdoc />
public ILearnableMoves Moves { get; }
/// <inheritdoc />
public ImmutableHashSet<StringKey> Flags { get; }
/// <inheritdoc />
public Result<TypeIdentifier> GetType(int index)
{
if (index < 0 || index >= Types.Count)
return Result.Fail(new OutOfRange("Type", index, Types.Count));
return Types[index];
}
/// <inheritdoc />
public ushort GetBaseStat(Statistic stat) => BaseStats.GetStatistic(stat);
/// <inheritdoc />
public AbilityIndex? FindAbilityIndex(IAbility ability)
{
for (var i = 0; i < Abilities.Count && i < 255; i++)
{
if (Abilities[i] == ability.Name)
return new AbilityIndex
{
IsHidden = false,
Index = (byte)i
};
}
for (var i = 0; i < HiddenAbilities.Count && i < 255; i++)
{
if (HiddenAbilities[i] == ability.Name)
return new AbilityIndex
{
IsHidden = true,
Index = (byte)i
};
}
return null;
}
/// <inheritdoc />
public Result<StringKey> GetAbility(AbilityIndex index)
{
var array = index.IsHidden ? HiddenAbilities : Abilities;
if (index.Index >= array.Count)
return Result.Fail(new OutOfRange("Ability", index.Index, array.Count));
return array[index.Index];
}
/// <inheritdoc />
public StringKey GetRandomAbility(IRandom rand)
{
return Abilities[rand.GetInt(Abilities.Count)];
}
/// <inheritdoc />
public StringKey GetRandomHiddenAbility(IRandom rand)
{
return HiddenAbilities[rand.GetInt(HiddenAbilities.Count)];
}
/// <inheritdoc />
public bool HasFlag(string key)
{
return Flags.Contains(key);
}
}

View File

@ -0,0 +1,21 @@
namespace PkmnLib.Static;
/// <summary>
/// Gender is a Pokemon characteristic.
///
/// Required for standard pokemon functions, but somewhat controversial nowadays. Consider adding a feature
/// that allows for a more progressive gender system for those that want it?
/// </summary>
public enum Gender : byte
{
/// The Pokemon has no gender.
Genderless,
/// <summary>
/// The Pokemon is male.
/// </summary>
Male,
/// <summary>
/// The Pokemon is female.
/// </summary>
Female
}

View File

@ -0,0 +1,58 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Static.Species;
/// <summary>
/// The storage of the moves a Pokemon can learn.
/// </summary>
public interface ILearnableMoves
{
/// <summary>
/// Adds a new level move the Pokemon can learn.
/// </summary>
/// <param name="level">The level the Pokemon learns the move at.</param>
/// <param name="move">The move the Pokemon learns.</param>
/// <returns>Whether the move was added successfully.</returns>
void AddLevelMove(LevelInt level, StringKey move);
/// <summary>
/// Gets all moves a Pokemon can learn when leveling up to a specific level.
/// </summary>
/// <param name="level">The level the Pokemon is learning moves at.</param>
/// <returns>The moves the Pokemon learns at that level.</returns>
IReadOnlyList<StringKey> GetLearnedByLevel(LevelInt level);
/// <summary>
/// Gets the distinct moves a Pokemon can learn through leveling up.
/// </summary>
/// <returns>The moves the Pokemon can learn through leveling up.</returns>
IReadOnlyList<StringKey> GetDistinctLevelMoves();
}
public class LearnableMovesImpl : ILearnableMoves
{
private readonly Dictionary<LevelInt, List<StringKey>> _learnedByLevel = new();
private readonly HashSet<StringKey> _distinctLevelMoves = new();
public void AddLevelMove(LevelInt level, StringKey move)
{
if (!_learnedByLevel.TryGetValue(level, out var value))
_learnedByLevel[level] = [move];
else
value.Add(move);
_distinctLevelMoves.Add(move);
}
public IReadOnlyList<StringKey> GetLearnedByLevel(LevelInt level)
{
if (!_learnedByLevel.TryGetValue(level, out var value))
return Array.Empty<StringKey>();
return value;
}
public IReadOnlyList<StringKey> GetDistinctLevelMoves()
{
return _distinctLevelMoves.ToList();
}
}

View File

@ -0,0 +1,148 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using PkmnLib.Static.Utils;
namespace PkmnLib.Static.Species;
/// <summary>
/// The data belonging to a Pokemon with certain characteristics.
/// </summary>
public interface ISpecies
{
/// <summary>
/// The national dex identifier of the Pokemon.
/// </summary>
ushort Id { get; }
/// <summary>
/// The name of the Pokemon.
/// </summary>
StringKey Name { get; }
/// <summary>
/// The chance between 0.0 and 1.0 that a Pokemon is female. 0.0 means always male, 1.0 means always female.
/// If less than 0, the Pokemon is genderless.
/// </summary>
float GenderRate { get; }
/// <summary>
/// How much experience is required for a level.
/// </summary>
StringKey GrowthRate { get; }
/// <summary>
/// How hard it is to capture a Pokemon. 255 means this will be always caught, 0 means this is
/// uncatchable.
/// </summary>
byte CaptureRate { get; }
/// <summary>
/// The base happiness of the Pokemon.
/// </summary>
byte BaseHappiness { get; }
/// <summary>
/// The forms that belong to this Pokemon.
/// </summary>
IReadOnlyDictionary<StringKey, IForm> Forms { get; }
/// <summary>
/// The arbitrary flags that can be set on a Pokemon for script use.
/// </summary>
ImmutableHashSet<StringKey> Flags { get; }
/// <summary>
/// Gets a form by name.
/// </summary>
bool TryGetForm(StringKey id, [MaybeNullWhen(false)] out IForm form);
/// <summary>
/// Gets the form the Pokemon will have by default, if no other form is specified.
/// </summary>
IForm GetDefaultForm();
/// <summary>
/// Gets a random gender.
/// </summary>
Gender GetRandomGender(IRandom rand);
/// <summary>
/// Check whether the Pokemon has a specific flag set.
/// </summary>
bool HasFlag(string key);
/// <summary>
/// The data regarding into which Pokemon this species can evolve, and how.
/// </summary>
IReadOnlyList<IEvolution> EvolutionData { get; }
}
/// <inheritdoc />
public class SpeciesImpl : ISpecies
{
/// <inheritdoc cref="SpeciesImpl" />
public SpeciesImpl(ushort id, StringKey name, float genderRate, StringKey growthRate, byte captureRate,
byte baseHappiness, IReadOnlyDictionary<StringKey, IForm> forms, ImmutableHashSet<StringKey> flags,
IReadOnlyList<IEvolution> evolutionData)
{
Id = id;
Name = name;
GenderRate = genderRate;
GrowthRate = growthRate;
CaptureRate = captureRate;
BaseHappiness = baseHappiness;
Forms = forms;
Flags = flags;
EvolutionData = evolutionData;
if (Forms.Count == 0)
throw new ArgumentException("Species must have at least one form.");
if (!Forms.ContainsKey("default"))
throw new ArgumentException("Species must have a default form.");
}
/// <inheritdoc />
public ushort Id { get; }
/// <inheritdoc />
public StringKey Name { get; }
/// <inheritdoc />
public float GenderRate { get; }
/// <inheritdoc />
public StringKey GrowthRate { get; }
/// <inheritdoc />
public byte CaptureRate { get; }
/// <inheritdoc />
public byte BaseHappiness { get; }
/// <inheritdoc />
public IReadOnlyDictionary<StringKey, IForm> Forms { get; }
/// <inheritdoc />
public ImmutableHashSet<StringKey> Flags { get; }
/// <inheritdoc />
public IReadOnlyList<IEvolution> EvolutionData { get; }
/// <inheritdoc />
public bool TryGetForm(StringKey id, [MaybeNullWhen(false)] out IForm form) => Forms.TryGetValue(id, out form);
/// <inheritdoc />
public IForm GetDefaultForm() => Forms["default"];
/// <inheritdoc />
public Gender GetRandomGender(IRandom rand)
{
if (GenderRate < 0.0f)
return Gender.Genderless;
var v = rand.GetFloat();
return v < GenderRate ? Gender.Female : Gender.Male;
}
/// <inheritdoc />
public bool HasFlag(string key) => Flags.Contains(key);
}

View File

@ -0,0 +1,32 @@
namespace PkmnLib.Static;
/// <summary>
/// Stats are numerical values on Pokemon that are used in battle.
/// </summary>
public enum Statistic : byte
{
/// <summary>
/// Health Points determine how much damage a Pokemon can receive before fainting.
/// </summary>
Hp,
/// <summary>
/// Attack determines how much damage a Pokemon deals when using a physical attack.
/// </summary>
Attack,
/// <summary>
/// Defense determines how much damage a Pokemon receives when it is hit by a physical attack.
/// </summary>
Defense,
/// <summary>
/// Special Attack determines how much damage a Pokemon deals when using a special attack.
/// </summary>
SpecialAttack,
/// <summary>
/// Special Defense determines how much damage a Pokemon receives when it is hit by a special attack.
/// </summary>
SpecialDefense,
/// <summary>
/// Speed determines the order that a Pokemon can act in battle.
/// </summary>
Speed
}

View File

@ -0,0 +1,155 @@
using System;
namespace PkmnLib.Static;
public record StaticStatisticSet<T>
where T : struct
{
/// <summary>
/// The health points stat value.
/// </summary>
public T Hp { get; protected set; }
/// <summary>
/// The physical attack stat value.
/// </summary>
public T Attack { get; protected set; }
/// <summary>
/// The physical defense stat value.
/// </summary>
public T Defense { get; protected set; }
/// <summary>
/// The special attack stat value.
/// </summary>
public T SpecialAttack { get; protected set; }
/// <summary>
/// The special defense stat value.
/// </summary>
public T SpecialDefense { get; protected set; }
/// <summary>
/// The speed stat value.
/// </summary>
public T Speed { get; protected set; }
public StaticStatisticSet(T hp, T attack, T defense, T specialAttack, T specialDefense, T speed)
{
Hp = hp;
Attack = attack;
Defense = defense;
SpecialAttack = specialAttack;
SpecialDefense = specialDefense;
Speed = speed;
}
public T GetStatistic(Statistic stat)
{
return stat switch
{
Statistic.Hp => Hp,
Statistic.Attack => Attack,
Statistic.Defense => Defense,
Statistic.SpecialAttack => SpecialAttack,
Statistic.SpecialDefense => SpecialDefense,
Statistic.Speed => Speed,
_ => throw new ArgumentException("Invalid statistic.")
};
}
}
public record StatisticSet<T> : StaticStatisticSet<T>
where T : struct
{
public StatisticSet(T hp, T attack, T defense, T specialAttack, T specialDefense, T speed) : base(hp, attack,
defense, specialAttack, specialDefense, speed)
{
}
public void SetStatistic(Statistic stat, T value)
{
switch (stat)
{
case Statistic.Hp:
Hp = value;
break;
case Statistic.Attack:
Attack = value;
break;
case Statistic.Defense:
Defense = value;
break;
case Statistic.SpecialAttack:
SpecialAttack = value;
break;
case Statistic.SpecialDefense:
SpecialDefense = value;
break;
case Statistic.Speed:
Speed = value;
break;
default:
throw new ArgumentException("Invalid statistic.");
}
}
public void IncreaseStatistic(Statistic stat, T value)
{
switch (stat)
{
case Statistic.Hp:
Hp = (T)Convert.ChangeType(Convert.ToInt32(Hp) + Convert.ToInt32(value), typeof(T));
break;
case Statistic.Attack:
Attack = (T)Convert.ChangeType(Convert.ToInt32(Attack) + Convert.ToInt32(value), typeof(T));
break;
case Statistic.Defense:
Defense = (T)Convert.ChangeType(Convert.ToInt32(Defense) + Convert.ToInt32(value), typeof(T));
break;
case Statistic.SpecialAttack:
SpecialAttack =
(T)Convert.ChangeType(Convert.ToInt32(SpecialAttack) + Convert.ToInt32(value), typeof(T));
break;
case Statistic.SpecialDefense:
SpecialDefense =
(T)Convert.ChangeType(Convert.ToInt32(SpecialDefense) + Convert.ToInt32(value), typeof(T));
break;
case Statistic.Speed:
Speed = (T)Convert.ChangeType(Convert.ToInt32(Speed) + Convert.ToInt32(value), typeof(T));
break;
default:
throw new ArgumentException("Invalid statistic.");
}
}
public void DecreaseStatistic(Statistic stat, T value)
{
switch (stat)
{
case Statistic.Hp:
Hp = (T)Convert.ChangeType(Convert.ToInt32(Hp) - Convert.ToInt32(value), typeof(T));
break;
case Statistic.Attack:
Attack = (T)Convert.ChangeType(Convert.ToInt32(Attack) - Convert.ToInt32(value), typeof(T));
break;
case Statistic.Defense:
Defense = (T)Convert.ChangeType(Convert.ToInt32(Defense) - Convert.ToInt32(value), typeof(T));
break;
case Statistic.SpecialAttack:
SpecialAttack =
(T)Convert.ChangeType(Convert.ToInt32(SpecialAttack) - Convert.ToInt32(value), typeof(T));
break;
case Statistic.SpecialDefense:
SpecialDefense =
(T)Convert.ChangeType(Convert.ToInt32(SpecialDefense) - Convert.ToInt32(value), typeof(T));
break;
case Statistic.Speed:
Speed = (T)Convert.ChangeType(Convert.ToInt32(Speed) - Convert.ToInt32(value), typeof(T));
break;
default:
throw new ArgumentException("Invalid statistic.");
}
}
}

View File

@ -0,0 +1,28 @@
namespace PkmnLib.Static;
/// <summary>
/// The time of day. These values are the 4 different groups of time of day in Pokemon games since
/// gen 5. The exact times these correspond to differ between games.
/// </summary>
public enum TimeOfDay : byte
{
/// <summary>
/// The morning.
/// </summary>
Morning = 0,
/// <summary>
/// The day.
/// </summary>
Day = 1,
/// <summary>
/// The evening.
/// </summary>
Evening = 2,
/// <summary>
/// The night.
/// </summary>
Night = 3,
}

View File

@ -0,0 +1,15 @@
namespace PkmnLib.Static;
public readonly record struct TypeIdentifier
{
private byte Value { get; init; }
public TypeIdentifier(byte value)
{
Value = value;
}
public static implicit operator TypeIdentifier(byte value) => new(value);
public override int GetHashCode() => Value.GetHashCode();
}

View File

@ -0,0 +1,11 @@
using FluentResults;
namespace PkmnLib.Static.Utils.Errors;
public class OutOfRange : Error
{
public OutOfRange(string hint, int index, int max)
: base($"{hint} index {index} is out of range. Must be between 0 and {max - 1}.")
{
}
}

View File

@ -0,0 +1,43 @@
namespace PkmnLib.Static.Utils;
public interface IRandom
{
public int GetInt(int min, int max);
public int GetInt(int max);
public int GetInt();
public float GetFloat();
public float GetFloat(float min, float max);
public bool GetBool();
}
public class RandomImpl : IRandom
{
private Random _random;
public RandomImpl(Random random)
{
_random = random;
}
public RandomImpl(int seed)
{
_random = new Random(seed);
}
public RandomImpl()
{
_random = new Random();
}
public int GetInt(int min, int max) => _random.Next(min, max);
public int GetInt(int max) => _random.Next(max);
public int GetInt() => _random.Next();
public float GetFloat() => (float)_random.NextDouble();
public float GetFloat(float min, float max) => (float)(_random.NextDouble() * (max - min) + min);
public bool GetBool() => _random.Next(2) == 1;
}

View File

@ -0,0 +1,35 @@
using System;
namespace PkmnLib.Static.Utils;
/// <summary>
/// A case-insensitive string key. We use this class for things like looking up data from dictionaries, etc.
/// </summary>
/// <remarks>
/// This is a struct, as it's effectively just a wrapper around a single reference object. Heap allocation would be silly.
/// </remarks>
public readonly record struct StringKey
{
private readonly string _key;
public StringKey(string key)
{
_key = key;
}
public static implicit operator string(StringKey key) => key._key;
public static implicit operator StringKey(string key) => new(key);
public override string ToString() => _key.ToLowerInvariant();
public bool Equals(StringKey other)
{
return string.Equals(_key, other._key, StringComparison.InvariantCultureIgnoreCase);
}
public override int GetHashCode()
{
return StringComparer.InvariantCultureIgnoreCase.GetHashCode(_key);
}
}

View File

@ -0,0 +1,4 @@
global using NUnit.Framework;
global using FluentAssertions;
global using LevelInt = byte;

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
<PackageReference Include="NUnit" Version="3.13.3"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,46 @@
using PkmnLib.Static;
namespace PkmnLib.Tests.Static;
public class GrowthRateTests
{
[Test]
public void EmptyLookupGrowthRateTestShouldThrowArgumentException()
{
Action act = () => _ = new LookupGrowthRate("Test", []);
act.Should().Throw<ArgumentException>().WithMessage("Experience table must have at least one entry.");
}
[Test]
public void NonZeroLookupGrowthRateTestShouldThrowArgumentException()
{
Action act = () => _ = new LookupGrowthRate("Test", [1]);
act.Should().Throw<ArgumentException>().WithMessage("Experience table must start at 0.");
}
[Test]
public void TooLargeLookupGrowthRateTestShouldThrowArgumentException()
{
Action act = () => _ = new LookupGrowthRate("Test", Enumerable.Range(0, LevelInt.MaxValue + 1).Select(i => (uint)i));
act.Should().Throw<ArgumentException>().WithMessage($"Experience table may have at most {LevelInt.MaxValue} entries.");
}
[Test]
public void LookupGrowthRateTest()
{
var growthRate = new LookupGrowthRate("Test", [0, 1, 2, 3, 4, 5]);
growthRate.CalculateLevel(0).Should().Be(1);
growthRate.CalculateLevel(1).Should().Be(2);
growthRate.CalculateLevel(2).Should().Be(3);
growthRate.CalculateLevel(3).Should().Be(4);
growthRate.CalculateLevel(4).Should().Be(5);
growthRate.CalculateLevel(5).Should().Be(6);
growthRate.CalculateExperience(1).Should().Be(0);
growthRate.CalculateExperience(2).Should().Be(1);
growthRate.CalculateExperience(3).Should().Be(2);
growthRate.CalculateExperience(4).Should().Be(3);
growthRate.CalculateExperience(5).Should().Be(4);
growthRate.CalculateExperience(6).Should().Be(5);
}
}

View File

@ -0,0 +1,48 @@
using System.Collections;
using PkmnLib.Static.Utils;
namespace PkmnLib.Tests.Static;
public class StringKeyTests
{
private static IEnumerable StringKeyEqualityTestCases
{
get
{
yield return new TestCaseData("test", "test").Returns(true);
yield return new TestCaseData("test", "test2").Returns(false);
yield return new TestCaseData("test2", "test2").Returns(true);
yield return new TestCaseData("Test", "test").Returns(true);
yield return new TestCaseData("TeSt", "tesT").Returns(true);
yield return new TestCaseData("TeSt", "tesv").Returns(false);
}
}
[Test]
[TestCaseSource(nameof(StringKeyEqualityTestCases))]
public bool StringKeyEqualityTest(string k1, string k2)
{
var sk1 = new StringKey(k1);
var sk2 = new StringKey(k2);
return sk1 == sk2;
}
[Test]
[TestCaseSource(nameof(StringKeyEqualityTestCases))]
public bool HashCodeEqualityTest(string k1, string k2)
{
var sk1 = new StringKey(k1);
var sk2 = new StringKey(k2);
return sk1.GetHashCode() == sk2.GetHashCode();
}
[Test]
[TestCaseSource(nameof(StringKeyEqualityTestCases))]
public bool HashSetEqualityTest(string k1, string k2)
{
var sk1 = new StringKey(k1);
var sk2 = new StringKey(k2);
var hs = new HashSet<StringKey> { sk1 };
return hs.Contains(sk2);
}
}