mirror of
https://github.com/wisplite/a-star-go.git
synced 2026-06-27 15:37:07 -05:00
Compare commits
23 Commits
3a74dcac7c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 17fbabc3e2 | |||
| cd08eebdbd | |||
| cfe855944b | |||
| fc3631c6a0 | |||
| 9ac99cf488 | |||
| 754ea5bd18 | |||
| 50db827e57 | |||
| 1291fff451 | |||
| bb85e11895 | |||
| 06f36a625f | |||
| c20f98cf90 | |||
| 260c39eedc | |||
| 60a28cea88 | |||
| 589663d3d0 | |||
| 6719f70c73 | |||
| 85855f00ff | |||
| 5b4887f0cc | |||
| 29bce85e82 | |||
| 5b4c246b4f | |||
| 48f889bd44 | |||
| 910f0e56cf | |||
| da36fcd0ba | |||
| b5a03b2461 |
@@ -0,0 +1,373 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"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
|
||||
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
|
||||
parentsMu sync.RWMutex // live preview reads parents from the UI thread while CalculatePathLive writes
|
||||
openSet PriorityQueue
|
||||
closedSet []bool
|
||||
width int
|
||||
height int
|
||||
heuristic func(x int, y int, endX int, endY int) float32
|
||||
timeTaken time.Duration
|
||||
}
|
||||
|
||||
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, 2000000) // pre-allocate space for 2 million cells to avoid reallocations
|
||||
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
|
||||
}
|
||||
a.parentsMu.Lock()
|
||||
for i := range a.parents {
|
||||
a.parents[i] = parentNone
|
||||
}
|
||||
a.parentsMu.Unlock()
|
||||
}
|
||||
|
||||
func (a *AStar) ResetGrid(withTypes bool) {
|
||||
for i := range a.gScores {
|
||||
if withTypes {
|
||||
a.gridTypes[i] = 0
|
||||
}
|
||||
a.gScores[i] = math.MaxFloat32
|
||||
a.closedSet[i] = false
|
||||
}
|
||||
a.parentsMu.Lock()
|
||||
for i := range a.parents {
|
||||
a.parents[i] = parentNone
|
||||
}
|
||||
a.parentsMu.Unlock()
|
||||
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.parentsMu.Lock()
|
||||
a.parents = make([]byte, width*height)
|
||||
a.openSet = make(PriorityQueue, 0, 2000000)
|
||||
a.closedSet = make([]bool, width*height)
|
||||
a.width = width
|
||||
a.height = height
|
||||
for i := range a.parents {
|
||||
a.parents[i] = parentNone
|
||||
}
|
||||
a.parentsMu.Unlock()
|
||||
}
|
||||
|
||||
func (a *AStar) SetHeuristic(heuristic int32) {
|
||||
switch heuristic {
|
||||
case 0:
|
||||
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
|
||||
}
|
||||
case 1:
|
||||
a.heuristic = func(x int, y int, endX int, endY int) float32 {
|
||||
return float32(math.Sqrt(float64(x-endX)*float64(x-endX) + float64(y-endY)*float64(y-endY))) // Euclidean distance
|
||||
}
|
||||
case 2:
|
||||
a.heuristic = func(x int, y int, endX int, endY int) float32 {
|
||||
return float32(math.Max(math.Abs(float64(x-endX)), math.Abs(float64(y-endY)))) // Chebyshev: max(|dx|, |dy|)
|
||||
}
|
||||
case 3:
|
||||
a.heuristic = func(x int, y int, endX int, endY int) float32 {
|
||||
return float32(math.Pow(float64(x-endX), 2) + math.Pow(float64(y-endY), 2)) // Squared Euclidean (extremely fast but not optimal/inadmissible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) GetGridTypes() []byte {
|
||||
return a.gridTypes
|
||||
}
|
||||
|
||||
func (a *AStar) GetClosedSet() []bool {
|
||||
return a.closedSet
|
||||
}
|
||||
|
||||
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) {
|
||||
a.parentsMu.Lock()
|
||||
defer a.parentsMu.Unlock()
|
||||
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 {
|
||||
a.parentsMu.RLock()
|
||||
defer a.parentsMu.RUnlock()
|
||||
return a.parents[y*a.width+x]
|
||||
}
|
||||
|
||||
func (a *AStar) ParentsSnapshot() []byte {
|
||||
a.parentsMu.RLock()
|
||||
defer a.parentsMu.RUnlock()
|
||||
out := make([]byte, len(a.parents))
|
||||
copy(out, a.parents)
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *AStar) ParentIndexToXY(childx int, childy int, parent byte) (int, int) {
|
||||
switch parent {
|
||||
case 0:
|
||||
return childx - 1, childy // parent left
|
||||
case 1:
|
||||
return childx, childy - 1 // parent above
|
||||
case 2:
|
||||
return childx + 1, childy // parent right
|
||||
case 3:
|
||||
return childx, childy + 1 // parent below
|
||||
default:
|
||||
return -1, -1
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) GetEvaluatedCells() int {
|
||||
cellsEvaluated := 0
|
||||
for _, closed := range a.closedSet {
|
||||
if closed {
|
||||
cellsEvaluated++
|
||||
}
|
||||
}
|
||||
return cellsEvaluated
|
||||
}
|
||||
|
||||
func (a *AStar) GetTimeTaken() time.Duration {
|
||||
return a.timeTaken
|
||||
}
|
||||
|
||||
func (a *AStar) CalculatePath(startX int, startY int, endX int, endY 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)
|
||||
a.parentsMu.RLock()
|
||||
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
|
||||
}
|
||||
a.parentsMu.RUnlock()
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
func (a *AStar) CalculatePathLive(startX int, startY int, endX int, endY int, updateChan chan int, speed *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)
|
||||
a.parentsMu.RLock()
|
||||
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
|
||||
}
|
||||
a.parentsMu.RUnlock()
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
if *speed != 1000 {
|
||||
time.Sleep(time.Duration(1000-*speed) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
return make([][]int, 0)
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
rg "github.com/gen2brain/raylib-go/raygui"
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
@@ -11,6 +15,219 @@ func canvasMouse() rl.Vector2 {
|
||||
return rl.GetMousePosition()
|
||||
}
|
||||
|
||||
// Keeps GPU texture uploads in sync with mapImage edits. Full uploads are used after
|
||||
// path rebuilds; edits use bounding boxes via UpdateTextureRec.
|
||||
type texSync struct {
|
||||
needUpload bool
|
||||
uploadFull bool
|
||||
partialOk bool
|
||||
x0, y0 int
|
||||
x1, y1 int
|
||||
rectBuf []color.RGBA
|
||||
}
|
||||
|
||||
func absInt(v int) int {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (t *texSync) markFull() {
|
||||
t.needUpload = true
|
||||
t.uploadFull = true
|
||||
}
|
||||
|
||||
func (t *texSync) markRegion(minX, minY, maxX, maxY, gridW, gridH int) {
|
||||
if minX > maxX {
|
||||
minX, maxX = maxX, minX
|
||||
}
|
||||
if minY > maxY {
|
||||
minY, maxY = maxY, minY
|
||||
}
|
||||
if minX < 0 {
|
||||
minX = 0
|
||||
}
|
||||
if minY < 0 {
|
||||
minY = 0
|
||||
}
|
||||
if maxX >= gridW {
|
||||
maxX = gridW - 1
|
||||
}
|
||||
if maxY >= gridH {
|
||||
maxY = gridH - 1
|
||||
}
|
||||
if minX > maxX || minY > maxY {
|
||||
return
|
||||
}
|
||||
t.needUpload = true
|
||||
if t.uploadFull {
|
||||
return
|
||||
}
|
||||
if !t.partialOk {
|
||||
t.partialOk = true
|
||||
t.x0, t.y0 = minX, minY
|
||||
t.x1, t.y1 = maxX, maxY
|
||||
return
|
||||
}
|
||||
if minX < t.x0 {
|
||||
t.x0 = minX
|
||||
}
|
||||
if minY < t.y0 {
|
||||
t.y0 = minY
|
||||
}
|
||||
if maxX > t.x1 {
|
||||
t.x1 = maxX
|
||||
}
|
||||
if maxY > t.y1 {
|
||||
t.y1 = maxY
|
||||
}
|
||||
}
|
||||
|
||||
func (t *texSync) flush(img *rl.Image, tex *rl.Texture2D, gridW, gridH int) {
|
||||
if !t.needUpload {
|
||||
return
|
||||
}
|
||||
if t.uploadFull {
|
||||
ptr := (*color.RGBA)(unsafe.Pointer(img.Data))
|
||||
pixels := unsafe.Slice(ptr, gridW*gridH)
|
||||
rl.UpdateTexture(*tex, pixels)
|
||||
t.uploadFull = false
|
||||
t.partialOk = false
|
||||
} else if t.partialOk {
|
||||
w := t.x1 - t.x0 + 1
|
||||
h := t.y1 - t.y0 + 1
|
||||
n := w * h
|
||||
if cap(t.rectBuf) < n {
|
||||
t.rectBuf = make([]color.RGBA, n)
|
||||
} else {
|
||||
t.rectBuf = t.rectBuf[:n]
|
||||
}
|
||||
ptr := (*color.RGBA)(unsafe.Pointer(img.Data))
|
||||
full := unsafe.Slice(ptr, gridW*gridH)
|
||||
for row := 0; row < h; row++ {
|
||||
src := (t.y0+row)*gridW + t.x0
|
||||
copy(t.rectBuf[row*w:], full[src:src+w])
|
||||
}
|
||||
rec := rl.NewRectangle(float32(t.x0), float32(t.y0), float32(w), float32(h))
|
||||
rl.UpdateTextureRec(*tex, rec, t.rectBuf)
|
||||
t.partialOk = false
|
||||
}
|
||||
t.needUpload = false
|
||||
}
|
||||
|
||||
// paintWallLine updates both mapImage and the A* grid for every grid cell crossed by the segment.
|
||||
func paintWallLine(a *AStar, mapImage *rl.Image, x0, y0, x1, y1, gridW, gridH int, col color.RGBA, tex *texSync) {
|
||||
minX := x0
|
||||
if x1 < minX {
|
||||
minX = x1
|
||||
}
|
||||
maxX := x0
|
||||
if x1 > maxX {
|
||||
maxX = x1
|
||||
}
|
||||
minY := y0
|
||||
if y1 < minY {
|
||||
minY = y1
|
||||
}
|
||||
maxY := y0
|
||||
if y1 > maxY {
|
||||
maxY = y1
|
||||
}
|
||||
tex.markRegion(minX, minY, maxX, maxY, gridW, gridH)
|
||||
|
||||
dx := absInt(x1 - x0)
|
||||
dy := -absInt(y1 - y0)
|
||||
sx := 1
|
||||
sy := 1
|
||||
if x0 > x1 {
|
||||
sx = -1
|
||||
}
|
||||
if y0 > y1 {
|
||||
sy = -1
|
||||
}
|
||||
err := dx + dy
|
||||
|
||||
for {
|
||||
if x0 >= 0 && x0 < gridW && y0 >= 0 && y0 < gridH {
|
||||
switch a.GetGridType(x0, y0) {
|
||||
case 2, 3: // start / end
|
||||
default:
|
||||
rl.ImageDrawPixel(mapImage, int32(x0), int32(y0), col)
|
||||
a.SetGridType(x0, y0, 1)
|
||||
}
|
||||
}
|
||||
if x0 == x1 && y0 == y1 {
|
||||
break
|
||||
}
|
||||
e2 := 2 * err
|
||||
if e2 >= dy {
|
||||
err += dy
|
||||
x0 += sx
|
||||
}
|
||||
if e2 <= dx {
|
||||
err += dx
|
||||
y0 += sy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// paintEraseLine clears wall cells along the segment (same grid traversal as paintWallLine).
|
||||
func paintEraseLine(a *AStar, mapImage *rl.Image, x0, y0, x1, y1, gridW, gridH int, empty color.RGBA, tex *texSync) {
|
||||
minX := x0
|
||||
if x1 < minX {
|
||||
minX = x1
|
||||
}
|
||||
maxX := x0
|
||||
if x1 > maxX {
|
||||
maxX = x1
|
||||
}
|
||||
minY := y0
|
||||
if y1 < minY {
|
||||
minY = y1
|
||||
}
|
||||
maxY := y0
|
||||
if y1 > maxY {
|
||||
maxY = y1
|
||||
}
|
||||
tex.markRegion(minX, minY, maxX, maxY, gridW, gridH)
|
||||
|
||||
dx := absInt(x1 - x0)
|
||||
dy := -absInt(y1 - y0)
|
||||
sx := 1
|
||||
sy := 1
|
||||
if x0 > x1 {
|
||||
sx = -1
|
||||
}
|
||||
if y0 > y1 {
|
||||
sy = -1
|
||||
}
|
||||
err := dx + dy
|
||||
|
||||
for {
|
||||
if x0 >= 0 && x0 < gridW && y0 >= 0 && y0 < gridH {
|
||||
switch a.GetGridType(x0, y0) {
|
||||
case 2, 3: // start / end
|
||||
default:
|
||||
rl.ImageDrawPixel(mapImage, int32(x0), int32(y0), empty)
|
||||
a.SetGridType(x0, y0, 0)
|
||||
}
|
||||
}
|
||||
if x0 == x1 && y0 == y1 {
|
||||
break
|
||||
}
|
||||
e2 := 2 * err
|
||||
if e2 >= dy {
|
||||
err += dy
|
||||
x0 += sx
|
||||
}
|
||||
if e2 <= dx {
|
||||
err += dx
|
||||
y0 += sy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func drawInfiniteGridLines(camera rl.Camera2D, canvasW float32, canvasH float32, cellSize float32, width int, height int) {
|
||||
// 1. Level of Detail (LOD) Check
|
||||
// If we are zoomed out too far, don't draw the gridlines at all
|
||||
@@ -48,10 +265,10 @@ func drawInfiniteGridLines(camera rl.Camera2D, canvasW float32, canvasH float32,
|
||||
|
||||
func main() {
|
||||
rl.SetConfigFlags(rl.FlagWindowResizable)
|
||||
rl.InitWindow(int32(800), int32(450), "A* Visualizer")
|
||||
rl.InitWindow(int32(1200), int32(800), "A* Visualizer")
|
||||
defer rl.CloseWindow()
|
||||
|
||||
scale := rl.GetWindowScaleDPI().X
|
||||
scale := rl.GetWindowScaleDPI().X + 0.25
|
||||
if scale == 0 {
|
||||
scale = 2.0 // Fallback value
|
||||
}
|
||||
@@ -73,14 +290,49 @@ func main() {
|
||||
heightInputValue := "10"
|
||||
editModeWidth := false
|
||||
editModeHeight := false
|
||||
generateGridError := false
|
||||
posError := false
|
||||
|
||||
toolOptions := []string{"Wall", "Start", "End", "Erase"}
|
||||
toolOptionsText := strings.Join(toolOptions, ";")
|
||||
activeTool := int32(0)
|
||||
toolDropdownOpen := false
|
||||
var tex texSync // GPU uploads: partial rects while painting; full after path resets
|
||||
|
||||
heuristicOptions := []string{"Manhattan", "Euclidean", "Chebyshev", "Squared Euclidean"}
|
||||
heuristicOptionsText := strings.Join(heuristicOptions, ";")
|
||||
activeHeuristic := int32(0)
|
||||
heuristicDropdownOpen := false
|
||||
|
||||
mazeOptions := []string{"Recursive Divison", "Iterative DFS", "Cellular Automata"}
|
||||
mazeOptionsText := strings.Join(mazeOptions, ";")
|
||||
activeMaze := int32(0)
|
||||
mazeDropdownOpen := false
|
||||
|
||||
cellSize := float32(25)
|
||||
|
||||
lastMousePos := rl.NewVector2(-1, -1)
|
||||
startPos := rl.NewVector2(-1, -1)
|
||||
endPos := rl.NewVector2(-1, -1)
|
||||
|
||||
mapImage := rl.GenImageColor(width, height, rl.NewColor(240, 240, 240, 255))
|
||||
mapTexture := rl.LoadTextureFromImage(mapImage)
|
||||
defer rl.UnloadTexture(mapTexture)
|
||||
defer rl.UnloadImage(mapImage)
|
||||
|
||||
updateChan := make(chan int, 100000)
|
||||
|
||||
autoCompute := false
|
||||
speed := 1000
|
||||
speedInputValue := "100"
|
||||
editModeSpeed := 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())
|
||||
@@ -100,9 +352,9 @@ func main() {
|
||||
// 1. Where is the mouse in the world BEFORE zooming?
|
||||
worldPosBefore := rl.GetScreenToWorld2D(mousePos, camera)
|
||||
|
||||
// 2. Apply the zoom
|
||||
camera.Zoom += float32(wheel) * 0.1
|
||||
if camera.Zoom < 0.01 {
|
||||
// 2. Apply proportional zoom so wheel steps get smaller as the view zooms out.
|
||||
camera.Zoom *= float32(math.Pow(1.1, float64(wheel)))
|
||||
if camera.Zoom < 0.01 && width > 2000 && height > 2000 {
|
||||
camera.Zoom = 0.01
|
||||
}
|
||||
|
||||
@@ -121,6 +373,130 @@ func main() {
|
||||
camera.Target.Y -= delta.Y / camera.Zoom
|
||||
}
|
||||
|
||||
// Paint logic
|
||||
if rl.IsMouseButtonDown(rl.MouseLeftButton) {
|
||||
mousePos := rl.GetMousePosition()
|
||||
if int(mousePos.X) < int(canvasWidth) {
|
||||
worldPos := rl.GetScreenToWorld2D(rl.GetMousePosition(), camera)
|
||||
x := int(worldPos.X / cellSize)
|
||||
y := int(worldPos.Y / cellSize)
|
||||
if x >= 0 && x < width && y >= 0 && y < height {
|
||||
// mapImage is one pixel per cell; drawing uses cell indices, not raw world coords.
|
||||
switch activeTool {
|
||||
case 0: // Wall — Bresenham line into image + simulator grid (skips start/end cells)
|
||||
wallCol := color.RGBA{A: 255}
|
||||
if lastMousePos.X != -1 && lastMousePos.Y != -1 {
|
||||
prevX := int(lastMousePos.X / cellSize)
|
||||
prevY := int(lastMousePos.Y / cellSize)
|
||||
paintWallLine(&astar, mapImage, prevX, prevY, x, y, width, height, wallCol, &tex)
|
||||
} else {
|
||||
paintWallLine(&astar, mapImage, x, y, x, y, width, height, wallCol, &tex)
|
||||
}
|
||||
case 1: // Start — must run on first paint frame (lastMousePos may be -1 after release)
|
||||
if int(startPos.X) != x || int(startPos.Y) != y {
|
||||
if startPos.X >= 0 && startPos.Y >= 0 {
|
||||
ox, oy := int(startPos.X), int(startPos.Y)
|
||||
rl.ImageDrawPixel(mapImage, int32(startPos.X), int32(startPos.Y), rl.NewColor(240, 240, 240, 255))
|
||||
astar.SetGridType(ox, oy, 0)
|
||||
tex.markRegion(ox, oy, ox, oy, width, height)
|
||||
}
|
||||
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 255, 0, 255))
|
||||
tex.markRegion(x, y, x, y, width, height)
|
||||
startPos = rl.NewVector2(float32(x), float32(y))
|
||||
astar.SetGridType(x, y, 2)
|
||||
}
|
||||
case 2: // End
|
||||
if int(endPos.X) != x || int(endPos.Y) != y {
|
||||
if endPos.X >= 0 && endPos.Y >= 0 {
|
||||
ox, oy := int(endPos.X), int(endPos.Y)
|
||||
rl.ImageDrawPixel(mapImage, int32(endPos.X), int32(endPos.Y), rl.NewColor(240, 240, 240, 255))
|
||||
astar.SetGridType(ox, oy, 0)
|
||||
tex.markRegion(ox, oy, ox, oy, width, height)
|
||||
}
|
||||
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(255, 0, 0, 255))
|
||||
tex.markRegion(x, y, x, y, width, height)
|
||||
endPos = rl.NewVector2(float32(x), float32(y))
|
||||
astar.SetGridType(x, y, 3)
|
||||
}
|
||||
case 3: // Erase — same cell indices as walls; ImageDrawLine used world coords before (wrong space).
|
||||
emptyCol := color.RGBA{R: 240, G: 240, B: 240, A: 255}
|
||||
if lastMousePos.X != -1 && lastMousePos.Y != -1 {
|
||||
prevX := int(lastMousePos.X / cellSize)
|
||||
prevY := int(lastMousePos.Y / cellSize)
|
||||
paintEraseLine(&astar, mapImage, prevX, prevY, x, y, width, height, emptyCol, &tex)
|
||||
} else {
|
||||
paintEraseLine(&astar, mapImage, x, y, x, y, width, height, emptyCol, &tex)
|
||||
}
|
||||
}
|
||||
lastMousePos = worldPos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rl.IsMouseButtonReleased(rl.MouseLeftButton) {
|
||||
if autoCompute && lastMousePos.X != -1 && lastMousePos.Y != -1 && 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 {
|
||||
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
|
||||
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()
|
||||
astar.SetHeuristic(activeHeuristic)
|
||||
path := astar.CalculatePath(int(startPos.X), int(startPos.Y), int(endPos.X), int(endPos.Y))
|
||||
closedSet := astar.GetClosedSet()
|
||||
for i, closed := range closedSet {
|
||||
if closed {
|
||||
x := i % width
|
||||
y := i / width
|
||||
if x != int(startPos.X) || y != int(startPos.Y) {
|
||||
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 0, 255, 255))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range path {
|
||||
if p[0] != int(startPos.X) || p[1] != int(startPos.Y) { // we want to keep the start position green
|
||||
rl.ImageDrawPixel(mapImage, int32(p[0]), int32(p[1]), rl.NewColor(255, 255, 0, 255))
|
||||
}
|
||||
}
|
||||
tex.markFull()
|
||||
}
|
||||
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()
|
||||
rl.ClearBackground(rl.White)
|
||||
@@ -128,6 +504,7 @@ func main() {
|
||||
// 1. Draw Canvas
|
||||
rl.BeginScissorMode(0, 0, int32(canvasWidth), int32(screenHeight))
|
||||
rl.BeginMode2D(camera)
|
||||
tex.flush(mapImage, &mapTexture, width, height)
|
||||
rl.DrawTextureEx(
|
||||
mapTexture,
|
||||
rl.NewVector2(0, 0), // Position
|
||||
@@ -135,6 +512,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.ParentsSnapshot()
|
||||
pathThickness := float32(16.0)
|
||||
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()
|
||||
@@ -143,28 +556,251 @@ func main() {
|
||||
sidebarX := canvasWidth
|
||||
rl.DrawRectangleRec(rl.NewRectangle(sidebarX, 0, sidebarWidth, screenHeight), rl.RayWhite)
|
||||
|
||||
// Width Input
|
||||
if rg.TextBox(rl.NewRectangle(sidebarX+(10*scale), (10*scale), (80*scale), (20*scale)), &widthInputValue, 20, editModeWidth) {
|
||||
editModeWidth = !editModeWidth
|
||||
if editModeWidth {
|
||||
editModeHeight = false
|
||||
editModeSpeed = false
|
||||
}
|
||||
}
|
||||
rg.Label(rl.NewRectangle(sidebarX+(95*scale), (10*scale), (10*scale), (20*scale)), "x")
|
||||
|
||||
// Height Input
|
||||
if rg.TextBox(rl.NewRectangle(sidebarX+(110*scale), (10*scale), (80*scale), (20*scale)), &heightInputValue, 20, editModeHeight) {
|
||||
editModeHeight = !editModeHeight
|
||||
if editModeHeight {
|
||||
editModeWidth = false
|
||||
editModeSpeed = false
|
||||
}
|
||||
}
|
||||
|
||||
if generateGridError {
|
||||
result := rg.MessageBox(rl.NewRectangle(screenWidth/2-(100*scale), screenHeight/2-(40*scale), (200*scale), (120*scale)), "Error", "Width and/or height must be\nless than 16000", "OK")
|
||||
if result >= 0 {
|
||||
generateGridError = false
|
||||
}
|
||||
}
|
||||
|
||||
if posError {
|
||||
result := rg.MessageBox(rl.NewRectangle(screenWidth/2-(100*scale), screenHeight/2-(40*scale), (200*scale), (120*scale)), "Error", "Start and end positions must be\nwithin the generated grid", "OK")
|
||||
if result >= 0 {
|
||||
posError = false
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Grid Button
|
||||
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (40*scale), (180*scale), (30*scale)), "Generate Grid") {
|
||||
width, _ = strconv.Atoi(widthInputValue)
|
||||
height, _ = strconv.Atoi(heightInputValue)
|
||||
mapImage = rl.GenImageColor(width, height, rl.NewColor(240, 240, 240, 255))
|
||||
mapTexture = rl.LoadTextureFromImage(mapImage)
|
||||
defer rl.UnloadTexture(mapTexture)
|
||||
defer rl.UnloadImage(mapImage)
|
||||
if width > 16000 || height > 16000 {
|
||||
generateGridError = true
|
||||
} else {
|
||||
rl.UnloadTexture(mapTexture)
|
||||
rl.UnloadImage(mapImage)
|
||||
mapImage = rl.GenImageColor(width, height, rl.NewColor(240, 240, 240, 255))
|
||||
mapTexture = rl.LoadTextureFromImage(mapImage)
|
||||
astar.RebuildGrid(width, height)
|
||||
startPos = rl.NewVector2(-1, -1)
|
||||
endPos = rl.NewVector2(-1, -1)
|
||||
tex = texSync{rectBuf: tex.rectBuf}
|
||||
}
|
||||
}
|
||||
|
||||
// While a dropdown list is open it overlaps controls below; those widgets are handled
|
||||
// earlier in the frame and would otherwise steal the release-click. GuiLock skips input
|
||||
// for other controls; DropdownBox still handles input when its editMode is true.
|
||||
if toolDropdownOpen || heuristicDropdownOpen || mazeDropdownOpen {
|
||||
rg.Lock()
|
||||
}
|
||||
|
||||
// Generate Maze Button
|
||||
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (260*scale), (180*scale), (30*scale)), "Generate Maze") {
|
||||
astar.GenerateMaze(activeMaze)
|
||||
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
|
||||
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()
|
||||
}
|
||||
|
||||
// Reset Visualization Button
|
||||
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(120*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
|
||||
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()
|
||||
}
|
||||
|
||||
// Speed Slider
|
||||
speedText := ""
|
||||
if speed == 1000 {
|
||||
speedText = "Uncapped"
|
||||
} else {
|
||||
speedText = strconv.Itoa(speed/10) + "%"
|
||||
}
|
||||
speedLabel := "Speed: " + speedText
|
||||
rg.Label(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(185*scale)), (180*scale), (30*scale)), speedLabel)
|
||||
newSpeed := int(rg.SliderBar(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(160*scale)), (140*scale), (30*scale)), "", "", float32(speed), 0, 1000))
|
||||
if editModeSpeed {
|
||||
if newSpeed != speed {
|
||||
speed = newSpeed
|
||||
speedInputValue = strconv.Itoa(speed)
|
||||
editModeSpeed = false
|
||||
}
|
||||
} else {
|
||||
speed = newSpeed
|
||||
speedInputValue = strconv.Itoa(speed)
|
||||
}
|
||||
if rg.TextBox(rl.NewRectangle(sidebarX+(160*scale), (screenHeight-(160*scale)), (30*scale), (30*scale)), &speedInputValue, 20, editModeSpeed) {
|
||||
editModeSpeed = !editModeSpeed
|
||||
if editModeSpeed {
|
||||
speedInputValue = strconv.Itoa(speed)
|
||||
editModeWidth = false
|
||||
editModeHeight = false
|
||||
} else {
|
||||
v, err := strconv.Atoi(strings.TrimSpace(speedInputValue))
|
||||
if err != nil {
|
||||
speedInputValue = strconv.Itoa(speed)
|
||||
} else {
|
||||
if v < 0 {
|
||||
v = 0
|
||||
} else if v > 1000 {
|
||||
v = 1000
|
||||
}
|
||||
speed = v
|
||||
speedInputValue = strconv.Itoa(speed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Path (Live) Button
|
||||
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, &speed)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Calculate Path Button
|
||||
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(40*scale)), (140*scale), (30*scale)), "Calculate Path") {
|
||||
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))
|
||||
}
|
||||
}
|
||||
path := astar.CalculatePath(int(startPos.X), int(startPos.Y), int(endPos.X), int(endPos.Y))
|
||||
closedSet := astar.GetClosedSet()
|
||||
for i, closed := range closedSet {
|
||||
if closed {
|
||||
x := i % width
|
||||
y := i / width
|
||||
if x != int(startPos.X) || y != int(startPos.Y) {
|
||||
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 0, 255, 255))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range path {
|
||||
if p[0] != int(startPos.X) || p[1] != int(startPos.Y) { // we want to keep the start position green
|
||||
rl.ImageDrawPixel(mapImage, int32(p[0]), int32(p[1]), rl.NewColor(255, 255, 0, 255))
|
||||
}
|
||||
}
|
||||
tex.markFull()
|
||||
}
|
||||
}
|
||||
|
||||
// Status Label
|
||||
rg.Label(rl.NewRectangle((10*scale), (screenHeight-(30*scale)), (canvasWidth-(20*scale)), (30*scale)), "Evaluated "+strconv.Itoa(astar.GetEvaluatedCells())+" cells in "+astar.GetTimeTaken().String())
|
||||
|
||||
// FPS counter
|
||||
rg.Label(rl.NewRectangle((10*scale), (0*scale), (canvasWidth-(20*scale)), (30*scale)), "FPS: "+strconv.Itoa(int(rl.GetFPS())))
|
||||
|
||||
rg.Unlock()
|
||||
|
||||
// Tool Selector (text must be "opt1;opt2;..." — raygui splits on ';' and needs 2+ items)
|
||||
rg.Label(rl.NewRectangle(sidebarX+(10*scale), (75*scale), (180*scale), (30*scale)), "Tool:")
|
||||
if rg.DropdownBox(rl.NewRectangle(sidebarX+(10*scale), (100*scale), (180*scale), (30*scale)), toolOptionsText, &activeTool, toolDropdownOpen) {
|
||||
toolDropdownOpen = !toolDropdownOpen
|
||||
}
|
||||
|
||||
// Heuristic Selector
|
||||
if !toolDropdownOpen {
|
||||
rg.Label(rl.NewRectangle(sidebarX+(10*scale), (135*scale), (180*scale), (30*scale)), "Heuristic:")
|
||||
if rg.DropdownBox(rl.NewRectangle(sidebarX+(10*scale), (160*scale), (180*scale), (30*scale)), heuristicOptionsText, &activeHeuristic, heuristicDropdownOpen) {
|
||||
heuristicDropdownOpen = !heuristicDropdownOpen
|
||||
}
|
||||
}
|
||||
|
||||
// Maze Selector
|
||||
if !toolDropdownOpen && !heuristicDropdownOpen {
|
||||
rg.Label(rl.NewRectangle(sidebarX+(10*scale), (195*scale), (180*scale), (30*scale)), "Maze:")
|
||||
if rg.DropdownBox(rl.NewRectangle(sidebarX+(10*scale), (220*scale), (180*scale), (30*scale)), mazeOptionsText, &activeMaze, mazeDropdownOpen) {
|
||||
mazeDropdownOpen = !mazeDropdownOpen
|
||||
}
|
||||
}
|
||||
|
||||
rl.EndDrawing()
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (a *AStar) GenerateMaze(mazeType int32) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
switch mazeType {
|
||||
case 0:
|
||||
a.GenerateRecursiveDivisionMaze(r)
|
||||
case 1:
|
||||
a.GenerateIterativeDFSMaze(r)
|
||||
case 2:
|
||||
a.GenerateCellularAutomataMaze(r)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AStar) GenerateRecursiveDivisionMaze(r *rand.Rand) {
|
||||
// 1. Start with an entirely empty grid
|
||||
for i := range a.gridTypes {
|
||||
a.gridTypes[i] = 0
|
||||
}
|
||||
|
||||
// 2. Draw outer boundary walls
|
||||
for x := 0; x < a.width; x++ {
|
||||
a.gridTypes[x] = 1
|
||||
a.gridTypes[(a.height-1)*a.width+x] = 1
|
||||
}
|
||||
for y := 0; y < a.height; y++ {
|
||||
a.gridTypes[y*a.width] = 1
|
||||
a.gridTypes[y*a.width+(a.width-1)] = 1
|
||||
}
|
||||
|
||||
// 3. Declare the recursive closure
|
||||
var divide func(x, y, w, h int)
|
||||
divide = func(x, y, w, h int) {
|
||||
// Base case: room is too small to divide safely
|
||||
if w <= 3 || h <= 3 {
|
||||
return
|
||||
}
|
||||
|
||||
// Choose orientation based on proportions to keep rooms somewhat square
|
||||
horizontal := h > w
|
||||
if w == h {
|
||||
horizontal = r.Intn(2) == 0
|
||||
}
|
||||
|
||||
if horizontal {
|
||||
// Horizontal Wall: Must be on an EVEN local Y coordinate
|
||||
wallY := (r.Intn((h-2)/2) * 2) + 2
|
||||
// Gap (door): Must be on an ODD local X coordinate
|
||||
gapX := (r.Intn((w-1)/2) * 2) + 1
|
||||
|
||||
for px := 0; px < w; px++ {
|
||||
if px != gapX {
|
||||
a.gridTypes[(y+wallY)*a.width+(x+px)] = 1
|
||||
}
|
||||
}
|
||||
// Recurse top and bottom
|
||||
divide(x, y, w, wallY)
|
||||
divide(x, y+wallY, w, h-wallY)
|
||||
|
||||
} else {
|
||||
// Vertical Wall: Must be on an EVEN local X coordinate
|
||||
wallX := (r.Intn((w-2)/2) * 2) + 2
|
||||
// Gap (door): Must be on an ODD local Y coordinate
|
||||
gapY := (r.Intn((h-1)/2) * 2) + 1
|
||||
|
||||
for py := 0; py < h; py++ {
|
||||
if py != gapY {
|
||||
a.gridTypes[(y+py)*a.width+(x+wallX)] = 1
|
||||
}
|
||||
}
|
||||
// Recurse left and right
|
||||
divide(x, y, wallX, h)
|
||||
divide(x+wallX, y, w-wallX, h)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the recursion on the inside of the boundary walls
|
||||
divide(1, 1, a.width-2, a.height-2)
|
||||
}
|
||||
|
||||
func (a *AStar) GenerateIterativeDFSMaze(r *rand.Rand) {
|
||||
// 1. Fill the entire grid with walls (type 1)
|
||||
for i := range a.gridTypes {
|
||||
a.gridTypes[i] = 1
|
||||
}
|
||||
|
||||
// 2. The Stack (We use a Go slice instead of recursion to prevent Stack Overflow)
|
||||
stack := make([]int, 0)
|
||||
|
||||
// 3. Pick a random starting cell (MUST be odd coordinates for the step-by-two math)
|
||||
startX := (r.Intn(a.width/2) * 2) + 1
|
||||
startY := (r.Intn(a.height/2) * 2) + 1
|
||||
|
||||
// Bounds check just in case
|
||||
if startX >= a.width {
|
||||
startX = a.width - 2
|
||||
}
|
||||
if startY >= a.height {
|
||||
startY = a.height - 2
|
||||
}
|
||||
|
||||
startIdx := startY*a.width + startX
|
||||
a.gridTypes[startIdx] = 0 // Carve the first floor
|
||||
stack = append(stack, startIdx)
|
||||
|
||||
// Directions for stepping by TWO (Up, Down, Left, Right)
|
||||
dirs := [][]int{{0, -2}, {0, 2}, {-2, 0}, {2, 0}}
|
||||
|
||||
// 4. The DFS Loop
|
||||
for len(stack) > 0 {
|
||||
// Pop the top of the stack
|
||||
currentIdx := stack[len(stack)-1]
|
||||
cx := currentIdx % a.width
|
||||
cy := currentIdx / a.width
|
||||
|
||||
// Find all valid, unvisited neighbors (distance 2)
|
||||
validNeighbors := make([][]int, 0)
|
||||
for _, dir := range dirs {
|
||||
nx, ny := cx+dir[0], cy+dir[1]
|
||||
// Check bounds
|
||||
if nx > 0 && nx < a.width-1 && ny > 0 && ny < a.height-1 {
|
||||
// If it's still a wall, we haven't visited it yet
|
||||
if a.gridTypes[ny*a.width+nx] == 1 {
|
||||
validNeighbors = append(validNeighbors, []int{nx, ny, dir[0], dir[1]})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(validNeighbors) > 0 {
|
||||
// Pick a random valid neighbor
|
||||
next := validNeighbors[r.Intn(len(validNeighbors))]
|
||||
nx, ny, dx, dy := next[0], next[1], next[2], next[3]
|
||||
|
||||
// Carve the neighbor (distance 2)
|
||||
a.gridTypes[ny*a.width+nx] = 0
|
||||
|
||||
// Carve the wall BETWEEN current and neighbor (distance 1)
|
||||
wallX, wallY := cx+(dx/2), cy+(dy/2)
|
||||
a.gridTypes[wallY*a.width+wallX] = 0
|
||||
|
||||
// Push the neighbor to the stack
|
||||
stack = append(stack, ny*a.width+nx)
|
||||
} else {
|
||||
// Backtrack! No valid neighbors, so pop it permanently
|
||||
stack = stack[:len(stack)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AStar) GenerateCellularAutomataMaze(r *rand.Rand) {
|
||||
// 1. Initial State: Fill with random noise (approx 45% walls)
|
||||
for i := range a.gridTypes {
|
||||
// Leave the edges as walls to contain the caves
|
||||
x := i % a.width
|
||||
y := i / a.width
|
||||
if x == 0 || x == a.width-1 || y == 0 || y == a.height-1 {
|
||||
a.gridTypes[i] = 1
|
||||
} else if r.Float32() < 0.45 {
|
||||
a.gridTypes[i] = 1
|
||||
} else {
|
||||
a.gridTypes[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 2. The Smoothing Passes (5 iterations is usually the sweet spot)
|
||||
buffer := make([]byte, a.width*a.height)
|
||||
|
||||
for step := 0; step < 5; step++ {
|
||||
for y := 0; y < a.height; y++ {
|
||||
for x := 0; x < a.width; x++ {
|
||||
wallCount := 0
|
||||
|
||||
// Count the 8 surrounding neighbors
|
||||
for dy := -1; dy <= 1; dy++ {
|
||||
for dx := -1; dx <= 1; dx++ {
|
||||
if dx == 0 && dy == 0 {
|
||||
continue
|
||||
}
|
||||
nx, ny := x+dx, y+dy
|
||||
|
||||
// Edges of the map count as walls
|
||||
if nx < 0 || nx >= a.width || ny < 0 || ny >= a.height {
|
||||
wallCount++
|
||||
} else if a.gridTypes[ny*a.width+nx] == 1 {
|
||||
wallCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The Automata Rules:
|
||||
// If surrounded by walls, become a wall.
|
||||
// If surrounded by empty space, become empty.
|
||||
idx := y*a.width + x
|
||||
if wallCount > 4 {
|
||||
buffer[idx] = 1
|
||||
} else if wallCount < 4 {
|
||||
buffer[idx] = 0
|
||||
} else {
|
||||
buffer[idx] = a.gridTypes[idx] // Stays the same
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copy the buffer back to the main grid for the next pass
|
||||
copy(a.gridTypes, buffer)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user