From 5fba3c338a09aad7cf61694571ef33a25c9ad1d9 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 21:13:22 +0200 Subject: [PATCH] test: cover makeShopWorld and makeLibrarian --- tests/testTrade.cpp | 159 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 tests/testTrade.cpp diff --git a/tests/testTrade.cpp b/tests/testTrade.cpp new file mode 100644 index 0000000..024fe9e --- /dev/null +++ b/tests/testTrade.cpp @@ -0,0 +1,159 @@ +#include "testHarness.h" +#include "vf/Trade.h" + +using namespace vf; + +namespace { + +constexpr int groundTop = 4; +constexpr int walkY = groundTop + 1; +const Vec3i villageCenter{0, walkY, 0}; + +// Index of the fixed "4 emeralds for one comparator" librarian offer: the one +// entry whose counts the seed jitter leaves alone, so prices are exact. +constexpr int comparatorOffer = 5; + +World makeShopWorld() { + World world; + for (int x = -12; x <= 12; ++x) + for (int z = -12; z <= 12; ++z) + for (int y = 0; y <= groundTop; ++y) + world.setBlock(x, y, z, Block::Stone); + world.setBlock(-4, walkY, 0, Block::Lectern); + world.setBlock(4, walkY, 0, Block::Bed); + return world; +} + +Villager makeLibrarian(int level) { + Villager villager = makeVillager(1, {-1.5f, static_cast(walkY), 0.5f}, 17u); + villager.profession = VillagerProfession::Librarian; + villager.level = level; + villager.experience = villagerExperienceForLevel(level); + return villager; +} + +Container makeWallet(int emeralds) { + Container inventory(27); + inventory.insert(ItemId::EmeraldGem, emeralds); + return inventory; +} + +int stockOf(const Container& inventory, ItemId item) { + int total = 0; + for (int i = 0; i < inventory.size(); ++i) { + const ItemSlot& slot = inventory.slot(i); + if (!slot.empty() && slot.item == item) total += slot.count; + } + return total; +} + +int firstOfferWithTier(const TradeOfferList& list, TradeTier tier) { + for (int i = 0; i < list.size(); ++i) + if (list.at(i).tier == tier) return i; + return -1; +} + +} // namespace + +VF_TEST(offerTableIsIdenticalForTheSameProfessionAndSeedAndMovesWithTheSeed) { + TradeOfferList a = buildTradeOffers(VillagerProfession::Librarian, 99u); + TradeOfferList b = buildTradeOffers(VillagerProfession::Librarian, 99u); + CHECK_EQ(a.size(), b.size()); + bool identical = true; + for (int i = 0; i < a.size(); ++i) { + if (a.at(i).inputACount != b.at(i).inputACount) identical = false; + if (a.at(i).outputCount != b.at(i).outputCount) identical = false; + if (a.at(i).output != b.at(i).output) identical = false; + } + CHECK(identical); + + TradeOfferList other = buildTradeOffers(VillagerProfession::Librarian, 4242u); + bool differs = false; + for (int i = 0; i < a.size(); ++i) + if (a.at(i).inputACount != other.at(i).inputACount || + a.at(i).outputCount != other.at(i).outputCount) differs = true; + CHECK(differs); + + for (int i = 0; i < a.size(); ++i) { + CHECK(a.at(i).inputACount >= 1); + CHECK(a.at(i).outputCount >= 1); + CHECK(a.at(i).output != ItemId::None); + CHECK(a.at(i).inputA != ItemId::None); + } +} + +VF_TEST(everyTradingProfessionCoversAllFiveTiersWhileNitwitsOfferNothing) { + for (int raw = 2; raw < static_cast(VillagerProfession::Count); ++raw) { + VillagerProfession profession = static_cast(raw); + TradeOfferList list = buildTradeOffers(profession, 7u); + CHECK_EQ(list.size(), 8); + CHECK(list.profession == profession); + for (int tier = 0; tier < static_cast(TradeTier::Count); ++tier) + CHECK(list.countForTier(static_cast(tier)) >= 1); + CHECK_EQ(list.unlockedCount(1), list.countForTier(TradeTier::Novice)); + CHECK_EQ(list.unlockedCount(villagerMaxLevel), list.size()); + } + + CHECK_EQ(buildTradeOffers(VillagerProfession::Nitwit, 7u).size(), 0); + CHECK_EQ(buildTradeOffers(VillagerProfession::Unemployed, 7u).size(), 0); +} + +VF_TEST(lockedTiersRefuseTheTradeUntilTheVillagerHasLevelledUp) { + Villager villager = makeLibrarian(1); + TradeOfferList offers = buildTradeOffers(VillagerProfession::Librarian, 1u); + Container wallet = makeWallet(200); + TradeSession session(villager, offers, wallet); + + int masterOffer = firstOfferWithTier(offers, TradeTier::Master); + CHECK(masterOffer >= 0); + CHECK(!session.available(masterOffer)); + TradeResult locked = session.trade(masterOffer); + CHECK(!locked.success); + CHECK(locked.reason == TradeResult::Reason::LevelLocked); + CHECK_EQ(stockOf(wallet, ItemId::EmeraldGem), 200); + + villager.level = villagerMaxLevel; + CHECK(session.available(masterOffer)); + TradeResult unlocked = session.trade(masterOffer); + CHECK(unlocked.success); + CHECK(unlocked.received == ItemId::DiamondGem); + CHECK_EQ(stockOf(wallet, ItemId::DiamondGem), unlocked.receivedCount); +} + +VF_TEST(repeatedTradesRaiseThePriceThroughRisingDemand) { + Villager villager = makeLibrarian(3); + TradeOfferList offers = buildTradeOffers(VillagerProfession::Librarian, 1u); + Container wallet = makeWallet(400); + TradeSession session(villager, offers, wallet); + + CHECK(offers.at(comparatorOffer).tier == TradeTier::Journeyman); + CHECK(offers.at(comparatorOffer).output == ItemId::Comparator); + int basePrice = offers.at(comparatorOffer).basePrice(); + CHECK_EQ(session.priceOf(comparatorOffer), basePrice); + + int firstPrice = session.priceOf(comparatorOffer); + int trades = 0; + int lastPrice = firstPrice; + while (session.available(comparatorOffer)) { + int price = session.priceOf(comparatorOffer); + CHECK(price >= lastPrice); + lastPrice = price; + TradeResult outcome = session.trade(comparatorOffer); + CHECK(outcome.success); + CHECK_EQ(outcome.pricePaid, price); + ++trades; + } + + CHECK_EQ(trades, offers.at(comparatorOffer).maxUses); + CHECK_EQ(offers.at(comparatorOffer).demand, trades * tradeDemandPerUse); + CHECK(lastPrice > firstPrice); + CHECK_EQ(stockOf(wallet, ItemId::Comparator), trades); + CHECK(session.trade(comparatorOffer).reason == TradeResult::Reason::OfferExhausted); +} + +VF_TEST(restockRefillsUsesAndLetsDemandAndPriceFallBack) { + Villager villager = makeLibrarian(3); + TradeOfferList offers = buildTradeOffers(VillagerProfession::Librarian, 1u); + Container wallet = makeWallet(400); + TradeSession session(villager, offers, wallet); +