diff --git a/astar.go b/astar.go index 5f1df82..ee8af93 100644 --- a/astar.go +++ b/astar.go @@ -7,6 +7,9 @@ import ( "time" ) +// parentNone marks cells with no predecessor; must not collide with 0–3 (cardinal directions). +const parentNone byte = 0xff + type Item struct { index int // index of the cell in the grid (y * width + x) priority float32 // f = g + h @@ -71,6 +74,9 @@ func (a *AStar) Init(width int, height int) { for i := range a.gScores { a.gScores[i] = math.MaxFloat32 } + for i := range a.parents { + a.parents[i] = parentNone + } } func (a *AStar) ResetGrid(withTypes bool) { @@ -79,7 +85,7 @@ func (a *AStar) ResetGrid(withTypes bool) { a.gridTypes[i] = 0 } a.gScores[i] = math.MaxFloat32 - a.parents[i] = 0 + a.parents[i] = parentNone a.closedSet[i] = false } a.openSet = a.openSet[:0] @@ -93,6 +99,9 @@ func (a *AStar) RebuildGrid(width int, height int) { a.closedSet = make([]bool, width*height) a.width = width a.height = height + for i := range a.parents { + a.parents[i] = parentNone + } } func (a *AStar) SetHeuristic(heuristic int32) { @@ -162,21 +171,30 @@ func (a *AStar) GetParent(x int, y int) byte { return a.parents[y*a.width+x] } +func (a *AStar) GetParents() []byte { + return a.parents +} + func (a *AStar) ParentIndexToXY(childx int, childy int, parent byte) (int, int) { - if parent == 0 { + switch parent { + case 0: return childx - 1, childy // parent left - } else if parent == 1 { + case 1: return childx, childy - 1 // parent above - } else if parent == 2 { + case 2: return childx + 1, childy // parent right - } else if parent == 3 { + case 3: return childx, childy + 1 // parent below + default: + return -1, -1 } - return childx, childy } func (a *AStar) ParentIndexToXYIndex(childx int, childy int, parent byte) int { x, y := a.ParentIndexToXY(childx, childy, parent) + if x < 0 || y < 0 || x >= a.width || y >= a.height { + return -1 + } return y*a.width + x } @@ -235,9 +253,15 @@ func (a *AStar) CalculatePath(startX int, startY int, endX int, endY int) [][]in // 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]) + for currentIndex := current.index; currentIndex != startIndex; { + p := a.parents[currentIndex] + next := a.ParentIndexToXYIndex(currentIndex%a.width, currentIndex/a.width, p) + if next < 0 { + break + } + x, y := a.ParentIndexToXY(currentIndex%a.width, currentIndex/a.width, p) path = append(path, []int{x, y}) + currentIndex = next } return path } @@ -264,3 +288,60 @@ func (a *AStar) CalculatePath(startX int, startY int, endX int, endY int) [][]in } return make([][]int, 0) } + +func (a *AStar) CalculatePathLive(startX int, startY int, endX int, endY int, updateChan chan int) [][]int { + timer := time.Now() + defer func() { + a.timeTaken = time.Since(timer) + }() + 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; { + p := a.parents[currentIndex] + next := a.ParentIndexToXYIndex(currentIndex%a.width, currentIndex/a.width, p) + if next < 0 { + break + } + x, y := a.ParentIndexToXY(currentIndex%a.width, currentIndex/a.width, p) + path = append(path, []int{x, y}) + currentIndex = next + } + return path + } + + a.closedSet[current.index] = true + updateChan <- current.index + + for _, neighborIndex := range a.GetNeighbors(current.index%a.width, current.index/a.width) { + if a.closedSet[neighborIndex] { + continue + } + if a.gridTypes[neighborIndex] == 1 { + a.gScores[neighborIndex] = math.MaxFloat32 + 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 1c3ad36..ce55eef 100644 --- a/main.go +++ b/main.go @@ -315,11 +315,16 @@ func main() { defer rl.UnloadTexture(mapTexture) defer rl.UnloadImage(mapImage) + updateChan := make(chan int, 100000) + autoCompute := false astar := AStar{} astar.Init(width, height) + // Above the channel loop + lastEvaluatedNode := -1 // Track the "tip of the spear" + for !rl.WindowShouldClose() { screenWidth := float32(rl.GetScreenWidth()) screenHeight := float32(rl.GetScreenHeight()) @@ -459,6 +464,30 @@ func main() { } lastMousePos = rl.NewVector2(-1, -1) } + updatesProcessed := 0 + DrainLoop: // Use a label so we can break out of the infinite 'for' loop + for { + select { + case nodeIndex := <-updateChan: + x := nodeIndex % width + y := nodeIndex / width + if x != int(startPos.X) || y != int(startPos.Y) { + // Bake the blue pixel into the image + rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 0, 255, 255)) + tex.markRegion(x, y, x, y, width, height) + } + + lastEvaluatedNode = nodeIndex // Save the absolute latest node + updatesProcessed++ + + if updatesProcessed > 50000 { + break DrainLoop + } + default: + // Channel is empty + break DrainLoop + } + } // --- DRAWING --- rl.BeginDrawing() @@ -475,6 +504,42 @@ func main() { cellSize, // Scale factor rl.White, // Tint (White means no tint) ) + + // 2. Trace parent chain and draw a yellow polyline (cell centers) each frame. + if lastEvaluatedNode != -1 { + startIndex := int(startPos.Y)*width + int(startPos.X) + parents := astar.GetParents() + pathThickness := float32(10.0) / (camera.Zoom / 0.75) + if pathThickness < 1 { + pathThickness = 1 + } + pathColor := rl.NewColor(255, 255, 0, 255) + + var centers []rl.Vector2 + for idx := lastEvaluatedNode; idx >= 0 && idx < width*height && idx != startIndex; { + cx := (float32(idx%width) + 0.5) * cellSize + cy := (float32(idx/width) + 0.5) * cellSize + centers = append(centers, rl.NewVector2(cx, cy)) + + if idx == startIndex { + break + } + p := parents[idx] + if p == parentNone { + break + } + next := astar.ParentIndexToXYIndex(idx%width, idx/width, p) + if next < 0 || next == idx { + break + } + idx = next + } + + for i := 0; i < len(centers)-1; i++ { + rl.DrawLineEx(centers[i], centers[i+1], pathThickness, pathColor) + } + } + drawInfiniteGridLines(camera, canvasWidth, screenHeight, cellSize, width, height) rl.EndMode2D() rl.EndScissorMode() @@ -550,6 +615,7 @@ func main() { if !toolDropdownOpen && !heuristicDropdownOpen { if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (200*scale), (180*scale), (30*scale)), "Reset Visualization") { astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating + lastEvaluatedNode = -1 gridTypes := astar.GetGridTypes() for i, gridType := range gridTypes { // reset the map image @@ -568,6 +634,34 @@ func main() { } } + if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(80*scale)), (180*scale), (30*scale)), "Calculate Path (Live)") { + if int(startPos.X) < 0 || int(startPos.X) >= width || int(startPos.Y) < 0 || int(startPos.Y) >= height || int(endPos.X) < 0 || int(endPos.X) >= width || int(endPos.Y) < 0 || int(endPos.Y) >= height { + posError = true + } else { + astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating + astar.SetHeuristic(activeHeuristic) + lastEvaluatedNode = -1 + gridTypes := astar.GetGridTypes() + for i, gridType := range gridTypes { + // reset the map image + switch gridType { + case 0: + rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255)) + case 1: + rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255)) + case 2: + rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255)) + case 3: + rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255)) + } + } + tex.markFull() + go func() { + astar.CalculatePathLive(int(startPos.X), int(startPos.Y), int(endPos.X), int(endPos.Y), updateChan) + }() + } + } + // AutoCompute autoCompute = rg.CheckBox(rl.NewRectangle(sidebarX+(160*scale), (screenHeight-(40*scale)), (30*scale), (30*scale)), "", autoCompute) // no title because it would overlap the button @@ -578,6 +672,7 @@ func main() { } else { astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating astar.SetHeuristic(activeHeuristic) + lastEvaluatedNode = -1 gridTypes := astar.GetGridTypes() for i, gridType := range gridTypes { // reset the map image