#include #include "testHarness.h" #include "vf/Brewing.h" using namespace vf; namespace { using Ingredient = BrewRecipe::Ingredient; BrewingStandState fueledStand(const PotionItem& bottle, int blazePowder = 5) { BrewingStandState stand; setBottle(stand, 0, bottle); addFuel(stand, blazePowder); return stand; } // Loads one ingredient and runs the stand until the brew lands. Returns the // bottle in the first slot afterwards. PotionItem brewOnce(BrewingStandState& stand, Ingredient ingredient) { setIngredient(stand, ingredient, 1); for (int tick = 0; tick < brewDurationTicks; ++tick) tickBrewing(stand); return stand.bottles[0].potion; } // Walks a whole chain of ingredients through a single bottle. PotionItem brewChain(const PotionItem& start, const std::vector& chain) { BrewingStandState stand = fueledStand(start, 10); PotionItem bottle = start; for (Ingredient ingredient : chain) bottle = brewOnce(stand, ingredient); return bottle; } PotionItem potionOf(PotionKind kind) { PotionItem potion; potion.kind = kind; return potion; } int durationOf(PotionKind kind, bool upgraded, bool extended) { PotionItem potion = potionOf(kind); potion.upgraded = upgraded; potion.extended = extended; return potionEffect(potion).durationTicks; } int levelOf(PotionKind kind, bool upgraded, bool extended) { PotionItem potion = potionOf(kind); potion.upgraded = upgraded; potion.extended = extended; return potionEffect(potion).level(); } } // namespace VF_TEST(theStandsPublishedConstantsAreTheOnesTheRestOfTheEngineWasBuiltAgainst) { CHECK_EQ(brewingBottleSlots, 3); CHECK_EQ(brewDurationTicks, 400); CHECK_EQ(brewDurationTicks, 20 * ticksPerSecond); CHECK_EQ(brewsPerBlazePowder, 20); CHECK_EQ(static_cast(PotionKind::Count), 17); CHECK_EQ(static_cast(BrewRecipe::Ingredient::None), 0); // A stand with one blaze powder in it runs a literal twenty second brew and // has nineteen brews of fuel left afterwards. BrewingStandState stand; setBottle(stand, 0, PotionItem{}); addFuel(stand, 1); setIngredient(stand, Ingredient::NetherWart, 1); CHECK_EQ(stand.fuel, 20); int completedOn = 0; for (int tick = 1; tick <= 500; ++tick) { if (tickBrewing(stand) && completedOn == 0) completedOn = tick; } CHECK_EQ(completedOn, 400); CHECK_EQ(stand.fuel, 19); } VF_TEST(recipeTableHasNoDuplicateInputAndIngredientPairs) { const std::vector& recipes = brewRecipes(); CHECK(recipes.size() >= 19); int collisions = 0; int selfLoops = 0; int emptyIngredients = 0; for (std::size_t i = 0; i < recipes.size(); ++i) { if (recipes[i].ingredient == Ingredient::None) ++emptyIngredients; if (recipes[i].input == recipes[i].output) ++selfLoops; for (std::size_t j = i + 1; j < recipes.size(); ++j) { if (recipes[i].input == recipes[j].input && recipes[i].ingredient == recipes[j].ingredient) { ++collisions; } } } CHECK_EQ(collisions, 0); CHECK_EQ(selfLoops, 0); CHECK_EQ(emptyIngredients, 0); } VF_TEST(netherWartTurnsWaterIntoAwkwardAndAwkwardOpensEveryBasePotion) { bool changed = false; CHECK_EQ(brewResult(PotionItem{}, Ingredient::NetherWart, changed).kind, PotionKind::Awkward); CHECK(changed); const struct { Ingredient ingredient; PotionKind expected; } branches[] = { {Ingredient::Sugar, PotionKind::Swiftness}, {Ingredient::GlisteringMelon, PotionKind::Healing}, {Ingredient::SpiderEye, PotionKind::Poison}, {Ingredient::BlazePowder, PotionKind::Strength}, {Ingredient::MagmaCream, PotionKind::FireResistance}, {Ingredient::PufferFish, PotionKind::WaterBreathing}, {Ingredient::GoldenCarrot, PotionKind::NightVision}, {Ingredient::GhastTear, PotionKind::Regeneration}, {Ingredient::RabbitFoot, PotionKind::Leaping}, }; for (const auto& branch : branches) { bool branched = false; PotionItem brewed = brewResult(potionOf(PotionKind::Awkward), branch.ingredient, branched); CHECK(branched); CHECK_EQ(brewed.kind, branch.expected); } } VF_TEST(aStandTurnsWaterIntoAwkwardOnTheFourHundredthTickAndNotOneTickEarlier) { BrewingStandState stand = fueledStand(PotionItem{}, 1); CHECK_EQ(stand.fuel, brewsPerBlazePowder); CHECK(setIngredient(stand, Ingredient::NetherWart, 1)); bool completedEarly = false; for (int tick = 1; tick < brewDurationTicks; ++tick) { if (tickBrewing(stand)) completedEarly = true; } CHECK(!completedEarly); CHECK(stand.brewing); CHECK_EQ(stand.progressTicks, brewDurationTicks - 1); CHECK_EQ(stand.bottles[0].potion.kind, PotionKind::Water); CHECK(tickBrewing(stand)); CHECK_EQ(stand.bottles[0].potion.kind, PotionKind::Awkward); CHECK_EQ(stand.progressTicks, 0); CHECK(!stand.brewing); CHECK_EQ(stand.fuel, brewsPerBlazePowder - 1); CHECK_EQ(stand.ingredientCount, 0); CHECK_EQ(stand.ingredient, Ingredient::None); } VF_TEST(theFullWaterToUpgradedSplashStrengthCycleRunsOnOneStand) { BrewingStandState stand = fueledStand(PotionItem{}, 1); CHECK_EQ(brewOnce(stand, Ingredient::NetherWart).kind, PotionKind::Awkward); CHECK_EQ(brewOnce(stand, Ingredient::BlazePowder).kind, PotionKind::Strength); PotionItem upgraded = brewOnce(stand, Ingredient::Glowstone); CHECK(upgraded.upgraded); CHECK(!upgraded.extended); PotionItem splash = brewOnce(stand, Ingredient::Gunpowder); CHECK_EQ(splash.form, PotionItem::Form::Splash); CHECK_EQ(splash.kind, PotionKind::Strength); CHECK(splash.upgraded); CHECK_EQ(stand.fuel, brewsPerBlazePowder - 4); EffectInstance effect = potionEffect(splash); CHECK_EQ(effect.kind, EffectKind::Strength); CHECK_EQ(effect.level(), 2); CHECK_EQ(effect.durationTicks, 1800); } VF_TEST(glowstoneRaisesTheLevelAndShortensWhileRedstoneOnlyLengthens) { PotionItem plain = potionOf(PotionKind::Swiftness); PotionItem upgraded = brewChain(plain, {Ingredient::Glowstone}); PotionItem extended = brewChain(plain, {Ingredient::Redstone}); CHECK_EQ(potionEffect(plain).level(), 1); CHECK_EQ(potionEffect(plain).durationTicks, 3600); CHECK(upgraded.upgraded); CHECK_EQ(potionEffect(upgraded).level(), 2); CHECK_EQ(potionEffect(upgraded).durationTicks, 1800); CHECK(extended.extended); CHECK_EQ(potionEffect(extended).level(), 1); CHECK_EQ(potionEffect(extended).durationTicks, 9600); } VF_TEST(glowstoneAndRedstoneReplaceEachOtherAndNeverStack) { BrewingStandState stand = fueledStand(potionOf(PotionKind::Swiftness), 5); PotionItem extended = brewOnce(stand, Ingredient::Redstone); CHECK(extended.extended); PotionItem upgraded = brewOnce(stand, Ingredient::Glowstone); CHECK(upgraded.upgraded); CHECK(!upgraded.extended); PotionItem backToExtended = brewOnce(stand, Ingredient::Redstone); CHECK(backToExtended.extended); CHECK(!backToExtended.upgraded); bool changed = false; brewResult(backToExtended, Ingredient::Redstone, changed); CHECK(!changed); } VF_TEST(potionsThatIgnoreAModifierRejectItInsteadOfSilentlyConsumingTheBrew) { const PotionKind noGlowstone[] = {PotionKind::Weakness, PotionKind::FireResistance, PotionKind::WaterBreathing, PotionKind::NightVision, PotionKind::Invisibility}; for (PotionKind kind : noGlowstone) { bool changed = false; brewResult(potionOf(kind), Ingredient::Glowstone, changed); CHECK(!changed); } const PotionKind noRedstone[] = {PotionKind::Healing, PotionKind::Harming}; for (PotionKind kind : noRedstone) { bool changed = false; brewResult(potionOf(kind), Ingredient::Redstone, changed); CHECK(!changed); } bool healingUpgrades = false; PotionItem strongHealing = brewResult(potionOf(PotionKind::Healing), Ingredient::Glowstone, healingUpgrades); CHECK(healingUpgrades); CHECK_EQ(potionEffect(strongHealing).level(), 2); } VF_TEST(fermentedSpiderEyeCorruptsAlongTheWholeInversionChain) { const struct { PotionKind from; PotionKind to; } inversions[] = { {PotionKind::Water, PotionKind::Weakness}, {PotionKind::NightVision, PotionKind::Invisibility}, {PotionKind::Healing, PotionKind::Harming}, {PotionKind::Poison, PotionKind::Harming}, {PotionKind::Swiftness, PotionKind::Slowness}, {PotionKind::Leaping, PotionKind::Slowness}, {PotionKind::Strength, PotionKind::Weakness}, }; for (const auto& inversion : inversions) { bool changed = false; PotionItem corrupted = brewResult(potionOf(inversion.from), Ingredient::FermentedSpiderEye, changed); CHECK(changed); CHECK_EQ(corrupted.kind, inversion.to); } const PotionKind uncorruptible[] = {PotionKind::Regeneration, PotionKind::FireResistance, PotionKind::WaterBreathing, PotionKind::Awkward}; for (PotionKind kind : uncorruptible) { bool changed = false; brewResult(potionOf(kind), Ingredient::FermentedSpiderEye, changed); CHECK(!changed); } } VF_TEST(aKindChangeWipesTheModifiersButKeepsTheSplashForm) { PotionItem source = potionOf(PotionKind::Swiftness); source.upgraded = true; source.form = PotionItem::Form::Splash; bool changed = false; PotionItem corrupted = brewResult(source, Ingredient::FermentedSpiderEye, changed); CHECK(changed); CHECK_EQ(corrupted.kind, PotionKind::Slowness); CHECK(!corrupted.upgraded); CHECK(!corrupted.extended); CHECK_EQ(corrupted.form, PotionItem::Form::Splash); } VF_TEST(brewResultLeavesTheInputAloneWhenTheIngredientDoesNothing) { PotionItem source = potionOf(PotionKind::Regeneration); source.extended = true; bool changed = true; PotionItem untouched = brewResult(source, Ingredient::SpiderEye, changed); CHECK(!changed); CHECK(untouched == source); changed = true; PotionItem noIngredient = brewResult(source, Ingredient::None, changed); CHECK(!changed); CHECK(noIngredient == source); } VF_TEST(theStandConvertsEveryOccupiedBottleInASinglePass) { BrewingStandState stand; CHECK(setBottle(stand, 0, PotionItem{})); CHECK(setBottle(stand, 1, PotionItem{})); CHECK(setBottle(stand, 2, PotionItem{})); addFuel(stand, 1); setIngredient(stand, Ingredient::NetherWart, 1); for (int tick = 0; tick < brewDurationTicks; ++tick) tickBrewing(stand); for (const BrewingSlot& bottle : stand.bottles) { CHECK(bottle.occupied); CHECK_EQ(bottle.potion.kind, PotionKind::Awkward); } CHECK_EQ(stand.fuel, brewsPerBlazePowder - 1); } VF_TEST(bottlesTheIngredientCannotChangeAreLeftAsTheyWere) { BrewingStandState stand; setBottle(stand, 0, potionOf(PotionKind::Awkward)); setBottle(stand, 1, PotionItem{}); addFuel(stand, 1); setIngredient(stand, Ingredient::Sugar, 1); CHECK(canBrew(stand)); for (int tick = 0; tick < brewDurationTicks; ++tick) tickBrewing(stand);