// Build+run: g++ -std=c++17 -I../include test_edit_order.cpp -o test_edit_order && ./test_edit_order
//
// EDIT COMPOSITION IS NOT COMMUTATIVE — hyper-games #42 P1.
//
// The edit list is ORDERED. Every producer of it owes a total order that is
// identical on every peer.
//
// DCEditList::apply composes with min (subtract) and max (add), and min and max
// do not commute across each other. With r = 0, an add of 5 and a subtract of -3:
//
// min(max(0, 5), -3) = -3 add then subtract
// max(min(0, -3), 5) = 5 subtract then add
//
// Nothing in the codebase said so, so the terrain journal was built as an
// unordered bag: terrain_edit_manager.gd:90 iterates keys_with_prefix() with no
// sort, and live arrivals apply in arrival order. Two peers holding the SAME
// journal can therefore materialise different geometry, and nothing detects it.
// The level journal already folds by (seq, author) — this is the fact that says
// terrain must too.
//
// The second half of the file bounds the damage: it measures WHICH pairs are
// order-dependent, because that decides whether the journal fix is urgent or
// merely correct.
#include "dc_edit.h"
#include <cstdio>
#include <cmath>
#include <vector>
static int failures = 0;
static int sgn(float v) { return v > 0.f ? 1 : (v < 0.f ? -1 : 0); }
static void expect(bool cond, const char* what) {
printf(" %-58s %s\n", what, cond ? "OK" : "FAIL");
if (!cond) failures++;
}
// Two hard-CSG spheres that overlap at the origin, applied in both orders.
// Returns the density at the overlap for each order.
static void both_orders(float strength_a, float strength_b, float base,
float& ab, float& ba) {
DCEditList A, B;
A.add_sphere(-0.5f, 0, 0, 2.0f, strength_a, TerrainEdit::OP_HARD);
A.add_sphere( 0.5f, 0, 0, 2.0f, strength_b, TerrainEdit::OP_HARD);
B.add_sphere( 0.5f, 0, 0, 2.0f, strength_b, TerrainEdit::OP_HARD);
B.add_sphere(-0.5f, 0, 0, 2.0f, strength_a, TerrainEdit::OP_HARD);
ab = DCEditList::apply(A.snapshot(), 0, 0, 0, base);
ba = DCEditList::apply(B.snapshot(), 0, 0, 0, base);
}
int main() {
printf("edit composition: min/max do not commute, so the list is ORDERED\n\n");
// ---- 1. The arithmetic, with no geometry in the way --------------------
{
const float r = 0.0f, add = 5.0f, sub = -3.0f;
const float add_then_sub = std::min(std::max(r, add), sub);
const float sub_then_add = std::max(std::min(r, sub), add);
printf(" base %.0f, add %.0f, subtract %.0f -> %.0f one way, %.0f the other\n",
r, add, sub, add_then_sub, sub_then_add);
expect(add_then_sub != sub_then_add, "min and max do not commute");
}
// ---- 2. The same thing through the real apply() ------------------------
printf("\n through DCEditList::apply, two overlapping hard spheres:\n");
{
float ab, ba;
both_orders(+1.0f, -1.0f, 0.0f, ab, ba);
printf(" add-then-subtract %+.4f subtract-then-add %+.4f\n", ab, ba);
expect(ab != ba, "an add and an overlapping subtract are order-dependent");
expect(sgn(ab) != sgn(ba),
"and they disagree on SOLID vs AIR — geometry differs");
}
// ---- 3. WHICH pairs actually care ---------------------------------------
//
// This is the number that says how bad the unordered journal is. If only
// mixed add/subtract pairs diverge, then an all-carve session converges by
// luck and the bug shows up the first time someone fills something in.
printf("\n which pairs are order-dependent (at the overlap, both bases):\n");
struct Pair { const char* name; float a, b; };
const Pair pairs[] = {
{ "subtract, subtract", -1.0f, -1.0f },
{ "add, add", +1.0f, +1.0f },
{ "add, subtract", +1.0f, -1.0f },
{ "subtract, add", -1.0f, +1.0f },
};
int order_dependent = 0;
for (const Pair& p : pairs) {
bool differs = false;
for (float base : { -50.0f, 50.0f, 0.0f }) {
float ab, ba;
both_orders(p.a, p.b, base, ab, ba);
if (ab != ba) differs = true;
}
printf(" %-22s %s\n", p.name, differs ? "ORDER MATTERS" : "commutes");
if (differs) order_dependent++;
}
expect(order_dependent > 0, "at least one pair is order-dependent");
expect(order_dependent < 4,
"not every pair is — same-op runs commute, so the bug is intermittent");
// ---- 4. Determinism: the SAME order is always the same answer ----------
//
// The property a fold provides. Without it the list is not merely
// order-dependent, it is unrepeatable, and no ordering rule could fix it.
{
float ab1, ba1, ab2, ba2;
both_orders(+1.0f, -1.0f, 0.0f, ab1, ba1);
both_orders(+1.0f, -1.0f, 0.0f, ab2, ba2);
expect(ab1 == ab2 && ba1 == ba2,
"a given order is reproducible — order is the ONLY variable");
}
printf("\n%s (%d)\n", failures ? "FAIL" : "PASS", failures);
return failures ? 1 : 0;
}