Files
go-maze/maze/maze.go

261 lines
6.1 KiB
Go
Raw Permalink Normal View History

2026-02-26 13:45:37 +02:00
package maze
import (
"errors"
"math/rand/v2"
)
// Grid represents a maze as a matrix of cells.
// A value of 0 is a wall and 1 is a walkable path.
type Grid [][]int
2026-02-26 22:25:01 +02:00
const GOLDEN_RATION_BIT_MIXER = 0x9e3779b97f4a7c15
2026-02-26 13:45:37 +02:00
var (
// ErrInvalidDimensions is returned when width or height are not positive.
ErrInvalidDimensions = errors.New("maze dimensions must be greater than zero")
// ErrInvalidGrid is returned when a maze grid is empty or malformed.
ErrInvalidGrid = errors.New("maze grid must be non-empty and rectangular")
// ErrNoEntranceExit is returned when the maze has no valid top entrance or bottom exit.
ErrNoEntranceExit = errors.New("maze grid must contain a top entrance and bottom exit")
// ErrNoPath is returned when no path exists between entrance and exit.
ErrNoPath = errors.New("no path exists between maze entrance and exit")
)
// Generate creates a maze grid with the given width and height.
//
// The returned grid uses 0 for walls and 1 for open cells.
// The algorithm is randomized depth-first carving with one entrance on the top border
// and one exit on the bottom border.
func Generate(width, height int) (Grid, error) {
2026-02-26 22:07:09 +02:00
return generate(width, height, rand.IntN)
}
// GenerateWithSeed creates a maze grid with deterministic randomness.
//
// The same width, height, and seed always produce the same maze.
func GenerateWithSeed(width, height int, seed uint64) (Grid, error) {
2026-02-26 22:25:01 +02:00
rng := rand.New(rand.NewPCG(seed, seed^GOLDEN_RATION_BIT_MIXER))
2026-02-26 22:07:09 +02:00
return generate(width, height, rng.IntN)
}
func generate(width, height int, intN func(int) int) (Grid, error) {
2026-02-26 13:45:37 +02:00
if width <= 0 || height <= 0 {
return nil, ErrInvalidDimensions
}
cells := make([]int, width*height)
grid := make(Grid, height)
for y := 0; y < height; y++ {
rowStart := y * width
grid[y] = cells[rowStart : rowStart+width]
}
if width < 3 || height < 3 {
return grid, nil
}
startX, startY := 1, 1
grid[startY][startX] = 1
stackX := make([]int, 1, width*height/2)
stackY := make([]int, 1, width*height/2)
stackX[0], stackY[0] = startX, startY
2026-02-26 22:25:01 +02:00
// Depth-first backtracking carve:
// grow the maze from the current cell into a random unvisited neighbor,
// and backtrack when no further expansion is possible.
2026-02-26 13:45:37 +02:00
for len(stackX) > 0 {
last := len(stackX) - 1
x, y := stackX[last], stackY[last]
2026-02-26 22:07:09 +02:00
dirs := shuffledDirections(intN)
2026-02-26 13:45:37 +02:00
carved := false
for _, d := range dirs {
dx, dy := directionDelta(d)
nx, ny := x+dx, y+dy
if nx <= 0 || nx >= width-1 || ny <= 0 || ny >= height-1 {
continue
}
if grid[ny][nx] == 1 {
continue
}
grid[y+dy/2][x+dx/2] = 1
grid[ny][nx] = 1
stackX = append(stackX, nx)
stackY = append(stackY, ny)
carved = true
break
}
if !carved {
stackX = stackX[:last]
stackY = stackY[:last]
}
}
2026-02-26 22:25:01 +02:00
// Choose a top-border entrance that connects to an already carved cell.
2026-02-26 13:45:37 +02:00
topChoices := make([]int, 0, width/2)
for x := 1; x < width-1; x++ {
if grid[1][x] == 1 {
topChoices = append(topChoices, x)
}
}
if len(topChoices) > 0 {
2026-02-26 22:07:09 +02:00
entranceX := topChoices[intN(len(topChoices))]
2026-02-26 13:45:37 +02:00
grid[0][entranceX] = 1
}
2026-02-26 22:25:01 +02:00
// Choose a bottom-border exit that connects to an already carved cell.
2026-02-26 13:45:37 +02:00
bottomChoices := make([]int, 0, width/2)
for x := 1; x < width-1; x++ {
if grid[height-2][x] == 1 {
bottomChoices = append(bottomChoices, x)
}
}
if len(bottomChoices) > 0 {
2026-02-26 22:07:09 +02:00
exitX := bottomChoices[intN(len(bottomChoices))]
2026-02-26 13:45:37 +02:00
grid[height-1][exitX] = 1
}
return grid, nil
}
// Solve finds a path from the top entrance to the bottom exit in a maze.
//
// It returns a matrix with the same dimensions as the input grid, where true
// marks cells that belong to the computed path.
func Solve(grid Grid) ([][]bool, error) {
width, height, err := validateGrid(grid)
if err != nil {
return nil, err
}
startX := -1
endX := -1
for x := 0; x < width; x++ {
if grid[0][x] == 1 {
startX = x
break
}
}
for x := 0; x < width; x++ {
if grid[height-1][x] == 1 {
endX = x
break
}
}
if startX == -1 || endX == -1 {
return nil, ErrNoEntranceExit
}
start := startX
end := (height-1)*width + endX
parent := make([]int, width*height)
for i := range parent {
parent[i] = -1
}
visited := make([]bool, width*height)
queue := make([]int, 1, width*height)
queue[0] = start
visited[start] = true
dx := [4]int{0, 1, 0, -1}
dy := [4]int{-1, 0, 1, 0}
2026-02-26 22:25:01 +02:00
// Breadth-first search from entrance to exit while recording parent links
// so the shortest path can be reconstructed afterward.
2026-02-26 13:45:37 +02:00
found := false
for head := 0; head < len(queue); head++ {
idx := queue[head]
if idx == end {
found = true
break
}
x := idx % width
y := idx / width
for i := 0; i < 4; i++ {
nx := x + dx[i]
ny := y + dy[i]
if nx < 0 || nx >= width || ny < 0 || ny >= height {
continue
}
if grid[ny][nx] == 0 {
continue
}
nIdx := ny*width + nx
if visited[nIdx] {
continue
}
visited[nIdx] = true
parent[nIdx] = idx
queue = append(queue, nIdx)
}
}
if !found {
return nil, ErrNoPath
}
2026-02-26 22:25:01 +02:00
// Reconstruct the path by following parent pointers backward from exit to entrance.
2026-02-26 13:45:37 +02:00
pathCells := make([]bool, width*height)
for idx := end; idx != -1; idx = parent[idx] {
pathCells[idx] = true
if idx == start {
break
}
}
2026-02-26 22:25:01 +02:00
// Expose the flat path buffer as a 2D matrix aligned with the maze grid.
2026-02-26 13:45:37 +02:00
path := make([][]bool, height)
for y := 0; y < height; y++ {
rowStart := y * width
path[y] = pathCells[rowStart : rowStart+width]
}
return path, nil
}
func validateGrid(grid Grid) (width, height int, err error) {
height = len(grid)
if height == 0 {
return 0, 0, ErrInvalidGrid
}
width = len(grid[0])
if width == 0 {
return 0, 0, ErrInvalidGrid
}
for _, row := range grid {
if len(row) != width {
return 0, 0, ErrInvalidGrid
}
}
return width, height, nil
}
func directionDelta(direction uint8) (dx, dy int) {
switch direction {
case 0:
return 0, -2
case 1:
return 2, 0
case 2:
return 0, 2
default:
return -2, 0
}
}
2026-02-26 22:07:09 +02:00
func shuffledDirections(intN func(int) int) [4]uint8 {
2026-02-26 13:45:37 +02:00
dirs := [4]uint8{0, 1, 2, 3}
for i := 3; i > 0; i-- {
2026-02-26 22:07:09 +02:00
j := intN(i + 1)
2026-02-26 13:45:37 +02:00
dirs[i], dirs[j] = dirs[j], dirs[i]
}
return dirs
}