mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
199 lines
8.8 KiB
C#
199 lines
8.8 KiB
C#
using RadicadeServer.RecNet.Enums.Store;
|
|
using RadicadeServer.Utils;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// How a completion turns into reward boxes: which activities offer the player a choice, what the
|
|
/// server falls back to when the context the client named is not one the activity pays, and what
|
|
/// the boxes contain.
|
|
/// </summary>
|
|
public class RroRewardSelectionTests
|
|
{
|
|
private static string ShippedCatalogueJson() =>
|
|
File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json"));
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Choice or no choice
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
[Theory]
|
|
// The map games hand the player a watch selection.
|
|
[InlineData("Clearcut", true)]
|
|
[InlineData("River", true)]
|
|
[InlineData("Spillway", true)]
|
|
[InlineData("DriveIn", true)]
|
|
[InlineData("DiscGolfPropulsion", true)]
|
|
[InlineData("3DCharades", true)]
|
|
[InlineData("Soccer", true)]
|
|
[InlineData("Paddleball", true)]
|
|
// A quest pays the rank the run earned; there is nothing to choose.
|
|
[InlineData("GoldenTrophy", false)]
|
|
[InlineData("TheRiseofJumbotron", false)]
|
|
[InlineData("CrimsonCauldron", false)]
|
|
[InlineData("Crescendo", false)]
|
|
[InlineData("IsleOfLostSkulls", false)]
|
|
[InlineData("StuntRunner", false)]
|
|
// Not a reward activity at all.
|
|
[InlineData("DormRoom", false)]
|
|
[InlineData("RecCenter", false)]
|
|
[InlineData(null, false)]
|
|
public void OnlyTheMapGamesOfferAChoice(string? room, bool offersSelection)
|
|
{
|
|
Assert.Equal(offersSelection, RroRewardPools.OffersSelection(room));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// What an out-of-place claim falls back to
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
[Theory]
|
|
[InlineData("GoldenTrophy", GiftContext.Quest_Goblin_C)] // 4002, the quest's lowest rank
|
|
[InlineData("TheRiseofJumbotron", GiftContext.Quest_SciFi_C)]
|
|
[InlineData("CrimsonCauldron", GiftContext.Quest_Cauldron_C)]
|
|
[InlineData("Crescendo", GiftContext.Quest_Dracula1_C)]
|
|
[InlineData("IsleOfLostSkulls", GiftContext.Quest_Pirate1_C)] // its lowest defined rank
|
|
[InlineData("StuntRunner", GiftContext.StuntRunner_TheMainEvent_C)]
|
|
[InlineData("Clearcut", GiftContext.Paintball_ClearCut)] // the map's only context
|
|
[InlineData("3DCharades", GiftContext.Charades)]
|
|
public void AClaimTheRoomDoesNotPayFallsToTheLowestRankItDoes(string room, GiftContext expected)
|
|
{
|
|
Assert.True(RroRewardPools.TryFloorContext(room, out var floor));
|
|
Assert.Equal(expected, floor);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheFloorIsNeverAboveWhatTheRoomWouldPay()
|
|
{
|
|
// The point of the floor: an inflated claim can only ever drop a player to the activity's
|
|
// lowest rank, never lift them, and it can never hand out another activity's context.
|
|
Assert.False(RroRewardPools.TryFloorContext("DormRoom", out var none));
|
|
Assert.Equal(GiftContext.None, none);
|
|
|
|
foreach (var room in new[] { "GoldenTrophy", "CrimsonCauldron", "Clearcut", "Soccer" })
|
|
{
|
|
Assert.True(RroRewardPools.TryFloorContext(room, out var floor));
|
|
Assert.Contains(floor, RroRewardPools.ContextsForRoom(room).Values);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// What a completion pays
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void AQuestPaysOneBoxUnderItsOwnRankContext()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
var drops = RroRewardPools.BuildDrops(GiftContext.Quest_Goblin_S, selection: false, null, new Random(5), json);
|
|
|
|
Assert.Single(drops);
|
|
Assert.Equal(GiftContext.Quest_Goblin_S, drops[0].Context);
|
|
Assert.Equal(4003, drops[0].GiftContext);
|
|
Assert.False(string.IsNullOrWhiteSpace(drops[0].AvatarItemDesc));
|
|
}
|
|
|
|
[Fact]
|
|
public void AMapGameFillsThreeBoxesWithThreeDifferentItems()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
// Clear Cut pays seven Lumberjack pieces, so a selection of three is always available.
|
|
var drops = RroRewardPools.BuildDrops(GiftContext.Paintball_ClearCut, selection: true, null, new Random(11), json);
|
|
|
|
Assert.Equal(3, drops.Count);
|
|
Assert.Equal(3, drops.Select(x => x.AvatarItemDesc).Distinct(StringComparer.Ordinal).Count());
|
|
Assert.All(drops, drop => Assert.Equal(GiftContext.Paintball_ClearCut, drop.Context));
|
|
Assert.All(drops, drop => Assert.Contains("Lumberjack", drop.FriendlyName, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void ASelectionStopsAtWhatThePoolHasLeft()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
var pool = RroRewardPools.Resolve(GiftContext.Quest_Dracula1_S, json);
|
|
|
|
// Crescendo's S rank pays two pieces, so a selection can offer at most those two.
|
|
Assert.Equal(2, pool.Count);
|
|
Assert.Equal(2, RroRewardPools.BuildDrops(GiftContext.Quest_Dracula1_S, selection: true, null, new Random(3), json).Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void ABoxIsNeverOfferedForSomethingThePlayerAlreadyOwns()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
var pool = RroRewardPools.Resolve(GiftContext.Paintball_ClearCut, json);
|
|
|
|
// Everything but two pieces is owned, so a selection of three can only offer those two.
|
|
var owned = pool.Take(pool.Count - 2)
|
|
.Select(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc))
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var drops = RroRewardPools.BuildDrops(GiftContext.Paintball_ClearCut, selection: true, owned, new Random(9), json);
|
|
Assert.Equal(2, drops.Count);
|
|
|
|
var remaining = pool.Skip(pool.Count - 2).Select(x => x.AvatarItemDesc).ToHashSet(StringComparer.Ordinal);
|
|
Assert.All(drops, drop => Assert.Contains(drop.AvatarItemDesc, remaining));
|
|
Assert.All(drops, drop => Assert.DoesNotContain(
|
|
LevelUpRewardPool.OwnershipKey(drop.AvatarItemDesc), owned));
|
|
}
|
|
|
|
[Fact]
|
|
public void AnExhaustedPoolPaysNoBoxesAtAll()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
var owned = RroRewardPools.Resolve(GiftContext.Quest_Goblin_S, json)
|
|
.Select(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc))
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
Assert.Empty(RroRewardPools.BuildDrops(GiftContext.Quest_Goblin_S, selection: false, owned, new Random(1), json));
|
|
}
|
|
|
|
[Fact]
|
|
public void AGapInTheDefinitionsPaysNoBoxesRatherThanAStandIn()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
|
|
// A tier the definitions name but the catalogue cannot satisfy pays nothing here, which is
|
|
// what sends the caller to the fallback rather than to a guessed item.
|
|
Assert.Empty(RroRewardPools.BuildDrops(GiftContext.Quest_Pirate1_X, selection: false, null, new Random(1), json));
|
|
Assert.Empty(RroRewardPools.BuildDrops(GiftContext.StuntRunner_TheMainEvent_D, selection: false, null, new Random(1), json));
|
|
Assert.Empty(RroRewardPools.BuildDrops(GiftContext.RecRoyale, selection: true, null, new Random(1), json));
|
|
}
|
|
|
|
[Fact]
|
|
public void TheSameCompletionTwiceDoesNotRepeatItself()
|
|
{
|
|
// Two runs of the same quest must not hand out the same piece: once the first is owned, the
|
|
// second has to come from what is left.
|
|
var json = ShippedCatalogueJson();
|
|
var first = RroRewardPools.BuildDrops(GiftContext.Quest_Goblin_B, selection: false, null, new Random(4), json);
|
|
Assert.Single(first);
|
|
|
|
var owned = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
LevelUpRewardPool.OwnershipKey(first[0].AvatarItemDesc)
|
|
};
|
|
|
|
var second = RroRewardPools.BuildDrops(GiftContext.Quest_Goblin_B, selection: false, owned, new Random(4), json);
|
|
Assert.Single(second);
|
|
Assert.NotEqual(first[0].AvatarItemDesc, second[0].AvatarItemDesc);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheBoxesCarryNoCurrencyOrXp()
|
|
{
|
|
// A pool reward is the item and nothing else: it must not also pay tokens, or a quest clear
|
|
// would quietly become a currency source.
|
|
var json = ShippedCatalogueJson();
|
|
var drops = RroRewardPools.BuildDrops(GiftContext.Quest_Goblin_A, selection: false, null, new Random(2), json);
|
|
|
|
Assert.Single(drops);
|
|
Assert.Equal(0, drops[0].Currency);
|
|
Assert.Equal(0, drops[0].Xp);
|
|
// And nothing for the client to paint as currency: with the default CurrencyType an item box
|
|
// rendered as a stack of coins with "Value: 0" under the item's name.
|
|
Assert.Equal(CurrencyType.Invalid, drops[0].CurrencyType);
|
|
}
|
|
}
|