using NUnit.Framework; using ProjectM.Server; using ProjectM.Simulation; using Unity.Collections; using Unity.Core; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Tests { /// /// Pins the boon pool math (: deterministic, 3 distinct, class-filtered, /// weight-0 excluded) and (one deal per RoomEpoch; expedition players only; /// owner-seeded per player so co-op offers differ). /// public class BoonOfferTests { [Test] public void PickBoons_Deterministic_Distinct_ClassFiltered() { var blob = BoonCatalogData.BuildDefault(Allocator.Temp); ref var pool = ref blob.Value; for (byte classId = 0; classId <= 1; classId++) { for (uint seed = 1; seed < 200; seed += 7) { int n = BoonMath.PickBoons(seed, classId, ref pool, out byte a0, out byte a1, out byte a2); Assert.AreEqual(3, n, "the default pool always fills 3 options"); Assert.AreNotEqual(a0, a1, "distinct"); Assert.AreNotEqual(a1, a2, "distinct"); Assert.AreNotEqual(a0, a2, "distinct"); // Deterministic re-draw. BoonMath.PickBoons(seed, classId, ref pool, out byte b0, out byte b1, out byte b2); Assert.AreEqual(a0, b0); Assert.AreEqual(a1, b1); Assert.AreEqual(a2, b2); // Every option is class-legal. byte bit = BoonMath.MaskFor(classId); foreach (var id in new[] { a0, a1, a2 }) { int idx = BoonMath.FindDef(ref pool, id); Assert.GreaterOrEqual(idx, 0, "offered id exists"); Assert.AreNotEqual(0, pool.Defs[idx].ClassMask & bit, $"boon {id} offered to class {classId} must pass its ClassMask"); } } } blob.Dispose(); } [Test] public void OfferSystem_DealsOncePerRoom_ExpeditionOnly_PerPlayerSeeds() { var world = new World("BoonOfferTest"); var group = world.GetOrCreateSystemManaged(); group.AddSystemToUpdateList(world.GetOrCreateSystem()); group.SortSystems(); world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); var em = world.EntityManager; var dir = em.CreateEntity(typeof(RunInfo), typeof(RunRuntime)); em.SetComponentData(dir, new RunInfo { Lifecycle = RunLifecycle.RoomReward, CurrentRoom = 1 }); em.SetComponentData(dir, new RunRuntime { RunSeed = 4242u, RoomEpoch = 2 }); var catalog = em.CreateEntity(typeof(BoonCatalog)); em.SetComponentData(catalog, new BoonCatalog { Value = BoonCatalogData.BuildDefault() }); Entity MakePlayer(int netId, byte region, byte classId) { var e = em.CreateEntity(typeof(PlayerTag), typeof(BoonOffer), typeof(GhostOwner), typeof(RegionTag), typeof(PlayerClass)); em.SetComponentData(e, new GhostOwner { NetworkId = netId }); em.SetComponentData(e, new RegionTag { Region = region }); em.SetComponentData(e, new PlayerClass { ClassId = classId }); return e; } var out1 = MakePlayer(1, RegionId.Expedition, 0); var out2 = MakePlayer(2, RegionId.Expedition, 1); var home = MakePlayer(3, RegionId.Base, 0); group.Update(); // one-shot state attach group.Update(); // deal var offer1 = em.GetComponentData(out1); var offer2 = em.GetComponentData(out2); Assert.AreEqual(1, offer1.Pending, "expedition player 1 dealt"); Assert.AreEqual(1, offer2.Pending, "expedition player 2 dealt"); Assert.AreEqual(0, em.GetComponentData(home).Pending, "home player dealt NOTHING"); bool differ = offer1.Option0 != offer2.Option0 || offer1.Option1 != offer2.Option1 || offer1.Option2 != offer2.Option2; Assert.IsTrue(differ, "per-player seeds -> co-op offers differ (seed folds NetworkId)"); // Same epoch -> no re-deal (clear one offer and confirm it stays cleared). em.SetComponentData(out1, default(BoonOffer)); group.Update(); Assert.AreEqual(0, em.GetComponentData(out1).Pending, "one deal per RoomEpoch (latch)"); world.Dispose(); } } }