From 17d9903f17ab6dc022f4db3b2e725fab6a8b2f62 Mon Sep 17 00:00:00 2001 From: dwrz Date: Sun, 25 Oct 2026 02:51:07 +0000 Subject: [PATCH] Add maze pkg --- pkg/maze/cell/cell.go | 21 +++++ pkg/maze/cell/cell_test.go | 43 ++++++++++ pkg/maze/direction/direction.go | 43 ++++++++++ pkg/maze/generate.go | 84 +++++++++++++++++++ pkg/maze/maze.go | 144 ++++++++++++++++++++++++++++++++ pkg/maze/position/position.go | 53 ++++++++++++ 6 files changed, 388 insertions(+) create mode 100644 pkg/maze/cell/cell.go create mode 100644 pkg/maze/cell/cell_test.go create mode 100644 pkg/maze/direction/direction.go create mode 100644 pkg/maze/generate.go create mode 100644 pkg/maze/maze.go create mode 100644 pkg/maze/position/position.go diff --git a/pkg/maze/cell/cell.go b/pkg/maze/cell/cell.go new file mode 100644 index 0000000..adaa4fb --- /dev/null +++ b/pkg/maze/cell/cell.go @@ -0,0 +1,21 @@ +// Package cell represents a single cell in a maze grid. +package cell + +import ( + "code.chimeric.al/dwrz/minotaur/pkg/maze/direction" +) + +// Cell is a bitmask of open passages from a cell to its neighbors. +// Bit d (1 << d) is 1 when the passage in direction d is open, and 0 when a +// wall blocks that direction. +type Cell uint8 + +// HasDirection reports whether the passage in direction d is open. +func (c Cell) HasDirection(d direction.Direction) bool { + return c&(1<= w { + return false + } + if y < 0 || y >= h { + return false + } + + return true +} + +// http://weblog.jamisbuck.org/2011/1/27/maze-generation-growing-tree-algorithm +func generate(h, w int) [][]cell.Cell { + var layout = make([][]cell.Cell, h) + + // Create the rows. + for i := range layout { + layout[i] = make([]cell.Cell, w) + for j := range layout[i] { + layout[i][j] = 0 + } + } + + // Connect the cells. + var ( + current = position.Position{X: 0, Y: 0} + stack = []position.Position{current} + visited = map[position.Position]struct{}{ + current: {}, + } + ) + for len(stack) > 0 { + // Get newest. + current = stack[len(stack)-1] + + dirs := direction.RandomDirections() + found := false + for _, d := range dirs { + var neighbor = current.WithDirection(d) + + // Ignore if out of bounds. + if !inBounds(h, w, neighbor.X, neighbor.Y) { + continue + } + + // Ignore if already visited. + if _, seen := visited[neighbor]; seen { + continue + } + + // We've found a cell to connect. + found = true + + // Mark it as visited. + visited[neighbor] = struct{}{} + + // Add it to the queue. + stack = append(stack, neighbor) + + // Connect the cells. + layout[current.Y][current.X].SetDirection(d) + layout[neighbor.Y][neighbor.X].SetDirection( + d.Opposite(), + ) + + break + } + + // If no neighbor was found, remove this cell from the list. + if !found { + stack = stack[:len(stack)-1] + } + } + + return layout +} diff --git a/pkg/maze/maze.go b/pkg/maze/maze.go new file mode 100644 index 0000000..41ef674 --- /dev/null +++ b/pkg/maze/maze.go @@ -0,0 +1,144 @@ +package maze + +import ( + "bytes" + + "code.chimeric.al/dwrz/minotaur/pkg/maze/cell" + "code.chimeric.al/dwrz/minotaur/pkg/maze/direction" + "code.chimeric.al/dwrz/minotaur/pkg/maze/position" + "code.chimeric.al/dwrz/minotaur/pkg/terminal" +) + +type Maze struct { + height int + layout [][]cell.Cell + width int +} + +type Parameters struct { + Height int + Width int +} + +func New(p Parameters) (*Maze, error) { + return &Maze{ + height: p.Height, + layout: generate(p.Height, p.Width), + width: p.Width, + }, nil +} + +func (m *Maze) Cell(p position.Position) cell.Cell { + return m.layout[p.Y][p.X] +} + +func (m *Maze) Solve(start, end position.Position) (s []position.Position) { + var q = [][]position.Position{{start}} + for len(q) > 0 { + // Get the current search from the queue. + s, q = q[0], q[1:] + + // Use the last cell as the current candidate. + var ( + current = s[len(s)-1] + cell = m.Cell(current) + previous *position.Position + ) + + // If we've reached the end, we have a solution. + if current.Equal(end) { + return s + } + + // Explore new routes. + // Determine the previous cell, to avoid backtracking. + if len(s) > 1 { + previous = &s[len(s)-2] + } + for _, d := range direction.Directions() { + if !cell.HasDirection(d) { + continue + } + + // Get the next position with this direction. + next := current.WithDirection(d) + + // Prevent backtracking. + if previous != nil && next.Equal(*previous) { + continue + } + + // Next is valid; add it to the search queue. + // TODO: is there a way to reuse the backing array? + var route = make([]position.Position, len(s)+1) + copy(route, s) + route[len(route)-1] = next + + q = append(q, route) + } + } + + return nil +} + +type RenderParameters struct { + Passage string + // Positions is used to render a cell with the mapped string. + // The order in which you list positions matters. + // If there are overlapping positions, the last set one wins. + Positions map[position.Position]string + Wall string +} + +func (m *Maze) Render(p RenderParameters) []byte { + var buf bytes.Buffer + + buf.WriteString(terminal.CursorTopLeft) + + // Print the top wall. + // We need to print each cell, and its right connection. + // Thus the wall needs to be twice the width. + // We also need to cover the right wall, thus +1. + topWidth := 2*m.width + 1 + for range topWidth { + buf.WriteString(p.Wall) + } + buf.WriteString("\r\n") + + for y, row := range m.layout { + buf.WriteString(p.Wall) // left wall + + // Print each cell and the passage or wall to its right. + for x, cell := range row { + var block = p.Positions[position.Position{X: x, Y: y}] + if block == "" { + block = p.Passage + } + + buf.WriteString(block) + + if cell.HasDirection(direction.Right) { + buf.WriteString(p.Passage) + } else { + buf.WriteString(p.Wall) + } + } + buf.WriteString("\r\n") + + // Print the cell's bottom passage or wall. + // Print a wall to the cell's lower-right. + buf.WriteString(p.Wall) // left wall + for _, cell := range row { + if cell.HasDirection(direction.Down) { + buf.WriteString(p.Passage) + } else { + buf.WriteString(p.Wall) + } + + buf.WriteString(p.Wall) + } + buf.WriteString("\r\n") + } + + return buf.Bytes() +} diff --git a/pkg/maze/position/position.go b/pkg/maze/position/position.go new file mode 100644 index 0000000..0a9b52b --- /dev/null +++ b/pkg/maze/position/position.go @@ -0,0 +1,53 @@ +package position + +import ( + "math/rand" + + "code.chimeric.al/dwrz/minotaur/pkg/maze/direction" +) + +type Position struct { + X, Y int +} + +func (p Position) Equal(v Position) bool { + return p.X == v.X && p.Y == v.Y +} + +func (p Position) Down() Position { + return Position{X: p.X, Y: p.Y + 1} +} + +func (p Position) Left() Position { + return Position{X: p.X - 1, Y: p.Y} +} + +func (p Position) Right() Position { + return Position{X: p.X + 1, Y: p.Y} +} + +func (p Position) Up() Position { + return Position{X: p.X, Y: p.Y - 1} +} + +func (p Position) WithDirection(d direction.Direction) Position { + switch d { + case direction.Down: + return p.Down() + case direction.Left: + return p.Left() + case direction.Right: + return p.Right() + case direction.Up: + return p.Up() + default: + return p + } +} + +func Random(width, height int) Position { + return Position{ + X: rand.Intn(width), + Y: rand.Intn(height), + } +}