/* * Copyright 2025, Matthew Gretton-Dann * SPDX-License-Identifier: Apache-2.0 */ #define PARTRIDGE_TESTING #include "../main.cc" #include #include #include #include #include namespace { struct Placement { std::uint64_t x; std::uint64_t y; std::uint64_t side; auto operator==(Placement const &) const noexcept -> bool = default; }; struct Validation { std::vector diagnostics; [[nodiscard]] auto valid() const noexcept -> bool { return diagnostics.empty(); } [[nodiscard]] auto text() const -> std::string { std::ostringstream result; for (auto const &diagnostic: diagnostics) { result << diagnostic << '\n'; } return result.str(); } }; struct IndependentResult { std::uint64_t width; std::uint64_t height; std::vector placements; }; auto describe(std::size_t index, Placement const &placement) -> std::string { return "placement " + std::to_string(index) + " at (" + std::to_string(placement.x) + ", " + std::to_string(placement.y) + ") with side " + std::to_string(placement.side); } auto validate(std::uint64_t order, std::uint64_t width, std::uint64_t height, std::vector const &placements) -> Validation { Validation result; auto const expected_side = order * (order + 1) / 2; if (width != expected_side || height != expected_side) { result.diagnostics.emplace_back( "board dimensions must both equal the triangular number for the order"); } std::vector multiplicities(order + 1); std::vector occupied(width * height, -1); for (std::size_t index = 0; index < placements.size(); ++index) { auto const &placement = placements[index]; if (placement.side == 0 || placement.side > order) { result.diagnostics.push_back(describe(index, placement) + " has an invalid side length"); continue; } ++multiplicities[placement.side]; if (placement.x >= width || placement.y >= height || placement.side > width - placement.x || placement.side > height - placement.y) { result.diagnostics.push_back(describe(index, placement) + " is outside the board bounds"); continue; } int overlapping_placement = -1; for (auto y = placement.y; y < placement.y + placement.side; ++y) { for (auto x = placement.x; x < placement.x + placement.side; ++x) { auto &cell = occupied[x + y * width]; if (cell != -1) { overlapping_placement = cell; } else { cell = static_cast(index); } } } if (overlapping_placement != -1) { result.diagnostics.push_back( describe(index, placement) + " overlaps placement " + std::to_string(overlapping_placement)); } } for (std::uint64_t side = 1; side <= order; ++side) { if (multiplicities[side] != side) { result.diagnostics.push_back( "side " + std::to_string(side) + " has multiplicity " + std::to_string(multiplicities[side]) + "; expected " + std::to_string(side)); } } if (std::ranges::find(occupied, -1) != occupied.end()) { result.diagnostics.emplace_back("board is not completely covered"); } return result; } auto to_independent(Results const &result) -> IndependentResult { IndependentResult converted{result.length(), result.length(), {}}; converted.placements.reserve(result.squares().size()); for (auto const &square: result.squares()) { converted.placements.push_back({ square.pos() % result.length(), square.pos() / result.length(), square.length(), }); } return converted; } auto validate(std::uint64_t order, Results const &result) -> Validation { auto const converted = to_independent(result); return validate(order, converted.width, converted.height, converted.placements); } auto known_order_8() -> std::vector { return { {0, 0, 8}, {8, 0, 8}, {16, 0, 8}, {24, 0, 8}, {32, 0, 4}, {32, 4, 4}, {0, 8, 8}, {8, 8, 8}, {16, 8, 8}, {24, 8, 6}, {30, 8, 6}, {24, 14, 5}, {29, 14, 7}, {0, 16, 6}, {6, 16, 3}, {9, 16, 8}, {17, 16, 7}, {6, 19, 3}, {24, 19, 5}, {29, 21, 7}, {0, 22, 7}, {7, 22, 2}, {17, 23, 1}, {18, 23, 6}, {7, 24, 6}, {13, 24, 5}, {24, 24, 5}, {29, 28, 3}, {32, 28, 4}, {0, 29, 7}, {13, 29, 7}, {20, 29, 7}, {27, 29, 2}, {7, 30, 6}, {27, 31, 5}, {32, 32, 4}, }; } auto renderable_result(std::uint64_t side, std::vector const &placements) -> Results { std::vector squares; squares.reserve(placements.size()); for (auto const &placement: placements) { squares.emplace_back(placement.x + placement.y * side, placement.side); } return Results(side, std::move(squares)); } auto expect(bool condition, std::string const &message) -> int { if (condition) { return 0; } std::cerr << "FAIL: " << message << '\n'; return 1; } auto has(Validation const &validation, std::string_view diagnostic) -> bool { return std::ranges::any_of(validation.diagnostics, [diagnostic](std::string const &candidate) { return candidate.find(diagnostic) != std::string::npos; }); } auto test_validator() -> int { int failures = 0; auto const valid = known_order_8(); auto validation = validate(8, 36, 36, valid); failures += expect(validation.valid(), "known order-8 solution was rejected:\n" + validation.text()); validation = validate(8, 35, 36, valid); failures += expect(has(validation, "dimensions"), "invalid board dimensions were not diagnosed"); auto invalid = valid; invalid.pop_back(); validation = validate(8, 36, 36, invalid); failures += expect(has(validation, "side 4 has multiplicity 3; expected 4"), "invalid square multiplicity lacked side and counts"); failures += expect(has(validation, "completely covered"), "incomplete coverage was not diagnosed"); invalid = valid; invalid.front().x = 36; validation = validate(8, 36, 36, invalid); failures += expect( has(validation, "placement 0 at (36, 0) with side 8 is outside the board bounds"), "out-of-bounds diagnostic lacked placement details"); invalid = valid; invalid[1].x = invalid[0].x; invalid[1].y = invalid[0].y; validation = validate(8, 36, 36, invalid); failures += expect( has(validation, "placement 1 at (0, 0) with side 8 overlaps placement 0"), "overlap diagnostic lacked placement details"); invalid = valid; invalid.front().side = 9; validation = validate(8, 36, 36, invalid); failures += expect( has(validation, "placement 0 at (0, 0) with side 9 has an invalid side length"), "invalid-side diagnostic lacked placement details"); return failures; } auto test_construction() -> int { auto predecessor = renderable_result(36, known_order_8()); auto const predecessor_count = predecessor.squares().size(); auto const constructed = construct_odd_solution(9, std::move(predecessor)); auto const converted = to_independent(constructed); auto const validation = validate(9, constructed); int failures = 0; failures += expect( validation.valid(), "even-to-odd construction was rejected:\n" + validation.text()); failures += expect(constructed.length() == 45, "constructed board has the wrong side length"); failures += expect(converted.placements.size() == predecessor_count + 9, "construction did not add exactly nine squares"); auto const expected_border = std::array{{ {36, 0, 9}, {36, 9, 9}, {36, 18, 9}, {36, 27, 9}, {0, 36, 9}, {9, 36, 9}, {18, 36, 9}, {27, 36, 9}, {36, 36, 9}, }}; failures += expect( std::ranges::equal( std::span(converted.placements).subspan(predecessor_count), expected_border), "constructed border coordinates are incorrect"); failures += expect( std::ranges::equal( std::span(converted.placements).first(predecessor_count), known_order_8()), "construction translated predecessor coordinates unexpectedly"); return failures; } auto test_odd_solver_route() -> int { SearchCounters even_counters; SearchCounters odd_counters; auto const even = find_solution_instrumented(8, even_counters); auto const odd = find_solution_instrumented(9, odd_counters); int failures = 0; failures += expect( !uses_odd_construction(1) && !uses_odd_construction(7) && !uses_odd_construction(8) && uses_odd_construction(9) && !uses_odd_construction(10) && uses_odd_construction(11), "odd construction route does not preserve direct handling boundaries"); auto const validation = validate(9, odd); failures += expect(validation.valid(), "order-9 routed result is invalid:\n" + validation.text()); failures += expect(odd.squares().size() == even.squares().size() + 9, "order-9 route did not construct from order 8"); failures += expect( odd_counters.search_nodes == even_counters.search_nodes && odd_counters.loop_iterations == even_counters.loop_iterations && odd_counters.attempted_placements == even_counters.attempted_placements && odd_counters.backtracks == even_counters.backtracks, "order-9 route did not perform exactly the order-8 search"); return failures; } auto test_rendering() -> int { auto const result = renderable_result(36, known_order_8()); std::ostringstream rendered; auto *const original_buffer = std::cout.rdbuf(rendered.rdbuf()); result.output(); std::cout.rdbuf(original_buffer); int failures = 0; std::istringstream lines(rendered.str()); std::string line; std::uint64_t line_count = 0; while (std::getline(lines, line)) { ++line_count; failures += expect(line.size() == result.length(), "rendered row has incorrect width"); failures += expect(line.find('.') == std::string::npos, "valid solution left an unrendered cell"); } failures += expect(line_count == result.length(), "rendered output has incorrect height"); return failures; } auto test_small_solver() -> int { int failures = 0; for (auto const order: std::array{2, 3}) { auto const solution = find_solution(order); auto const converted = to_independent(solution); auto const expected_side = order * (order + 1) / 2; failures += expect(converted.width == expected_side && converted.height == expected_side, "result adapter returned incorrect board dimensions"); failures += expect(!has(validate(order, solution), "dimensions"), "result adapter supplied invalid validator dimensions"); failures += expect(converted.placements.empty(), "solver reported a solution for an unsatisfiable order"); } Results const encoded(10, {Square(23, 2)}); auto const converted = to_independent(encoded); failures += expect( converted.width == 10 && converted.height == 10 && converted.placements.size() == 1 && converted.placements.front().x == 3 && converted.placements.front().y == 2 && converted.placements.front().side == 2, "result adapter did not convert encoded placement coordinates"); return failures; } auto test_search_counters() -> int { SearchCounters counters; auto const solution = find_solution_instrumented(2, counters); int failures = 0; failures += expect(solution.squares().empty(), "instrumented solver changed an infeasible result"); failures += expect(counters.search_nodes > 0 && counters.loop_iterations >= counters.search_nodes, "instrumented solver did not count search work"); failures += expect(counters.attempted_placements > 0 && counters.backtracks > 0, "instrumented solver did not count placements/backtracks"); failures += expect(counters.prune_checks == 0 && counters.prune_hits == 0 && counters.generated_tasks == 0 && counters.completed_tasks == 0, "unimplemented solver counters were not zero"); return failures; } auto test_skyline_search() -> int { int failures = 0; auto const narrowest = smallest_valley(std::vector{4, 2, 2, 4, 0, 0, 0, 4}); failures += expect( narrowest.x == 1 && narrowest.height == 2 && narrowest.width == 2, "skyline did not select the smallest-width valley"); auto const tie = smallest_valley(std::vector{4, 1, 4, 4, 2, 4}); failures += expect(tie.x == 1 && tie.height == 1 && tie.width == 1, "skyline valley tie-break is not deterministic"); SearchCounters descending_counters; auto const descending = search_solution_instrumented( 8, descending_counters, CandidateOrder::descending); auto validation = validate(8, descending); failures += expect( validation.valid(), "descending skyline search returned an invalid order-8 solution:\n" + validation.text()); SearchCounters direct_nine_counters; auto const direct_nine = search_solution_instrumented( 9, direct_nine_counters, CandidateOrder::ascending); validation = validate(9, direct_nine); failures += expect( validation.valid(), "direct skyline search returned an invalid order-9 solution:\n" + validation.text()); failures += expect( direct_nine_counters.search_nodes != descending_counters.search_nodes, "direct order-9 coverage unexpectedly reused predecessor construction"); return failures; } auto test_solver_completion() -> int { int failures = 0; for (auto const order: std::array{1, 8}) { auto const solution = find_solution(order); auto const validation = validate(order, solution); failures += expect( validation.valid(), "solver returned an invalid order-" + std::to_string(order) + " solution:\n" + validation.text()); } return failures; } } int main(int argc, char **argv) { if (argc != 2) { std::cerr << "usage: partridge_tests TEST-NAME\n"; return 2; } auto const test = std::string_view(argv[1]); if (test == "validator") { return test_validator(); } if (test == "construction") { return test_construction(); } if (test == "solver-odd-route") { return test_odd_solver_route(); } if (test == "rendering") { return test_rendering(); } if (test == "solver-small") { return test_small_solver(); } if (test == "solver-completion") { return test_solver_completion(); } if (test == "search-counters") { return test_search_counters(); } if (test == "skyline-search") { return test_skyline_search(); } std::cerr << "unknown test: " << test << '\n'; return 2; }