Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions Sources/Everglow.UnitTests/Modules/Yggdrasil/CrowdActivityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
using Everglow.Yggdrasil.Mechanics.TownCrowd;
using Microsoft.Xna.Framework;

namespace Everglow.UnitTests.Modules.Yggdrasil;

[TestClass]
public class CrowdActivityTests
{
[TestMethod]
public void SittingIsAnActivityAndConversationHasItsOwnLongerDuration()
{
Assert.IsTrue(Enum.TryParse<CrowdActivity>("Sit", out var sitting));
Assert.IsFalse(CrowdActivityRules.IsExit(sitting));
Assert.AreEqual(8 * 60, CrowdSettings.ChatMinTicks);
Assert.AreEqual(15 * 60, CrowdSettings.ChatMaxTicks);
}

[TestMethod]
public void RepeatedlyBlockedReturnsCanRetireOffscreenWithoutInterruptingAPlannedExit()
{
var retire = typeof(CrowdActivityRules).GetMethod("CanRetireAfterFailedDeparture");
Assert.IsNotNull(retire);
bool Check(int age, bool visible, int failures) => (bool)retire.Invoke(null, [age, visible, failures])!;
Assert.IsTrue(Check(12000, false, 3));
Assert.IsFalse(Check(12000, true, 3));
Assert.IsFalse(Check(12000, false, 0));
Assert.IsFalse(Check(12000, false, 2));
Assert.IsFalse(Check(6000, false, 3));
}

[TestMethod]
public void ReturnIntentPersistsAndVisitorsHaveDifferentStayDurations()
{
var decision = typeof(CrowdActivityRules).GetMethod("ShouldDepart");
Assert.IsNotNull(decision);
bool Decide(bool returning, int age, int visits, int seed) => (bool)decision.Invoke(null, [returning, age, visits, seed])!;
Assert.IsTrue(Decide(true, 100, 0, 42));
Assert.IsFalse(Decide(false, 300, 0, 42));
Assert.IsFalse(Decide(false, 300, 5, 42));
Assert.IsTrue(Decide(false, 10801, 0, 42));
Assert.IsTrue(Decide(false, 3000, 5, 42));
Assert.IsTrue(Enumerable.Range(0, 6000).Select(seed => Decide(false, 6000, 0, seed)).Distinct().Count() == 2);
}

[TestMethod]
public void ReturningVisitorsCanReuseTheirArrivalPylonAndPreferItToADoor()
{
float pylon = CrowdActivityRules.Weight(CrowdActivity.LeavePylon, true, true, 0, 2, 300, 1);
float door = CrowdActivityRules.Weight(CrowdActivity.EnterBuilding, true, false, 0, 2, 300, 1);
Assert.IsTrue(pylon > door, "Returning home must remain possible even when this pylon was the last sightseeing stop.");
Assert.AreEqual(0f, CrowdActivityRules.Weight(CrowdActivity.LeavePylon, true, true, 2, 2, 300, 1));
Assert.AreEqual(0f, CrowdActivityRules.Weight(CrowdActivity.LeavePylon, false, false, 0, 2, 300, 1));
}

[TestMethod]
public void PylonDepartureWaitsBeforeFadingAndDoorDepartureKeepsItsShortFade()
{
var opacity = typeof(CrowdActivityRules).GetMethod("DepartureOpacity");
Assert.IsNotNull(opacity);
float At(CrowdActivity activity, int tick) => (float)opacity.Invoke(null, [activity, tick])!;
Assert.AreEqual(1f, At(CrowdActivity.LeavePylon, 0));
Assert.AreEqual(1f, At(CrowdActivity.LeavePylon, 59));
Assert.AreEqual(0.5f, At(CrowdActivity.LeavePylon, 75), 0.001f);
Assert.AreEqual(0f, At(CrowdActivity.LeavePylon, 90));
Assert.AreEqual(0f, At(CrowdActivity.LeavePylon, 120));
Assert.AreEqual(0.5f, At(CrowdActivity.EnterBuilding, 30), 0.001f);
}

[TestMethod]
public void CrowdNamePoolIsVariedAndStableForTheSameIdentity()
{
var names = typeof(TownCrowdSystem).Assembly.GetType("Everglow.Yggdrasil.Mechanics.TownCrowd.CrowdNames");
Assert.IsNotNull(names);
var key = names.GetMethod("KeyFor");
Assert.IsNotNull(key);
string Key(int seed) => (string)key.Invoke(null, [seed])!;
var keys = Enumerable.Range(0, 256).Select(Key).ToArray();
Assert.IsTrue(keys.Distinct().Count() >= 24);
Assert.IsTrue(keys.All(value => value.StartsWith("Mods.Everglow.Travelers.Names.")));
Assert.AreEqual(Key(931), Key(931));
Assert.IsFalse(string.IsNullOrWhiteSpace(Key(int.MinValue)));
}

[TestMethod]
public void LifetimeOnlyReclaimsTravelersOutsideView()
{
Assert.IsFalse(CrowdActivityRules.CanRetire(10801, true));
Assert.IsFalse(CrowdActivityRules.CanRetire(900, false));
Assert.IsTrue(CrowdActivityRules.CanRetire(10801, false));
}

[TestMethod]
public void TrainLoadOnlyCountsSuccessfulCreationAndResetsOnceAfterOffscreenDeparture()
{
var load = new CrowdTrainLoad();
load.Observe(false, false, false);
Assert.AreEqual(0, load.Loaded);
load.RecordLoaded();
load.Observe(false, false, true);
Assert.AreEqual(1, load.Loaded);
load.Observe(true, true, true);
load.Observe(false, true, false);
Assert.AreEqual(1, load.Loaded);
load.Observe(false, false, true);
Assert.AreEqual(1, load.Loaded);
load.Observe(false, false, false);
Assert.AreEqual(0, load.Loaded);
load.RecordLoaded();
load.Observe(false, false, false);
Assert.AreEqual(1, load.Loaded);
}

[TestMethod]
public void TrainSlotsAreDistinctAndSmallLoadsCanReachAnExitDuringOneStop()
{
var offsets = Enumerable.Range(0, 12).Select(index => CrowdTrainLoad.SeatOffset(index, 1696)).ToArray();
Assert.AreEqual(12, offsets.Distinct().Count());
Assert.IsTrue(offsets.All(x => x >= 16 && x <= 1680));
Assert.IsTrue(CrowdActivityRules.CanFinishTrainWalk(true, 30, 600, (int)((offsets[0] + 64) / 1.2f)));
Assert.AreEqual(1696 - offsets[0], offsets[1]);
}

[TestMethod]
public void StreetSpawnRequiresTownRoadOutsideViewAndReachableActivity()
{
var town = new Rectangle(0, 0, 2000, 1000);
var view = new Rectangle(500, 0, 1000, 1000);
Assert.IsTrue(CrowdActivityRules.CanSpawnOnStreet(new Vector2(200, 500), town, view));
Assert.IsFalse(CrowdActivityRules.CanSpawnOnStreet(new Vector2(1000, 500), town, view));
Assert.IsFalse(CrowdActivityRules.CanSpawnOnStreet(new Vector2(-200, 500), town, view));
}

[TestMethod]
public void TownActivitiesDoNotRequireDepartureIntent()
{
Assert.IsTrue(CrowdActivityRules.Weight(CrowdActivity.Browse, false, false, 0, 2, 300, 1) > 0);
Assert.AreEqual(0f, CrowdActivityRules.Weight(CrowdActivity.BoardTrain, false, false, 0, 2, 300, 1));
Assert.AreEqual(0f, CrowdActivityRules.Weight(CrowdActivity.Browse, false, true, 0, 2, 300, 1));
Assert.AreEqual(0f, CrowdActivityRules.Weight(CrowdActivity.Browse, false, false, 2, 2, 300, 1));
Assert.IsTrue(CrowdActivityRules.Weight(CrowdActivity.BoardTrain, true, false, 0, 2, 300, 1) > 0);
}

[TestMethod]
public void ArrivalBatchOccursOncePerStopAndCanRepeatAtNextStop()
{
var batch = new CrowdStopBatch();
Assert.IsFalse(batch.TryBegin(true, 12, 600));
Assert.IsTrue(batch.TryBegin(true, 30, 600));
Assert.IsFalse(batch.TryBegin(true, 31, 600));
Assert.IsFalse(batch.TryBegin(true, 400, 600));
Assert.IsFalse(batch.TryBegin(false, 600, 600));
Assert.IsTrue(batch.TryBegin(true, 30, 600));
}

[TestMethod]
public void LateArrivalCannotStartDisembarking()
{
Assert.IsFalse(new CrowdStopBatch().TryBegin(true, 520, 600));
Assert.IsTrue(CrowdActivityRules.CanFinishTrainWalk(true, 30, 600, 400));
Assert.IsFalse(CrowdActivityRules.CanFinishTrainWalk(true, 300, 600, 400));
Assert.IsFalse(CrowdActivityRules.CanFinishTrainWalk(false, 30, 600, 10));
}

[TestMethod]
public void TrainDispatchRetriesFailuresButSpacesPassengersAndCapsSuccessfulBatch()
{
var batch = new CrowdStopBatch();
batch.TryBegin(true, 30, 600);
Assert.IsTrue(batch.TryDispatch(2, 3, out int first));
Assert.AreEqual(-1, first);
batch.TryBegin(true, 31, 600);
Assert.IsFalse(batch.TryDispatch(2, 3, out _));
batch.TryBegin(true, 60, 600);
Assert.IsTrue(batch.TryDispatch(2, 3, out int second));
Assert.AreEqual(1, second);
batch.RecordArrival();
batch.TryBegin(true, 90, 600);
Assert.IsFalse(batch.TryDispatch(2, 1, out _));
Assert.IsTrue(batch.TryDispatch(2, 3, out _));
batch.RecordArrival();
batch.TryBegin(true, 120, 600);
Assert.IsFalse(batch.TryDispatch(2, 3, out _));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Everglow.Yggdrasil.Mechanics.TownCrowd;

namespace Everglow.UnitTests.Modules.Yggdrasil;

[TestClass]
public class CrowdAvoidanceTests
{
private static Type StateType
{
get
{
var type = typeof(TownCrowdSystem).Assembly.GetType("Everglow.Yggdrasil.Mechanics.TownCrowd.CrowdAvoidance");
Assert.IsNotNull(type);
return type;
}
}

[TestMethod]
public void RecoveryNeedsThreeContinuousSafeSeconds()
{
var type = StateType;
var state = Activator.CreateInstance(type)!;
bool Tick(bool danger, bool safe) => (bool)type.GetMethod("Update")!.Invoke(state, [danger, safe, true, 300])!;
bool Alarmed() => (bool)type.GetProperty("Alarmed")!.GetValue(state)!;
Tick(true, false);
Assert.IsTrue(Alarmed());
for (int i = 0; i < 179; i++)
{
Tick(false, true);
}
Assert.IsTrue(Alarmed());
Tick(false, false);
for (int i = 0; i < 179; i++)
{
Tick(false, true);
}
Assert.IsTrue(Alarmed());
Tick(false, true);
Assert.IsFalse(Alarmed());
}

[TestMethod]
public void ReturningOnscreenResetsRetirementAndSafeOffscreenStillRetires()
{
var type = StateType;
var state = Activator.CreateInstance(type)!;
bool Tick(bool danger, bool safe, bool visible) => (bool)type.GetMethod("Update")!.Invoke(state, [danger, safe, visible, 300])!;
Tick(true, false, true);
for (int i = 0; i < 299; i++)
{
Assert.IsFalse(Tick(false, true, false));
}
Assert.IsFalse(Tick(false, false, true));
Tick(true, false, true);
for (int i = 0; i < 299; i++)
{
Assert.IsFalse(Tick(false, true, false));
}
Assert.IsTrue(Tick(false, true, false));
}
}
46 changes: 46 additions & 0 deletions Sources/Everglow.UnitTests/Modules/Yggdrasil/CrowdConfigTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Reflection;
using Everglow.Yggdrasil.Mechanics.TownCrowd;
using Terraria.ModLoader.Config;

namespace Everglow.UnitTests.Modules.Yggdrasil;

[TestClass]
public class CrowdConfigTests
{
[TestMethod]
public void DensityScalesStreetAndTrainPopulationTogether()
{
var levels = new[] { CrowdDensity.Sparse, CrowdDensity.Standard, CrowdDensity.Busy }.Select(CrowdSettings.ForDensity).ToArray();
for (int i = 1; i < levels.Length; i++)
{
Assert.IsTrue(levels[i].PopulationLimit > levels[i - 1].PopulationLimit);
Assert.IsTrue(levels[i].SpawnIntervalTicks < levels[i - 1].SpawnIntervalTicks);
Assert.IsTrue(levels[i].InitialFill > levels[i - 1].InitialFill);
Assert.IsTrue(levels[i].TrainInitialPassengers > levels[i - 1].TrainInitialPassengers);
Assert.IsTrue(levels[i].TrainPassengers > levels[i - 1].TrainPassengers);
}
}

[TestMethod]
public void ConfigRoundTripKeepsPreferencesAndIgnoresRemovedTuning()
{
var config = Newtonsoft.Json.JsonConvert.DeserializeObject<CrowdConfig>("{\"Enabled\":false,\"Density\":2,\"ChatChancePercent\":0,\"ReturnPercent\":100}")!;
var loaded = Newtonsoft.Json.JsonConvert.DeserializeObject<CrowdConfig>(Newtonsoft.Json.JsonConvert.SerializeObject(config))!;
Assert.IsFalse(loaded.Enabled);
Assert.AreEqual(CrowdDensity.Busy, loaded.Density);
Assert.AreEqual(CrowdDensity.Standard, new CrowdConfig().Density);
Assert.AreSame(CrowdSettings.ForDensity(CrowdDensity.Standard), CrowdSettings.ForDensity((CrowdDensity)999));
}

[TestMethod]
public void PlayerSettingsOnlyExposeEnableAndDensityOnTheClient()
{
var type = typeof(TownCrowdSystem).Assembly.GetType("Everglow.Yggdrasil.Mechanics.TownCrowd.CrowdConfig");
Assert.IsNotNull(type, "Crowd settings must be available in the normal mod configuration menu.");
var config = (ModConfig)Activator.CreateInstance(type)!;
Assert.AreEqual(ConfigScope.ClientSide, config.Mode);
CollectionAssert.AreEquivalent(new[] { "Enabled", "Density" }, type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Select(field => field.Name).ToArray());
Assert.AreEqual(true, type.GetField("Enabled")!.GetValue(config));
Assert.AreEqual("Standard", type.GetField("Density")!.GetValue(config)!.ToString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Reflection;
using Everglow.Yggdrasil.Mechanics.TownCrowd;
using Microsoft.Xna.Framework;

namespace Everglow.UnitTests.Modules.Yggdrasil;

[TestClass]
public class CrowdLifeRulesTests
{
private static Type Rules
{
get
{
var type = typeof(TownCrowdSystem).Assembly.GetType("Everglow.Yggdrasil.Mechanics.TownCrowd.CrowdLifeRules");
Assert.IsNotNull(type);
return type;
}
}

[TestMethod]
public void InitialIntentIsIndependentOfStreetOrPylonAppearance()
{
var choose = Rules.GetMethod("ChooseIntent")!;
Assert.AreEqual("ReturnHome", choose.Invoke(null, [false, false, 0, 100, 0])!.ToString());
Assert.AreEqual("TakeTrain", choose.Invoke(null, [true, false, 0, 100, 0])!.ToString());
Assert.AreEqual("TakeTrain", choose.Invoke(null, [true, false, 0, 0, 100])!.ToString());
Assert.AreEqual("EnterGuild", choose.Invoke(null, [false, false, 0, 0, 100])!.ToString());
Assert.AreEqual("VisitTown", choose.Invoke(null, [false, true, 0, 0, 100])!.ToString());
}

[TestMethod]
public void GuildTriggerRequiresWholeBodyInsideOpening()
{
var contains = Rules.GetMethod("InsideGuildOpening")!;
Assert.IsTrue((bool)contains.Invoke(null, [new Point(10, 20), new Vector2(216, 448)])!);
Assert.IsFalse((bool)contains.Invoke(null, [new Point(10, 20), new Vector2(160, 448)])!);
Assert.IsFalse((bool)contains.Invoke(null, [new Point(10, 20), new Vector2(216, 480)])!);
}

[TestMethod]
public void ZoomedCameraIncludesExactlyItsWorldSpaceExtent()
{
var camera = CrowdLifeRules.CameraBounds(new Vector2(1000, 2000), new Vector2(800, 600),
Matrix.CreateTranslation(-400, -300, 0) * Matrix.CreateScale(2, 2, 1) * Matrix.CreateTranslation(400, 300, 0));
Assert.AreEqual(new Rectangle(1200, 2150, 400, 300), camera);
var regions = new CrowdRegions();
Assert.IsTrue(regions.Observe(camera, 0).Length > 0);
Assert.AreEqual(0, regions.Observe(camera, 1).Length);
Assert.IsTrue(regions.Observe(new Rectangle(20000, 20000, 400, 300), 2).Length > 0);
Assert.AreEqual(0, regions.Observe(camera, 3).Length);
}

[TestMethod]
public void RegionExposureRemembersShortRevisitsAndBoundsMemory()
{
var type = typeof(TownCrowdSystem).Assembly.GetType("Everglow.Yggdrasil.Mechanics.TownCrowd.CrowdRegions");
Assert.IsNotNull(type);
var regions = Activator.CreateInstance(type)!;
var observe = type.GetMethod("Observe")!;
Rectangle[] Observe(Rectangle view, long clock) => (Rectangle[])observe.Invoke(regions, [view, clock])!;
var first = new Rectangle(20, 20, 100, 100);
Assert.AreEqual(1, Observe(first, 0).Length);
Assert.AreEqual(0, Observe(first, 1).Length);
Assert.AreEqual(1, Observe(new Rectangle(800, 20, 100, 100), 2).Length);
Assert.AreEqual(0, Observe(first, 3).Length);
for (int i = 0; i < 400; i++)
{
Observe(new Rectangle(i * 768, 400, 100, 100), i + 4);
}
Assert.IsTrue((int)type.GetProperty("Count")!.GetValue(regions)! <= 128);
type.GetMethod("Clear")!.Invoke(regions, null);
Assert.AreEqual(1, Observe(first, 500).Length);
}
}
Loading
Loading