From 98136418ac861b975636e2553812deaba9225920 Mon Sep 17 00:00:00 2001 From: Shiqing Date: Sat, 13 Jul 2019 11:22:12 +0800 Subject: Improve support for directed graphs in AStar --- core/math/a_star.cpp | 53 ++++++++++++++++++++++++++++++++-------------------- core/math/a_star.h | 8 ++------ 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/core/math/a_star.cpp b/core/math/a_star.cpp index 60b7326c29..482d7d8cd5 100644 --- a/core/math/a_star.cpp +++ b/core/math/a_star.cpp @@ -164,23 +164,21 @@ void AStar::connect_points(int p_id, int p_with_id, bool bidirectional) { } Segment s(p_id, p_with_id); - if (s.from == p_id) { - s.from_point = a; - s.to_point = b; - } else { - s.from_point = b; - s.to_point = a; - } - + s.from_point = a; + s.to_point = b; segments.insert(s); + + if (bidirectional) { + SWAP(s.from, s.to); + SWAP(s.from_point, s.to_point); + segments.insert(s); + } } -void AStar::disconnect_points(int p_id, int p_with_id) { +void AStar::disconnect_points(int p_id, int p_with_id, bool bidirectional) { Segment s(p_id, p_with_id); - ERR_FAIL_COND(!segments.has(s)); - - segments.erase(s); + Segment t(p_with_id, p_id); Point *a; bool a_exists = points.lookup(p_id, a); @@ -190,10 +188,24 @@ void AStar::disconnect_points(int p_id, int p_with_id) { bool b_exists = points.lookup(p_with_id, b); CRASH_COND(!b_exists); - a->neighbours.remove(b->id); - a->unlinked_neighbours.remove(b->id); - b->neighbours.remove(a->id); - b->unlinked_neighbours.remove(a->id); + bool warned = false; + + if (segments.has(s)) { + segments.erase(s); + a->neighbours.remove(b->id); + b->unlinked_neighbours.remove(a->id); + } else { + warned = true; + WARN_PRINT("The edge to be removed does not exist."); + } + + if (bidirectional && segments.has(t)) { + segments.erase(t); + b->neighbours.remove(a->id); + a->unlinked_neighbours.remove(b->id); + } else if (bidirectional && !warned) { + WARN_PRINT("The reverse edge to be removed does not exist."); + } } bool AStar::has_point(int p_id) const { @@ -227,10 +239,11 @@ PoolVector AStar::get_point_connections(int p_id) { return point_list; } -bool AStar::are_points_connected(int p_id, int p_with_id) const { +bool AStar::are_points_connected(int p_id, int p_with_id, bool bidirectional) const { Segment s(p_id, p_with_id); - return segments.has(s); + Segment t(p_with_id, p_id); + return segments.has(s) || (bidirectional && segments.has(t)); } void AStar::clear() { @@ -532,8 +545,8 @@ void AStar::_bind_methods() { ClassDB::bind_method(D_METHOD("is_point_disabled", "id"), &AStar::is_point_disabled); ClassDB::bind_method(D_METHOD("connect_points", "id", "to_id", "bidirectional"), &AStar::connect_points, DEFVAL(true)); - ClassDB::bind_method(D_METHOD("disconnect_points", "id", "to_id"), &AStar::disconnect_points); - ClassDB::bind_method(D_METHOD("are_points_connected", "id", "to_id"), &AStar::are_points_connected); + ClassDB::bind_method(D_METHOD("disconnect_points", "id", "to_id", "bidirectional"), &AStar::disconnect_points, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("are_points_connected", "id", "to_id", "bidirectional"), &AStar::are_points_connected, DEFVAL(true)); ClassDB::bind_method(D_METHOD("get_point_count"), &AStar::get_point_count); ClassDB::bind_method(D_METHOD("get_point_capacity"), &AStar::get_point_capacity); diff --git a/core/math/a_star.h b/core/math/a_star.h index ec2a06f07f..94e42483f1 100644 --- a/core/math/a_star.h +++ b/core/math/a_star.h @@ -93,10 +93,6 @@ class AStar : public Reference { bool operator<(const Segment &p_s) const { return key < p_s.key; } Segment() { key = 0; } Segment(int p_from, int p_to) { - if (p_from > p_to) { - SWAP(p_from, p_to); - } - from = p_from; to = p_to; } @@ -133,8 +129,8 @@ public: bool is_point_disabled(int p_id) const; void connect_points(int p_id, int p_with_id, bool bidirectional = true); - void disconnect_points(int p_id, int p_with_id); - bool are_points_connected(int p_id, int p_with_id) const; + void disconnect_points(int p_id, int p_with_id, bool bidirectional = true); + bool are_points_connected(int p_id, int p_with_id, bool bidirectional = true) const; int get_point_count() const; int get_point_capacity() const; -- cgit v1.2.3 From add0004a787fdb374da2bee780f676d0a5c62092 Mon Sep 17 00:00:00 2001 From: Shiqing Date: Sat, 13 Jul 2019 16:59:41 +0800 Subject: Revise and update AStar documentation --- doc/classes/AStar.xml | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/doc/classes/AStar.xml b/doc/classes/AStar.xml index 9ca09371dd..67b377fe8c 100644 --- a/doc/classes/AStar.xml +++ b/doc/classes/AStar.xml @@ -1,11 +1,23 @@ - AStar class representation that uses 3d-vectors as edges. + An implementation of A* to find shortest paths among connected points in space. - A* (A star) is a computer algorithm that is widely used in pathfinding and graph traversal, the process of plotting an efficiently directed path between multiple points. It enjoys widespread use due to its performance and accuracy. Godot's A* implementation make use of vectors as points. - You must add points manually with [method add_point] and create segments manually with [method connect_points]. So you can test if there is a path between two points with the [method are_points_connected] function, get the list of existing ids in the found path with [method get_id_path], or the points list with [method get_point_path]. + A* (A star) is a computer algorithm that is widely used in pathfinding and graph traversal, the process of plotting short paths among vertices (points), passing through a given set of edges (segments). It enjoys widespread use due to its performance and accuracy. Godot's A* implementation uses points in three-dimensional space and Euclidean distances by default. + You must add points manually with [method add_point] and create segments manually with [method connect_points]. Then you can test if there is a path between two points with the [method are_points_connected] function, get a path containing indices by [method get_id_path], or one containing actual coordinates with [method get_point_path]. + It is also possible to use non-Euclidean distances. To do so, create a class that extends [code]AStar[/code] and override methods [method _compute_cost] and [method _estimate_cost]. Both take two indices and return a length, as is shown in the following example. + [codeblock] + class MyAStar: + extends AStar + + func _compute_cost(u, v): + return abs(u - v) + + func _estimate_cost(u, v): + return min(0, abs(u - v) - 1) + [/codeblock] + [method _estimate_cost] should return a lower bound of the distance, i.e. [code]_estimate_cost(u, v) <= _compute_cost(u, v)[/code]. This serves as a hint to the algorithm because the custom [code]_compute_cost[/code] might be computation-heavy. If this is not the case, make [method _estimate_cost] return the same value as [method _compute_cost] to provide the algorithm with the most accurate information. @@ -19,6 +31,7 @@ Called when computing the cost between two connected points. + Note that this function is hidden in the default [code]AStar[/code] class. @@ -30,6 +43,7 @@ Called when estimating the cost between a point and the path's ending point. + Note that this function is hidden in the default [code]AStar[/code] class. @@ -57,8 +71,10 @@ + + - Returns whether there is a connection/segment between the given points. + Returns whether the two given points are directly connected by a segment. If [code]bidirectional[/code] is [code]false[/code], returns whether movement from [code]id[/code] to [code]to_id[/code] is possible through this segment. @@ -94,8 +110,10 @@ + + - Deletes the segment between the given points. + Deletes the segment between the given points. If [code]bidirectional[/code] is [code]false[/code], only movement from [code]id[/code] to [code]to_id[/code] is prevented, and a unidirectional segment possibly remains. -- cgit v1.2.3 From c2b824687d5e18028de5b71d71cf5be478bf838e Mon Sep 17 00:00:00 2001 From: Shiqing Date: Tue, 16 Jul 2019 01:01:50 +0800 Subject: Reduce memory usage for edges in A* and add tests --- core/math/a_star.cpp | 77 +++++++++++++++----------- core/math/a_star.h | 29 +++++++--- main/tests/test_astar.cpp | 134 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 37 deletions(-) diff --git a/core/math/a_star.cpp b/core/math/a_star.cpp index 482d7d8cd5..b390bdd976 100644 --- a/core/math/a_star.cpp +++ b/core/math/a_star.cpp @@ -164,22 +164,24 @@ void AStar::connect_points(int p_id, int p_with_id, bool bidirectional) { } Segment s(p_id, p_with_id); - s.from_point = a; - s.to_point = b; - segments.insert(s); - - if (bidirectional) { - SWAP(s.from, s.to); - SWAP(s.from_point, s.to_point); - segments.insert(s); + if (bidirectional) s.direction = Segment::BIDIRECTIONAL; + + Set::Element *element = segments.find(s); + if (element != NULL) { + s.direction |= element->get().direction; + if (s.direction == Segment::BIDIRECTIONAL) { + // Both are neighbours of each other now + a->unlinked_neighbours.remove(b->id); + b->unlinked_neighbours.remove(a->id); + } + segments.erase(element); } + + segments.insert(s); } void AStar::disconnect_points(int p_id, int p_with_id, bool bidirectional) { - Segment s(p_id, p_with_id); - Segment t(p_with_id, p_id); - Point *a; bool a_exists = points.lookup(p_id, a); CRASH_COND(!a_exists); @@ -188,23 +190,32 @@ void AStar::disconnect_points(int p_id, int p_with_id, bool bidirectional) { bool b_exists = points.lookup(p_with_id, b); CRASH_COND(!b_exists); - bool warned = false; + Segment s(p_id, p_with_id); + int remove_direction = bidirectional ? (int)Segment::BIDIRECTIONAL : s.direction; + + Set::Element *element = segments.find(s); + if (element != NULL) { + // s is the new segment + // Erase the directions to be removed + s.direction = (element->get().direction & ~remove_direction); - if (segments.has(s)) { - segments.erase(s); a->neighbours.remove(b->id); - b->unlinked_neighbours.remove(a->id); - } else { - warned = true; - WARN_PRINT("The edge to be removed does not exist."); - } + if (bidirectional) { + b->neighbours.remove(a->id); + if (element->get().direction != Segment::BIDIRECTIONAL) { + a->unlinked_neighbours.remove(b->id); + b->unlinked_neighbours.remove(a->id); + } + } else { + if (s.direction == Segment::NONE) + b->unlinked_neighbours.remove(a->id); + else + a->unlinked_neighbours.set(b->id, b); + } - if (bidirectional && segments.has(t)) { - segments.erase(t); - b->neighbours.remove(a->id); - a->unlinked_neighbours.remove(b->id); - } else if (bidirectional && !warned) { - WARN_PRINT("The reverse edge to be removed does not exist."); + segments.erase(element); + if (s.direction != Segment::NONE) + segments.insert(s); } } @@ -242,8 +253,10 @@ PoolVector AStar::get_point_connections(int p_id) { bool AStar::are_points_connected(int p_id, int p_with_id, bool bidirectional) const { Segment s(p_id, p_with_id); - Segment t(p_with_id, p_id); - return segments.has(s) || (bidirectional && segments.has(t)); + const Set::Element *element = segments.find(s); + + return element != NULL && + (bidirectional || (element->get().direction & s.direction) == s.direction); } void AStar::clear() { @@ -297,13 +310,17 @@ Vector3 AStar::get_closest_position_in_segment(const Vector3 &p_point) const { for (const Set::Element *E = segments.front(); E; E = E->next()) { - if (!(E->get().from_point->enabled && E->get().to_point->enabled)) { + Point *from_point = nullptr, *to_point = nullptr; + points.lookup(E->get().u, from_point); + points.lookup(E->get().v, to_point); + + if (!(from_point->enabled && to_point->enabled)) { continue; } Vector3 segment[2] = { - E->get().from_point->pos, - E->get().to_point->pos, + from_point->pos, + to_point->pos, }; Vector3 p = Geometry::get_closest_point_to_segment(p_point, segment); diff --git a/core/math/a_star.h b/core/math/a_star.h index 94e42483f1..dba89f1db2 100644 --- a/core/math/a_star.h +++ b/core/math/a_star.h @@ -81,20 +81,35 @@ class AStar : public Reference { struct Segment { union { struct { - int32_t from; - int32_t to; + int32_t u; + int32_t v; }; uint64_t key; }; - Point *from_point; - Point *to_point; + enum { + NONE = 0, + FORWARD = 1, + BACKWARD = 2, + BIDIRECTIONAL = FORWARD | BACKWARD + }; + unsigned char direction; bool operator<(const Segment &p_s) const { return key < p_s.key; } - Segment() { key = 0; } + Segment() { + key = 0; + direction = NONE; + } Segment(int p_from, int p_to) { - from = p_from; - to = p_to; + if (p_from < p_to) { + u = p_from; + v = p_to; + direction = FORWARD; + } else { + u = p_to; + v = p_from; + direction = BACKWARD; + } } }; diff --git a/main/tests/test_astar.cpp b/main/tests/test_astar.cpp index d34ff0d95e..b2002cd5d9 100644 --- a/main/tests/test_astar.cpp +++ b/main/tests/test_astar.cpp @@ -87,11 +87,145 @@ bool test_abcx() { return ok; } +bool test_add_remove() { + AStar a; + bool ok = true; + + // Manual tests + a.add_point(1, Vector3(0, 0, 0)); + a.add_point(2, Vector3(0, 1, 0)); + a.add_point(3, Vector3(1, 1, 0)); + a.add_point(4, Vector3(2, 0, 0)); + a.connect_points(1, 2, true); + a.connect_points(1, 3, true); + a.connect_points(1, 4, false); + + ok = ok && (a.are_points_connected(2, 1) == true); + ok = ok && (a.are_points_connected(4, 1) == true); + ok = ok && (a.are_points_connected(2, 1, false) == true); + ok = ok && (a.are_points_connected(4, 1, false) == false); + + a.disconnect_points(1, 2, true); + ok = ok && (a.get_point_connections(1).size() == 2); // 3, 4 + ok = ok && (a.get_point_connections(2).size() == 0); + + a.disconnect_points(4, 1, false); + ok = ok && (a.get_point_connections(1).size() == 2); // 3, 4 + ok = ok && (a.get_point_connections(4).size() == 0); + + a.disconnect_points(4, 1, true); + ok = ok && (a.get_point_connections(1).size() == 1); // 3 + ok = ok && (a.get_point_connections(4).size() == 0); + + a.connect_points(2, 3, false); + ok = ok && (a.get_point_connections(2).size() == 1); // 3 + ok = ok && (a.get_point_connections(3).size() == 1); // 1 + + a.connect_points(2, 3, true); + ok = ok && (a.get_point_connections(2).size() == 1); // 3 + ok = ok && (a.get_point_connections(3).size() == 2); // 1, 2 + + a.disconnect_points(2, 3, false); + ok = ok && (a.get_point_connections(2).size() == 0); + ok = ok && (a.get_point_connections(3).size() == 2); // 1, 2 + + a.connect_points(4, 3, true); + ok = ok && (a.get_point_connections(3).size() == 3); // 1, 2, 4 + ok = ok && (a.get_point_connections(4).size() == 1); // 3 + + a.disconnect_points(3, 4, false); + ok = ok && (a.get_point_connections(3).size() == 2); // 1, 2 + ok = ok && (a.get_point_connections(4).size() == 1); // 3 + + a.remove_point(3); + ok = ok && (a.get_point_connections(1).size() == 0); + ok = ok && (a.get_point_connections(2).size() == 0); + ok = ok && (a.get_point_connections(4).size() == 0); + + a.add_point(0, Vector3(0, -1, 0)); + a.add_point(3, Vector3(2, 1, 0)); + // 0: (0, -1) + // 1: (0, 0) + // 2: (0, 1) + // 3: (2, 1) + // 4: (2, 0) + + // Tests for get_closest_position_in_segment + a.connect_points(2, 3); + ok = ok && (a.get_closest_position_in_segment(Vector3(0.5, 0.5, 0)) == Vector3(0.5, 1, 0)); + + a.connect_points(3, 4); + a.connect_points(0, 3); + a.connect_points(1, 4); + a.disconnect_points(1, 4, false); + a.disconnect_points(4, 3, false); + a.disconnect_points(3, 4, false); + // Remaining edges: <2, 3>, <0, 3>, <1, 4> (directed) + ok = ok && (a.get_closest_position_in_segment(Vector3(2, 0.5, 0)) == Vector3(1.75, 0.75, 0)); + ok = ok && (a.get_closest_position_in_segment(Vector3(-1, 0.2, 0)) == Vector3(0, 0, 0)); + ok = ok && (a.get_closest_position_in_segment(Vector3(3, 2, 0)) == Vector3(2, 1, 0)); + + int seed = 0; + + // Random tests for connectivity checks + for (int i = 0; i < 20000; i++) { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + int u = (seed / 5) % 5; + int v = seed % 5; + if (u == v) { + i--; + continue; + } + if (seed % 2 == 1) { + // Add a (possibly existing) directed edge and confirm connectivity + a.connect_points(u, v, false); + ok = ok && (a.are_points_connected(u, v, false) == true); + } else { + // Remove a (possibly nonexistent) directed edge and confirm disconnectivity + a.disconnect_points(u, v, false); + ok = ok && (a.are_points_connected(u, v, false) == false); + } + } + + // Random tests for point removal + for (int i = 0; i < 20000; i++) { + a.clear(); + for (int j = 0; j < 5; j++) + a.add_point(j, Vector3(0, 0, 0)); + + // Add or remove random edges + for (int j = 0; j < 10; j++) { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + int u = (seed / 5) % 5; + int v = seed % 5; + if (u == v) { + j--; + continue; + } + if (seed % 2 == 1) + a.connect_points(u, v, false); + else + a.disconnect_points(u, v, false); + } + + // Remove point 0 + a.remove_point(0); + // White box: this will check all edges remaining in the segments set + for (int j = 1; j < 5; j++) { + ok = ok && (a.are_points_connected(0, j, true) == false); + } + } + + // It's been great work, cheers \(^ ^)/ + return ok; +} + typedef bool (*TestFunc)(void); TestFunc test_funcs[] = { test_abc, test_abcx, + test_add_remove, NULL }; -- cgit v1.2.3 From 0c35994f2f18bb978b931cc1cc7a65c08af5425d Mon Sep 17 00:00:00 2001 From: Shiqing Date: Tue, 23 Jul 2019 12:20:04 +0800 Subject: Add stress test between A* and Floyd-Warshall --- main/tests/test_astar.cpp | 159 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 142 insertions(+), 17 deletions(-) diff --git a/main/tests/test_astar.cpp b/main/tests/test_astar.cpp index b2002cd5d9..4b60a3e94a 100644 --- a/main/tests/test_astar.cpp +++ b/main/tests/test_astar.cpp @@ -31,8 +31,10 @@ #include "test_astar.h" #include "core/math/a_star.h" +#include "core/math/math_funcs.h" #include "core/os/os.h" +#include #include namespace TestAStar { @@ -165,18 +167,14 @@ bool test_add_remove() { ok = ok && (a.get_closest_position_in_segment(Vector3(-1, 0.2, 0)) == Vector3(0, 0, 0)); ok = ok && (a.get_closest_position_in_segment(Vector3(3, 2, 0)) == Vector3(2, 1, 0)); - int seed = 0; + Math::seed(0); // Random tests for connectivity checks for (int i = 0; i < 20000; i++) { - seed = (seed * 1103515245 + 12345) & 0x7fffffff; - int u = (seed / 5) % 5; - int v = seed % 5; - if (u == v) { - i--; - continue; - } - if (seed % 2 == 1) { + int u = Math::rand() % 5; + int v = Math::rand() % 4; + if (u == v) v = 4; + if (Math::rand() % 2 == 1) { // Add a (possibly existing) directed edge and confirm connectivity a.connect_points(u, v, false); ok = ok && (a.are_points_connected(u, v, false) == true); @@ -195,14 +193,10 @@ bool test_add_remove() { // Add or remove random edges for (int j = 0; j < 10; j++) { - seed = (seed * 1103515245 + 12345) & 0x7fffffff; - int u = (seed / 5) % 5; - int v = seed % 5; - if (u == v) { - j--; - continue; - } - if (seed % 2 == 1) + int u = Math::rand() % 5; + int v = Math::rand() % 4; + if (u == v) v = 4; + if (Math::rand() % 2 == 1) a.connect_points(u, v, false); else a.disconnect_points(u, v, false); @@ -220,12 +214,143 @@ bool test_add_remove() { return ok; } +bool test_solutions() { + // Random stress tests with Floyd-Warshall + + const int N = 30; + Math::seed(0); + + for (int test = 0; test < 1000; test++) { + AStar a; + Vector3 p[N]; + bool adj[N][N] = { { false } }; + + // Assign initial coordinates + for (int u = 0; u < N; u++) { + p[u].x = Math::rand() % 100; + p[u].y = Math::rand() % 100; + p[u].z = Math::rand() % 100; + a.add_point(u, p[u]); + } + + // Generate a random sequence of operations + for (int i = 0; i < 1000; i++) { + // Pick two different vertices + int u, v; + u = Math::rand() % N; + v = Math::rand() % (N - 1); + if (u == v) v = N - 1; + + // Pick a random operation + int op = Math::rand(); + switch (op % 9) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + // Add edge (u, v); possibly bidirectional + a.connect_points(u, v, op % 2); + adj[u][v] = true; + if (op % 2) adj[v][u] = true; + break; + case 6: + case 7: + // Remove edge (u, v); possibly bidirectional + a.disconnect_points(u, v, op % 2); + adj[u][v] = false; + if (op % 2) adj[v][u] = false; + break; + case 8: + // Remove point u and add it back; clears adjacent edges and changes coordinates + a.remove_point(u); + p[u].x = Math::rand() % 100; + p[u].y = Math::rand() % 100; + p[u].z = Math::rand() % 100; + a.add_point(u, p[u]); + for (v = 0; v < N; v++) + adj[u][v] = adj[v][u] = false; + break; + } + } + + // Floyd-Warshall + float d[N][N]; + for (int u = 0; u < N; u++) + for (int v = 0; v < N; v++) + d[u][v] = (u == v || adj[u][v]) ? p[u].distance_to(p[v]) : INFINITY; + + for (int w = 0; w < N; w++) + for (int u = 0; u < N; u++) + for (int v = 0; v < N; v++) + if (d[u][v] > d[u][w] + d[w][v]) + d[u][v] = d[u][w] + d[w][v]; + + // Display statistics + int count = 0; + for (int u = 0; u < N; u++) + for (int v = 0; v < N; v++) + if (adj[u][v]) count++; + printf("Test #%4d: %3d edges, ", test + 1, count); + count = 0; + for (int u = 0; u < N; u++) + for (int v = 0; v < N; v++) + if (!Math::is_inf(d[u][v])) count++; + printf("%3d/%d pairs of reachable points\n", count - N, N * (N - 1)); + + // Check A*'s output + bool match = true; + for (int u = 0; u < N; u++) + for (int v = 0; v < N; v++) + if (u != v) { + PoolVector route = a.get_id_path(u, v); + if (!Math::is_inf(d[u][v])) { + // Reachable + if (route.size() == 0) { + printf("From %d to %d: A* did not find a path\n", u, v); + match = false; + goto exit; + } + float astar_dist = 0; + for (int i = 1; i < route.size(); i++) { + if (!adj[route[i - 1]][route[i]]) { + printf("From %d to %d: edge (%d, %d) does not exist\n", + u, v, route[i - 1], route[i]); + match = false; + goto exit; + } + astar_dist += p[route[i - 1]].distance_to(p[route[i]]); + } + if (!Math::is_equal_approx(astar_dist, d[u][v])) { + printf("From %d to %d: Floyd-Warshall gives %.6f, A* gives %.6f\n", + u, v, d[u][v], astar_dist); + match = false; + goto exit; + } + } else { + // Unreachable + if (route.size() > 0) { + printf("From %d to %d: A* somehow found a nonexistent path\n", u, v); + match = false; + goto exit; + } + } + } + + exit: + if (!match) return false; + } + return true; +} + typedef bool (*TestFunc)(void); TestFunc test_funcs[] = { test_abc, test_abcx, test_add_remove, + test_solutions, NULL }; -- cgit v1.2.3