From 2b51443179361377e2ac545cfdf7bbf34abac491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20K=C3=BCderli?= Date: Mon, 27 Oct 2025 22:52:38 +0100 Subject: [PATCH] full visualization ready :))) --- include/astar.h | 8 +-- include/visual.h | 2 + src/astar.c | 135 +++++++++++++++++++++++++++++++++-------------- src/main.c | 58 ++++++-------------- src/visual.c | 103 ++++++++++++++++++++++++++++++++++++ target | Bin 20648 -> 21440 bytes 6 files changed, 221 insertions(+), 85 deletions(-) create mode 100644 include/visual.h create mode 100644 src/visual.c diff --git a/include/astar.h b/include/astar.h index f96e2e6..850f525 100644 --- a/include/astar.h +++ b/include/astar.h @@ -25,9 +25,11 @@ typedef struct AField { afield_t generate_astar_field(int rows, int columns, int** col_map); -void get_a_star_path(afield_t* field, Vector2 start, Vector2 goal); +Vector2* get_a_star_path(afield_t *field, Vector2 start, Vector2 goal, int max_searches, int* path_length); -void setup_start_field(afield_t* field, Vector2 start, Vector2 target); +Vector2* do_full_a_star_search(int width, int height, int** collisions, Vector2 start, Vector2 target, int max_searches, int* path_len); + +void setup_start_cell(afield_t* field, Vector2 start, Vector2 target); Vector2 get_best_node(afield_t *field); void collapse_node_at(afield_t *field, Vector2 node_pos); @@ -35,6 +37,6 @@ int assign_node_as_parent(afield_t *field, Vector2 target, Vector2 node_pos); void debug_print_field_collisions(afield_t* field); -void debug_print_field(afield_t* field, bool show_parent); +void debug_print_field(afield_t* field); int adjusted_euclidean(Vector2 a, Vector2 b); diff --git a/include/visual.h b/include/visual.h new file mode 100644 index 0000000..4d0bd6d --- /dev/null +++ b/include/visual.h @@ -0,0 +1,2 @@ + +int run_visual(void); diff --git a/src/astar.c b/src/astar.c index 944b7cb..6232169 100644 --- a/src/astar.c +++ b/src/astar.c @@ -41,6 +41,17 @@ afield_t generate_astar_field(int rows, int columns, int** col_map) { return field; } +void free_field(afield_t* field) { + if (!field || !field->map) + return; + + for (int i = 0; i < field->rows; i++) { + free(field->map[i]); + } + free(field->map); + field->map = NULL; +} + int adjusted_euclidean(Vector2 a, Vector2 b) { int dx = b.x - a.x; int dy = b.y - a.y; @@ -182,40 +193,91 @@ Vector2 get_best_node(afield_t *field) { return best_pos; } -void get_a_star_path(afield_t *field, Vector2 start, Vector2 goal) { - // TODO: check if start or end node is obstacle +Vector2* reconstruct_path(afield_t* field, Vector2 start, Vector2 end, int *out_length) { + // entirely chatgpt i had no motivation left + int rows = field->rows; + int columns = field->columns; + anode_t** map = field->map; - if (start.x == goal.x && start.y == goal.y) { - // no path required - return; // TODO: Return valid empty path - } + int max_len = rows * columns; // safe upper bound + Vector2 *path = malloc(max_len * sizeof(Vector2)); + if (!path) return NULL; - // setup start node - anode_t *start_node_p = &field->map[(int)start.y][(int)start.x]; - start_node_p->g = 0; - int heuristic = octile_heuristic(start, goal); - start_node_p->h = heuristic; - start_node_p->f = heuristic; - start_node_p->is_collapsed = true; + int count = 0; + Vector2 current = end; - // set values of surrounding nodes - assign_node_as_parent(field, goal, start); + while (!(current.x == start.x && current.y == start.y)) { + path[count++] = current; - bool found_target = false; - while (!found_target) { - // get node with best f score - Vector2 best = get_best_node(field); - if (best.x == -1 && best.y == -1) { - printf("Target can not be reached!!"); - } - // collase node with best f score and recalculate children - collapse_node_at(field, best); - assign_node_as_parent(field, goal, best); - // repeat until child is target - } + int px = (int)current.x; + int py = (int)current.y; + + // bounds check + if (px < 0 || px >= columns || py < 0 || py >= rows) break; + + current = map[py][px].parent; + + if (count >= max_len) break; // prevent infinite loops + } + + path[count++] = start; + + // reverse path so it goes start -> end + for (int i = 0; i < count / 2; i++) { + Vector2 tmp = path[i]; + path[i] = path[count - 1 - i]; + path[count - 1 - i] = tmp; + } + + *out_length = count; + return path; } -void setup_start_field(afield_t* field, Vector2 start, Vector2 target) { +Vector2* get_a_star_path(afield_t *field, Vector2 start, Vector2 goal, int max_searches, int* path_length) { + setup_start_cell(field, start, goal); + collapse_node_at(field, start); + + assign_node_as_parent(field, goal, start); + debug_print_field(field); + + for (int x = 0; x < max_searches; x++) { + Vector2 best = get_best_node(field); + if (best.x == -1 || best.y == -1) { + // all search paths exausted + debug_print_field(field); + printf("TARGET NOT REACHABLE!\n"); + + *path_length = 0; + return NULL; + } + // printf("Best node: %d %d\n", (int)best.x, (int)best.y); + + int found = assign_node_as_parent(field, goal, best); + collapse_node_at(field, best); + // printf("collapsed node at: %d %d", (int)best.x, (int)best.y); + + if (found) { + // debug_print_field(field); + printf("FOUND TARGET!\n"); + return reconstruct_path(field, start, goal, path_length); + } + } + + printf("All searches wasted, target not found, quitting!\n"); + *path_length = 0; + return NULL; +} + +Vector2* do_full_a_star_search(int width, int height, int** collisions, Vector2 start, Vector2 target, int max_searches, int* path_len) { + afield_t field = generate_astar_field(height, width, collisions); + + Vector2* path = get_a_star_path(&field, start, target, max_searches, path_len); + + free_field(&field); + return path; +} + +void setup_start_cell(afield_t* field, Vector2 start, Vector2 target) { int heuristic = octile_heuristic(start, target); field->map[(int)start.y][(int)start.x].g = 0; @@ -234,7 +296,7 @@ void debug_print_field_collisions(afield_t* field) { printf("\n"); } -void debug_print_field(afield_t* field, bool show_parent) { +void debug_print_field(afield_t* field) { char* arrows[3][3] = { {"↖", "↑", "↗"}, {"←", "•", "→"}, @@ -252,18 +314,13 @@ void debug_print_field(afield_t* field, bool show_parent) { // printf("[]"); // } else { - if (show_parent) { - Vector2 parent = field->map[r][c].parent; - if (parent.x != -1 && field->map[r][c].is_collapsed) { - Vector2 par_offs = Vector2Subtract(parent, (Vector2) {c, r}); - printf("%s", arrows[(int)par_offs.y+1][(int)par_offs.x+1]); - } - else { - printf(" "); - } + Vector2 parent = field->map[r][c].parent; + if (parent.x != -1 && field->map[r][c].is_collapsed) { + Vector2 par_offs = Vector2Subtract(parent, (Vector2) {c, r}); + printf("%s", arrows[(int)par_offs.y+1][(int)par_offs.x+1]); } else { - printf("%2i", field->map[r][c].f); + printf(" "); } } printf("| "); diff --git a/src/main.c b/src/main.c index ee9c545..534c9ab 100644 --- a/src/main.c +++ b/src/main.c @@ -4,6 +4,7 @@ #include #include "../include/astar.h" +#include "../include/visual.h" int** create_array(int x, int y) { int rows = y, cols = x; @@ -12,22 +13,9 @@ int** create_array(int x, int y) { myArray[i] = malloc(cols * sizeof(int)); } - int values[10][10] = { - {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, - {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, - {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, - {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, - {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, - {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, - {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, - {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, - {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, - {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1} - }; - for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { - myArray[i][j] = values[i][j]; + myArray[i][j] = -1; } } @@ -35,36 +23,20 @@ int** create_array(int x, int y) { } int main() { - int** dyn_array = create_array(10, 10); - afield_t field = generate_astar_field(10, 10, dyn_array); + run_visual(); - Vector2 start = { 0, 0 }; - Vector2 goal = { 9, 0 }; - - setup_start_field(&field, start, goal); - collapse_node_at(&field, start); - - assign_node_as_parent(&field, goal, start); - debug_print_field(&field, true); - - for (int x = 0; x < 100; x++) { - Vector2 best = get_best_node(&field); - if (best.x == -1 || best.y == -1) { - // all search paths exausted - debug_print_field(&field, true); - printf("TARGET NOT REACHABLE!\n"); - return 0; - } - // printf("Best node: %d %d\n", (int)best.x, (int)best.y); + // int** collision = create_array(20, 20); + // afield_t field = generate_astar_field(20, 20, collision); - int found = assign_node_as_parent(&field, goal, best); - collapse_node_at(&field, best); - // printf("collapsed node at: %d %d", (int)best.x, (int)best.y); + // Vector2 start = { 0, 0 }; + // Vector2 goal = { 19, 19 }; + + // int path_len = -1; + // Vector2* path = do_full_a_star_search(20, 20, collision, start, goal, 100, &path_len); + // if (!path) { printf("no path"); return 0; } - if (found) { - debug_print_field(&field, true); - printf("FOUND TARGET!\n"); - return 0; - } - } + // for (int x = 0; x < path_len; x++) { + // printf("node at [%d|%d]\n", (int)path[x].x, (int)path[x].y); + // } + // free(path); } diff --git a/src/visual.c b/src/visual.c new file mode 100644 index 0000000..a43c79e --- /dev/null +++ b/src/visual.c @@ -0,0 +1,103 @@ +#include +#include +#include +#include + +#include "../include/astar.h" + +#define GRID_SIZE 20 + +int run_visual(void) { + const int screenWidth = 800; + const int screenHeight = 800; + + const int cellSize = screenWidth / GRID_SIZE; + + int path_len = -1; + Vector2* path = NULL; + + InitWindow(screenWidth, screenHeight, "A*"); + + bool grid[GRID_SIZE][GRID_SIZE] = {0}; + + SetTargetFPS(60); + + while (!WindowShouldClose()) { + // Handle mouse clicks + if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { + Vector2 mousePos = GetMousePosition(); + int x = mousePos.x / cellSize; + int y = mousePos.y / cellSize; + + if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE) { + grid[y][x] = true; + } + } + if (IsMouseButtonDown(MOUSE_RIGHT_BUTTON)) { + Vector2 mousePos = GetMousePosition(); + int x = mousePos.x / cellSize; + int y = mousePos.y / cellSize; + + if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE) { + grid[y][x] = false; + } + } + if (IsKeyDown(KEY_SPACE)) { + int **collision = malloc(GRID_SIZE * sizeof(int*)); + for(int i = 0; i < GRID_SIZE; i++) + collision[i] = malloc(GRID_SIZE * sizeof(int)); + + + for(int i = 0; i < GRID_SIZE; i++) { + for(int j = 0; j < GRID_SIZE; j++) { + collision[i][j] = grid[i][j] ? 8 : -1; + } + } + + afield_t field = generate_astar_field(GRID_SIZE, GRID_SIZE, collision); + + Vector2 start = { 0, 0 }; + Vector2 goal = { GRID_SIZE-1, GRID_SIZE-1 }; + + if (path) { free(path); } + path = do_full_a_star_search(GRID_SIZE, GRID_SIZE, collision, start, goal, 1000, &path_len); + + if (!path) { printf("no path"); } + } + + BeginDrawing(); + ClearBackground(RAYWHITE); + + // Draw grid + for (int y = 0; y < GRID_SIZE; y++) { + for (int x = 0; x < GRID_SIZE; x++) { + bool cell_in_path = false; + if (path) { + for (int i = 0; i < path_len; i++) { + if (path[i].x == x && path[i].y == y) { + cell_in_path = true; + } + } + } + + Rectangle cell = { x * cellSize, y * cellSize, cellSize, cellSize }; + + if (cell_in_path) { + DrawRectangleRec(cell, RED); + } + + if (grid[y][x]) { + DrawRectangleRec(cell, DARKBLUE); + } + + DrawRectangleLines(cell.x, cell.y, cell.width, cell.height, GRAY); + } + } + + EndDrawing(); + } + + CloseWindow(); + return 0; +} + diff --git a/target b/target index 24823b995c3cbbb0b107ea62ff704e581deaba90..0ed9c467c7393ae4ea054deed554cef91cc4e1e9 100755 GIT binary patch literal 21440 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 delta 5336 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 RKe_0EuwjZuj=*{){2z}WQmp_0