#include #include #include "testHarness.h" #include "vf/PotionCatalog.h" using namespace vf; namespace { using Ingredient = BrewRecipe::Ingredient; PotionItem bottleOf(PotionKind kind, bool upgraded = false, bool extended = false, PotionItem::Form form = PotionItem::Form::Drinkable) { PotionItem potion; potion.kind = kind; potion.upgraded = upgraded; potion.extended = extended; potion.form = form; return potion; } // Runs a whole ingredient list through an actual brewing stand, one full // four hundred tick brew per step. PotionItem replayOnAStand(const std::vector& steps) { BrewingStandState stand; setBottle(stand, 0, PotionItem{}); addFuel(stand, blazePowderForBrews(static_cast(steps.size())) + 1); for (Ingredient ingredient : steps) { setIngredient(stand, ingredient, 1); for (int tick = 0; tick < brewDurationTicks; ++tick) tickBrewing(stand); } return stand.bottles[0].potion; } PotionItem replayThroughSteps(const std::vector& steps) { PotionItem bottle; for (Ingredient ingredient : steps) { bool changed = false; bottle = catalogBrewStep(bottle, ingredient, changed); if (!changed) return PotionItem{}; } return bottle; } int splashDurationThroughTheStand(const PotionItem& potion) { PotionItem splash = potion; splash.form = PotionItem::Form::Splash; EffectSet target; if (!splashPotion(target, splash, 0.0f, 4.0f)) return 0; return target.active[0].durationTicks; } } // namespace VF_TEST(theCatalogsPublishedConstantsAreTheOnesTheNumbersWereDerivedFrom) { CHECK_NEAR(splashDurationScale, 0.75f, 0.0001f); CHECK_NEAR(lingeringDoseScale, 0.25f, 0.0001f); CHECK_EQ(lingeringCloudTicks, 600); CHECK_EQ(lingeringCloudTicks, 30 * ticksPerSecond); // Pinned against literals rather than the constants, so shifting a constant // cannot quietly shift the expectations with it. const PotionCatalog& catalog = potionCatalog(); PotionItem swiftness = bottleOf(PotionKind::Swiftness); CHECK_EQ(catalog.variantTicks(swiftness), 3600); swiftness.form = PotionItem::Form::Splash; CHECK_EQ(catalog.variantTicks(swiftness), 2700); swiftness.form = PotionItem::Form::Lingering; CHECK_EQ(catalog.variantTicks(swiftness), 900); CHECK_EQ(catalog.cloudTicks(swiftness), 600); } VF_TEST(theCatalogHasExactlyOneRowPerPotionKindInEnumOrder) { const PotionCatalog& catalog = potionCatalog(); CHECK_EQ(catalog.size(), static_cast(PotionKind::Count)); for (int index = 0; index < static_cast(PotionKind::Count); ++index) { PotionKind kind = static_cast(index); const PotionEntry& row = catalog.entry(kind); CHECK_EQ(row.kind, kind); CHECK(row.name != nullptr); CHECK(std::strlen(row.name) > 0); } int duplicateNames = 0; const std::vector& rows = catalog.entries(); for (std::size_t i = 0; i < rows.size(); ++i) { for (std::size_t j = i + 1; j < rows.size(); ++j) { if (std::strcmp(rows[i].name, rows[j].name) == 0) ++duplicateNames; } } CHECK_EQ(duplicateNames, 0); } VF_TEST(everyCatalogDurationAndLevelMatchesWhatTheStandActuallyApplies) { const PotionCatalog& catalog = potionCatalog(); for (int index = 0; index < static_cast(PotionKind::Count); ++index) { PotionKind kind = static_cast(index); const PotionEntry& row = catalog.entry(kind); const struct { bool upgraded; bool extended; } modifiers[] = { {false, false}, {true, false}, {false, true}, {true, true}}; for (const auto& modifier : modifiers) { PotionItem potion = bottleOf(kind, modifier.upgraded, modifier.extended); EffectInstance applied = potionEffect(potion); CHECK_EQ(catalog.variantTicks(potion), applied.durationTicks); if (row.carriesEffect()) { CHECK_EQ(catalog.variantLevel(potion), applied.level()); CHECK_EQ(row.effect, applied.kind); } else { CHECK_EQ(catalog.variantLevel(potion), 0); CHECK_EQ(applied.kind, EffectKind::Count); } } } } VF_TEST(theSplashColumnOfTheCatalogMatchesWhatASplashBottleLandsAtTheImpactPoint) { const PotionCatalog& catalog = potionCatalog(); int checkedTimedPotions = 0; for (const PotionEntry& row : catalog.entries()) { if (!row.carriesEffect()) continue; const struct { bool upgraded; bool extended; } modifiers[] = { {false, false}, {true, false}, {false, true}}; for (const auto& modifier : modifiers) { PotionItem drinkable = bottleOf(row.kind, modifier.upgraded, modifier.extended); PotionItem splash = drinkable; splash.form = PotionItem::Form::Splash; CHECK_EQ(catalog.variantTicks(splash), splashDurationThroughTheStand(drinkable)); CHECK_EQ(catalog.variantLevel(splash), catalog.variantLevel(drinkable)); if (!row.instant) ++checkedTimedPotions; } } CHECK(checkedTimedPotions >= 30); } VF_TEST(lingeringBottlesDoseAQuarterAndLeaveACloudBehindThem) { const PotionCatalog& catalog = potionCatalog(); PotionItem swiftness = bottleOf(PotionKind::Swiftness); CHECK_EQ(catalog.variantTicks(swiftness), 3600); CHECK_EQ(catalog.cloudTicks(swiftness), 0); PotionItem lingering = bottleOf(PotionKind::Swiftness, false, false, PotionItem::Form::Lingering); CHECK_EQ(catalog.variantTicks(lingering), 900); CHECK_EQ(catalog.cloudTicks(lingering), lingeringCloudTicks); PotionItem lingeringStrong = bottleOf(PotionKind::Swiftness, true, false, PotionItem::Form::Lingering); CHECK_EQ(catalog.variantTicks(lingeringStrong), 450); CHECK_EQ(catalog.variantLevel(lingeringStrong), 2); PotionItem lingeringWater = bottleOf(PotionKind::Water, false, false, PotionItem::Form::Lingering); CHECK_EQ(catalog.variantTicks(lingeringWater), 0); CHECK_EQ(catalog.cloudTicks(lingeringWater), 0); } VF_TEST(instantPotionsStayAtOneTickInEveryFormAndModifier) { const PotionCatalog& catalog = potionCatalog(); const PotionKind instants[] = {PotionKind::Healing, PotionKind::Harming}; const PotionItem::Form forms[] = {PotionItem::Form::Drinkable, PotionItem::Form::Splash, PotionItem::Form::Lingering}; for (PotionKind kind : instants) { CHECK(catalog.entry(kind).instant); CHECK(!catalog.entry(kind).acceptsRedstone()); CHECK(catalog.entry(kind).acceptsGlowstone()); for (PotionItem::Form form : forms) { CHECK_EQ(catalog.variantTicks(bottleOf(kind, false, false, form)), 1); CHECK_EQ(catalog.variantTicks(bottleOf(kind, true, false, form)), 1); CHECK_EQ(catalog.variantLevel(bottleOf(kind, true, false, form)), 2); } } } VF_TEST(everyCatalogRouteIsARealRecipeAndAlsoTheShortestOne) { const PotionCatalog& catalog = potionCatalog(); const BrewGraph& graph = brewGraph(); int routesChecked = 0; for (const PotionEntry& row : catalog.entries()) { if (row.ingredient == Ingredient::None) { CHECK_EQ(row.kind, PotionKind::Water); continue; } bool changed = false; PotionItem brewed = brewResult(bottleOf(row.base), row.ingredient, changed); CHECK(changed); CHECK_EQ(brewed.kind, row.kind); CHECK_EQ(graph.depthOf(bottleOf(row.kind)), graph.depthOf(bottleOf(row.base)) + 1); ++routesChecked; } CHECK_EQ(routesChecked, static_cast(PotionKind::Count) - 1); } VF_TEST(harmfulFlagsAndEffectLookupsAgreeWithTheEffectModule) { const PotionCatalog& catalog = potionCatalog(); for (const PotionEntry& row : catalog.entries()) { if (!row.carriesEffect()) { CHECK(!row.harmful); CHECK(!row.instant); continue; } CHECK_EQ(row.harmful, isHarmfulEffect(row.effect)); CHECK_EQ(row.instant, isInstantEffect(row.effect)); CHECK_EQ(catalog.kindForEffect(row.effect), row.kind); } const EffectKind uncovered[] = {EffectKind::Haste, EffectKind::MiningFatigue, EffectKind::Nausea, EffectKind::Resistance, EffectKind::Wither, EffectKind::HealthBoost, EffectKind::Absorption, EffectKind::Saturation, EffectKind::Levitation, EffectKind::Count}; for (EffectKind effect : uncovered) CHECK_EQ(catalog.kindForEffect(effect), PotionKind::Count); } VF_TEST(invertedKindReadsTheFermentedSpiderEyeBranchesStraightOffTheRecipeTable) { const PotionCatalog& catalog = potionCatalog(); CHECK_EQ(catalog.invertedKind(PotionKind::Water), PotionKind::Weakness); CHECK_EQ(catalog.invertedKind(PotionKind::NightVision), PotionKind::Invisibility); CHECK_EQ(catalog.invertedKind(PotionKind::Healing), PotionKind::Harming); CHECK_EQ(catalog.invertedKind(PotionKind::Poison), PotionKind::Harming); CHECK_EQ(catalog.invertedKind(PotionKind::Swiftness), PotionKind::Slowness); CHECK_EQ(catalog.invertedKind(PotionKind::Leaping), PotionKind::Slowness); CHECK_EQ(catalog.invertedKind(PotionKind::Strength), PotionKind::Weakness); const PotionKind untouched[] = {PotionKind::Awkward, PotionKind::Regeneration, PotionKind::FireResistance, PotionKind::WaterBreathing, PotionKind::Harming, PotionKind::Slowness}; for (PotionKind kind : untouched) CHECK_EQ(catalog.invertedKind(kind), PotionKind::Count); // Every inversion the catalog reports has to survive a real brew. for (int index = 0; index < static_cast(PotionKind::Count); ++index) { PotionKind kind = static_cast(index); PotionKind inverted = catalog.invertedKind(kind); bool changed = false; PotionItem brewed = brewResult(bottleOf(kind), Ingredient::FermentedSpiderEye, changed); CHECK_EQ(changed, inverted != PotionKind::Count); if (changed) CHECK_EQ(brewed.kind, inverted); } } VF_TEST(everyPotionInTheCatalogHasABrewingPathThatARealStandCanReplay) { const PotionCatalog& catalog = potionCatalog(); const BrewGraph& graph = brewGraph(); std::vector paths = graph.drinkablePaths(); CHECK_EQ(paths.size(), catalog.size()); for (std::size_t i = 0; i < paths.size(); ++i) { const BrewPath& path = paths[i]; const PotionEntry& row = catalog.entries()[i]; CHECK(path.reachable); CHECK_EQ(path.target.kind, row.kind); CHECK_EQ(path.totalTicks, path.stepCount() * brewDurationTicks); CHECK_EQ(path.stepCount(), graph.depthOf(path.target)); PotionItem brewed = replayOnAStand(path.steps); CHECK_EQ(brewed.kind, row.kind); CHECK_EQ(brewed.form, PotionItem::Form::Drinkable); CHECK(!brewed.upgraded); CHECK(!brewed.extended); } } VF_TEST(everyReachableVariantReplaysExactlyThroughItsOwnShortestPath) { const BrewGraph& graph = brewGraph(); int replayed = 0; for (int index = 0; index < BrewGraph::stateCount(); ++index) { PotionItem state = BrewGraph::stateAt(index); BrewPath path = graph.shortestPath(state); if (!path.reachable) continue; CHECK(replayThroughSteps(path.steps) == state); CHECK_EQ(path.stepCount(), graph.depthOf(state)); ++replayed; } CHECK_EQ(replayed, graph.reachableStateCount()); } VF_TEST(theGraphReachesExactlyTheVariantsTheCatalogDeclaresAndNothingElse) { const PotionCatalog& catalog = potionCatalog(); const BrewGraph& graph = brewGraph(); int expected = 0; for (const PotionEntry& row : catalog.entries()) expected += row.variantCount() * 3; CHECK_EQ(graph.reachableStateCount(), expected); CHECK(graph.reachableStateCount() < BrewGraph::stateCount()); int fromDepths = 0; for (int depth = 0; depth <= BrewGraph::stateCount(); ++depth) { fromDepths += static_cast(graph.statesAtDepth(depth).size()); } CHECK_EQ(fromDepths, graph.reachableStateCount()); CHECK_EQ(graph.statesAtDepth(0).size(), std::size_t{1}); CHECK(graph.statesAtDepth(0)[0] == PotionItem{}); } VF_TEST(modifiersAPotionRefusesLeaveThatVariantWithNoPathAtAll) { const BrewGraph& graph = brewGraph(); const PotionCatalog& catalog = potionCatalog(); const PotionKind noGlowstone[] = {PotionKind::Weakness, PotionKind::FireResistance, PotionKind::WaterBreathing, PotionKind::NightVision, PotionKind::Invisibility}; for (PotionKind kind : noGlowstone) { CHECK(!catalog.entry(kind).acceptsGlowstone()); CHECK(!graph.reachable(bottleOf(kind, true, false))); CHECK_EQ(graph.depthOf(bottleOf(kind, true, false)), -1); CHECK(graph.reachable(bottleOf(kind, false, true))); }