From 2d5ad51975afc569d2d528dcdb845f325784055d Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 23:44:40 +0200 Subject: [PATCH] test: extend coverage of testLootTables --- tests/testLootTables.cpp | 161 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/tests/testLootTables.cpp b/tests/testLootTables.cpp index e37cf31..2005e7a 100644 --- a/tests/testLootTables.cpp +++ b/tests/testLootTables.cpp @@ -317,3 +317,164 @@ VF_TEST(everyBlockThatDeclaresACertainDropActuallyHandsItOver) { int certainDrops = 0; int missed = 0; + for (int index = 0; index < static_cast(Block::Count); ++index) { + Block block = static_cast(index); + const BlockDropRule& rule = registry.blockRule(block); + if (rule.drop == ItemId::None || rule.dropChance < 1.0f) continue; + ++certainDrops; + + LootContext context = contextWith(bestToolFor(rule.harvestTool), seedFor(index)); + LootRoll roll = registry.rollBlock(block, context); + if (roll.countOf(rule.drop) < rule.minCount) ++missed; + } + CHECK_EQ(missed, 0); + CHECK_EQ(certainDrops, 62); +} + +VF_TEST(blocksTheItemPaletteCannotExpressStillCarryARuleThatYieldsNothing) { + const LootRegistry& registry = lootRegistry(); + int barren = 0; + for (int index = 0; index < static_cast(Block::Count); ++index) { + Block block = static_cast(index); + const BlockDropRule& rule = registry.blockRule(block); + if (!rule.yieldsNothing()) continue; + ++barren; + CHECK_EQ(rule.block, block); + CHECK(blockRuleTable(rule).empty()); + + LootContext generous = enchantedWith(bestToolFor(rule.harvestTool), EnchantKind::Fortune, 3, + seedFor(index)); + addEnchant(generous.toolEnchants, EnchantKind::SilkTouch, 1); + CHECK(registry.rollBlock(block, generous).empty()); + } + CHECK_EQ(barren, static_cast(Block::Count) - 64); +} + +VF_TEST(theSameSeedReproducesTheSameDropForEveryBlockInThePalette) { + const LootRegistry& registry = lootRegistry(); + int divergences = 0; + for (int index = 0; index < static_cast(Block::Count); ++index) { + Block block = static_cast(index); + LootContext context = enchantedWith(ItemId::DiamondPickaxe, EnchantKind::Fortune, 2, + seedFor(index)); + LootRoll first = registry.rollBlock(block, context); + LootRoll second = registry.rollBlock(block, context); + if (!sameRoll(first, second)) ++divergences; + } + CHECK_EQ(divergences, 0); +} + +VF_TEST(everyMobRuleOnlyEverProducesItemsItDeclaredUpFront) { + const LootRegistry& registry = lootRegistry(); + int strayItems = 0; + for (int index = 0; index < static_cast(Species::Count); ++index) { + Species species = static_cast(index); + const MobDropRule& rule = registry.mobRule(species); + CHECK_EQ(rule.species, species); + CHECK(rule.maxCount >= rule.minCount); + + for (int sample = 0; sample < 40; ++sample) { + LootContext context = contextWith(ItemId::IronSword, seedFor(sample)); + context.playerKill = sample % 2 == 0; + context.fiery = sample % 3 == 0; + if (sample % 5 == 0) addEnchant(context.toolEnchants, EnchantKind::Looting, 3); + + for (const ItemSlot& stack : registry.rollMob(species, context).items) { + bool declared = stack.item == rule.drop || stack.item == rule.firedDrop || + stack.item == rule.rareDrop; + if (!declared) ++strayItems; + } + } + } + CHECK_EQ(strayItems, 0); + CHECK(registry.rollMob(Species::Count, contextWith(ItemId::None, 1u)).empty()); +} + +VF_TEST(theRegistryAgreesWithTheSalvagedMobTablesOnEverySharedSpecies) { + const LootRegistry& registry = lootRegistry(); + const struct { Species species; LootTable::MobKind kind; } shared[] = { + {Species::Zombie, LootTable::MobKind::Zombie}, + {Species::Skeleton, LootTable::MobKind::Skeleton}, + {Species::Creeper, LootTable::MobKind::Creeper}, + {Species::Sheep, LootTable::MobKind::Sheep}, + {Species::Cow, LootTable::MobKind::Cow}, + {Species::Pig, LootTable::MobKind::Pig}, + }; + for (const auto& pair : shared) { + const MobDropRule& rule = registry.mobRule(pair.species); + const LootTable& salvaged = mobLootTable(pair.kind); + CHECK_EQ(rule.drop, firstEntryOfPool(salvaged, 0)); + if (rule.rareDrop != ItemId::None) { + CHECK_EQ(rule.rareDrop, firstEntryOfPool(salvaged, 1)); + } + } +} + +VF_TEST(livestockComesOutCookedWhenTheKillWasFieryAndRawOtherwise) { + const LootRegistry& registry = lootRegistry(); + const struct { Species species; ItemId raw; ItemId cooked; } herd[] = { + {Species::Sheep, ItemId::RawMutton, ItemId::CookedMutton}, + {Species::Cow, ItemId::RawBeef, ItemId::CookedBeef}, + {Species::Pig, ItemId::RawPorkchop, ItemId::CookedPorkchop}, + }; + for (const auto& animal : herd) { + CHECK(!registry.mobRule(animal.species).hostile); + int raw = 0; + int cooked = 0; + for (int i = 0; i < 300; ++i) { + LootContext cold = contextWith(ItemId::IronSword, seedFor(i)); + LootRoll rawRoll = registry.rollMob(animal.species, cold); + raw += rawRoll.countOf(animal.raw); + CHECK_EQ(rawRoll.countOf(animal.cooked), 0); + + LootContext burning = cold; + burning.fiery = true; + LootRoll cookedRoll = registry.rollMob(animal.species, burning); + cooked += cookedRoll.countOf(animal.cooked); + CHECK_EQ(cookedRoll.countOf(animal.raw), 0); + } + CHECK(raw > 0); + CHECK_EQ(raw, cooked); + } +} + +VF_TEST(dropsThatNeedAPlayerKillNeverShowUpFromAnEnvironmentalDeath) { + const LootRegistry& registry = lootRegistry(); + CHECK(registry.mobRule(Species::Blaze).dropNeedsPlayerKill); + + int fromPlayers = 0; + int fromTheWorld = 0; + int witchGlowstoneFromTheWorld = 0; + for (int i = 0; i < 2000; ++i) { + LootContext byPlayer = contextWith(ItemId::IronSword, seedFor(i)); + byPlayer.playerKill = true; + LootContext byWorld = contextWith(ItemId::None, seedFor(i)); + + fromPlayers += registry.rollMob(Species::Blaze, byPlayer).countOf(ItemId::CoalLump); + fromTheWorld += registry.rollMob(Species::Blaze, byWorld).countOf(ItemId::CoalLump); + witchGlowstoneFromTheWorld += + registry.rollMob(Species::Witch, byWorld).countOf(ItemId::Glowstone); + } + CHECK(fromPlayers > 0); + CHECK_EQ(fromTheWorld, 0); + + // A witch is the counter example: her glowstone is not player gated. + CHECK(!registry.mobRule(Species::Witch).rareNeedsPlayerKill); + CHECK(witchGlowstoneFromTheWorld > 0); +} + +VF_TEST(ironGolemsIgnoreLootingWhileZombiesDoNot) { + const LootRegistry& registry = lootRegistry(); + CHECK(!registry.mobRule(Species::IronGolem).lootingRaisesCount); + + int golemLowest = 100; + int golemHighest = 0; + int golemLootedHighest = 0; + int zombieHighest = 0; + int zombieLootedHighest = 0; + for (int i = 0; i < 1500; ++i) { + LootContext plain = contextWith(ItemId::IronSword, seedFor(i)); + plain.playerKill = true; + LootContext looted = plain; + addEnchant(looted.toolEnchants, EnchantKind::Looting, 3); +