Skip to content

Commit

Permalink
Fix error in overcharm logic handling. Add tests for EquipCharmVariable.
Browse files Browse the repository at this point in the history
  • Loading branch information
homothetyhk committed Aug 2, 2024
1 parent f97ca27 commit e061177
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 98 deletions.
136 changes: 54 additions & 82 deletions RandomizerMod/RC/StateVariables/EquipCharmVariable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,39 +136,66 @@ protected virtual bool HasStateRequirements<T>(ProgressionManager pm, T state) w
return true;
}

public bool CanEquipNonovercharm<T>(ProgressionManager pm, T state) where T : IState
/// <summary>
/// Determines whether the charm can be equipped with or without overcharming for the given state. Does not check progression or state requirements.
/// </summary>
protected EquipResult HasNotchRequirements<T>(ProgressionManager pm, T state) where T : IState
{
if (HasStateRequirements(pm, state))
if (IsEquipped(state))
{
if (state.GetBool(CharmBool)) return !state.GetBool(OvercharmBool);
if (state.GetInt(UsedNotchesInt) + GetNotchCost(pm, state) <= pm.Get(NotchesTerm)) return true;
return state.GetBool(OvercharmBool) ? EquipResult.Overcharm : EquipResult.Nonovercharm; // Already equipped
}
return false;

int notchCost = GetNotchCost(pm, state);

if (notchCost <= 0)
{
return state.GetBool(OvercharmBool) ? EquipResult.Overcharm : EquipResult.Nonovercharm; // free to equip
}

int netNotches = pm.Get(NotchesTerm) - state.GetInt(UsedNotchesInt) - notchCost;

if (netNotches >= 0)
{
return EquipResult.Nonovercharm;
}

int overcharmSave = Math.Max(state.GetInt(MaxNotchCost), notchCost);

if (netNotches + overcharmSave > 0)
{
return EquipResult.Overcharm; // charm is not 0 notches, so it requires an open notch to overcharm
}

return EquipResult.None;
}

public bool CanEquipNonovercharm<T>(ProgressionManager pm, T state) where T : IState
{
return HasCharmProgression(pm) && HasStateRequirements(pm, state) && HasNotchRequirements(pm, state) == EquipResult.Nonovercharm;
}

public bool CanEquipOvercharm<T>(ProgressionManager pm, T state) where T : IState
{
if (HasStateRequirements(pm, state))
{
if (state.GetBool(CharmBool)) return true;
if (state.GetInt(UsedNotchesInt) < pm.Get(NotchesTerm)) return true;
}
return false;
return HasCharmProgression(pm) && HasStateRequirements(pm, state) && HasNotchRequirements(pm, state) != EquipResult.None;
}

public EquipResult CanEquip(ProgressionManager pm, StateUnion? localState)
{
if (localState is null || !HasCharmProgression(pm)) return EquipResult.None;
bool overcharm = false;
for (int i = 0; i < localState.Count; i++)
{
if (CanEquipNonovercharm(pm, localState[i])) return EquipResult.Nonovercharm;
}
for (int i = 0; i < localState.Count; i++)
{
if (CanEquipOvercharm(pm, localState[i])) return EquipResult.Overcharm;
if (!HasStateRequirements(pm, localState[i])) continue;
switch (HasNotchRequirements(pm, localState[i]))
{
case EquipResult.None: continue;
case EquipResult.Overcharm: overcharm = true; continue;
case EquipResult.Nonovercharm: return EquipResult.Nonovercharm;
}
}

return EquipResult.None;
return overcharm ? EquipResult.Overcharm : EquipResult.None;
}

/// <summary>
Expand All @@ -177,9 +204,7 @@ public EquipResult CanEquip(ProgressionManager pm, StateUnion? localState)
public EquipResult CanEquip<T>(ProgressionManager pm, T state) where T : IState
{
if (!HasCharmProgression(pm) || !HasStateRequirements(pm, state)) return EquipResult.None;
if (CanEquipNonovercharm(pm, state)) return EquipResult.Nonovercharm;
if (CanEquipOvercharm(pm, state)) return EquipResult.Overcharm;
return EquipResult.None;
return HasNotchRequirements(pm, state);
}

public override IEnumerable<LazyStateBuilder> ModifyState(object? sender, ProgressionManager pm, LazyStateBuilder state)
Expand All @@ -194,38 +219,12 @@ public bool TryEquip(object? sender, ProgressionManager pm, ref LazyStateBuilder
return true;
}

if (!HasCharmProgression(pm) || !HasStateRequirements(pm, state))
if (CanEquip(pm, state) != EquipResult.None)
{
return false;
}

int notchCost = GetNotchCost(pm, state);
if (notchCost <= 0)
{
DoEquipCharm(pm, notchCost, ref state);
return true;
}

int netNotches = pm.Get(NotchesTerm) - state.GetInt(UsedNotchesInt);

if (netNotches <= 0)
{
int oldMaxNotch = state.GetInt(MaxNotchCost);
if (oldMaxNotch > notchCost && netNotches + oldMaxNotch > 0)
{
DoEquipCharm(pm, notchCost, ref state);
}
return false;
}
else
{
if (netNotches < notchCost && state.GetBool(HasTakenDamage))
{
return false; // state cannot overcharm!
}
DoEquipCharm(pm, notchCost, ref state);
DoEquipCharm(pm, GetNotchCost(pm, state), ref state);
return true;
}
return false;
}

public bool TryEquip(object? sender, ProgressionManager pm, in LazyStateBuilder state, out LazyStateBuilder newState)
Expand All @@ -235,41 +234,13 @@ public bool TryEquip(object? sender, ProgressionManager pm, in LazyStateBuilder
return true;
}

if (!HasCharmProgression(pm) || !HasStateRequirements(pm, state))
{
return false;
}

int notchCost = GetNotchCost(pm, state);
if (notchCost <= 0)
{
newState = new(state);
DoEquipCharm(pm, notchCost, ref newState);
return true;
}

int netNotches = pm.Get(NotchesTerm) - state.GetInt(UsedNotchesInt);

if (netNotches <= 0)
{
int oldMaxNotch = state.GetInt(MaxNotchCost);
if (oldMaxNotch > notchCost && netNotches + oldMaxNotch > 0)
{
newState = new(state);
DoEquipCharm(pm, notchCost, ref newState);
}
return false;
}
else
if (CanEquip(pm, state) != EquipResult.None)
{
if (netNotches < notchCost && state.GetBool(HasTakenDamage))
{
return false; // state cannot overcharm!
}
newState = new(state);
DoEquipCharm(pm, notchCost, ref newState);
DoEquipCharm(pm, GetNotchCost(pm, state), ref newState);
return true;
}
return false;
}

protected virtual void DoEquipCharm(ProgressionManager pm, int notchCost, ref LazyStateBuilder state)
Expand All @@ -280,8 +251,9 @@ protected virtual void DoEquipCharm(ProgressionManager pm, int notchCost, ref La
if (state.GetInt(UsedNotchesInt) > pm.Get(NotchesTerm)) state.SetBool(OvercharmBool, true);
}

public virtual bool IsEquipped(LazyStateBuilder state) => state.GetBool(CharmBool);
public virtual void SetUnequippable(ref LazyStateBuilder state) => state.SetBool(AnticharmBool, true);
public bool IsEquipped(LazyStateBuilder state) => state.GetBool(CharmBool);
public bool IsEquipped<T>(T state) where T : IState => state.GetBool(CharmBool);
public void SetUnequippable(ref LazyStateBuilder state) => state.SetBool(AnticharmBool, true);
public int GetAvailableNotches(ProgressionManager pm, LazyStateBuilder state)
{
return pm.Get(NotchesTerm) - state.GetInt(UsedNotchesInt);
Expand Down
10 changes: 8 additions & 2 deletions RandomizerModTests/LogicFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ namespace RandomizerModTests
public class LogicFixture
{
public LogicManager LM { get; }
public RandoModContext Default_CTX { get; }

private RandoModContext Default_CTX { get; }

public LogicFixture()
{
Expand All @@ -18,9 +19,14 @@ public LogicFixture()
LM = Default_CTX.LM;
}

public RandoModContext GetContext()
{
return new(Default_CTX);
}

public ProgressionManager GetProgressionManager(Dictionary<string, int> pmFieldValues)
{
ProgressionManager pm = new(LM, Default_CTX);
ProgressionManager pm = new(LM, new RandoModContext(Default_CTX));
foreach (var kvp in pmFieldValues) pm.Set(kvp.Key, kvp.Value);
return pm;
}
Expand Down
39 changes: 25 additions & 14 deletions RandomizerModTests/RandomizerModTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion>
<HollowKnightRefs>..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed</HollowKnightRefs>
</PropertyGroup>

<Import Project="LocalOverrides.targets" Condition="Exists('LocalOverrides.targets')" />

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="FluentAssertions.Analyzers" Version="0.24.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
Expand All @@ -24,46 +32,49 @@
</ItemGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\Assembly-CSharp.dll</HintPath>
<HintPath>$(HollowKnightRefs)\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="ItemChanger">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\Mods\ItemChanger\ItemChanger.dll</HintPath>
<HintPath>$(HollowKnightRefs)\Mods\ItemChanger\ItemChanger.dll</HintPath>
</Reference>
<Reference Include="MenuChanger">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\Mods\MenuChanger\MenuChanger.dll</HintPath>
<HintPath>$(HollowKnightRefs)\Mods\MenuChanger\MenuChanger.dll</HintPath>
</Reference>
<Reference Include="MMHOOK_Assembly-CSharp">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\MMHOOK_Assembly-CSharp.dll</HintPath>
<HintPath>$(HollowKnightRefs)\MMHOOK_Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="MMHOOK_PlayMaker">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\MMHOOK_PlayMaker.dll</HintPath>
<HintPath>$(HollowKnightRefs)\MMHOOK_PlayMaker.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\Newtonsoft.Json.dll</HintPath>
<HintPath>$(HollowKnightRefs)\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="PlayMaker">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\PlayMaker.dll</HintPath>
<HintPath>$(HollowKnightRefs)\PlayMaker.dll</HintPath>
</Reference>
<Reference Include="RandomizerCore">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\Mods\RandomizerCore\RandomizerCore.dll</HintPath>
<HintPath>$(HollowKnightRefs)\Mods\RandomizerCore\RandomizerCore.dll</HintPath>
</Reference>
<Reference Include="RandomizerCore.Json">
<HintPath>$(HollowKnightRefs)\Mods\RandomizerCore.Json\RandomizerCore.Json.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\UnityEngine.dll</HintPath>
<HintPath>$(HollowKnightRefs)\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
<HintPath>$(HollowKnightRefs)\UnityEngine.CoreModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ImageConversionModule">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\UnityEngine.ImageConversionModule.dll</HintPath>
<HintPath>$(HollowKnightRefs)\UnityEngine.ImageConversionModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.IMGUIModule">
<HintPath>..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\UnityEngine.IMGUIModule.dll</HintPath>
<HintPath>$(HollowKnightRefs)\UnityEngine.IMGUIModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TextRenderingModule">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\UnityEngine.TextRenderingModule.dll</HintPath>
<HintPath>$(HollowKnightRefs)\UnityEngine.TextRenderingModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UI">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Hollow Knight\hollow_knight_Data\Managed\UnityEngine.UI.dll</HintPath>
<HintPath>$(HollowKnightRefs)\UnityEngine.UI.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
97 changes: 97 additions & 0 deletions RandomizerModTests/StateVariables/EquipCharmVariableTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using RandomizerCore.Logic;
using RandomizerMod.RC;
using RandomizerMod.RC.StateVariables;
using FluentAssertions;
using RandomizerCore.Logic.StateLogic;
using EquipResult = RandomizerMod.RC.StateVariables.EquipCharmVariable.EquipResult;

namespace RandomizerModTests.StateVariables
{
[Collection("Logic Collection")]
public class EquipCharmVariableTests
{
LogicFixture Fix { get; }

public EquipCharmVariableTests(LogicFixture fix)
{
Fix = fix;
}

public static Dictionary<string, int> CharmStateBase => new() { ["NOPASSEDCHARMEQUIP"] = 0 };

[Fact]
public void BasicEquipRequirementsTest()
{
LogicManager lm = Fix.LM;
Term swarmTerm = lm.GetTermStrict("Gathering_Swarm");
EquipCharmVariable swarm = (EquipCharmVariable)lm.GetVariableStrict(EquipCharmVariable.GetName(swarmTerm.Name));
var pm = Fix.GetProgressionManager(new() { [swarmTerm.Name] = 1, ["NOTCHES"] = 1 });

LazyStateBuilder state = new(lm.StateManager.DefaultState);
swarm.TryEquip(null, pm, ref state).Should().BeFalse();
swarm.IsEquipped(state).Should().BeFalse();
state.GetInt(lm.StateManager.GetIntStrict("USEDNOTCHES")).Should().Be(0);
state.GetInt(lm.StateManager.GetIntStrict("MAXNOTCHCOST")).Should().Be(0);

state.SetBool(lm.StateManager.GetBoolStrict("NOPASSEDCHARMEQUIP"), false);
swarm.TryEquip(null, pm, in state, out LazyStateBuilder result).Should().BeTrue();
swarm.IsEquipped(state).Should().BeFalse();
state.GetInt(lm.StateManager.GetIntStrict("USEDNOTCHES")).Should().Be(0);
state.GetInt(lm.StateManager.GetIntStrict("MAXNOTCHCOST")).Should().Be(0);
swarm.IsEquipped(result).Should().BeTrue();
result.GetInt(lm.StateManager.GetIntStrict("USEDNOTCHES")).Should().Be(1);
result.GetInt(lm.StateManager.GetIntStrict("MAXNOTCHCOST")).Should().Be(1);

result = new(state);
swarm.SetUnequippable(ref state);
swarm.TryEquip(null, pm, ref state).Should().BeFalse();
swarm.IsEquipped(state).Should().BeFalse();
state.GetInt(lm.StateManager.GetIntStrict("USEDNOTCHES")).Should().Be(0);
state.GetInt(lm.StateManager.GetIntStrict("MAXNOTCHCOST")).Should().Be(0);

swarm.TryEquip(null, pm, ref result).Should().BeTrue();
swarm.IsEquipped(result).Should().BeTrue();
result.GetInt(lm.StateManager.GetIntStrict("USEDNOTCHES")).Should().Be(1);
result.GetInt(lm.StateManager.GetIntStrict("MAXNOTCHCOST")).Should().Be(1);

swarm.TryEquip(null, pm, ref result).Should().BeTrue();
swarm.IsEquipped(result).Should().BeTrue();
result.GetInt(lm.StateManager.GetIntStrict("USEDNOTCHES")).Should().Be(1);
result.GetInt(lm.StateManager.GetIntStrict("MAXNOTCHCOST")).Should().Be(1);
}

[Theory]
[InlineData(1, (int[])[1], (bool[])[true], false)]
[InlineData(3, (int[])[1, 2, 1], (bool[])[true, true, true], true)]
[InlineData(3, (int[])[1, 2, 2], (bool[])[true, true, false], false)]
[InlineData(3, (int[])[2, 2, 1], (bool[])[true, true, false], true)]
[InlineData(3, (int[])[2, 2, 0], (bool[])[true, true, true], true)]
[InlineData(3, (int[])[4, 1, 1], (bool[])[true, true, true], true)]
[InlineData(3, (int[])[4, 1, 1, 1], (bool[])[true, true, true, false], true)]
[InlineData(3, (int[])[1, 1, 1, 4], (bool[])[true, true, true, false], false)]
[InlineData(3, (int[])[0, 1, 0], (bool[])[true, true, true], false)]
public void EquipCharmNotchTests(int notches, int[] notchCosts, bool[] equipResults, bool endedOvercharmed)
{
LogicManager lm = Fix.LM;

var terms = lm.Terms.GetTermList(TermType.SignedByte).Skip(lm.GetTermStrict("Gathering_Swarm").Index).Take(notchCosts.Length);
var charms = terms.Select(t => lm.GetVariableStrict(EquipCharmVariable.GetName(t.Name))).Cast<EquipCharmVariable>().ToArray();

var state = Fix.GetState(CharmStateBase);
var pm = Fix.GetProgressionManager(terms.ToDictionary(t => t.Name, t => 1));
RandoModContext ctx = (RandoModContext)pm.ctx;
for (int i = 0; i < notchCosts.Length; i++) ctx.notchCosts[i] = notchCosts[i];
pm.Set("NOTCHES", notches);

for (int i = 0; i < notchCosts.Length; i++)
{
charms[i].TryEquip(null, pm, ref state).Should().Be(equipResults[i], lm.StateManager.PrettyPrint(state));
}

state.GetBool(lm.StateManager.GetBoolStrict("OVERCHARMED")).Should().Be(endedOvercharmed, lm.StateManager.PrettyPrint(state));

}


}
}

0 comments on commit e061177

Please sign in to comment.