diff --git a/tests/testEnchant.cpp b/tests/testEnchant.cpp new file mode 100644 index 0000000..4aa1a88 --- /dev/null +++ b/tests/testEnchant.cpp @@ -0,0 +1,166 @@ +#include + +#include "testHarness.h" +#include "vf/Enchant.h" + +using namespace vf; + +namespace { + +// Places `count` shelves on the ring the table reads, filling the lower layer +// before the upper one. +void surroundWithShelves(World& world, const Vec3i& table, int count) { + int placed = 0; + for (int dy = 0; dy <= 1 && placed < count; ++dy) { + for (int dx = -2; dx <= 2 && placed < count; ++dx) { + for (int dz = -2; dz <= 2 && placed < count; ++dz) { + if (std::max(dx < 0 ? -dx : dx, dz < 0 ? -dz : dz) != 2) continue; + world.setBlock(table.x + dx, table.y + dy, table.z + dz, Block::Bookshelf); + ++placed; + } + } + } +} + +bool sameOffer(const EnchantOffer& a, const EnchantOffer& b) { + if (a.requiredLevels != b.requiredLevels || a.lapisCost != b.lapisCost) return false; + if (a.enchants.entries.size() != b.enchants.entries.size()) return false; + for (std::size_t i = 0; i < a.enchants.entries.size(); ++i) { + if (a.enchants.entries[i].kind != b.enchants.entries[i].kind) return false; + if (a.enchants.entries[i].level != b.enchants.entries[i].level) return false; + } + return true; +} + +EnchantSet setOf(EnchantKind kind, int level) { + EnchantSet set; + addEnchant(set, kind, level); + return set; +} + +} // namespace + +VF_TEST(bookshelvesOnTheRingRaiseTheOfferedLevelsUpToTheCap) { + World bare; + CHECK_EQ(bookshelfCount(bare, {0, 64, 0}), 0); + + World lightlyStocked; + surroundWithShelves(lightlyStocked, {0, 64, 0}, 6); + CHECK_EQ(bookshelfCount(lightlyStocked, {0, 64, 0}), 6); + + World library; + surroundWithShelves(library, {0, 64, 0}, 30); + CHECK_EQ(bookshelfCount(library, {0, 64, 0}), maxBookshelfPower); +} + +VF_TEST(shelvesWithSomethingSolidInBetweenDoNotCount) { + World world; + world.setBlock(2, 64, 0, Block::Bookshelf); + CHECK_EQ(bookshelfCount(world, {0, 64, 0}), 1); + + world.setBlock(1, 64, 0, Block::Stone); + CHECK_EQ(bookshelfCount(world, {0, 64, 0}), 0); + + // A torch in the gap is see-through, so the shelf counts again. + world.setBlock(1, 64, 0, Block::Torch); + CHECK_EQ(bookshelfCount(world, {0, 64, 0}), 1); +} + +VF_TEST(offersAreDeterministicForTheSameSeedAndMoveOnAfterOneIsTaken) { + EnchantTableState first; + first.seed = 20250904u; + first.bookshelves = 15; + first.target = Enchantment::Target::Sword; + first.enchantability = 10; + refreshOffers(first); + + EnchantTableState second; + second.seed = 20250904u; + second.bookshelves = 15; + second.target = Enchantment::Target::Sword; + second.enchantability = 10; + refreshOffers(second); + + for (int slot = 0; slot < 3; ++slot) { + CHECK(sameOffer(first.offers[static_cast(slot)], + second.offers[static_cast(slot)])); + } + + uint32_t seedBefore = first.seed; + EnchantOffer granted = takeOffer(first, 2, 40, 3); + CHECK(granted.valid()); + CHECK(first.seed != seedBefore); + CHECK_EQ(granted.lapisCost, 3); +} + +VF_TEST(topRowNeedsThirtyLevelsInAFullyShelvedLibrary) { + for (uint32_t seed = 1; seed <= 40; ++seed) { + EnchantTableState table; + table.seed = seed * 2654435761u + 1u; + table.bookshelves = 15; + table.target = Enchantment::Target::Sword; + table.enchantability = 14; + refreshOffers(table); + CHECK_EQ(table.offers[2].requiredLevels, 30); + CHECK_EQ(table.offers[2].lapisCost, 3); + CHECK(table.offers[0].requiredLevels <= table.offers[1].requiredLevels); + CHECK(table.offers[1].requiredLevels <= table.offers[2].requiredLevels); + } +} + +VF_TEST(anEmptyTableOffersFarLessThanAFullyShelvedOne) { + int bareTotal = 0; + int shelvedTotal = 0; + for (uint32_t seed = 1; seed <= 40; ++seed) { + EnchantTableState bare; + bare.seed = seed * 40503u + 7u; + bare.bookshelves = 0; + bare.target = Enchantment::Target::Pickaxe; + bare.enchantability = 10; + refreshOffers(bare); + + EnchantTableState shelved = bare; + shelved.bookshelves = 15; + refreshOffers(shelved); + + for (int slot = 0; slot < 3; ++slot) { + bareTotal += bare.offers[static_cast(slot)].requiredLevels; + shelvedTotal += shelved.offers[static_cast(slot)].requiredLevels; + } + } + CHECK(shelvedTotal > bareTotal * 2); +} + +VF_TEST(takingAnOfferFailsWithoutEnoughLevelsOrLapis) { + EnchantTableState table; + table.seed = 987654321u; + table.bookshelves = 15; + table.target = Enchantment::Target::Chestplate; + table.enchantability = 9; + refreshOffers(table); + + int cost = table.offers[2].requiredLevels; + CHECK(cost > 0); + CHECK(!takeOffer(table, 2, cost - 1, 3).valid()); + CHECK(!takeOffer(table, 2, cost, 2).valid()); + CHECK(!takeOffer(table, 7, 100, 64).valid()); + CHECK(takeOffer(table, 2, cost, 3).valid()); +} + +VF_TEST(rolledEnchantmentsRespectTargetsCapsAndConflicts) { + for (uint32_t seed = 1; seed <= 300; ++seed) { + uint32_t rng = seed * 2246822519u + 3u; + EnchantSet rolled = rollEnchantments(Enchantment::Target::Sword, 14, 30, rng); + for (std::size_t i = 0; i < rolled.entries.size(); ++i) { + const Enchantment& entry = rolled.entries[i]; + CHECK(entry.level >= 1); + CHECK(entry.level <= maxEnchantLevel(entry.kind)); + CHECK(enchantApplies(entry.kind, Enchantment::Target::Sword)); + for (std::size_t j = i + 1; j < rolled.entries.size(); ++j) { + CHECK(entry.kind != rolled.entries[j].kind); + CHECK(!enchantsConflict(entry.kind, rolled.entries[j].kind)); + } + } + } +} +