OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / libdc / test / test_edit_order.cpp

118 lines · cpp ref:
// 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;
}