Updatez
This commit is contained in:
237
maze/maze.go
Normal file
237
maze/maze.go
Normal file
@@ -0,0 +1,237 @@
|
||||
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
|
||||
|
||||
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) {
|
||||
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
|
||||
|
||||
for len(stackX) > 0 {
|
||||
last := len(stackX) - 1
|
||||
x, y := stackX[last], stackY[last]
|
||||
dirs := shuffledDirections()
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
entranceX := topChoices[rand.IntN(len(topChoices))]
|
||||
grid[0][entranceX] = 1
|
||||
}
|
||||
|
||||
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 {
|
||||
exitX := bottomChoices[rand.IntN(len(bottomChoices))]
|
||||
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}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
pathCells := make([]bool, width*height)
|
||||
for idx := end; idx != -1; idx = parent[idx] {
|
||||
pathCells[idx] = true
|
||||
if idx == start {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func shuffledDirections() [4]uint8 {
|
||||
dirs := [4]uint8{0, 1, 2, 3}
|
||||
for i := 3; i > 0; i-- {
|
||||
j := rand.IntN(i + 1)
|
||||
dirs[i], dirs[j] = dirs[j], dirs[i]
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
106
maze/render.go
Normal file
106
maze/render.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package maze
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultWallColor is used for wall cells.
|
||||
DefaultWallColor = color.RGBA{R: 0, G: 0, B: 0, A: 255}
|
||||
// DefaultPathColor is used for open maze cells.
|
||||
DefaultPathColor = color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||
// DefaultSolutionColor is used to highlight solution cells.
|
||||
DefaultSolutionColor = color.RGBA{R: 0, G: 180, B: 0, A: 255}
|
||||
)
|
||||
|
||||
// RenderOptions controls how a maze image is rendered.
|
||||
type RenderOptions struct {
|
||||
Scale int
|
||||
WallColor color.RGBA
|
||||
PathColor color.RGBA
|
||||
SolutionColor color.RGBA
|
||||
HighlightPath bool
|
||||
}
|
||||
|
||||
// DefaultRenderOptions returns baseline rendering options.
|
||||
func DefaultRenderOptions() RenderOptions {
|
||||
return RenderOptions{
|
||||
Scale: 5,
|
||||
WallColor: DefaultWallColor,
|
||||
PathColor: DefaultPathColor,
|
||||
SolutionColor: DefaultSolutionColor,
|
||||
HighlightPath: false,
|
||||
}
|
||||
}
|
||||
|
||||
// RenderImage converts a maze grid into an RGBA image.
|
||||
//
|
||||
// If options.HighlightPath is true, Solve is used and solution cells are painted
|
||||
// with options.SolutionColor.
|
||||
func RenderImage(grid Grid, options RenderOptions) (*image.RGBA, error) {
|
||||
width, height, err := validateGrid(grid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if options.Scale <= 0 {
|
||||
return nil, fmt.Errorf("scale must be greater than zero")
|
||||
}
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, width*options.Scale, height*options.Scale))
|
||||
|
||||
var solutionPath [][]bool
|
||||
if options.HighlightPath {
|
||||
solutionPath, err = Solve(grid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for cellY := 0; cellY < height; cellY++ {
|
||||
startY := cellY * options.Scale
|
||||
for cellX := 0; cellX < width; cellX++ {
|
||||
startX := cellX * options.Scale
|
||||
|
||||
pixel := options.WallColor
|
||||
if grid[cellY][cellX] == 1 {
|
||||
pixel = options.PathColor
|
||||
if options.HighlightPath && solutionPath[cellY][cellX] {
|
||||
pixel = options.SolutionColor
|
||||
}
|
||||
}
|
||||
|
||||
for dy := 0; dy < options.Scale; dy++ {
|
||||
row := img.Pix[(startY+dy)*img.Stride:]
|
||||
for dx := 0; dx < options.Scale; dx++ {
|
||||
offset := (startX + dx) * 4
|
||||
row[offset+0] = pixel.R
|
||||
row[offset+1] = pixel.G
|
||||
row[offset+2] = pixel.B
|
||||
row[offset+3] = pixel.A
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// SavePNG renders a maze image and writes it to a PNG file.
|
||||
func SavePNG(grid Grid, outputPath string, options RenderOptions) error {
|
||||
img, err := RenderImage(grid, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return png.Encode(f, img)
|
||||
}
|
||||
Reference in New Issue
Block a user