From b4aae0528af48f9ebedec40f9c97a4ceddc06893 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 23:39:08 +0200 Subject: [PATCH] test: cover testLootTables --- tests/testLootTables.cpp | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/tests/testLootTables.cpp b/tests/testLootTables.cpp index 8caf423..e37cf31 100644 --- a/tests/testLootTables.cpp +++ b/tests/testLootTables.cpp @@ -160,3 +160,160 @@ VF_TEST(theRegistryIsNeverMorePermissiveThanTheSalvagedToolCheck) { CHECK(gatedCases > 100); } +VF_TEST(harvestLevelsGateTheOresExactlyWhereTheTiersSay) { + CHECK_EQ(toolHarvestLevel(ToolTier::None), 0); + CHECK_EQ(toolHarvestLevel(ToolTier::Wood), 1); + CHECK_EQ(toolHarvestLevel(ToolTier::Gold), 1); + CHECK_EQ(toolHarvestLevel(ToolTier::Stone), 2); + CHECK_EQ(toolHarvestLevel(ToolTier::Iron), 3); + CHECK_EQ(toolHarvestLevel(ToolTier::Diamond), 4); + + const LootRegistry& registry = lootRegistry(); + const struct { Block block; ItemId tool; bool allowed; } cases[] = { + {Block::Stone, ItemId::None, false}, + {Block::Stone, ItemId::DiamondShovel, false}, + {Block::Stone, ItemId::WoodPickaxe, true}, + {Block::IronOre, ItemId::WoodPickaxe, false}, + {Block::IronOre, ItemId::GoldPickaxe, false}, + {Block::IronOre, ItemId::StonePickaxe, true}, + {Block::DiamondOre, ItemId::StonePickaxe, false}, + {Block::DiamondOre, ItemId::IronPickaxe, true}, + {Block::Obsidian, ItemId::IronPickaxe, false}, + {Block::Obsidian, ItemId::DiamondPickaxe, true}, + {Block::CryingObsidian, ItemId::IronPickaxe, false}, + {Block::CopperOre, ItemId::WoodPickaxe, false}, + {Block::CopperOre, ItemId::StonePickaxe, true}, + {Block::Dirt, ItemId::None, true}, + {Block::Wood, ItemId::None, true}, + }; + for (const auto& item : cases) { + CHECK_EQ(toolAllows(registry.blockRule(item.block), item.tool), item.allowed); + } + + CHECK(registry.rollBlock(Block::Obsidian, contextWith(ItemId::IronPickaxe, 9u)).empty()); + CHECK_EQ(registry.rollBlock(Block::Obsidian, contextWith(ItemId::DiamondPickaxe, 9u)) + .countOf(ItemId::Obsidian), + 1); +} + +VF_TEST(stoneBecomesCobblestoneUnlessItIsTakenWholeOrBrokenInsideFire) { + const LootRegistry& registry = lootRegistry(); + + LootContext plain = contextWith(ItemId::IronPickaxe, 17u); + CHECK_EQ(registry.rollBlock(Block::Stone, plain).countOf(ItemId::Cobblestone), 1); + + LootContext silk = enchantedWith(ItemId::IronPickaxe, EnchantKind::SilkTouch, 1, 17u); + LootRoll harvested = registry.rollBlock(Block::Stone, silk); + CHECK_EQ(harvested.countOf(ItemId::Stone), 1); + CHECK_EQ(harvested.totalCount(), 1); + + LootContext burning = contextWith(ItemId::IronPickaxe, 17u); + burning.fiery = true; + LootRoll melted = registry.rollBlock(Block::Stone, burning); + CHECK_EQ(melted.countOf(ItemId::Stone), 1); + CHECK_EQ(melted.countOf(ItemId::Cobblestone), 0); + + // Cobblestone bakes back into stone the same way, and iron and gold ore + // come out as ingots. + LootRoll cobble = registry.rollBlock(Block::Cobblestone, burning); + CHECK_EQ(cobble.countOf(ItemId::Stone), 1); + CHECK_EQ(registry.rollBlock(Block::IronOre, burning).countOf(ItemId::IronIngot), 1); + CHECK_EQ(registry.rollBlock(Block::GoldOre, burning).countOf(ItemId::GoldIngot), 1); + CHECK_EQ(registry.rollBlock(Block::IronOre, plain).countOf(ItemId::IronOre), 1); +} + +VF_TEST(oreRulesMultiplyWithFortuneAndHandBackTheOreBlockUnderSilkTouch) { + const LootRegistry& registry = lootRegistry(); + const struct { Block block; ItemId raw; int lowest; int highest; } ores[] = { + {Block::CoalOre, ItemId::CoalLump, 1, 1}, + {Block::DiamondOre, ItemId::DiamondGem, 1, 1}, + {Block::RedstoneOre, ItemId::RedstoneDust, 4, 5}, + {Block::LapisOre, ItemId::LapisLazuli, 4, 9}, + {Block::Glowstone, ItemId::Glowstone, 2, 4}, + }; + for (const auto& ore : ores) { + int seenLowest = 1000; + int seenHighest = 0; + int fortuneHighest = 0; + for (int i = 0; i < 500; ++i) { + LootContext plain = contextWith(ItemId::DiamondPickaxe, seedFor(i)); + int count = registry.rollBlock(ore.block, plain).countOf(ore.raw); + seenLowest = std::min(seenLowest, count); + seenHighest = std::max(seenHighest, count); + + LootContext lucky = enchantedWith(ItemId::DiamondPickaxe, EnchantKind::Fortune, 3, + seedFor(i)); + fortuneHighest = std::max(fortuneHighest, + registry.rollBlock(ore.block, lucky).countOf(ore.raw)); + } + CHECK_EQ(seenLowest, ore.lowest); + CHECK_EQ(seenHighest, ore.highest); + CHECK_EQ(fortuneHighest, ore.highest * 4); + + LootContext silk = enchantedWith(ItemId::DiamondPickaxe, EnchantKind::SilkTouch, 1, 5u); + LootRoll whole = registry.rollBlock(ore.block, silk); + CHECK_EQ(whole.countOf(itemForBlock(ore.block)), 1); + CHECK_EQ(whole.totalCount(), 1); + } +} + +VF_TEST(shearClassToolsTakeTheBlockItselfWithoutNeedingSilkTouch) { + const LootRegistry& registry = lootRegistry(); + + for (ItemId hoe : {ItemId::WoodHoe, ItemId::DiamondHoe}) { + LootRoll sheared = registry.rollBlock(Block::Leaves, contextWith(hoe, 42u)); + CHECK_EQ(sheared.countOf(ItemId::Leaves), 1); + CHECK_EQ(sheared.totalCount(), 1); + } + + // A pickaxe falls through to the chance table instead. + int leafBlocks = 0; + int cuttings = 0; + for (int i = 0; i < 800; ++i) { + LootRoll roll = registry.rollBlock(Block::Leaves, + contextWith(ItemId::IronPickaxe, seedFor(i))); + leafBlocks += roll.countOf(ItemId::Leaves); + cuttings += roll.countOf(ItemId::Wood); + } + CHECK_EQ(leafBlocks, 0); + CHECK(cuttings > 0); + + // Shearing a block the palette has no item for still yields nothing. + for (Block block : {Block::Wool, Block::Cobweb, Block::Vine, Block::Kelp, Block::Moss}) { + CHECK(registry.blockRule(block).shearHarvest); + CHECK(registry.rollBlock(block, contextWith(ItemId::DiamondHoe, 8u)).empty()); + } +} + +VF_TEST(everyBlockRuleOnlyEverProducesItemsItDeclaredUpFront) { + const LootRegistry& registry = lootRegistry(); + int strayItems = 0; + int rollsMade = 0; + + for (int index = 0; index < static_cast(Block::Count); ++index) { + Block block = static_cast(index); + const BlockDropRule& rule = registry.blockRule(block); + + for (int sample = 0; sample < 12; ++sample) { + LootContext context = contextWith(bestToolFor(rule.harvestTool), seedFor(sample)); + if (sample % 3 == 1) addEnchant(context.toolEnchants, EnchantKind::SilkTouch, 1); + if (sample % 3 == 2) addEnchant(context.toolEnchants, EnchantKind::Fortune, 3); + if (sample % 4 == 3) context.fiery = true; + + LootRoll roll = registry.rollBlock(block, context); + ++rollsMade; + for (const ItemSlot& stack : roll.items) { + if (!ruleCanYield(rule, stack.item)) ++strayItems; + if (stack.count <= 0) ++strayItems; + } + } + } + CHECK_EQ(strayItems, 0); + CHECK_EQ(rollsMade, static_cast(Block::Count) * 12); +} + +VF_TEST(everyBlockThatDeclaresACertainDropActuallyHandsItOver) { + const LootRegistry& registry = lootRegistry(); + int certainDrops = 0; + int missed = 0; +