mirror of
https://github.com/wisplite/a-star-go.git
synced 2026-06-27 15:37:07 -05:00
maze generation!
This commit is contained in:
@@ -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)
|
||||
@@ -512,7 +517,7 @@ func main() {
|
||||
if lastEvaluatedNode != -1 {
|
||||
startIndex := int(startPos.Y)*width + int(startPos.X)
|
||||
parents := astar.GetParents()
|
||||
pathThickness := float32(10.0) / (camera.Zoom / 0.75)
|
||||
pathThickness := float32(16.0)
|
||||
if pathThickness < 1 {
|
||||
pathThickness = 1
|
||||
}
|
||||
@@ -605,12 +610,34 @@ func main() {
|
||||
// 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 {
|
||||
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), (200*scale), (180*scale), (30*scale)), "Reset Visualization") {
|
||||
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()
|
||||
@@ -638,8 +665,8 @@ func main() {
|
||||
speedText = strconv.Itoa(speed/10) + "%"
|
||||
}
|
||||
speedLabel := "Speed: " + speedText
|
||||
rg.Label(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(145*scale)), (180*scale), (30*scale)), speedLabel)
|
||||
newSpeed := int(rg.SliderBar(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(120*scale)), (140*scale), (30*scale)), "", "", float32(speed), 0, 1000))
|
||||
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
|
||||
@@ -650,7 +677,7 @@ func main() {
|
||||
speed = newSpeed
|
||||
speedInputValue = strconv.Itoa(speed)
|
||||
}
|
||||
if rg.TextBox(rl.NewRectangle(sidebarX+(160*scale), (screenHeight-(120*scale)), (30*scale), (30*scale)), &speedInputValue, 20, editModeSpeed) {
|
||||
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)
|
||||
@@ -749,6 +776,9 @@ 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)
|
||||
@@ -765,6 +795,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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