Files
Project-M/Assets/_Project/Tests/EditMode/BoonOfferTests.cs
T
2026-07-13 18:30:41 -07:00

146 lines
6.7 KiB
C#

using NUnit.Framework;
using ProjectM.Server;
using ProjectM.Simulation;
using Unity.Collections;
using Unity.Core;
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Tests
{
/// <summary>
/// Pins the boon pool math (<see cref="BoonMath.PickBoons"/>: deterministic, 3 distinct, class-filtered,
/// weight-0 excluded) and <see cref="BoonOfferSystem"/> (one deal per RoomEpoch; expedition players only;
/// owner-seeded per player so co-op offers differ).
/// </summary>
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, default(BoonEffects), 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, default(BoonEffects), 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");
using (world)
{
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<BoonOfferSystem>());
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), typeof(BoonEffects));
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<BoonOffer>(out1);
var offer2 = em.GetComponentData<BoonOffer>(out2);
Assert.AreEqual(1, offer1.Pending, "expedition player 1 dealt");
Assert.AreEqual(1, offer2.Pending, "expedition player 2 dealt");
Assert.AreEqual(0, em.GetComponentData<BoonOffer>(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<BoonOffer>(out1).Pending, "one deal per RoomEpoch (latch)");
}
}
static byte FamilyOf(ref BoonCatalogBlob pool, byte id)
{
int idx = BoonMath.FindDef(ref pool, id);
return idx >= 0 ? pool.Defs[idx].Family : (byte)0;
}
[Test]
public void PickBoons_NeverOffersTwoSameFamily_InOneDeal()
{
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 += 3)
{
BoonMath.PickBoons(seed, classId, default(BoonEffects), ref pool, out byte a0, out byte a1, out byte a2);
byte f0 = FamilyOf(ref pool, a0), f1 = FamilyOf(ref pool, a1), f2 = FamilyOf(ref pool, a2);
Assert.AreNotEqual(f0, f1, "dominated-offer protection: no two same-family options in one deal");
Assert.AreNotEqual(f1, f2, "dominated-offer protection: no two same-family options in one deal");
Assert.AreNotEqual(f0, f2, "dominated-offer protection: no two same-family options in one deal");
}
blob.Dispose();
}
[Test]
public void PickBoons_ExcludesOwnedNonStackingFlag()
{
var blob = BoonCatalogData.BuildDefault(Allocator.Temp);
ref var pool = ref blob.Value;
var owned = new BoonEffects { Flags = BoonFlag.DashTrail }; // already own Blade Dash (id 5, both classes)
for (byte classId = 0; classId <= 1; classId++)
for (uint seed = 1; seed < 300; seed += 3)
{
BoonMath.PickBoons(seed, classId, owned, ref pool, out byte a0, out byte a1, out byte a2);
Assert.IsFalse(a0 == 5 || a1 == 5 || a2 == 5, "an owned non-stacking flag boon (Blade Dash) is never re-offered");
}
blob.Dispose();
}
}
}