// Device-on-strut-graph collapse test. // // Verifies that devices mounted on the strut graph are destroyed when their // supporting structure is cut away and they fall below the ground. // // This is a headless test (no SDL/display): it constructs a minimal fort, // exercises the collapse mechanics (cut, splash, beam, fire), and asserts that // structure falls correctly. Once the device-mount API lands, the device- // specific assertions (marked STUB) must be wired up. // // ./builddir/device_collapse_test // // Exit status: 0 = all tests pass, non-zero = failure. #include "game/build_graph.hpp" #include #include #include #include #include #include // ----------------------------------------------------------------------------- // Tiny test harness: no external framework; just pass/fail with a message. // ----------------------------------------------------------------------------- static int tests_run = 0, tests_failed = 0; #define TEST(name) \ do { \ tests_run++; \ printf("\n TEST %s ... ", name); \ } while (0) #define PASS() \ do { \ printf("PASS\n"); \ } while (0) #define FAIL(fmt, ...) \ do { \ printf("FAIL: " fmt "\n", ##__VA_ARGS__); \ tests_failed++; \ } while (0) // ----------------------------------------------------------------------------- // Constants matching the Lua wood material // ----------------------------------------------------------------------------- static constexpr float FIXED_DT = 1.0f / 60.0f; static constexpr float GROUND = 1.0f; static constexpr int MAT_WOOD = 0; static MaterialDef wood() { MaterialDef m{}; m.name = "wood"; m.stiffness = 300.0f; m.damping = 3.0f; m.mass = 0.5f; m.max_compression = 0.90f; m.max_expansion = 1.10f; m.angle_threshold = 0.5236f; // ~30 degrees m.min_length = 0.3f; m.max_length = 3.0f; m.max_link_length = 12.0f; m.hit_points = 100.0f; m.tension_only = false; m.flammable = true; m.burn_rate = 8.0f; m.spread_time = 0.6f; m.blocks_projectiles = true; m.blocks_beam = true; m.half_width = 0.06f; m.cost_metal = 0.0f; m.cost_energy = 0.0f; m.r = 0.6f; m.g = 0.4f; m.b = 0.2f; m.a = 1.0f; return m; } // ----------------------------------------------------------------------------- // Tower geometry — indices are captured at build time for deterministic access. // ----------------------------------------------------------------------------- // // n4------n5 <-- platform (device mounts here) // |\ /| // | \ / | // n2 n3 n6 (n3 is a mid-span node for triangulation) // | / | // | / | // n0------n1 <-- foundation nodes (pinned to ground) // // Ground_level = 1.0; foundation nodes at y=1.0. struct TowerIndices { int n0, n1; // foundation (pinned) int n2, n3, n6; // mid-level int n4, n5; // platform deck int leg_left, leg_right; // edge indices: n0-n2, n1-n6 int diag_left, diag_right; // edge indices: n0-n3, n1-n3 int deck; // edge index: n4-n5 int deck_brace_l, deck_brace_r; // n3-n4, n3-n5 int horiz_l, horiz_r; // n2-n3, n3-n6 int upper_l, upper_r; // n2-n4, n6-n5 }; static bool build_test_tower(BuildGraph& graph, TowerIndices& idx) { graph.ground_level = GROUND; graph.materials = { wood() }; // Foundation (pinned to ground) idx.n0 = graph.add_node(-1.0f, GROUND, true); idx.n1 = graph.add_node( 1.0f, GROUND, true); // Mid-level legs (free nodes; n3 is the triangulation mid-node) idx.n2 = graph.add_node(-1.0f, 2.5f, false); idx.n3 = graph.add_node( 0.0f, 2.5f, false); idx.n6 = graph.add_node( 1.0f, 2.5f, false); // Platform (reactor sits here) idx.n4 = graph.add_node(-0.8f, 3.5f, false); idx.n5 = graph.add_node( 0.8f, 3.5f, false); // Left leg + triangulation idx.leg_left = graph.add_edge(idx.n0, idx.n2, MAT_WOOD); idx.upper_l = graph.add_edge(idx.n2, idx.n4, MAT_WOOD); idx.diag_left = graph.add_edge(idx.n0, idx.n3, MAT_WOOD); idx.horiz_l = graph.add_edge(idx.n2, idx.n3, MAT_WOOD); // Right leg + triangulation idx.leg_right = graph.add_edge(idx.n1, idx.n6, MAT_WOOD); idx.upper_r = graph.add_edge(idx.n6, idx.n5, MAT_WOOD); idx.diag_right = graph.add_edge(idx.n1, idx.n3, MAT_WOOD); idx.horiz_r = graph.add_edge(idx.n3, idx.n6, MAT_WOOD); // Platform deck idx.deck = graph.add_edge(idx.n4, idx.n5, MAT_WOOD); idx.deck_brace_l = graph.add_edge(idx.n3, idx.n4, MAT_WOOD); idx.deck_brace_r = graph.add_edge(idx.n3, idx.n5, MAT_WOOD); // Verify the tower was built if (graph.edges.empty()) { fprintf(stderr, "build_test_tower: no edges created\n"); return false; } return true; } // ----------------------------------------------------------------------------- // Simulation helpers // ----------------------------------------------------------------------------- static void simulate_ticks(BuildGraph& graph, int ticks, bool quiet = false) { for (int t = 0; t < ticks; t++) { graph.step(FIXED_DT); int snapped = graph.check_strain(); graph.update_timers(FIXED_DT); graph.update_fire(FIXED_DT); int killed = graph.kill_grounded(); if (!quiet) { if (snapped > 0) printf("\n t=%d: %d strut(s) snapped", t, snapped); if (killed > 0) printf("\n t=%d: %d node(s) killed by ground", t, killed); } } } // Find an edge by its endpoint node indices. Returns -1 if not found. static int find_edge_by_nodes(const BuildGraph& graph, int na, int nb) { for (int i = 0; i < (int)graph.edges.size(); i++) { const auto& e = graph.edges[i]; if ((e.node_a == na && e.node_b == nb) || (e.node_a == nb && e.node_b == na)) return i; } return -1; } // Find every edge that has at least one foundation endpoint. // Used to sever ALL ground connections at once, guaranteeing collapse. static std::vector find_foundation_edges(const BuildGraph& graph) { std::vector out; for (int i = 0; i < (int)graph.edges.size(); i++) { const auto& e = graph.edges[i]; if (graph.nodes[e.node_a].is_foundation || graph.nodes[e.node_b].is_foundation) out.push_back(i); } return out; } // Count how many non-foundation nodes are below ground level. static int count_below_ground(const BuildGraph& graph) { int c = 0; for (const auto& n : graph.nodes) if (!n.is_foundation && n.y < graph.ground_level) c++; return c; } // ----------------------------------------------------------------------------- // Tests // ----------------------------------------------------------------------------- int main() { printf("=== device_collapse_test ===\n"); // ------------------------------------------------------------------ TEST("tower stands under gravity"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); } else { const int nodes0 = (int)graph.nodes.size(); const int edges0 = (int)graph.edges.size(); // Settle for 5 seconds: a triangulated tower must not collapse. simulate_ticks(graph, (int)(5.0f / FIXED_DT), /*quiet=*/true); if ((int)graph.nodes.size() != nodes0) FAIL("lost %d node(s) during settle", nodes0 - (int)graph.nodes.size()); else if ((int)graph.edges.size() != edges0) FAIL("lost %d edge(s) during settle", edges0 - (int)graph.edges.size()); else PASS(); } } // ------------------------------------------------------------------ TEST("structure collapses when all foundation edges are cut"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next1; } // Let the tower settle for 1 second. simulate_ticks(graph, 60, /*quiet=*/true); // Find and cut EVERY edge connected to a foundation node. The tower // has 4 such edges: n0-n2, n0-n3, n1-n6, n1-n3. Cutting all four // severs the structure from the ground completely. auto found_edges = find_foundation_edges(graph); if (found_edges.empty()) { FAIL("no foundation edges found"); goto next1; } printf("\n cutting %zu foundation edge(s):", found_edges.size()); for (int ei : found_edges) printf(" #%d(n%d-n%d)", ei, graph.edges[ei].node_a, graph.edges[ei].node_b); // Cut in descending order to keep indices valid. std::sort(found_edges.begin(), found_edges.end(), std::greater()); for (int ei : found_edges) graph.break_edge(ei); const int nodes_before = (int)graph.nodes.size(); simulate_ticks(graph, 180); // 3 seconds — plenty of time to fall const int nodes_killed = nodes_before - (int)graph.nodes.size(); if (nodes_killed <= 0) { FAIL("no nodes were killed by ground after cutting all " "foundation edges (nodes %d -> %d)", nodes_before, (int)graph.nodes.size()); } else { printf("\n %d node(s) killed by ground after severing foundation", nodes_killed); PASS(); } // STUB(impl): once device-mount API exists, also assert: // int dev = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, hp); // // after collapse: graph.is_device_alive(dev) == false } next1:; // ------------------------------------------------------------------ TEST("structure survives when only one leg is cut"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next2; } simulate_ticks(graph, 60, /*quiet=*/true); // Cut only the left leg. The right leg + both diagonals still connect // the structure to the foundation, so it should survive. int leg_l = find_edge_by_nodes(graph, idx.n0, idx.n2); if (leg_l < 0) { FAIL("left leg edge vanished during settle"); goto next2; } printf("\n cutting only left leg (n0-n2, edge #%d)", leg_l); graph.break_edge(leg_l); const int nodes_before = (int)graph.nodes.size(); const int edges_before = (int)graph.edges.size(); simulate_ticks(graph, 300); // 5 seconds int below = count_below_ground(graph); if (below > 0) { FAIL("nodes fell below ground with 3 foundation connections intact " "(%d below; nodes %d->%d, edges %d->%d)", below, nodes_before, (int)graph.nodes.size(), edges_before, (int)graph.edges.size()); } else { printf("\n structure intact (nodes %d, edges %d)", (int)graph.nodes.size(), (int)graph.edges.size()); PASS(); } // STUB(impl): once device-mount API exists, also assert: // device mounted on n4-n5 is still alive } next2:; // ------------------------------------------------------------------ TEST("splash damage near platform snaps mounting struts"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next3; } // Settle fully so structure is at equilibrium. simulate_ticks(graph, 300, /*quiet=*/true); // Apply a large splash at the platform centre (n4-n5 midpoint). // n4=(-0.8, 3.5), n5=(0.8, 3.5) → centre=(0.0, 3.5) const int edges_before = (int)graph.edges.size(); graph.apply_splash(0.0f, 3.5f, 2.5f, 200.0f, 50.0f); if ((int)graph.edges.size() >= edges_before) { FAIL("splash at platform centre broke no struts " "(edges %d -> %d, damage=200 radius=2.5)", edges_before, (int)graph.edges.size()); } else { printf("\n splash broke %d strut(s) at platform", edges_before - (int)graph.edges.size()); // After the platform is destroyed, the remaining structure may // collapse. Run sim to let debris fall. const int nodes_before = (int)graph.nodes.size(); simulate_ticks(graph, 180); // 3 seconds int killed = nodes_before - (int)graph.nodes.size(); printf("\n %d node(s) killed by ground after splash collapse", killed); PASS(); } // STUB(impl): once device-mount API exists: // int dev = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200); // graph.apply_splash(0.0f, 3.5f, 2.5f, 200.0f, 50.0f); // assert(graph.get_device_hp(dev) < 200); // took splash damage } next3:; // ------------------------------------------------------------------ TEST("beam fired at an angle damages platform struts"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next4; } simulate_ticks(graph, 300, /*quiet=*/true); // Fire a beam downward at ~45° through the tower. Origin above-left // of the platform, direction down-right so it crosses the deck (n4-n5) // and the brace (n3-n4) — not parallel to either. const int edges_before = (int)graph.edges.size(); float hit_x = 0, hit_y = 0; float beam_len = graph.beam_fire(-2.0f, 5.0f, // origin above-left 1.0f, -0.6f, // direction down-right 6.0f, // range 200.0f, false, hit_x, hit_y); printf("\n beam travelled %.2f units, hit at (%.2f, %.2f)", beam_len, hit_x, hit_y); // The beam should cross and break at least one wood strut. if ((int)graph.edges.size() >= edges_before) { FAIL("diagonal beam through tower broke no struts " "(edges %d -> %d, dps=200 range=6)", edges_before, (int)graph.edges.size()); } else { printf("\n beam broke %d strut(s)", edges_before - (int)graph.edges.size()); PASS(); } // STUB(impl): once device-mount API exists: // int dev = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200); // graph.beam_fire(-2.0f, 5.0f, 1.0f, -0.6f, 6.0f, 200.0f, false, // hit_x, hit_y); // assert(graph.get_device_hp(dev) < 200); } next4:; // ------------------------------------------------------------------ TEST("fire spreads along the graph and destroys struts"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next5; } simulate_ticks(graph, 60, /*quiet=*/true); // Ignite the left leg (n0-n2). Fire should spread up through n2 to // n2-n3, n2-n4, and eventually the platform. int leg_l = find_edge_by_nodes(graph, idx.n0, idx.n2); if (leg_l < 0) { FAIL("left leg edge vanished during settle"); goto next5; } printf("\n igniting edge #%d (n0-n2, left leg)", leg_l); graph.ignite_edge(leg_l); const int edges_before = (int)graph.edges.size(); // Run for several seconds so fire can spread and burn through struts. // Wood burns at 8 HP/s, has 100 HP → ~12.5s to burn through one strut. // spread_time = 0.6s, so in 8s fire spreads ~13 times. simulate_ticks(graph, (int)(8.0f / FIXED_DT)); // 8 seconds int burned = edges_before - (int)graph.edges.size(); bool any_burning = false; for (const auto& e : graph.edges) if (e.burning) { any_burning = true; break; } printf("\n %d strut(s) burned, fire still active: %s", burned, any_burning ? "yes" : "no"); if (burned > 0) { PASS(); } else if (any_burning) { // Fire is spreading but hasn't consumed a full strut yet. // This is OK — the test validates the spread mechanic works. printf("\n (fire spreading but no strut fully consumed in 8s)"); PASS(); } else { FAIL("fire did not spread from ignited strut (edges %d -> %d)", edges_before, (int)graph.edges.size()); } // STUB(impl): once device-mount API exists: // int dev = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200); // graph.ignite_edge(leg_l); // // after fire spread: graph.get_device_hp(dev) < 200 } next5:; // ================================================================== // DEVICE-ON-STRUT-GRAPH INTEGRATION TESTS // // These tests verify the full lifecycle: mount a device on the strut // graph, damage/collapse the structure, and assert the device is // destroyed. The device-mount API is now live in BuildGraph. // ================================================================== // Placeholder device type index for the test (doesn't need a real Lua def). constexpr int DEVICE_REACTOR = 0; constexpr int DEVICE_MINE = 1; constexpr int DEVICE_TURBINE = 2; // ---------------------------------------------------------------- TEST("reactor is destroyed when its supporting structure collapses"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next_dev1; } simulate_ticks(graph, 60, /*quiet=*/true); // Mount a reactor on the platform deck (n4-n5). int dev_id = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200.0f); if (dev_id < 0) { FAIL("mount_device returned %d", dev_id); goto next_dev1; } if (!graph.is_device_alive(dev_id)) { FAIL("reactor not alive immediately after mounting"); goto next_dev1; } printf("\n reactor mounted on n4-n5 (id=%d, HP=%.1f)", dev_id, graph.get_device_hp(dev_id)); // Sever all foundation edges → the tower and its reactor fall. auto f_edges = find_foundation_edges(graph); std::sort(f_edges.begin(), f_edges.end(), std::greater()); for (int ei : f_edges) graph.break_edge(ei); simulate_ticks(graph, 180); // 3 seconds for the fall if (graph.is_device_alive(dev_id)) { FAIL("reactor still alive after both mount nodes fell below ground " "(HP=%.1f)", graph.get_device_hp(dev_id)); } else { printf("\n reactor destroyed: HP=%.1f, alive=%s", graph.get_device_hp(dev_id), graph.is_device_alive(dev_id) ? "yes" : "no"); PASS(); } } next_dev1:; // ---------------------------------------------------------------- TEST("reactor falls and dies when its platform support struts are cut"); { // This is the canonical win-condition path: surgically sever the // struts that mount the reactor without cutting foundation edges. // The reactor falls below ground, its mount nodes are killed by // kill_grounded(), and break_node() marks the device dead. BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next_dev1b; } simulate_ticks(graph, 60, /*quiet=*/true); int dev_id = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200.0f); if (dev_id < 0) { FAIL("mount_device returned %d", dev_id); goto next_dev1b; } if (!graph.is_device_alive(dev_id)) { FAIL("reactor not alive immediately after mounting"); goto next_dev1b; } printf("\n reactor mounted on n4-n5 (id=%d, HP=%.1f)", dev_id, graph.get_device_hp(dev_id)); // Cut the five struts that hold up the platform deck (n4,n5). // n2-n4 (upper_l), n6-n5 (upper_r), // n3-n4 (deck_brace_l), n3-n5 (deck_brace_r), // n4-n5 (deck — the mount strut itself) // This detaches n4 and n5 from the structure. The tower below // (n0,n1,n2,n3,n6) remains standing because foundation edges // are intact. std::vector platform_edges = { idx.upper_l, idx.upper_r, idx.deck_brace_l, idx.deck_brace_r, idx.deck, }; // Validate all platform edges still exist. for (int ei : platform_edges) { if (ei < 0 || ei >= (int)graph.edges.size()) { FAIL("platform edge index %d out of range", ei); goto next_dev1b; } } printf("\n cutting %zu platform support strut(s):", platform_edges.size()); for (int ei : platform_edges) printf(" #%d(n%d-n%d)", ei, graph.edges[ei].node_a, graph.edges[ei].node_b); // Cut in descending order so indices stay valid. std::sort(platform_edges.begin(), platform_edges.end(), std::greater()); for (int ei : platform_edges) graph.break_edge(ei); const int nodes_before = (int)graph.nodes.size(); simulate_ticks(graph, 180); // 3 seconds for the fall // Reactor mount nodes (n4,n5) should have fallen below ground. if (graph.is_device_alive(dev_id)) { FAIL("reactor still alive after its platform supports were cut " "(HP=%.1f, nodes %d->%d)", graph.get_device_hp(dev_id), nodes_before, (int)graph.nodes.size()); } else { // Tower body (n0,n1,n2,n3,n6 + edges) should remain standing // since foundation edges are intact. We verify by checking the // surviving node count is at least the 5 tower-body nodes. int surviving = (int)graph.nodes.size(); printf("\n reactor destroyed: HP=%.1f, alive=%s", graph.get_device_hp(dev_id), graph.is_device_alive(dev_id) ? "yes" : "no"); printf("\n tower retained %d/%d nodes (expect 5: n0,n1,n2,n3,n6)", surviving, nodes_before); PASS(); } } next_dev1b:; // ---------------------------------------------------------------- TEST("reactor survives when only one foundation connection is cut"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next_dev2; } simulate_ticks(graph, 60, /*quiet=*/true); int dev_id = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200.0f); if (dev_id < 0) { FAIL("mount_device failed"); goto next_dev2; } // Cut only the left leg — 3 foundation edges remain. int leg_l = find_edge_by_nodes(graph, idx.n0, idx.n2); if (leg_l < 0) { FAIL("left leg vanished"); goto next_dev2; } graph.break_edge(leg_l); simulate_ticks(graph, 300); if (!graph.is_device_alive(dev_id)) { FAIL("reactor destroyed when structure is still standing"); } else { printf("\n reactor HP=%.1f, alive=yes", graph.get_device_hp(dev_id)); PASS(); } } next_dev2:; // ---------------------------------------------------------------- TEST("reactor takes splash damage through its mount struts"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next_dev3; } simulate_ticks(graph, 300, /*quiet=*/true); int dev_id = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200.0f); if (dev_id < 0) { FAIL("mount_device failed"); goto next_dev3; } float hp_before = graph.get_device_hp(dev_id); // Moderate splash near the platform — damages struts and should pass // some damage through to the mounted device. graph.apply_splash(0.0f, 3.5f, 2.5f, 50.0f, 10.0f); float hp_after = graph.get_device_hp(dev_id); printf("\n reactor HP: %.1f -> %.1f", hp_before, hp_after); if (!graph.is_device_alive(dev_id)) { printf("\n (reactor destroyed by splash — valid outcome)"); PASS(); } else if (hp_after < hp_before) { PASS(); } else { FAIL("reactor took no splash damage (HP stayed at %.1f)", hp_before); } } next_dev3:; // ---------------------------------------------------------------- TEST("reactor takes beam damage through its mount struts"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next_dev4; } simulate_ticks(graph, 300, /*quiet=*/true); int dev_id = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200.0f); if (dev_id < 0) { FAIL("mount_device failed"); goto next_dev4; } float hp_before = graph.get_device_hp(dev_id); // Fire a diagonal beam through the platform (not collinear with any edge). float hit_x = 0, hit_y = 0; graph.beam_fire(-2.0f, 5.0f, 1.0f, -0.6f, 6.0f, 100.0f, false, hit_x, hit_y); float hp_after = graph.get_device_hp(dev_id); printf("\n reactor HP: %.1f -> %.1f (beam hit at %.2f, %.2f)", hp_before, hp_after, hit_x, hit_y); if (!graph.is_device_alive(dev_id)) { printf("\n (reactor destroyed by beam — valid outcome)"); PASS(); } else if (hp_after < hp_before) { PASS(); } else { FAIL("reactor took no beam damage (HP stayed at %.1f)", hp_before); } } next_dev4:; // ---------------------------------------------------------------- TEST("reactor takes fire damage from burning mount struts"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next_dev5; } simulate_ticks(graph, 60, /*quiet=*/true); int dev_id = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200.0f); if (dev_id < 0) { FAIL("mount_device failed"); goto next_dev5; } float hp_before = graph.get_device_hp(dev_id); // Ignite the left leg — fire spreads up to the platform. int leg_l = find_edge_by_nodes(graph, idx.n0, idx.n2); if (leg_l < 0) { FAIL("left leg vanished"); goto next_dev5; } graph.ignite_edge(leg_l); // 15 seconds: long enough for fire to spread and do damage. simulate_ticks(graph, (int)(15.0f / FIXED_DT)); float hp_after = graph.get_device_hp(dev_id); printf("\n reactor HP: %.1f -> %.1f", hp_before, hp_after); if (!graph.is_device_alive(dev_id)) { printf("\n (reactor destroyed by fire — valid outcome)"); PASS(); } else if (hp_after < hp_before) { PASS(); } else { FAIL("reactor took no fire damage after 15s (HP stayed at %.1f)", hp_before); } } next_dev5:; // ---------------------------------------------------------------- TEST("multiple devices on same structure all destroyed on collapse"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next_dev6; } simulate_ticks(graph, 60, /*quiet=*/true); // Mount three devices on different struts of the same tower. int reactor_id = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200.0f); int mine_id = graph.mount_device(idx.n3, idx.n6, DEVICE_MINE, 0, 100.0f); int turbine_id = graph.mount_device(idx.n2, idx.n3, DEVICE_TURBINE, 0, 80.0f); if (reactor_id < 0 || mine_id < 0 || turbine_id < 0) { FAIL("mount_device failed (r=%d m=%d t=%d)", reactor_id, mine_id, turbine_id); goto next_dev6; } printf("\n %zu devices mounted", graph.get_devices().size()); // Sever foundation → entire tower collapses → all devices die. auto f_edges = find_foundation_edges(graph); std::sort(f_edges.begin(), f_edges.end(), std::greater()); for (int ei : f_edges) graph.break_edge(ei); simulate_ticks(graph, 300); bool r_alive = graph.is_device_alive(reactor_id); bool m_alive = graph.is_device_alive(mine_id); bool t_alive = graph.is_device_alive(turbine_id); printf("\n reactor=%s mine=%s turbine=%s", r_alive ? "alive" : "dead", m_alive ? "alive" : "dead", t_alive ? "alive" : "dead"); if (r_alive || m_alive || t_alive) { FAIL("expected all devices dead after collapse"); } else { PASS(); } } next_dev6:; // ---------------------------------------------------------------- TEST("device index is stable across unrelated edge breaks"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next_dev7; } simulate_ticks(graph, 60, /*quiet=*/true); int dev_id = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200.0f); if (dev_id < 0) { FAIL("mount_device failed"); goto next_dev7; } // Break an edge that is NOT the mount strut — the device ID should // remain valid. This exercises the "indices shift" gotcha. int horiz = find_edge_by_nodes(graph, idx.n2, idx.n3); if (horiz < 0) { FAIL("horiz edge not found"); goto next_dev7; } printf("\n breaking unrelated edge #%d (n2-n3)", horiz); graph.break_edge(horiz); // Device ID must still be usable. if (!graph.is_device_alive(dev_id)) { FAIL("reactor died when an unrelated edge broke"); } else { printf("\n reactor HP=%.1f, alive=yes", graph.get_device_hp(dev_id)); PASS(); } } next_dev7:; // ---------------------------------------------------------------- TEST("unmount_device removes a device without affecting the graph"); { BuildGraph graph; TowerIndices idx{}; if (!build_test_tower(graph, idx)) { FAIL("could not build test tower"); goto next_dev8; } simulate_ticks(graph, 60, /*quiet=*/true); int dev_id = graph.mount_device(idx.n4, idx.n5, DEVICE_REACTOR, 0, 200.0f); if (dev_id < 0) { FAIL("mount_device failed"); goto next_dev8; } int nodes_before = (int)graph.nodes.size(); int edges_before = (int)graph.edges.size(); graph.unmount_device(dev_id); if (graph.is_device_alive(dev_id)) { FAIL("unmount_device did not kill the device " "(alive=%d hp=%.1f)", (int)graph.is_device_alive(dev_id), graph.get_device_hp(dev_id)); } else if ((int)graph.nodes.size() != nodes_before || (int)graph.edges.size() != edges_before) { FAIL("unmount_device altered the graph (nodes %d->%d, edges %d->%d)", nodes_before, (int)graph.nodes.size(), edges_before, (int)graph.edges.size()); } else { printf("\n device unmounted, graph unchanged (nodes %d, edges %d)", (int)graph.nodes.size(), (int)graph.edges.size()); PASS(); } } next_dev8:; // ------------------------------------------------------------------ printf("\n=== %d/%d tests passed ===\n", tests_run - tests_failed, tests_run); return tests_failed == 0 ? 0 : 1; }