mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
674 lines
32 KiB
C#
674 lines
32 KiB
C#
using RadicadeServer.Extensions;
|
|
using RadicadeServer.RecNet.Avatar;
|
|
using RadicadeServer.RecNet.Enums.Store;
|
|
using RadicadeServer.Utils;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// The historical RRO reward pools. The catalogue these assertions run against is the shipped
|
|
/// 2022-avatar.json fixture - the same one the catalogue tests read - so a set whose items are not
|
|
/// in the catalogue fails here rather than at runtime.
|
|
/// </summary>
|
|
public class RroRewardPoolsTests
|
|
{
|
|
private static string ShippedCatalogueJson() =>
|
|
File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json"));
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// The context values themselves
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
[Theory]
|
|
// Quests: each activity keeps its own block of values.
|
|
[InlineData(GiftContext.Quest_Goblin_A, 4000)]
|
|
[InlineData(GiftContext.Quest_Goblin_B, 4001)]
|
|
[InlineData(GiftContext.Quest_Goblin_C, 4002)]
|
|
[InlineData(GiftContext.Quest_Goblin_S, 4003)]
|
|
[InlineData(GiftContext.Quest_Goblin_Consumable, 4004)]
|
|
[InlineData(GiftContext.Quest_Cauldron_A, 4010)]
|
|
[InlineData(GiftContext.Quest_Cauldron_B, 4011)]
|
|
[InlineData(GiftContext.Quest_Cauldron_C, 4012)]
|
|
[InlineData(GiftContext.Quest_Cauldron_S, 4013)]
|
|
[InlineData(GiftContext.Quest_Cauldron_Consumable, 4014)]
|
|
[InlineData(GiftContext.Quest_Pirate1_A, 4100)]
|
|
[InlineData(GiftContext.Quest_Pirate1_B, 4101)]
|
|
[InlineData(GiftContext.Quest_Pirate1_C, 4102)]
|
|
[InlineData(GiftContext.Quest_Pirate1_S, 4103)]
|
|
[InlineData(GiftContext.Quest_Pirate1_X, 4104)]
|
|
[InlineData(GiftContext.Quest_Pirate1_Consumable, 4105)]
|
|
[InlineData(GiftContext.Quest_Dracula1_A, 4200)]
|
|
[InlineData(GiftContext.Quest_Dracula1_B, 4201)]
|
|
[InlineData(GiftContext.Quest_Dracula1_C, 4202)]
|
|
[InlineData(GiftContext.Quest_Dracula1_S, 4203)]
|
|
[InlineData(GiftContext.Quest_Dracula1_X, 4204)]
|
|
[InlineData(GiftContext.Quest_Dracula1_Consumable, 4205)]
|
|
[InlineData(GiftContext.Quest_Dracula1_SS, 4206)]
|
|
[InlineData(GiftContext.Quest_SciFi_A, 4500)]
|
|
[InlineData(GiftContext.Quest_SciFi_B, 4501)]
|
|
[InlineData(GiftContext.Quest_SciFi_C, 4502)]
|
|
[InlineData(GiftContext.Quest_SciFi_S, 4503)]
|
|
[InlineData(GiftContext.Quest_Scifi_Consumable, 4504)]
|
|
// Stunt Runner.
|
|
[InlineData(GiftContext.StuntRunner_TheMainEvent_A, 11000)]
|
|
[InlineData(GiftContext.StuntRunner_TheMainEvent_B, 11001)]
|
|
[InlineData(GiftContext.StuntRunner_TheMainEvent_C, 11002)]
|
|
[InlineData(GiftContext.StuntRunner_TheMainEvent_D, 11003)]
|
|
[InlineData(GiftContext.StuntRunner_TheMainEvent_S, 11004)]
|
|
[InlineData(GiftContext.StuntRunner_TheMainEvent_X, 11005)]
|
|
[InlineData(GiftContext.StuntRunner_TheMainEvent_Consumable, 11006)]
|
|
[InlineData(GiftContext.StuntRunner_TheMainEvent_SS, 11007)]
|
|
// Map games.
|
|
[InlineData(GiftContext.Paintball_ClearCut, 2010)]
|
|
[InlineData(GiftContext.Paintball_Homestead, 2011)]
|
|
[InlineData(GiftContext.Paintball_Quarry, 2012)]
|
|
[InlineData(GiftContext.Paintball_River, 2013)]
|
|
[InlineData(GiftContext.Paintball_Dam, 2014)]
|
|
[InlineData(GiftContext.Paintball_DriveIn, 2015)]
|
|
[InlineData(GiftContext.Discgolf_Propulsion, 3010)]
|
|
[InlineData(GiftContext.Discgolf_Lake, 3011)]
|
|
[InlineData(GiftContext.Charades, 5001)]
|
|
[InlineData(GiftContext.Soccer, 6001)]
|
|
[InlineData(GiftContext.Paddleball, 7001)]
|
|
[InlineData(GiftContext.Dodgeball, 8001)]
|
|
[InlineData(GiftContext.Lasertag, 9001)]
|
|
[InlineData(GiftContext.Bowling, 10001)]
|
|
[InlineData(GiftContext.RecRoyale, 1200)]
|
|
// The generic pair, for packages with no specialised activity.
|
|
[InlineData(GiftContext.GameRewards, 50)]
|
|
[InlineData(GiftContext.GameRewards_Tokens, 51)]
|
|
// Deprecated: kept so old data still reads, never chosen for a new reward.
|
|
[InlineData(GiftContext.DEPRECATED_Paintball_ClearCut, 2000)]
|
|
[InlineData(GiftContext.DEPRECATED_Paintball_Homestead, 2001)]
|
|
[InlineData(GiftContext.DEPRECATED_Paintball_Quarry, 2002)]
|
|
[InlineData(GiftContext.DEPRECATED_Paintball_River, 2003)]
|
|
[InlineData(GiftContext.DEPRECATED_Paintball_Dam, 2004)]
|
|
[InlineData(GiftContext.DEPRECATED_Paintball_DriveIn, 2005)]
|
|
[InlineData(GiftContext.DEPRECATED_Discgolf_Propulsion, 3000)]
|
|
[InlineData(GiftContext.DEPRECATED_Discgolf_Lake, 3001)]
|
|
[InlineData(GiftContext.DEPRECATED_Charades, 5000)]
|
|
[InlineData(GiftContext.DEPRECATED_Soccer, 6000)]
|
|
[InlineData(GiftContext.DEPRECATED_Paddleball, 7000)]
|
|
[InlineData(GiftContext.DEPRECATED_Dodgeball, 8000)]
|
|
[InlineData(GiftContext.DEPRECATED_Lasertag, 9000)]
|
|
[InlineData(GiftContext.DEPRECATED_Bowling, 10000)]
|
|
public void TheContextKeepsItsHistoricalValue(GiftContext context, int expected)
|
|
{
|
|
Assert.Equal(expected, (int)context);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoDeprecatedContextIsRewardable()
|
|
{
|
|
foreach (var context in RroRewardPools.Deprecated)
|
|
{
|
|
Assert.True(RroRewardPools.IsDeprecated(context));
|
|
// A deprecated context may have no pool at all; it must never have a non-empty one.
|
|
Assert.Empty(RroRewardPools.Resolve(context, ShippedCatalogueJson()));
|
|
}
|
|
|
|
Assert.Equal(RroRewardPools.Deprecated.Count, RroRewardPools.Deprecated.Distinct().Count());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Which room pays which context
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
[Theory]
|
|
[InlineData("GoldenTrophy", "S", GiftContext.Quest_Goblin_S)]
|
|
[InlineData("GoldenTrophy", "C", GiftContext.Quest_Goblin_C)]
|
|
[InlineData("TheRiseofJumbotron", "A", GiftContext.Quest_SciFi_A)]
|
|
[InlineData("CrimsonCauldron", "C", GiftContext.Quest_Cauldron_C)]
|
|
[InlineData("Crescendo", "S", GiftContext.Quest_Dracula1_S)]
|
|
[InlineData("StuntRunner", "B", GiftContext.StuntRunner_TheMainEvent_B)]
|
|
[InlineData("IsleOfLostSkulls", "S", GiftContext.Quest_Pirate1_S)]
|
|
// The map games take no rank.
|
|
[InlineData("Clearcut", null, GiftContext.Paintball_ClearCut)]
|
|
[InlineData("Spillway", null, GiftContext.Paintball_Dam)]
|
|
[InlineData("DriveIn", null, GiftContext.Paintball_DriveIn)]
|
|
[InlineData("DiscGolfPropulsion", null, GiftContext.Discgolf_Propulsion)]
|
|
[InlineData("DiscGolfLake", null, GiftContext.Discgolf_Lake)]
|
|
[InlineData("3DCharades", null, GiftContext.Charades)]
|
|
[InlineData("Soccer", null, GiftContext.Soccer)]
|
|
[InlineData("Paddleball", null, GiftContext.Paddleball)]
|
|
[InlineData("Dodgeball", null, GiftContext.Dodgeball)]
|
|
[InlineData("LaserTag", null, GiftContext.Lasertag)]
|
|
[InlineData("Bowling", null, GiftContext.Bowling)]
|
|
[InlineData("RecRoyaleSquads", null, GiftContext.RecRoyale)]
|
|
public void TheRoomNamesTheContext(string room, string? rank, GiftContext expected)
|
|
{
|
|
Assert.True(RroRewardPools.TryContext(room, rank, out var context));
|
|
Assert.Equal(expected, context);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("DormRoom")]
|
|
[InlineData("RecCenter")]
|
|
[InlineData("SomeonesInvention")]
|
|
[InlineData("")]
|
|
[InlineData(null)]
|
|
public void ARoomThatIsNotARewardActivityPaysNothing(string? room)
|
|
{
|
|
Assert.False(RroRewardPools.IsRewardActivity(room));
|
|
Assert.False(RroRewardPools.TryContext(room, "S", out var context));
|
|
Assert.Equal(GiftContext.None, context);
|
|
}
|
|
|
|
[Fact]
|
|
public void ARankAQuestDoesNotDefinePaysNothing()
|
|
{
|
|
// A rank the definitions do not name must not resolve: Golden Trophy never pays a D or an
|
|
// SS, and Isle of Lost Skulls has no C.
|
|
Assert.False(RroRewardPools.TryContext("GoldenTrophy", "SS", out _));
|
|
Assert.False(RroRewardPools.TryContext("GoldenTrophy", "D", out _));
|
|
// Stunt Runner does define a tier above S, under both of the names the client has used
|
|
// for it; D is the one it does not.
|
|
Assert.False(RroRewardPools.TryContext("StuntRunner", "D", out _));
|
|
Assert.False(RroRewardPools.TryContext("Crescendo", "SS", out _));
|
|
|
|
// A ranked quest still refuses a context from another activity, because the room is what
|
|
// decides: Golden Trophy cannot file a Jumbotron reward.
|
|
Assert.Equal(
|
|
GiftContext.Quest_Goblin_S,
|
|
RroRewardPools.ContextsForRoom("GoldenTrophy")["S"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheStuntRunnerTopTierPaysFeatheredHairUnderEitherName()
|
|
{
|
|
// The definitions call this tier SS and the watch shows it as S+; both names have to reach
|
|
// the same context, and the tier has to hand out the item it names.
|
|
var json = ShippedCatalogueJson();
|
|
var pool = RroRewardPools.Resolve(GiftContext.StuntRunner_TheMainEvent_SS, json);
|
|
|
|
Assert.Single(pool);
|
|
Assert.Equal("Feathered Hair", pool[0].FriendlyName);
|
|
|
|
Assert.True(RroRewardPools.TryContext("StuntRunner", "SS", out var viaSs));
|
|
Assert.Equal(GiftContext.StuntRunner_TheMainEvent_SS, viaSs);
|
|
Assert.Equal(11007, (int)viaSs);
|
|
|
|
Assert.True(RroRewardPools.TryContext("StuntRunner", "S+", out var viaSPlus));
|
|
Assert.Equal(GiftContext.StuntRunner_TheMainEvent_SS, viaSPlus);
|
|
|
|
// And the tier is reachable, which is what makes a claim of it honoured.
|
|
Assert.Contains(GiftContext.StuntRunner_TheMainEvent_SS,
|
|
RroRewardPools.ContextsForRoom("StuntRunner").Values);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoRoomEverPaysADeprecatedContext()
|
|
{
|
|
foreach (var room in new[] { "GoldenTrophy", "TheRiseofJumbotron", "CrimsonCauldron", "Crescendo",
|
|
"IsleOfLostSkulls", "StuntRunner", "Clearcut", "Spillway", "River", "DriveIn",
|
|
"DiscGolfPropulsion", "3DCharades", "Soccer", "Paddleball", "Dodgeball", "LaserTag",
|
|
"Bowling", "RecRoyaleSolos" })
|
|
{
|
|
foreach (var context in RroRewardPools.ContextsForRoom(room).Values)
|
|
{
|
|
Assert.False(RroRewardPools.IsDeprecated(context),
|
|
$"{room} pays the deprecated {context}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// The pools resolve against the shipped catalogue
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void EveryPoolThatNamesASetResolvesToShippedItems()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
var catalogue = (json.FromJson<List<AvatarItem>>() ?? [])
|
|
.ToDictionary(x => x.AvatarItemDesc, StringComparer.Ordinal);
|
|
|
|
foreach (var context in RroRewardPools.RewardableContexts)
|
|
{
|
|
var sets = RroRewardPools.SetsFor(context);
|
|
if (sets.Count == 0) continue;
|
|
|
|
var items = RroRewardPools.Resolve(context, json);
|
|
Assert.True(items.Count > 0, $"{context} names {sets.Count} set(s) but resolves to no item");
|
|
|
|
// Every item is a real catalogue row, and it came from one of the context's own sets.
|
|
foreach (var item in items)
|
|
{
|
|
Assert.True(catalogue.ContainsKey(item.AvatarItemDesc),
|
|
$"{context} resolved {item.AvatarItemDesc}, which is not a catalogue row");
|
|
Assert.False(string.IsNullOrWhiteSpace(item.FriendlyName));
|
|
Assert.Contains(sets, set =>
|
|
RroRewardPools.Resolve(set, json).Any(x => x.AvatarItemDesc == item.AvatarItemDesc));
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void EachQuestPaysEveryRankItDefinesAndNoItemTwice()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
|
|
// Each quest's ranks, so a rank can be checked against the quest it belongs to.
|
|
var quests = new Dictionary<string, GiftContext[]>
|
|
{
|
|
["Golden Trophy"] = [GiftContext.Quest_Goblin_C, GiftContext.Quest_Goblin_B,
|
|
GiftContext.Quest_Goblin_A, GiftContext.Quest_Goblin_S],
|
|
["Jumbotron"] = [GiftContext.Quest_SciFi_C, GiftContext.Quest_SciFi_B,
|
|
GiftContext.Quest_SciFi_A, GiftContext.Quest_SciFi_S],
|
|
["Crimson Cauldron"] = [GiftContext.Quest_Cauldron_C, GiftContext.Quest_Cauldron_B,
|
|
GiftContext.Quest_Cauldron_A, GiftContext.Quest_Cauldron_S],
|
|
["Crescendo"] = [GiftContext.Quest_Dracula1_C, GiftContext.Quest_Dracula1_B,
|
|
GiftContext.Quest_Dracula1_A, GiftContext.Quest_Dracula1_S],
|
|
["Stunt Runner"] = [GiftContext.StuntRunner_TheMainEvent_C, GiftContext.StuntRunner_TheMainEvent_B,
|
|
GiftContext.StuntRunner_TheMainEvent_A, GiftContext.StuntRunner_TheMainEvent_S],
|
|
};
|
|
|
|
foreach (var (quest, ranks) in quests)
|
|
{
|
|
foreach (var rank in ranks)
|
|
{
|
|
Assert.True(RroRewardPools.Resolve(rank, json).Count > 0,
|
|
$"{quest} pays nothing at one of its ranks ({rank})");
|
|
}
|
|
|
|
// Nothing here asserts that a quest's ranks are disjoint: the verified Isle of Lost
|
|
// Skulls tables deliberately pay the same piece at two ranks. What must not happen is a
|
|
// piece crossing from one activity to another, which NoItemIsPaidByTwoActivities covers.
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void NoItemIsPaidByTwoActivities()
|
|
{
|
|
// The rule that matters most: a Golden Trophy item must never be handed out as a Jumbotron
|
|
// or a paintball reward. A quest's own ranks may overlap - the verified Isle of Lost Skulls
|
|
// tables do - so the comparison is between activities, not between their tiers.
|
|
var json = ShippedCatalogueJson();
|
|
var owner = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
|
|
foreach (var (room, ranks) in new[]
|
|
{
|
|
("GoldenTrophy", new[] { GiftContext.Quest_Goblin_C, GiftContext.Quest_Goblin_B, GiftContext.Quest_Goblin_A, GiftContext.Quest_Goblin_S }),
|
|
("Jumbotron", new[] { GiftContext.Quest_SciFi_C, GiftContext.Quest_SciFi_B, GiftContext.Quest_SciFi_A, GiftContext.Quest_SciFi_S }),
|
|
("CrimsonCauldron", new[] { GiftContext.Quest_Cauldron_C, GiftContext.Quest_Cauldron_B, GiftContext.Quest_Cauldron_A, GiftContext.Quest_Cauldron_S }),
|
|
("Crescendo", new[] { GiftContext.Quest_Dracula1_C, GiftContext.Quest_Dracula1_B, GiftContext.Quest_Dracula1_A, GiftContext.Quest_Dracula1_S, GiftContext.Quest_Dracula1_SS }),
|
|
("StuntRunner", new[] { GiftContext.StuntRunner_TheMainEvent_C, GiftContext.StuntRunner_TheMainEvent_B, GiftContext.StuntRunner_TheMainEvent_A, GiftContext.StuntRunner_TheMainEvent_S, GiftContext.StuntRunner_TheMainEvent_SS }),
|
|
("IsleOfLostSkulls", new[] { GiftContext.Quest_Pirate1_C, GiftContext.Quest_Pirate1_B, GiftContext.Quest_Pirate1_A, GiftContext.Quest_Pirate1_S }),
|
|
("Paintball", new[] { GiftContext.Paintball_ClearCut, GiftContext.Paintball_Homestead, GiftContext.Paintball_Quarry, GiftContext.Paintball_River, GiftContext.Paintball_Dam, GiftContext.Paintball_DriveIn }),
|
|
("DiscGolf", new[] { GiftContext.Discgolf_Propulsion, GiftContext.Discgolf_Lake }),
|
|
("Charades", new[] { GiftContext.Charades }),
|
|
("Soccer", new[] { GiftContext.Soccer }),
|
|
("Paddleball", new[] { GiftContext.Paddleball }),
|
|
("Dodgeball", new[] { GiftContext.Dodgeball }),
|
|
("Bowling", new[] { GiftContext.Bowling }),
|
|
})
|
|
{
|
|
// A quest's ranks may pay the same piece - the verified Isle of Lost Skulls tables do -
|
|
// so an activity's pieces are gathered up before they are compared with another's.
|
|
var paidByRoom = ranks
|
|
.SelectMany(context => RroRewardPools.Resolve(context, json))
|
|
.Select(x => x.AvatarItemDesc)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
foreach (var descriptor in paidByRoom)
|
|
{
|
|
Assert.True(!owner.TryGetValue(descriptor, out var other),
|
|
$"{descriptor} is paid by both {other} and {room}");
|
|
owner[descriptor] = room;
|
|
}
|
|
}
|
|
|
|
Assert.NotEmpty(owner);
|
|
}
|
|
|
|
[Fact]
|
|
public void APoolTheDefinitionsNameButTheCatalogueCannotSatisfyPaysNothing()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
|
|
// The tiers that name no reward at all.
|
|
Assert.Empty(RroRewardPools.Resolve(GiftContext.Quest_Dracula1_X, json));
|
|
Assert.Empty(RroRewardPools.Resolve(GiftContext.Quest_Pirate1_X, json));
|
|
Assert.Empty(RroRewardPools.Resolve(GiftContext.StuntRunner_TheMainEvent_D, json));
|
|
Assert.Empty(RroRewardPools.Resolve(GiftContext.StuntRunner_TheMainEvent_X, json));
|
|
Assert.Empty(RroRewardPools.Resolve(GiftContext.RecRoyale, json));
|
|
|
|
// The Scallywag tiers pay now; only X names nothing.
|
|
Assert.Empty(RroRewardPools.Resolve(GiftContext.Quest_Pirate1_X, json));
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryGapIsWrittenDownRatherThanGuessed()
|
|
{
|
|
// The brief's rule: an item that cannot be verified is left unresolved, not invented. Every
|
|
// context that pays nothing has to be accounted for in the list, so a silently empty pool
|
|
// cannot slip through.
|
|
var known = string.Join("\n", RroRewardPools.Unresolved);
|
|
|
|
// The sets the catalogue genuinely cannot satisfy, and nothing else. Every other reward set
|
|
// turned out to be in the catalogue under its own family name.
|
|
Assert.Contains("Brown Witch Hunter", known);
|
|
Assert.Contains("Frontier Pass", known);
|
|
Assert.Contains("Isle of Lost Skulls X", known);
|
|
Assert.Contains("Stunt Runner D and X", known);
|
|
Assert.Contains("Crescendo X", known);
|
|
Assert.Contains("Rec Royale", known);
|
|
|
|
// And a set that was found must not still be described as missing.
|
|
Assert.DoesNotContain("Feathered Hair", known);
|
|
Assert.DoesNotContain("Dude Sweater", known);
|
|
Assert.DoesNotContain("Dracula's Set", known);
|
|
Assert.DoesNotContain("Farmer set is not", known);
|
|
Assert.DoesNotContain("Golfer set is not", known);
|
|
|
|
Assert.Equal(RroRewardPools.Unresolved.Count, RroRewardPools.Unresolved.Distinct().Count());
|
|
}
|
|
|
|
[Theory]
|
|
// Every set that the catalogue turned out to carry, named by a piece it must contain. These are
|
|
// the pools that were once wrongly written off as missing.
|
|
[InlineData(GiftContext.Paintball_Homestead, "Farmer Hat")]
|
|
[InlineData(GiftContext.Paintball_Homestead, "Farmer Overalls")]
|
|
[InlineData(GiftContext.Paintball_Quarry, "Construction Helmet (Yellow)")]
|
|
[InlineData(GiftContext.Paintball_Quarry, "Construction Shirt (Blue)")]
|
|
[InlineData(GiftContext.Discgolf_Lake, "Golfer Hat (Blue)")]
|
|
[InlineData(GiftContext.Discgolf_Lake, "Golfer Shirt (Tan)")]
|
|
[InlineData(GiftContext.Dodgeball, "Dodgeball Shirt (Orange)")]
|
|
[InlineData(GiftContext.Bowling, "Dude Sweater")]
|
|
[InlineData(GiftContext.Quest_Dracula1_SS, "Dracula Cape")]
|
|
[InlineData(GiftContext.Quest_Dracula1_SS, "Dracula Jacket")]
|
|
[InlineData(GiftContext.Quest_Pirate1_A, "Pirate Captain Jacket (Blue)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_S, "Pirate Captain Jacket (Gold)")]
|
|
public void ASetTheCatalogueCarriesPaysItsPieces(GiftContext context, string expected)
|
|
{
|
|
var names = RroRewardPools.Resolve(context, ShippedCatalogueJson())
|
|
.Select(x => x.FriendlyName)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
Assert.NotEmpty(names);
|
|
Assert.Contains(expected, names);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheHomesteadSetIsTwoPiecesAndTheBeardIsNotOne()
|
|
{
|
|
// The Farmer set is the hat and the overalls; the beard was a store item and must not be
|
|
// handed out as a Homestead reward.
|
|
var names = RroRewardPools.Resolve(GiftContext.Paintball_Homestead, ShippedCatalogueJson())
|
|
.Select(x => x.FriendlyName)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
Assert.Equal(2, names.Count);
|
|
Assert.Contains("Farmer Hat", names);
|
|
Assert.Contains("Farmer Overalls", names);
|
|
Assert.DoesNotContain("Farmer Beard", names);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheGolferSetIsThreePiecesInThreeColoursAndNothingElse()
|
|
{
|
|
// Three clothing types in tan, blue and grey. The catalogue's Golfer Hat also comes in red,
|
|
// which is a store colour rather than a reward one.
|
|
var names = RroRewardPools.Resolve(GiftContext.Discgolf_Lake, ShippedCatalogueJson())
|
|
.Select(x => x.FriendlyName)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
Assert.Equal(9, names.Count);
|
|
Assert.Contains("Golfer Hat (Tan)", names);
|
|
Assert.Contains("Golfer Shirt (Grey)", names);
|
|
Assert.Contains("Golfer Gloves (Blue)", names);
|
|
Assert.DoesNotContain("Golfer Hat (Red)", names);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheDodgerSetIsTheThreeCataloguePiecesUnderTheShirtName()
|
|
{
|
|
// The definitions call it the Dodger Vest in cyan, purple and orange; the catalogue carries
|
|
// the same three as Dodgeball Shirt in orange, purple and teal.
|
|
var names = RroRewardPools.Resolve(GiftContext.Dodgeball, ShippedCatalogueJson())
|
|
.Select(x => x.FriendlyName)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
Assert.Equal(3, names.Count);
|
|
Assert.Contains("Dodgeball Shirt (Orange)", names);
|
|
Assert.Contains("Dodgeball Shirt (Purple)", names);
|
|
Assert.Contains("Dodgeball Shirt (Teal)", names);
|
|
}
|
|
|
|
[Fact]
|
|
public void BowlingPaysItsSweaterOnceInTwoHundredAndFiftyGames()
|
|
{
|
|
Assert.Equal(1.0 / 250.0, RroRewardPools.DropChanceFor(GiftContext.Bowling), 10);
|
|
// Every other activity pays what its pool holds whenever it is earned.
|
|
Assert.Equal(1.0, RroRewardPools.DropChanceFor(GiftContext.Dodgeball));
|
|
Assert.Equal(1.0, RroRewardPools.DropChanceFor(GiftContext.Quest_Goblin_S));
|
|
}
|
|
|
|
[Fact]
|
|
public void LaserTagPaysTicketsInsteadOfDrawingACosmetic()
|
|
{
|
|
// Laser Tag is not a random-drop activity: its context exists for the record, and its
|
|
// reward is tickets for its own shop.
|
|
Assert.True(RroRewardPools.PaysTickets("LaserTag"));
|
|
Assert.False(RroRewardPools.PaysTickets("Dodgeball"));
|
|
Assert.False(RroRewardPools.PaysTickets(null));
|
|
|
|
Assert.Empty(RroRewardPools.Resolve(GiftContext.Lasertag, ShippedCatalogueJson()));
|
|
Assert.Empty(RroRewardPools.BuildDrops(GiftContext.Lasertag, selection: false, null, new Random(1), ShippedCatalogueJson()));
|
|
|
|
Assert.Equal(25, RroRewardPools.LaserTagTicketsPerGame);
|
|
Assert.Equal(100, RroRewardPools.LaserTagFirstGamesBonus);
|
|
Assert.Equal(5, RroRewardPools.LaserTagBonusGamesPerDay);
|
|
}
|
|
|
|
[Theory]
|
|
// Every rank of Isle of Lost Skulls, named by the pieces the verified tables give it.
|
|
[InlineData(GiftContext.Quest_Pirate1_C, "Scallywag Bracers (Brown)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_C, "Scallywag Belt (Red)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_C, "Pirate Captain Cuffs (Green)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_B, "Scallywag Vest (Brown)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_B, "Scallywag Vest (Red)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_B, "Pirate Captain Jacket (Blue)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_A, "Scallywag Hat (Brown)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_A, "Scallywag Hat (Red)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_A, "Pirate Captain Hat (Red)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_S, "Pirate Captain Hat (Gold)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_S, "Pirate Eyepatch (Black)")]
|
|
[InlineData(GiftContext.Quest_Pirate1_S, "Pirate Eyepatch (Skull)")]
|
|
public void EveryIsleOfLostSkullsRankPaysThePiecesItNames(GiftContext context, string expected)
|
|
{
|
|
var names = RroRewardPools.Resolve(context, ShippedCatalogueJson())
|
|
.Select(x => x.FriendlyName)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
Assert.NotEmpty(names);
|
|
Assert.Contains(expected, names);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheBlackScallywagSetIsNotACompletionReward()
|
|
{
|
|
// It is a shop set, so no Isle of Lost Skulls rank names it.
|
|
var json = ShippedCatalogueJson();
|
|
|
|
foreach (var context in new[]
|
|
{
|
|
GiftContext.Quest_Pirate1_C, GiftContext.Quest_Pirate1_B,
|
|
GiftContext.Quest_Pirate1_A, GiftContext.Quest_Pirate1_S,
|
|
})
|
|
{
|
|
Assert.All(
|
|
RroRewardPools.Resolve(context, json).Where(x => x.FriendlyName.StartsWith("Scallywag", StringComparison.Ordinal)),
|
|
item => Assert.DoesNotContain("(Black)", item.FriendlyName, StringComparison.Ordinal));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AQuestFallsBackToItsOwnLowerRanks()
|
|
{
|
|
// Strongest first, and never to another activity.
|
|
Assert.Equal(
|
|
new[] { GiftContext.Quest_Goblin_A, GiftContext.Quest_Goblin_B, GiftContext.Quest_Goblin_C },
|
|
RroRewardPools.LowerRanksOf(GiftContext.Quest_Goblin_S));
|
|
|
|
Assert.Equal(
|
|
new[] { GiftContext.Quest_Pirate1_B, GiftContext.Quest_Pirate1_C },
|
|
RroRewardPools.LowerRanksOf(GiftContext.Quest_Pirate1_A));
|
|
|
|
// The weakest rank of a quest has nothing beneath it, and a map game ranks nothing at all.
|
|
Assert.Empty(RroRewardPools.LowerRanksOf(GiftContext.Quest_Goblin_C));
|
|
Assert.Empty(RroRewardPools.LowerRanksOf(GiftContext.Quest_Pirate1_C));
|
|
Assert.Empty(RroRewardPools.LowerRanksOf(GiftContext.Paintball_ClearCut));
|
|
Assert.Empty(RroRewardPools.LowerRanksOf(GiftContext.Dodgeball));
|
|
}
|
|
|
|
[Fact]
|
|
public void TheLowerRanksHandedBackAreAlwaysTheSameActivitys()
|
|
{
|
|
foreach (var context in RroRewardPools.RewardableContexts)
|
|
{
|
|
foreach (var weaker in RroRewardPools.LowerRanksOf(context))
|
|
{
|
|
var sameRoom = RroRewardPools.ContextsForRoom("GoldenTrophy").Values.Contains(weaker)
|
|
|| RroRewardPools.ContextsForRoom("CrimsonCauldron").Values.Contains(weaker)
|
|
|| RroRewardPools.ContextsForRoom("IsleOfLostSkulls").Values.Contains(weaker)
|
|
|| RroRewardPools.ContextsForRoom("Crescendo").Values.Contains(weaker)
|
|
|| RroRewardPools.ContextsForRoom("StuntRunner").Values.Contains(weaker)
|
|
|| RroRewardPools.ContextsForRoom("TheRiseofJumbotron").Values.Contains(weaker);
|
|
|
|
Assert.True(sameRoom, $"{weaker} came back as a lower rank of {context} but is not a quest rank");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void RecRoyalePaysThroughItsPassRatherThanADrop()
|
|
{
|
|
Assert.True(RroRewardPools.PaysNoItemReward("RecRoyaleSolos"));
|
|
Assert.True(RroRewardPools.PaysNoItemReward("RecRoyaleSquads"));
|
|
Assert.False(RroRewardPools.PaysNoItemReward("Dodgeball"));
|
|
Assert.False(RroRewardPools.PaysNoItemReward(null));
|
|
|
|
Assert.Empty(RroRewardPools.Resolve(GiftContext.RecRoyale, ShippedCatalogueJson()));
|
|
}
|
|
|
|
[Fact]
|
|
public void GreyAndGrayAreOneColour()
|
|
{
|
|
// The catalogue spells it both ways depending on the item, so a set named one way still has
|
|
// to find the pieces spelled the other.
|
|
var json = ShippedCatalogueJson();
|
|
var grey = RroRewardPools.Resolve(new RroRewardPools.ItemSet("Knight", "Grey"), json);
|
|
var gray = RroRewardPools.Resolve(new RroRewardPools.ItemSet("Knight", "Gray"), json);
|
|
|
|
Assert.NotEmpty(grey);
|
|
Assert.Equal(
|
|
grey.Select(x => x.AvatarItemDesc).OrderBy(x => x, StringComparer.Ordinal),
|
|
gray.Select(x => x.AvatarItemDesc).OrderBy(x => x, StringComparer.Ordinal));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Picking an item
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void ThePickSkipsWhatThePlayerAlreadyOwns()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
var pool = RroRewardPools.Resolve(GiftContext.Quest_Goblin_S, json);
|
|
Assert.True(pool.Count >= 2);
|
|
|
|
// Everything but the last item is owned, so the pick can only be that one.
|
|
var owned = pool.Take(pool.Count - 1)
|
|
.Select(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc))
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var pick = RroRewardPools.Pick(GiftContext.Quest_Goblin_S, owned, new Random(1), json);
|
|
Assert.NotNull(pick);
|
|
Assert.Equal(pool[^1].AvatarItemDesc, pick!.AvatarItemDesc);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnExhaustedPoolPicksNothingRatherThanRepeating()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
var pool = RroRewardPools.Resolve(GiftContext.Quest_Goblin_S, json);
|
|
|
|
var owned = pool
|
|
.Select(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc))
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
Assert.Null(RroRewardPools.Pick(GiftContext.Quest_Goblin_S, owned, new Random(1), json));
|
|
Assert.Null(RroRewardPools.BuildGift(GiftContext.Quest_Goblin_S, owned, new Random(1), json));
|
|
}
|
|
|
|
[Fact]
|
|
public void AContextThatPaysNothingPicksNothing()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
Assert.Null(RroRewardPools.Pick(GiftContext.Quest_Pirate1_X, null, new Random(1), json));
|
|
Assert.Null(RroRewardPools.Pick(GiftContext.DEPRECATED_Bowling, null, new Random(1), json));
|
|
Assert.Null(RroRewardPools.Pick(GiftContext.GameRewards, null, new Random(1), json));
|
|
}
|
|
|
|
[Fact]
|
|
public void TheBoxCarriesTheContextItWasFiledUnder()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
var gift = RroRewardPools.BuildGift(GiftContext.Quest_Goblin_S, null, new Random(7), json);
|
|
|
|
Assert.NotNull(gift);
|
|
Assert.Equal(GiftContext.Quest_Goblin_S, gift!.Context);
|
|
// The wire field the client reads, and the one the admin panel stores, both agree.
|
|
Assert.Equal(4003, gift.GiftContext);
|
|
Assert.False(string.IsNullOrWhiteSpace(gift.AvatarItemDesc));
|
|
Assert.Equal(0, gift.Currency);
|
|
Assert.Equal(0, gift.Xp);
|
|
}
|
|
|
|
[Fact]
|
|
public void APaintballRewardIsFiledUnderItsOwnMap()
|
|
{
|
|
var json = ShippedCatalogueJson();
|
|
|
|
Assert.True(RroRewardPools.TryContext("Clearcut", null, out var clearcut));
|
|
var gift = RroRewardPools.BuildGift(clearcut, null, new Random(3), json);
|
|
|
|
Assert.NotNull(gift);
|
|
Assert.Equal(2010, gift!.GiftContext);
|
|
// The item is a Lumberjack piece, not a quest reward.
|
|
Assert.Contains("Lumberjack", gift.FriendlyName, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryRewardableContextEitherPaysItemsOrIsAWrittenDownGap()
|
|
{
|
|
// A pool that quietly loses all of its items is the failure this guards against: a context
|
|
// with pools must resolve, and one without must be a gap this file names on purpose.
|
|
var json = ShippedCatalogueJson();
|
|
|
|
GiftContext[] deliberatelyEmpty =
|
|
[
|
|
GiftContext.RecRoyale,
|
|
GiftContext.Lasertag,
|
|
GiftContext.Quest_Dracula1_X,
|
|
GiftContext.Quest_Pirate1_X,
|
|
GiftContext.StuntRunner_TheMainEvent_D,
|
|
GiftContext.StuntRunner_TheMainEvent_X,
|
|
];
|
|
|
|
foreach (var context in RroRewardPools.RewardableContexts)
|
|
{
|
|
if (RroRewardPools.Resolve(context, json).Count > 0) continue;
|
|
|
|
Assert.True(deliberatelyEmpty.Contains(context),
|
|
$"{context} pays nothing and is not one of the written-down gaps");
|
|
}
|
|
|
|
// And the reverse: none of the written-down gaps gained a pool behind our back.
|
|
foreach (var context in deliberatelyEmpty)
|
|
Assert.Empty(RroRewardPools.Resolve(context, json));
|
|
}
|
|
}
|