Files
a-star-go/maze.go
T
2026-05-14 13:45:06 -05:00

212 lines
5.3 KiB
Go

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)
}
}