Add maze pkg
This commit is contained in:
@@ -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<<d) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDirection opens the passage in direction d.
|
||||||
|
func (c *Cell) SetDirection(d direction.Direction) {
|
||||||
|
*c |= 1 << d
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package cell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.chimeric.al/dwrz/minotaur/pkg/maze/direction"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetDirection(t *testing.T) {
|
||||||
|
var c Cell
|
||||||
|
t.Logf("start: %08b", c)
|
||||||
|
|
||||||
|
c.SetDirection(direction.Right)
|
||||||
|
t.Logf("set right: %08b", c)
|
||||||
|
|
||||||
|
c.SetDirection(direction.Down)
|
||||||
|
t.Logf("set down: %08b", c)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hasRight = c.HasDirection(direction.Right)
|
||||||
|
hasDown = c.HasDirection(direction.Down)
|
||||||
|
hasLeft = c.HasDirection(direction.Left)
|
||||||
|
hasUp = c.HasDirection(direction.Up)
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Logf("has right: %v", hasRight)
|
||||||
|
t.Logf("has down: %v", hasDown)
|
||||||
|
t.Logf("has left: %v", hasLeft)
|
||||||
|
t.Logf("has up: %v", hasUp)
|
||||||
|
|
||||||
|
if !hasRight {
|
||||||
|
t.Errorf("expected cell to have right")
|
||||||
|
}
|
||||||
|
if !hasDown {
|
||||||
|
t.Errorf("expected cell to have down")
|
||||||
|
}
|
||||||
|
if hasLeft {
|
||||||
|
t.Errorf("expected cell to not have left")
|
||||||
|
}
|
||||||
|
if hasUp {
|
||||||
|
t.Errorf("expected cell to not have up")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package direction
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Direction int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Down Direction = iota
|
||||||
|
Left
|
||||||
|
Right
|
||||||
|
Up
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d Direction) Opposite() Direction {
|
||||||
|
switch d {
|
||||||
|
case Down:
|
||||||
|
return Up
|
||||||
|
case Left:
|
||||||
|
return Right
|
||||||
|
case Right:
|
||||||
|
return Left
|
||||||
|
case Up:
|
||||||
|
return Down
|
||||||
|
default:
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Directions() [4]Direction {
|
||||||
|
return [4]Direction{Down, Left, Right, Up}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomDirections() [4]Direction {
|
||||||
|
var dirs = [...]Direction{Down, Left, Right, Up}
|
||||||
|
|
||||||
|
rand.Shuffle(len(dirs), func(i, j int) {
|
||||||
|
dirs[i], dirs[j] = dirs[j], dirs[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package maze
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.chimeric.al/dwrz/minotaur/pkg/maze/cell"
|
||||||
|
"code.chimeric.al/dwrz/minotaur/pkg/maze/direction"
|
||||||
|
"code.chimeric.al/dwrz/minotaur/pkg/maze/position"
|
||||||
|
)
|
||||||
|
|
||||||
|
func inBounds(h, w, x, y int) bool {
|
||||||
|
if x < 0 || x >= 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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user