#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);