From b75b612f1210aee8bd59b8cded1ee3a321ad523e Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Thu, 26 Feb 2026 13:45:37 +0200 Subject: [PATCH] Updatez --- main.go | 71 ++++----------- maze/maze.go | 237 +++++++++++++++++++++++++++++++++++++++++++++++++ maze/render.go | 106 ++++++++++++++++++++++ mazer/maze.go | 192 --------------------------------------- 4 files changed, 361 insertions(+), 245 deletions(-) create mode 100644 maze/maze.go create mode 100644 maze/render.go delete mode 100644 mazer/maze.go diff --git a/main.go b/main.go index f739ec7..9427ef0 100644 --- a/main.go +++ b/main.go @@ -1,65 +1,30 @@ package main import ( - "image" - "image/color" - "image/png" - "os" - "test-maze/mazer" + "fmt" + "log" + + "test-maze/maze" ) -const SIZE_X = 21 -const SIZE_Y = 21 -const SCALE_FACTOR = 5 -const COLOR_SOLUTION_PATH = true +const ( + sizeX = 21 + sizeY = 21 +) func main() { - matrix := mazer.GenerateMaze(SIZE_X, SIZE_Y) - - var solutionPath [][]bool - if COLOR_SOLUTION_PATH { - solutionPath = mazer.FindSolutionPath(matrix) - } - - img := image.NewRGBA(image.Rect(0, 0, SIZE_X*SCALE_FACTOR, SIZE_Y*SCALE_FACTOR)) - - white := color.RGBA{R: 255, G: 255, B: 255, A: 255} - black := color.RGBA{R: 0, G: 0, B: 0, A: 255} - green := color.RGBA{R: 0, G: 180, B: 0, A: 255} - - for cellY := 0; cellY < SIZE_Y; cellY++ { - startY := cellY * SCALE_FACTOR - for cellX := 0; cellX < SIZE_X; cellX++ { - startX := cellX * SCALE_FACTOR - - p := black - if matrix[cellY][cellX] == 1 { - p = white - if COLOR_SOLUTION_PATH && solutionPath != nil && solutionPath[cellY][cellX] { - p = green - } - } - - for dy := 0; dy < SCALE_FACTOR; dy++ { - row := img.Pix[(startY+dy)*img.Stride:] - for dx := 0; dx < SCALE_FACTOR; dx++ { - offset := (startX + dx) * 4 - row[offset+0] = p.R - row[offset+1] = p.G - row[offset+2] = p.B - row[offset+3] = p.A - } - } - } - } - - f, err := os.Create("maze.png") + grid, err := maze.Generate(sizeX, sizeY) if err != nil { - panic(err) + log.Fatalf("generate maze: %v", err) } - defer f.Close() - if err := png.Encode(f, img); err != nil { - panic(err) + options := maze.DefaultRenderOptions() + options.HighlightPath = true + options.Scale = 5 + + if err := maze.SavePNG(grid, "maze.png", options); err != nil { + log.Fatalf("save maze image: %v", err) } + + fmt.Println("Saved maze to maze.png") } diff --git a/maze/maze.go b/maze/maze.go new file mode 100644 index 0000000..7949a6e --- /dev/null +++ b/maze/maze.go @@ -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 +} diff --git a/maze/render.go b/maze/render.go new file mode 100644 index 0000000..de73166 --- /dev/null +++ b/maze/render.go @@ -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) +} diff --git a/mazer/maze.go b/mazer/maze.go deleted file mode 100644 index 6352f87..0000000 --- a/mazer/maze.go +++ /dev/null @@ -1,192 +0,0 @@ -package mazer - -import "math/rand/v2" - -func GenerateMaze(width int, height int) [][]int { - if width <= 0 || height <= 0 { - return [][]int{} - } - - cells := make([]int, width*height) - matrix := make([][]int, height) - for y := 0; y < height; y++ { - rowStart := y * width - matrix[y] = cells[rowStart : rowStart+width] - } - - if width < 3 || height < 3 { - return matrix - } - - startX, startY := 1, 1 - matrix[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 := 0, 0 - switch d { - case 0: - dy = -2 - case 1: - dx = 2 - case 2: - dy = 2 - default: - dx = -2 - } - - nx, ny := x+dx, y+dy - if nx <= 0 || nx >= width-1 || ny <= 0 || ny >= height-1 { - continue - } - if matrix[ny][nx] == 1 { - continue - } - - matrix[y+dy/2][x+dx/2] = 1 - matrix[ny][nx] = 1 - stackX = append(stackX, nx) - stackY = append(stackY, ny) - carved = true - break - } - - if !carved { - stackX = stackX[:last] - stackY = stackY[:last] - } - } - - // Entrance on top border at a random connected X. - topChoices := make([]int, 0, width/2) - for x := 1; x < width-1; x++ { - if matrix[1][x] == 1 { - topChoices = append(topChoices, x) - } - } - entranceX := topChoices[rand.IntN(len(topChoices))] - matrix[0][entranceX] = 1 - - // Exit on bottom border at a random connected X. - bottomChoices := make([]int, 0, width/2) - for x := 1; x < width-1; x++ { - if matrix[height-2][x] == 1 { - bottomChoices = append(bottomChoices, x) - } - } - exitX := bottomChoices[rand.IntN(len(bottomChoices))] - matrix[height-1][exitX] = 1 - - return matrix -} - -func FindSolutionPath(matrix [][]int) [][]bool { - height := len(matrix) - if height == 0 { - return nil - } - width := len(matrix[0]) - if width == 0 { - return nil - } - - startX := -1 - endX := -1 - for x := 0; x < width; x++ { - if matrix[0][x] == 1 { - startX = x - break - } - } - for x := 0; x < width; x++ { - if matrix[height-1][x] == 1 { - endX = x - break - } - } - if startX == -1 || endX == -1 { - return nil - } - - 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 matrix[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 - } - - 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 -} - -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 -}