159 lines
5.8 KiB
C++
159 lines
5.8 KiB
C++
#include <algorithm>
|
|
|
|
#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<uint32_t>(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<LootEntry>& 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);
|
|
|