From 11af5422c4dd27a59261d710dca5d290fab75ef9 Mon Sep 17 00:00:00 2001 From: codegirl-007 Date: Mon, 6 Jul 2026 00:09:04 -0700 Subject: [PATCH] pathfinding with astar --- entity.odin | 2 +- pathfinding.odin | 123 +++++++++++++++++++++++++++++++++++++++++++++++ tilemap.odin | 35 +++++++++++++- 3 files changed, 157 insertions(+), 3 deletions(-) diff --git a/entity.odin b/entity.odin index 545125e..0a19d15 100644 --- a/entity.odin +++ b/entity.odin @@ -82,7 +82,7 @@ entity_set_move_target :: proc( } start := ew.position[i] - if !pathfind_bfs(tm, start, target, &ew.path[i]) { + if !pathfind_astar(tm, start, target, &ew.path[i]) { return false } ew.path_index[i] = 1 diff --git a/pathfinding.odin b/pathfinding.odin index a013d9b..b8160bc 100644 --- a/pathfinding.odin +++ b/pathfinding.odin @@ -5,6 +5,8 @@ Path :: struct { count: int, } +max_path_int :: 1_000_000_000 + path_clear :: proc(path: ^Path) { delete(path.tiles) path.tiles = nil @@ -15,6 +17,7 @@ pathfind_bfs :: proc(tilemap: ^Tilemap, start, goal: Tile_Coord, path: ^Path) -> path_clear(path) if start == goal { + path.tiles = make([]Tile_Coord, 1) // allocate because path_clear set tiles to nil path.tiles[0] = start path.count = 1 return true @@ -91,3 +94,123 @@ pathfind_bfs :: proc(tilemap: ^Tilemap, start, goal: Tile_Coord, path: ^Path) -> return true } +heuristic :: proc(a, b: Tile_Coord) -> int { + return abs(a.x - b.x) + abs(a.y - b.y) +} + +pathfind_astar :: proc(tm: ^Tilemap, start, goal: Tile_Coord, path: ^Path) -> bool { + path_clear(path) + + if start == goal { + path.tiles = make([]Tile_Coord, 1) + path.tiles[0] = start + path.count = 1 + return true + } + + if !tile_walkable(tm, goal.x, goal.y) { + return false + } + + w, h := tm.width, tm.height + + g_score: [MAP_HEIGHT][MAP_WIDTH]int + f_score: [MAP_HEIGHT][MAP_WIDTH]int + parents: [MAP_HEIGHT][MAP_WIDTH]Tile_Coord + in_open: [MAP_HEIGHT][MAP_WIDTH]bool + closed: [MAP_HEIGHT][MAP_WIDTH]bool + + for y in 0 ..< h { + for x in 0 ..< w { + g_score[y][x] = max_path_int + f_score[y][x] = max_path_int + parents[y][x] = {-1, -1} + } + } + + g_score[start.y][start.x] = 0 + f_score[start.y][start.x] = heuristic(start, goal) + parents[start.y][start.x] = start + in_open[start.y][start.x] = true + + open_list: [MAP_WIDTH * MAP_HEIGHT]Tile_Coord + open_count := 1 + open_list[0] = start + + found := false + + for open_count > 0 { + best_i := 0 + best_f := max_path_int + for i in 0 ..< open_count { + c := open_list[i] + if f_score[c.y][c.x] < best_f { + best_f = f_score[c.y][c.x] + best_i = i + } + } + + current := open_list[best_i] + open_list[best_i] = open_list[open_count - 1] + open_count -= 1 + in_open[current.y][current.x] = false + closed[current.y][current.x] = true + + if current == goal { + found = true + break + } + + for offset in NEIGHBOR_OFFSET { + nx := current.x + offset.x + ny := current.y + offset.y + if !tile_in_bounds(tm, nx, ny) || closed[ny][nx] { + continue + } + + step_cost := tile_cost(tm, nx, ny) + if step_cost >= max_path_int { + continue + } + + tentative := g_score[current.y][current.x] + step_cost + if tentative < g_score[ny][nx] { + parents[ny][nx] = current + g_score[ny][nx] = tentative + f_score[ny][nx] = tentative + heuristic({nx, ny}, goal) + + if !in_open[ny][nx] { + in_open[ny][nx] = true + open_list[open_count] = {nx, ny} + open_count += 1 + } + } + } + } + if !found { + return false + } + + rev: [MAX_PATH]Tile_Coord + rev_count := 0 + cur := goal + for { + rev[rev_count] = cur + rev_count += 1 + if cur == start { + break + } + cur = parents[cur.y][cur.x] // walk backward through the parent chain toward start + if rev_count >= MAX_PATH { + return false + } + } + + path.tiles = make([]Tile_Coord, rev_count) // allocate output slice since path_clear left it nil + path.count = rev_count + for i in 0 ..< rev_count { + path.tiles[i] = rev[rev_count - 1 - i] // reverse the collected tiles so order is start -> goal + } + return true +} + diff --git a/tilemap.odin b/tilemap.odin index 62a999b..38f70d9 100644 --- a/tilemap.odin +++ b/tilemap.odin @@ -15,8 +15,17 @@ tilemap_init :: proc() -> Tilemap { tiles = make([]u8, MAP_WIDTH * MAP_HEIGHT), } // test wall - for x in 15 ..< 25 { - tm.tiles[tile_index(&tm, x, 12)] = 1 + // Horizontal mud strip (expensive, cost 20 per step) + for y in 8 ..< 18 { + tm.tiles[tile_index(&tm, 5, y)] = 3 // mud + } + // Road detour (cheap, cost 1 per step) — goes right, down, left + for x in 5 ..< 12 { + tm.tiles[tile_index(&tm, x, 8)] = 2 // road top + tm.tiles[tile_index(&tm, x, 18)] = 2 // road bottom + } + for y in 8 ..< 19 { + tm.tiles[tile_index(&tm, 11, y)] = 2 // road right side } return tm } @@ -39,6 +48,12 @@ tile_color :: proc(type_id: u8) -> rl.Color { switch type_id { case 0: return {72, 112, 68, 255} + case 1: + return {90, 90, 95, 255} // wall — gray + case 2: + return {160, 140, 100, 255} // road — tan/brown + case 3: + return {120, 100, 70, 255} // mud — darker brown case: return rl.MAGENTA } @@ -87,3 +102,19 @@ tile_walkable :: proc(tilemap: ^Tilemap, x, y: int) -> bool { return tilemap.tiles[idx] != 1 } +tile_cost :: proc(tm: ^Tilemap, x, y: int) -> int { + if !tile_walkable(tm, x, y) { + return max_path_int + } + switch tm.tiles[tile_index(tm, x, y)] { + case 0: + return 10 + case 2: + return 1 + case 3: + return 20 + case: + return 10 + } +} +