From 5b4887f0cc4b3d01eab305ad75f694a7659bc891 Mon Sep 17 00:00:00 2001 From: wisplite Date: Thu, 14 May 2026 09:13:32 -0500 Subject: [PATCH] working a* --- astar.go | 215 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 19 +++++ 2 files changed, 234 insertions(+) create mode 100644 astar.go diff --git a/astar.go b/astar.go new file mode 100644 index 0000000..05dd197 --- /dev/null +++ b/astar.go @@ -0,0 +1,215 @@ +package main + +import ( + "container/heap" + "fmt" + "math" +) + +type Item struct { + index int // index of the cell in the grid (y * width + x) + priority float32 // f = g + h + gScore float32 // used to break f ties toward straighter paths +} + +type PriorityQueue []*Item + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less(i, j int) bool { + if pq[i].priority != pq[j].priority { + return pq[i].priority < pq[j].priority + } + // Same f: prefer larger g (smaller h → closer to goal) for cleaner grid paths. + return pq[i].gScore > pq[j].gScore +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + *pq = append(*pq, x.(*Item)) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + old[n-1] = nil + *pq = old[0 : n-1] + return item +} + +type AStar struct { + gridTypes []byte + gScores []float32 + parents []byte + openSet PriorityQueue + closedSet []bool + width int + height int + heuristic func(x int, y int, endX int, endY int) float32 +} + +func (a *AStar) Init(width int, height int) { + a.gridTypes = make([]byte, width*height) + a.gScores = make([]float32, width*height) + a.parents = make([]byte, width*height) + a.openSet = make(PriorityQueue, 0) + a.closedSet = make([]bool, width*height) + a.heuristic = func(x int, y int, endX int, endY int) float32 { + return float32(math.Abs(float64(x-endX)) + math.Abs(float64(y-endY))) // Manhattan distance default + } + a.width = width + a.height = height + + for i := range a.gScores { + a.gScores[i] = math.MaxFloat32 + } +} + +func (a *AStar) ResetGrid() { + for i := range a.gScores { + a.gridTypes[i] = 0 + a.gScores[i] = math.MaxFloat32 + a.parents[i] = 0 + a.closedSet[i] = false + } + a.openSet = a.openSet[:0] +} + +func (a *AStar) RebuildGrid(width int, height int) { + a.gridTypes = make([]byte, width*height) + a.gScores = make([]float32, width*height) + a.parents = make([]byte, width*height) + a.openSet = make(PriorityQueue, 0) + a.closedSet = make([]bool, width*height) + a.width = width + a.height = height +} + +func (a *AStar) SetGridType(x int, y int, gridType byte) { + /* + 0 = empty + 1 = wall + 2 = start + 3 = end + */ + a.gridTypes[y*a.width+x] = gridType +} + +func (a *AStar) GetGridType(x int, y int) byte { + return a.gridTypes[y*a.width+x] +} + +func (a *AStar) SetGScores(x int, y int, gScore float32) { + a.gScores[y*a.width+x] = gScore +} + +func (a *AStar) GetGScores(x int, y int) float32 { + return a.gScores[y*a.width+x] +} + +func (a *AStar) SetParent(x int, y int, parentx int, parenty int) { + if parentx < x { + a.parents[y*a.width+x] = byte(0) // left of the node + } else if parentx > x { + a.parents[y*a.width+x] = byte(2) // right of the node + } else if parenty < y { + a.parents[y*a.width+x] = byte(1) // above the node + } else if parenty > y { + a.parents[y*a.width+x] = byte(3) // below the node + } +} + +func (a *AStar) GetParent(x int, y int) byte { + return a.parents[y*a.width+x] +} + +func (a *AStar) ParentIndexToXY(childx int, childy int, parent byte) (int, int) { + if parent == 0 { + return childx - 1, childy // parent left + } else if parent == 1 { + return childx, childy - 1 // parent above + } else if parent == 2 { + return childx + 1, childy // parent right + } else if parent == 3 { + return childx, childy + 1 // parent below + } + return childx, childy +} + +func (a *AStar) ParentIndexToXYIndex(childx int, childy int, parent byte) int { + x, y := a.ParentIndexToXY(childx, childy, parent) + return y*a.width + x +} + +func (a *AStar) GetNeighbors(x int, y int) []int { + neighbors := make([]int, 0) + if x > 0 { + neighbors = append(neighbors, y*a.width+x-1) + } + if x < a.width-1 { + neighbors = append(neighbors, y*a.width+x+1) + } + if y > 0 { + neighbors = append(neighbors, y*a.width+x-a.width) + } + if y < a.height-1 { + neighbors = append(neighbors, y*a.width+x+a.width) + } + return neighbors +} + +func (a *AStar) GetTerrainCost(x int, y int) float32 { + return 1.0 +} + +func (a *AStar) CalculatePath(startX int, startY int, endX int, endY int) [][]int { + startIndex := startY*a.width + startX + endIndex := endY*a.width + endX + a.gScores[startIndex] = 0 + startF := a.heuristic(startX, startY, endX, endY) + heap.Push(&a.openSet, &Item{index: startIndex, priority: startF, gScore: 0}) + + for a.openSet.Len() > 0 { + current := heap.Pop(&a.openSet).(*Item) + if a.closedSet[current.index] { + continue + } + if current.index == endIndex { + // We've found the goal! + fmt.Println("Found the goal!") + path := make([][]int, 0) + for currentIndex := current.index; currentIndex != startIndex; currentIndex = a.ParentIndexToXYIndex(currentIndex%a.width, currentIndex/a.width, a.parents[currentIndex]) { + x, y := a.ParentIndexToXY(currentIndex%a.width, currentIndex/a.width, a.parents[currentIndex]) + path = append(path, []int{x, y}) + } + // path = append(path, []int{startX, startY}) preserve start position + return path + } + + a.closedSet[current.index] = true + + for _, neighborIndex := range a.GetNeighbors(current.index%a.width, current.index/a.width) { + if a.closedSet[neighborIndex] { + continue + } + if a.gridTypes[neighborIndex] == 1 { + continue + } + terrainCost := a.GetTerrainCost(neighborIndex%a.width, neighborIndex/a.width) + tentativeGScore := a.gScores[current.index] + terrainCost + if tentativeGScore < a.gScores[neighborIndex] { + a.SetParent(neighborIndex%a.width, neighborIndex/a.width, current.index%a.width, current.index/a.width) + a.gScores[neighborIndex] = tentativeGScore + priority := tentativeGScore + a.heuristic(neighborIndex%a.width, neighborIndex/a.width, endX, endY) + heap.Push(&a.openSet, &Item{index: neighborIndex, priority: priority, gScore: tentativeGScore}) + } + } + } + return make([][]int, 0) +} diff --git a/main.go b/main.go index 05a84a1..bb758c8 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "image/color" "strconv" "strings" @@ -95,6 +96,9 @@ func main() { defer rl.UnloadTexture(mapTexture) defer rl.UnloadImage(mapImage) + astar := AStar{} + astar.Init(width, height) + for !rl.WindowShouldClose() { screenWidth := float32(rl.GetScreenWidth()) screenHeight := float32(rl.GetScreenHeight()) @@ -150,6 +154,7 @@ func main() { prevX := int(lastMousePos.X / cellSize) prevY := int(lastMousePos.Y / cellSize) rl.ImageDrawLine(mapImage, int32(prevX), int32(prevY), int32(x), int32(y), rl.NewColor(0, 0, 0, 255)) + astar.SetGridType(x, y, 1) textureNeedsUpdate = true } case 1: // Start — must run on first paint frame (lastMousePos may be -1 after release) @@ -159,6 +164,7 @@ func main() { } rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 255, 0, 255)) startPos = rl.NewVector2(float32(x), float32(y)) + astar.SetGridType(x, y, 2) textureNeedsUpdate = true } case 2: // End @@ -168,6 +174,7 @@ func main() { } rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(255, 0, 0, 255)) endPos = rl.NewVector2(float32(x), float32(y)) + astar.SetGridType(x, y, 3) textureNeedsUpdate = true } } @@ -243,6 +250,7 @@ func main() { rl.UnloadImage(mapImage) mapImage = rl.GenImageColor(width, height, rl.NewColor(240, 240, 240, 255)) mapTexture = rl.LoadTextureFromImage(mapImage) + astar.RebuildGrid(width, height) } } @@ -251,6 +259,17 @@ func main() { if rg.DropdownBox(rl.NewRectangle(sidebarX+(10*scale), (100*scale), (180*scale), (30*scale)), toolOptionsText, &activeTool, toolDropdownOpen) { toolDropdownOpen = !toolDropdownOpen } + + // Calculate Path Button + if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(40*scale)), (180*scale), (30*scale)), "Calculate Path") { + path := astar.CalculatePath(int(startPos.X), int(startPos.Y), int(endPos.X), int(endPos.Y)) + fmt.Println(path) + for _, p := range path { + rl.ImageDrawPixel(mapImage, int32(p[0]), int32(p[1]), rl.NewColor(255, 255, 0, 255)) + } + textureNeedsUpdate = true + } + rl.EndDrawing() } }