using RadicadeServer.RecNet.Enums.Store;
using RadicadeServer.Utils;
using Xunit;
namespace RadicadeServer.Tests;
///
/// 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.
///
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(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);
}
}