#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); CHECK_EQ(maxOverSeeds(Block::DiamondOre, bare, ItemId::DiamondPickaxe, ItemId::DiamondGem, 400), 1); CHECK_EQ(maxOverSeeds(Block::DiamondOre, fortune, ItemId::DiamondPickaxe, ItemId::DiamondGem, 400), 4); // Silk Touch wins over Fortune: the ore block itself, never a multiplied gem. LootContext both = enchanted(ItemId::DiamondPickaxe, EnchantKind::SilkTouch, 1, 99u); addEnchant(both.toolEnchants, EnchantKind::Fortune, 3); LootRoll roll = rollBlockDrops(Block::DiamondOre, both); CHECK_EQ(roll.countOf(ItemId::DiamondOre), 1); CHECK_EQ(roll.countOf(ItemId::DiamondGem), 0); } VF_TEST(grassYieldsDirtAndBedrockYieldsNothingAtAll) { LootContext shovel = contextWith(ItemId::IronShovel, 5u); LootRoll grass = rollBlockDrops(Block::Grass, shovel); CHECK_EQ(grass.countOf(ItemId::Dirt), 1); CHECK_EQ(grass.countOf(ItemId::Grass), 0); LootContext pickaxe = contextWith(ItemId::DiamondPickaxe, 5u); CHECK(rollBlockDrops(Block::Bedrock, pickaxe).empty()); CHECK(blockLootTable(Block::Bedrock).empty()); LootContext silk = enchanted(ItemId::DiamondPickaxe, EnchantKind::SilkTouch, 1, 5u); CHECK(rollBlockDrops(Block::Bedrock, silk).empty()); } VF_TEST(leavesOnlyDropTheirSubstituteCuttingWhenSilkTouchIsAbsent) { LootContext silk = enchanted(ItemId::IronHoe, EnchantKind::SilkTouch, 1, 3u); LootRoll harvested = rollBlockDrops(Block::Leaves, silk); CHECK_EQ(harvested.countOf(ItemId::Leaves), 1); CHECK_EQ(harvested.totalCount(), 1); int cuttings = 0; int cuttingsWithFortune = 0; int leafBlocks = 0; EnchantSet fortune; addEnchant(fortune, EnchantKind::Fortune, 3); for (int i = 0; i < 1500; ++i) { LootContext plain = contextWith(ItemId::IronHoe, seedFor(i)); LootRoll roll = rollBlockDrops(Block::Leaves, plain); cuttings += roll.countOf(ItemId::Wood); leafBlocks += roll.countOf(ItemId::Leaves); LootContext lucky = contextWith(ItemId::IronHoe, seedFor(i)); lucky.toolEnchants = fortune; cuttingsWithFortune += rollBlockDrops(Block::Leaves, lucky).countOf(ItemId::Wood); } CHECK_EQ(leafBlocks, 0); CHECK(cuttings > 0); CHECK(cuttingsWithFortune > cuttings * 2); } VF_TEST(toolTierGatesTheOresTheWayTheHarvestLevelsSay) { CHECK(!toolQualifiesFor(Block::Stone, ItemId::None)); CHECK(!toolQualifiesFor(Block::Stone, ItemId::IronAxe)); CHECK(toolQualifiesFor(Block::Stone, ItemId::WoodPickaxe)); CHECK(!toolQualifiesFor(Block::IronOre, ItemId::WoodPickaxe)); CHECK(!toolQualifiesFor(Block::IronOre, ItemId::GoldPickaxe)); CHECK(toolQualifiesFor(Block::IronOre, ItemId::StonePickaxe)); CHECK(!toolQualifiesFor(Block::DiamondOre, ItemId::StonePickaxe)); CHECK(!toolQualifiesFor(Block::DiamondOre, ItemId::GoldPickaxe)); CHECK(toolQualifiesFor(Block::DiamondOre, ItemId::IronPickaxe)); CHECK(toolQualifiesFor(Block::EmeraldOre, ItemId::DiamondPickaxe)); // Nothing outside the pickaxe class is gated at all. CHECK(toolQualifiesFor(Block::Dirt, ItemId::None)); CHECK(toolQualifiesFor(Block::Leaves, ItemId::None)); CHECK(toolQualifiesFor(Block::Grass, ItemId::WoodShovel)); } VF_TEST(aBlockBrokenWithTooWeakAToolProducesNoDropsAtAll) { LootContext weak = contextWith(ItemId::StonePickaxe, 11u); CHECK(rollBlockDrops(Block::DiamondOre, weak).empty()); LootContext bare = contextWith(ItemId::None, 11u); CHECK(rollBlockDrops(Block::Stone, bare).empty()); // The gate runs before the table, so even Silk Touch cannot rescue it. LootContext silkOnAStonePick = enchanted(ItemId::StonePickaxe, EnchantKind::SilkTouch, 1, 11u); CHECK(rollBlockDrops(Block::DiamondOre, silkOnAStonePick).empty()); } VF_TEST(zombiesDropFleshAlwaysAndIronOnlyWhenAPlayerLandedTheKill) { int ingotsFromPlayers = 0; int ingotsFromTheWorld = 0; int fleshTotal = 0; for (int i = 0; i < 3000; ++i) { LootRoll byPlayer = rollMobDrops(LootTable::MobKind::Zombie, killContext(true, seedFor(i))); LootRoll byWorld = rollMobDrops(LootTable::MobKind::Zombie, killContext(false, seedFor(i))); ingotsFromPlayers += byPlayer.countOf(ItemId::IronIngot); ingotsFromTheWorld += byWorld.countOf(ItemId::IronIngot); fleshTotal += byPlayer.countOf(ItemId::RottenFlesh); } CHECK_EQ(ingotsFromTheWorld, 0); CHECK(ingotsFromPlayers > 0); CHECK(fleshTotal > 2000); CHECK(fleshTotal < 4500); } VF_TEST(lootingRaisesBothTheDropCeilingAndTheRareDropRate) { int plainCeiling = 0; int lootedCeiling = 0; int plainIngots = 0; int lootedIngots = 0; for (int i = 0; i < 3000; ++i) { LootContext plain = killContext(true, seedFor(i)); LootContext looted = killContext(true, seedFor(i)); addEnchant(looted.toolEnchants, EnchantKind::Looting, 3); LootRoll plainRoll = rollMobDrops(LootTable::MobKind::Zombie, plain); LootRoll lootedRoll = rollMobDrops(LootTable::MobKind::Zombie, looted); plainCeiling = std::max(plainCeiling, plainRoll.countOf(ItemId::RottenFlesh)); lootedCeiling = std::max(lootedCeiling, lootedRoll.countOf(ItemId::RottenFlesh)); plainIngots += plainRoll.countOf(ItemId::IronIngot); lootedIngots += lootedRoll.countOf(ItemId::IronIngot); } CHECK_EQ(plainCeiling, 2); CHECK_EQ(lootedCeiling, 5); CHECK(lootedIngots > plainIngots); } VF_TEST(livestockDropRawMeatThatComesOutCookedWhenTheKillWasFiery) { const struct { LootTable::MobKind kind; ItemId raw; ItemId cooked; int ceiling; } herd[] = { {LootTable::MobKind::Sheep, ItemId::RawMutton, ItemId::CookedMutton, 2}, {LootTable::MobKind::Cow, ItemId::RawBeef, ItemId::CookedBeef, 3}, {LootTable::MobKind::Pig, ItemId::RawPorkchop, ItemId::CookedPorkchop, 3}, }; for (const auto& animal : herd) { int rawSeen = 0; int cookedSeen = 0; int highest = 0; for (int i = 0; i < 300; ++i) { LootContext plain = killContext(true, seedFor(i)); LootRoll rawRoll = rollMobDrops(animal.kind, plain); rawSeen += rawRoll.countOf(animal.raw); highest = std::max(highest, rawRoll.countOf(animal.raw)); CHECK_EQ(rawRoll.countOf(animal.cooked), 0); LootContext burning = killContext(true, seedFor(i)); burning.fiery = true; LootRoll cookedRoll = rollMobDrops(animal.kind, burning); cookedSeen += cookedRoll.countOf(animal.cooked); CHECK_EQ(cookedRoll.countOf(animal.raw), 0); } CHECK_EQ(rawSeen, cookedSeen); CHECK_EQ(highest, animal.ceiling); } } VF_TEST(lootContextForCarriesTheKillOverAndRefusesADeadSeed) { EnchantSet enchants; addEnchant(enchants, EnchantKind::Looting, 2);