Evolve-With-Move: Refactor to be memory-like

Evolving with a move requires once knowing the move, should be a much better approximation solution here. Ideally we check in specific contexts as Pokemon may learn their requisite moves after evolution, but this should catch most for now.
This commit is contained in:
Kurt
2023-09-12 18:14:22 -07:00
parent 21725aa1c0
commit 9f340b592b
2 changed files with 52 additions and 211 deletions

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.Move;
using static PKHeX.Core.Species;
@@ -16,120 +15,32 @@ internal static class EvolutionRestrictions
/// <summary>
/// List of species that evolve from a previous species having a move while leveling up
/// </summary>
private static readonly Dictionary<ushort, MoveEvolution> SpeciesEvolutionWithMove = new()
private static ushort GetSpeciesEvolutionMove(ushort species) => species switch
{
{(int)Eevee, new()}, // FairyMoves
{(int)MimeJr, new(01, (int)Mimic)},
{(int)Bonsly, new(02, (int)Mimic)},
{(int)Aipom, new(03, (int)Mimic)},
{(int)Lickitung, new(04, (int)Rollout)},
{(int)Tangela, new(05, (int)AncientPower)},
{(int)Yanma, new(06, (int)AncientPower)},
{(int)Piloswine, new(07, (int)AncientPower)},
{(int)Steenee, new(08, (int)Stomp)},
{(int)Clobbopus, new(09, (int)Taunt)},
{(int)Stantler, new(10, (int)PsyshieldBash)},
{(int)Qwilfish, new(11, (int)BarbBarrage)},
{(int)Primeape, new(12, (int)RageFist)},
{(int)Girafarig, new(13, (int)TwinBeam)},
{(int)Dunsparce, new(14, (int)HyperDrill)},
(int)Eevee => 0,
(int)MimeJr => (int)Mimic,
(int)Bonsly => (int)Mimic,
(int)Aipom => (int)Mimic,
(int)Lickitung => (int)Rollout,
(int)Tangela => (int)AncientPower,
(int)Yanma => (int)AncientPower,
(int)Piloswine => (int)AncientPower,
(int)Steenee => (int)Stomp,
(int)Clobbopus => (int)Taunt,
(int)Stantler => (int)PsyshieldBash,
(int)Qwilfish => (int)BarbBarrage,
(int)Primeape => (int)RageFist,
(int)Girafarig => (int)TwinBeam,
(int)Dunsparce => (int)HyperDrill,
_ => NONE,
};
private readonly record struct MoveEvolution(byte ReferenceIndex, ushort Move);
private const ushort NONE = ushort.MaxValue;
private static ReadOnlySpan<ushort> FairyMoves => new ushort[]
private static ReadOnlySpan<ushort> EeveeFairyMoves => new ushort[]
{
(int)SweetKiss,
(int)Charm,
(int)Moonlight,
(int)DisarmingVoice,
(int)DrainingKiss,
(int)CraftyShield,
(int)FlowerShield,
(int)MistyTerrain,
(int)PlayRough,
(int)FairyWind,
(int)Moonblast,
(int)FairyLock,
(int)AromaticMist,
(int)Geomancy,
(int)DazzlingGleam,
(int)BabyDollEyes,
(int)LightofRuin,
(int)TwinkleTackleP,
(int)TwinkleTackleS,
(int)FloralHealing,
(int)GuardianofAlola,
(int)FleurCannon,
(int)NaturesMadness,
(int)LetsSnuggleForever,
(int)SparklySwirl,
(int)MaxStarfall,
(int)Decorate,
(int)SpiritBreak,
(int)StrangeSteam,
(int)MistyExplosion,
(int)SpringtideStorm,
};
/// <summary>
/// Minimum current level for a given species to have learned the evolve-move and be successfully evolved.
/// </summary>
/// <remarks>Having a value of 0 means the move can't be learned.</remarks>
private static ReadOnlySpan<byte> MinLevelEvolutionWithMove => new byte[]
{
00, 00, 00, 00, 00, 29, 09, 02, 02, 02, // Sylveon (Eevee with Fairy Move)
00, 00, 00, 00, 18, 15, 15, 02, 32, 00, // Mr. Mime (Mime Jr with Mimic)
00, 00, 00, 00, 17, 17, 15, 02, 16, 16, // Sudowoodo (Bonsly with Mimic)
00, 00, 00, 00, 32, 32, 32, 02, 32, 00, // Ambipom (Aipom with Double Hit)
00, 00, 02, 00, 02, 33, 33, 02, 06, 00, // Lickilicky (Lickitung with Rollout)
00, 00, 00, 00, 02, 36, 38, 02, 24, 00, // Tangrowth (Tangela with Ancient Power)
00, 00, 00, 00, 02, 33, 33, 02, 33, 00, // Yanmega (Yanma with Ancient Power)
00, 00, 00, 00, 02, 02, 02, 02, 02, 00, // Mamoswine (Piloswine with Ancient Power)
00, 00, 00, 00, 00, 00, 00, 02, 28, 28, // Tsareena (Steenee with Stomp)
00, 00, 00, 00, 00, 00, 00, 00, 35, 00, // Grapploct (Clobbopus with Taunt)
00, 00, 00, 00, 00, 00, 00, 00, 00, 00, // Wyrdeer (Stantler with AGILE Psyshield Bash)
00, 00, 00, 00, 00, 00, 00, 00, 00, 00, // Overqwil (Qwilfish-1 with STRONG Barb Barrage)
00, 00, 00, 00, 00, 00, 00, 00, 00, 35, // Annihilape (Primeape with Rage Fist)
00, 00, 00, 00, 00, 00, 00, 00, 00, 32, // Farigiraf (Girafarig with Twin Beam)
00, 00, 00, 00, 00, 00, 00, 00, 00, 32, // Dudunsparce (Dunsparce with Hyper Drill)
};
private const int MinLevelEvoWidth = 10;
private static ReadOnlySpan<byte> MinLevelEvolutionWithMove_8LA => new byte[]
{
00, // Sylveon (Eevee with Fairy Move)
25, // Mr. Mime (Mime Jr with Mimic)
29, // Sudowoodo (Bonsly with Mimic)
25, // Ambipom (Aipom with Double Hit)
34, // Lickilicky (Lickitung with Rollout)
34, // Tangrowth (Tangela with Ancient Power)
34, // Yanmega (Yanma with Ancient Power)
34, // Mamoswine (Piloswine with Ancient Power)
99, // Tsareena (Steenee with Stomp)
99, // Grapploct (Clobbopus with Taunt)
31, // Wyrdeer (Stantler with AGILE Psyshield Bash)
25, // Overqwil (Qwilfish-1 with STRONG Barb Barrage)
};
private static ReadOnlySpan<byte> CanEggHatchWithEvolveMove => new byte[]
{
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, // Sylveon (Eevee with Fairy Move)
0, 0, 0, 0, 1, 1, 1, 1, 1, 0, // Mr. Mime (Mime Jr with Mimic)
0, 0, 0, 0, 1, 1, 1, 1, 1, 1, // Sudowoodo (Bonsly with Mimic)
0, 0, 0, 0, 1, 1, 1, 1, 1, 0, // Ambipom (Aipom with Double Hit)
0, 0, 1, 0, 1, 1, 1, 1, 1, 0, // Lickilicky (Lickitung with Rollout)
0, 0, 0, 0, 1, 1, 1, 1, 1, 0, // Tangrowth (Tangela with Ancient Power)
0, 0, 0, 0, 1, 1, 1, 1, 1, 0, // Yanmega (Yanma with Ancient Power)
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, // Mamoswine (Piloswine with Ancient Power)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Tsareena (Steenee with Stomp)
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, // Grapploct (Clobbopus with Taunt)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Wyrdeer (Stantler with AGILE Psyshield Bash)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Overqwil (Qwilfish-1 with STRONG Barb Barrage)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Annihilape (Primeape with Rage Fist)
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // Farigiraf (Girafarig with Twin Beam)
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // Dudunsparce (Dunsparce with Hyper Drill)
};
/// <summary>
@@ -143,132 +54,57 @@ public static bool IsValidEvolutionWithMove(PKM pk, LegalInfo info)
return true;
// OK if un-evolved from original encounter
var enc = info.EncounterOriginal;
ushort species = pk.Species;
if (info.EncounterMatch.Species == species)
if (enc.Species == species)
return true;
// Exclude evolution paths that did not require a move w/level-up evolution
var enc = info.EncounterOriginal;
if (!SpeciesEvolutionWithMove.TryGetValue(enc.Species, out var entry))
return true;
var move = entry.Move;
var move = GetSpeciesEvolutionMove(enc.Species);
if (move is NONE)
return true; // not a move evolution
if (move == 0)
{
// Other evolutions are fine.
if (pk.Species != (int)Sylveon)
return true;
}
return species != (int)Sylveon || IsValidEvolutionWithMoveSylveon(pk, enc, info);
if (!IsMoveSlotAvailable(info.Moves))
return false;
// Check if the move was already known when it was originally encountered.
var gen = info.Generation;
var index = entry.ReferenceIndex;
if (enc is EncounterEgg)
{
var slice = CanEggHatchWithEvolveMove.Slice(index * MinLevelEvoWidth, MinLevelEvoWidth);
if (slice[gen] != 0) // true
{
var result = move == 0 ? IsMoveInherited(pk, info, FairyMoves) : IsMoveInherited(pk, info, move);
if (result)
return true;
}
}
else if (enc is IMoveset s)
{
var moves = s.Moves;
var result = move == 0 ? moves.ContainsAny(FairyMoves) : moves.Contains(move);
if (result)
return true;
}
// Current level must be at least the minimum post-evolution level.
var lvl = GetMinLevelKnowRequiredMove(pk, gen, index, info.EvoChainsAllGens);
return pk.CurrentLevel >= lvl;
}
private static int GetMinLevelKnowRequiredMove(PKM pk, int gen, int index, EvolutionHistory evos)
{
var lvl = GetLevelLearnMove(pk, gen, index);
// If has original met location the minimum evolution level is one level after met level
// Gen 3 pokemon in gen 4 games: minimum level is one level after transfer to generation 4
// VC pokemon: minimum level is one level after transfer to generation 7
// Sylveon: always one level after met level, for gen 4 and 5 Eevee in gen 6 games minimum for evolution is one level after transfer to generation 5
if (pk.HasOriginalMetLocation || (pk.Format == 4 && gen == 3) || pk.VC || pk.Species == (int)Sylveon)
lvl = Math.Max(pk.Met_Level + 1, lvl);
if (evos.HasVisitedPLA) // No Level Up required, and different levels than mainline SW/SH.
{
var la = MinLevelEvolutionWithMove_8LA[index];
if (la <= lvl)
return la;
}
return lvl;
}
private static int GetLevelLearnMove(PKM pk, int gen, int index)
{
// Get the minimum level in any generation when the pokemon could learn the evolve move
var levels = MinLevelEvolutionWithMove.Slice(index * MinLevelEvoWidth, MinLevelEvoWidth);
var lvl = 101;
var end = pk.Format;
for (int g = gen; g <= end; g++)
{
var l = levels[g];
if (l == 0)
continue;
if (l == 2)
return 2; // true minimum
if (l < lvl)
lvl = l; // best minimum
}
return lvl;
}
private static bool IsMoveInherited(PKM pk, LegalInfo info, ushort move)
{
// In 3DS games, the inherited move must be in the relearn moves.
if (info.Generation >= 6 && !pk.IsOriginalMovesetDeleted())
return pk.HasRelearnMove(move);
// In Pre-3DS games, the move is inherited if it has the move and it can be hatched with the move.
if (pk.HasMove(move))
return true;
return DidLearnAndForget(info);
var head = LearnGroupUtil.GetCurrentGroup(pk);
return MemoryPermissions.GetCanKnowMove(enc, move, info.EvoChainsAllGens, pk, head);
}
private static bool IsMoveInherited(PKM pk, LegalInfo info, ReadOnlySpan<ushort> moves)
private static bool IsValidEvolutionWithMoveSylveon(PKM pk, IEncounterTemplate enc, LegalInfo info)
{
// In 3DS games, the inherited move must be in the relearn moves.
if (info.Generation >= 6)
if (!IsMoveSlotAvailable(info.Moves))
return false;
foreach (var move in EeveeFairyMoves)
{
Span<ushort> relearn = stackalloc ushort[4];
pk.GetRelearnMoves(relearn);
return relearn.IndexOfAny(moves) != -1;
if (pk.HasMove(move))
return true;
}
// In Pre-3DS games, the move is inherited if it has the move and it can be hatched with the move.
Span<ushort> pkMoves = stackalloc ushort[4];
pk.GetMoves(pkMoves);
var index = pkMoves.IndexOfAny(moves);
if (index != -1)
return true;
return DidLearnAndForget(info);
var head = LearnGroupUtil.GetCurrentGroup(pk);
foreach (var move in EeveeFairyMoves)
{
if (MemoryPermissions.GetCanKnowMove(enc, move, info.EvoChainsAllGens, pk, head))
return true;
}
return false;
}
private static bool DidLearnAndForget(LegalInfo info)
private static bool IsMoveSlotAvailable(ReadOnlySpan<MoveResult> moves)
{
// If the pokemon does not currently have the move, it could have been an egg move that was forgotten.
// This requires the pokemon to not have 4 other moves identified as egg moves or inherited level up moves.
// If any move is not an egg source, then a slot could have been forgotten.
foreach (var move in info.Moves)
foreach (var move in moves)
{
if (!move.Info.Method.IsEggSource())
return false;
return true;
}
return true;
return false;
}
}

View File

@@ -159,6 +159,11 @@ private static bool GetCanKnowMove(PKM pk, ushort move, EntityContext context, E
else
return false;
return GetCanKnowMove(enc, move, history, pk, game);
}
public static bool GetCanKnowMove(IEncounterTemplate enc, ushort move, EvolutionHistory history, PKM pk, ILearnGroup game)
{
Span<MoveResult> result = stackalloc MoveResult[1];
Span<ushort> moves = stackalloc ushort[] { move };
LearnVerifierHistory.MarkAndIterate(result, moves, enc, pk, history, game, MoveSourceType.All, LearnOption.AtAnyTime);