test: cover Lcg and worldFingerprint

This commit is contained in:
dualfroz committed 2026-09-11 17:14:08 +02:00
1 parent 8af4cf3b24
commit 8454f86641
1 file changed
+161
+161
View File
@@ -0,0 +1,161 @@
#include "testHarness.h"
#include "vf/Codec.h"
#include "vf/EditHistory.h"
#include <algorithm>
#include <string>
#include <vector>
using namespace vf;
namespace {
struct Lcg {
uint64_t state;
explicit Lcg(uint64_t seed) : state(seed) {}
uint32_t next() {
state = state * 6364136223846793005ull + 1442695040888963407ull;
return static_cast<uint32_t>(state >> 32);
}
int below(int limit) { return static_cast<int>(next() % static_cast<uint32_t>(limit)); }
};
// Hashes the block storage itself: every non-empty chunk, in a stable
// coordinate order, over its raw byte array. Two worlds with the same
// fingerprint hold the same bytes in the same cells.
uint64_t worldFingerprint(const World& world) {
std::vector<Vec3i> coords;
for (const auto& entry : world.allChunks())
if (!entry.second->empty()) coords.push_back(entry.first);
std::sort(coords.begin(), coords.end(), [](const Vec3i& a, const Vec3i& b) {
if (a.x != b.x) return a.x < b.x;
if (a.y != b.y) return a.y < b.y;
return a.z < b.z;
});
uint64_t digest = 1469598103934665603ull;
for (const Vec3i& coord : coords) {
int32_t header[3] = {coord.x, coord.y, coord.z};
digest ^= fnv1a64(reinterpret_cast<const uint8_t*>(header), sizeof(header));
digest *= 1099511628211ull;
const Chunk* chunk = world.chunkAt(coord);
const auto& blocks = chunk->raw();
digest ^= fnv1a64(reinterpret_cast<const uint8_t*>(blocks.data()), blocks.size());
digest *= 1099511628211ull;
}
return digest;
}
World layeredWorld(int span, int depth) {
World world;
for (int z = 0; z < span; ++z)
for (int x = 0; x < span; ++x)
for (int y = 0; y < depth; ++y) {
Block block = y + 1 == depth ? Block::Grass : Block::Stone;
world.setBlock(x, y, z, block);
}
return world;
}
} // namespace
VF_TEST(recordingAnEditCapturesThePreviousBlockAndWritesTheNewOne) {
World world;
world.setBlock(1, 2, 3, Block::Stone);
EditHistory history;
CHECK(history.beginBatch("place", 1u));
CHECK(history.batchOpen());
CHECK(history.recordSet(world, {1, 2, 3}, Block::Planks));
CHECK(world.blockAt(1, 2, 3) == Block::Planks);
CHECK_EQ(history.openBatchSize(), 1u);
CHECK(history.endBatch());
CHECK(!history.batchOpen());
const EditBatch* batch = history.peekUndo();
CHECK(batch != nullptr);
CHECK(batch->label == std::string("place"));
CHECK_EQ(batch->ops.size(), 1u);
CHECK(batch->ops[0].before == Block::Stone);
CHECK(batch->ops[0].after == Block::Planks);
CHECK(batch->ops[0].position == Vec3i(1, 2, 3));
}
VF_TEST(editsRecordedWithoutAnOpenBatchAreRejected) {
World world;
EditHistory history;
CHECK(!history.recordSet(world, {0, 0, 0}, Block::Stone));
CHECK(world.blockAt(0, 0, 0) == Block::Air);
CHECK_EQ(history.undoDepth(), 0u);
CHECK(!history.endBatch());
CHECK(history.beginBatch("first", 0u));
CHECK(!history.beginBatch("second", 0u));
CHECK(history.recordSet(world, {0, 0, 0}, Block::Stone));
CHECK(history.endBatch());
CHECK_EQ(history.undoDepth(), 1u);
}
VF_TEST(undoTakesBackAWholeBatchAndRedoReplaysIt) {
World world = layeredWorld(4, 3);
uint64_t original = worldFingerprint(world);
EditHistory history;
history.beginBatch("tower", 10u);
for (int y = 3; y < 9; ++y) history.recordSet(world, {2, y, 2}, Block::Cobblestone);
CHECK(history.endBatch());
uint64_t built = worldFingerprint(world);
CHECK(built != original);
CHECK_EQ(history.stats().opCount, 6u);
CHECK(history.undo(world));
CHECK_EQ(worldFingerprint(world), original);
CHECK_EQ(history.undoDepth(), 0u);
CHECK_EQ(history.redoDepth(), 1u);
CHECK(!history.undo(world));
CHECK(history.redo(world));
CHECK_EQ(worldFingerprint(world), built);
CHECK_EQ(history.undoDepth(), 1u);
CHECK(!history.redo(world));
CHECK_EQ(history.stats().undoCount, 1u);
CHECK_EQ(history.stats().redoCount, 1u);
}
VF_TEST(undoingARandomSequenceOfTwoHundredEditsRestoresTheWorldByteForByte) {
World world = layeredWorld(8, 4);
EditHistory history(1u << 22);
const Block palette[5] = {Block::Air, Block::Stone, Block::Dirt, Block::Sand, Block::Glass};
std::vector<uint64_t> marks;
marks.push_back(worldFingerprint(world));
Lcg rng(0xfeedfaceull);
for (int batch = 0; batch < 200; ++batch) {
history.beginBatch("stroke" + std::to_string(batch), static_cast<uint64_t>(batch));
int edits = 1 + rng.below(6);
for (int edit = 0; edit < edits; ++edit) {
Vec3i cell{rng.below(8), rng.below(6), rng.below(8)};
history.recordSet(world, cell, palette[rng.below(5)]);
}
if (history.endBatch()) marks.push_back(worldFingerprint(world));
}
CHECK(marks.size() > 150u);
CHECK_EQ(history.undoDepth(), marks.size() - 1u);
CHECK_EQ(history.stats().droppedBatches, 0u);
for (std::size_t step = marks.size() - 1u; step > 0u; --step) {
CHECK(history.undo(world));
CHECK_EQ(worldFingerprint(world), marks[step - 1u]);
}
CHECK(!history.undo(world));
CHECK_EQ(worldFingerprint(world), marks.front());
for (std::size_t step = 1u; step < marks.size(); ++step) {
CHECK(history.redo(world));
CHECK_EQ(worldFingerprint(world), marks[step]);
}
CHECK(!history.redo(world));
}