// Unit test for openxcom-0001: AIModule _reachable/_reachableWithAttack // vector std::find O(N*R) -> unordered_set O(1) lookup // // Defect: AIModule stores reachable tile indices in std::vector and // uses std::find() for membership checks inside loops over map nodes // (setupAmbush, setupEscape, selectPointNearTarget, findFirePoint). // On a 50x50x4 map with hundreds of reachable tiles, each AI turn // performs O(N*R) linear scans where N = nodes checked and R = reachable tiles. // // Fix: Replace std::vector with std::unordered_set for O(1) lookup. #include #include #include #include #include #include #include // Simulate our defective pattern: vector + std::find in loop int benchmarkVector(const std::vector& reachable, const std::vector& queries) { int found = 0; for (size_t i = 0; i < queries.size(); ++i) { if (std::find(reachable.begin(), reachable.end(), queries[i]) != reachable.end()) { ++found; } } return found; } // Simulate our fixed pattern: unordered_set + count in loop int benchmarkSet(const std::unordered_set& reachable, const std::vector& queries) { int found = 0; for (size_t i = 0; i < queries.size(); ++i) { if (reachable.count(queries[i]) != 0) { ++found; } } return found; } int main() { // Typical battlescape: 50x50x4 = 10000 tiles, ~500 reachable const int R = 500; // reachable tiles const int N = 200; // nodes/positions checked per AI cycle (setupAmbush + findFirePoint + selectPointNearTarget) const int UNITS = 20; // alien units per battle const int ITERATIONS = UNITS; // each unit runs AI per turn // Build reachable tile indices std::vector reachableVec; reachableVec.reserve(R); for (int i = 0; i < R; ++i) { reachableVec.push_back(i * 20); // spread across map } std::unordered_set reachableSet(reachableVec.begin(), reachableVec.end()); // Build query positions (mix of reachable and unreachable) std::vector queries; queries.reserve(N); for (int i = 0; i < N; ++i) { queries.push_back(i * 10); // ~50% will hit } // Correctness check int vecResult = benchmarkVector(reachableVec, queries); int setResult = benchmarkSet(reachableSet, queries); assert(vecResult == setResult); std::cout << "Correctness: PASS (both found " << vecResult << " matches)" << std::endl; // Benchmark: vector (defective) auto t0 = std::chrono::high_resolution_clock::now(); int dummy1 = 0; for (int iter = 0; iter < ITERATIONS; ++iter) { dummy1 += benchmarkVector(reachableVec, queries); } auto t1 = std::chrono::high_resolution_clock::now(); // Benchmark: unordered_set (fixed) auto t2 = std::chrono::high_resolution_clock::now(); int dummy2 = 0; for (int iter = 0; iter < ITERATIONS; ++iter) { dummy2 += benchmarkSet(reachableSet, queries); } auto t3 = std::chrono::high_resolution_clock::now(); double vecUs = std::chrono::duration_cast(t1 - t0).count(); double setUs = std::chrono::duration_cast(t3 - t2).count(); double ratio = vecUs / (setUs > 0 ? setUs : 1); std::cout << "Vector (defective): " << vecUs << " us" << std::endl; std::cout << "Set (fixed): " << setUs << " us" << std::endl; std::cout << "Ratio: " << ratio << "x" << std::endl; std::cout << "Parameters: R=" << R << " tiles reachable, N=" << N << " queries, " << UNITS << " units per turn" << std::endl; // Verify significant speedup assert(ratio > 2.0); std::cout << "Performance: PASS (ratio > 2x)" << std::endl; // Prevent optimizer from removing work assert(dummy1 == dummy2); std::cout << "ALL TESTS PASSED" << std::endl; return 0; }