Compare commits

...

4 Commits

Author SHA1 Message Date
wisplite 17fbabc3e2 fixed the chebyshev implementation and a live rendering bug 2026-05-14 13:52:27 -05:00
wisplite cd08eebdbd maze generation! 2026-05-14 13:45:06 -05:00
wisplite cfe855944b speed control 2026-05-14 13:08:58 -05:00
wisplite fc3631c6a0 live preview 2026-05-14 12:38:52 -05:00
3 changed files with 527 additions and 21 deletions
+122 -15
View File
@@ -4,9 +4,13 @@ import (
"container/heap"
"fmt"
"math"
"sync"
"time"
)
// parentNone marks cells with no predecessor; must not collide with 03 (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
@@ -48,6 +52,7 @@ 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
@@ -71,6 +76,11 @@ func (a *AStar) Init(width int, height int) {
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) {
@@ -79,20 +89,29 @@ func (a *AStar) ResetGrid(withTypes bool) {
a.gridTypes[i] = 0
}
a.gScores[i] = math.MaxFloat32
a.parents[i] = 0
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) {
@@ -107,7 +126,7 @@ func (a *AStar) SetHeuristic(heuristic int32) {
}
case 2:
a.heuristic = func(x int, y int, endX int, endY int) float32 {
return float32(math.Max(float64(x-endX), float64(y-endY))) // Chebyshev distance
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 {
@@ -147,6 +166,8 @@ func (a *AStar) GetGScores(x int, y int) float32 {
}
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 {
@@ -159,24 +180,39 @@ func (a *AStar) SetParent(x int, y int, parentx int, parenty int) {
}
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) 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
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
}
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,10 +271,18 @@ 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])
path = append(path, []int{x, y})
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
}
@@ -264,3 +308,66 @@ 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, 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)
}
+204 -16
View File
@@ -265,7 +265,7 @@ func drawInfiniteGridLines(camera rl.Camera2D, canvasW float32, canvasH float32,
func main() {
rl.SetConfigFlags(rl.FlagWindowResizable)
rl.InitWindow(int32(800), int32(600), "A* Visualizer")
rl.InitWindow(int32(1200), int32(800), "A* Visualizer")
defer rl.CloseWindow()
scale := rl.GetWindowScaleDPI().X + 0.25
@@ -304,6 +304,11 @@ func main() {
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)
@@ -315,11 +320,19 @@ func main() {
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())
@@ -459,6 +472,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 +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()
@@ -488,6 +561,7 @@ func main() {
editModeWidth = !editModeWidth
if editModeWidth {
editModeHeight = false
editModeSpeed = false
}
}
rg.Label(rl.NewRectangle(sidebarX+(95*scale), (10*scale), (10*scale), (20*scale)), "x")
@@ -497,6 +571,7 @@ func main() {
editModeHeight = !editModeHeight
if editModeHeight {
editModeWidth = false
editModeSpeed = false
}
}
@@ -532,24 +607,18 @@ func main() {
}
}
// 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
// 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()
}
// 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
}
}
// Reset Visualization Button
if !toolDropdownOpen && !heuristicDropdownOpen {
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (200*scale), (180*scale), (30*scale)), "Reset Visualization") {
// 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
@@ -566,6 +635,97 @@ func main() {
}
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
@@ -578,6 +738,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
@@ -615,6 +776,33 @@ func main() {
// 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()
}
}
+211
View File
@@ -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)
}
}