From ea3d54436c3838308bc90374c27887206e8b3d0a Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 21:52:54 +0200 Subject: [PATCH] test: cover seedFor and contextWith --- tests/testLoot.cpp | 158 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/testLoot.cpp diff --git a/tests/testLoot.cpp b/tests/testLoot.cpp new file mode 100644 index 0000000..89820f5 --- /dev/null +++ b/tests/testLoot.cpp @@ -0,0 +1,158 @@ +#include + +#include "testHarness.h" +#include "vf/Loot.h" + +using namespace vf; + +namespace { + +// Consecutive small seeds share too much structure for a raw xorshift stream, +// so the sampling tests walk a scrambled sequence instead. +uint32_t seedFor(int index) { + uint32_t mixed = static_cast(index) + 1u; + mixed ^= mixed >> 16; + mixed *= 0x7feb352du; + mixed ^= mixed >> 15; + mixed *= 0x846ca68bu; + mixed ^= mixed >> 16; + return mixed == 0u ? 1u : mixed; +} + +LootContext contextWith(ItemId tool, uint32_t seed) { + LootContext context; + context.tool = tool; + context.seed = seed; + return context; +} + +LootContext enchanted(ItemId tool, EnchantKind kind, int level, uint32_t seed) { + LootContext context = contextWith(tool, seed); + addEnchant(context.toolEnchants, kind, level); + return context; +} + +LootContext killContext(bool byPlayer, uint32_t seed) { + LootContext context = contextWith(ItemId::IronSword, seed); + context.playerKill = byPlayer; + return context; +} + +bool sameRoll(const LootRoll& left, const LootRoll& right) { + if (left.items.size() != right.items.size()) return false; + for (std::size_t i = 0; i < left.items.size(); ++i) { + if (left.items[i].item != right.items[i].item) return false; + if (left.items[i].count != right.items[i].count) return false; + if (left.items[i].durability != right.items[i].durability) return false; + } + return true; +} + +LootEntry entryOf(ItemId item, uint32_t conditions) { + LootEntry entry; + entry.item = item; + entry.conditions = conditions; + return entry; +} + +LootTable singlePool(const std::vector& entries, int minRolls = 1, int maxRolls = 1) { + LootPool pool; + pool.entries = entries; + pool.minRolls = minRolls; + pool.maxRolls = maxRolls; + LootTable table; + table.pools.push_back(pool); + return table; +} + +int maxOverSeeds(Block block, const EnchantSet& enchants, ItemId tool, ItemId wanted, int samples) { + int best = 0; + for (int i = 0; i < samples; ++i) { + LootContext context = contextWith(tool, seedFor(i)); + context.toolEnchants = enchants; + best = std::max(best, rollBlockDrops(block, context).countOf(wanted)); + } + return best; +} + +} // namespace + +VF_TEST(theSameSeedAlwaysReproducesTheSameRollAcrossEveryTableFamily) { + LootContext blockContext = enchanted(ItemId::DiamondPickaxe, EnchantKind::Fortune, 3, 0xabcdef01u); + CHECK(sameRoll(rollBlockDrops(Block::LapisOre, blockContext), + rollBlockDrops(Block::LapisOre, blockContext))); + + LootContext mobContext = killContext(true, 0x1234567u); + addEnchant(mobContext.toolEnchants, EnchantKind::Looting, 2); + CHECK(sameRoll(rollMobDrops(LootTable::MobKind::Zombie, mobContext), + rollMobDrops(LootTable::MobKind::Zombie, mobContext))); + + LootContext chestContext = contextWith(ItemId::None, 0x55aa33ccu); + LootRoll first = rollChestContents(LootTable::ChestKind::Dungeon, chestContext); + LootRoll second = rollChestContents(LootTable::ChestKind::Dungeon, chestContext); + CHECK(sameRoll(first, second)); + CHECK(first.totalCount() > 0); +} + +VF_TEST(differentSeedsPullApartTheContentsOfTheSameChest) { + LootRoll reference = rollChestContents(LootTable::ChestKind::Dungeon, + contextWith(ItemId::None, seedFor(0))); + int different = 0; + for (int i = 1; i < 64; ++i) { + LootRoll roll = rollChestContents(LootTable::ChestKind::Dungeon, + contextWith(ItemId::None, seedFor(i))); + if (!sameRoll(reference, roll)) ++different; + } + CHECK(different >= 55); +} + +VF_TEST(stoneDropsCobblestoneUnlessTheSilkTouchBranchTakesOver) { + LootContext plain = contextWith(ItemId::StonePickaxe, 7u); + LootRoll broken = rollBlockDrops(Block::Stone, plain); + CHECK_EQ(broken.countOf(ItemId::Cobblestone), 1); + CHECK_EQ(broken.countOf(ItemId::Stone), 0); + + LootContext silk = enchanted(ItemId::StonePickaxe, EnchantKind::SilkTouch, 1, 7u); + LootRoll harvested = rollBlockDrops(Block::Stone, silk); + CHECK_EQ(harvested.countOf(ItemId::Stone), 1); + CHECK_EQ(harvested.countOf(ItemId::Cobblestone), 0); +} + +VF_TEST(oresDropTheirRawFormAndSilkTouchDropsTheOreBlockItself) { + const struct { Block block; ItemId raw; ItemId oreItem; } ores[] = { + {Block::CoalOre, ItemId::CoalLump, ItemId::CoalOre}, + {Block::IronOre, ItemId::IronOre, ItemId::IronOre}, + {Block::DiamondOre, ItemId::DiamondGem, ItemId::DiamondOre}, + {Block::EmeraldOre, ItemId::EmeraldGem, ItemId::EmeraldOre}, + }; + for (const auto& ore : ores) { + LootContext plain = contextWith(ItemId::DiamondPickaxe, 31u); + CHECK_EQ(rollBlockDrops(ore.block, plain).countOf(ore.raw), 1); + + LootContext silk = enchanted(ItemId::DiamondPickaxe, EnchantKind::SilkTouch, 1, 31u); + LootRoll harvested = rollBlockDrops(ore.block, silk); + CHECK_EQ(harvested.countOf(ore.oreItem), 1); + CHECK_EQ(harvested.totalCount(), 1); + } +} + +VF_TEST(fortuneMultipliesOreDropsAndNeverDropsBelowTheBaseYield) { + EnchantSet bare; + EnchantSet fortune; + addEnchant(fortune, EnchantKind::Fortune, 3); + + int worstPlain = 99; + int worstFortune = 99; + for (int i = 0; i < 400; ++i) { + LootContext plain = contextWith(ItemId::DiamondPickaxe, seedFor(i)); + worstPlain = std::min(worstPlain, rollBlockDrops(Block::DiamondOre, plain) + .countOf(ItemId::DiamondGem)); + + LootContext lucky = contextWith(ItemId::DiamondPickaxe, seedFor(i)); + lucky.toolEnchants = fortune; + worstFortune = std::min(worstFortune, rollBlockDrops(Block::DiamondOre, lucky) + .countOf(ItemId::DiamondGem)); + } + CHECK_EQ(worstPlain, 1); + CHECK_EQ(worstFortune, 1); +