2024-04-10 03:26:34 -04:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2024-04-10 01:04:22 -04:00
|
|
|
package maze
|
|
|
|
|
|
|
|
import (
|
|
|
|
"image"
|
|
|
|
"image/color"
|
|
|
|
"image/draw"
|
|
|
|
"math/rand"
|
|
|
|
)
|
|
|
|
|
|
|
|
type cell uint8
|
|
|
|
|
|
|
|
const (
|
|
|
|
// cellRight is set if movement to the right is allowed.
|
|
|
|
cellRight cell = 1 << iota
|
|
|
|
|
|
|
|
// cellDown is set if movement down is allowed.
|
|
|
|
cellDown
|
|
|
|
)
|
|
|
|
|
|
|
|
type Maze struct {
|
|
|
|
Size image.Rectangle
|
|
|
|
Start, End image.Point
|
|
|
|
|
|
|
|
// the cells in the maze, starting at size.Min and going left-to-right, top-to-bottom.
|
|
|
|
cells []cell
|
|
|
|
}
|
|
|
|
|
|
|
|
// if cells at i and j belong to different neighborhoods, they will be merged
|
|
|
|
// and true will be returned. Otherwise, false will be returned.
|
|
|
|
func tryJoinNeighborhoods(neighborhood []*[]int, i, j int) bool {
|
|
|
|
if aN, bN := neighborhood[i], neighborhood[j]; aN != bN {
|
|
|
|
// prefer the larger set absorbing the smaller set.
|
|
|
|
if len(*aN) < len(*bN) {
|
|
|
|
aN, bN = bN, aN
|
|
|
|
}
|
|
|
|
|
|
|
|
// aN is now the larger set, and bN is the smaller set.
|
|
|
|
|
|
|
|
// append the smaller set to the larger set.
|
|
|
|
*aN = append(*aN, *bN...)
|
|
|
|
|
|
|
|
// update the membership of the smaller set to point to the larger set.
|
|
|
|
for _, member := range *bN {
|
|
|
|
neighborhood[member] = aN
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make creates a new maze of the given size, using the given random number generator.
|
|
|
|
//
|
|
|
|
// If r is nil, the default random number generator will be used.
|
|
|
|
func Make(size image.Rectangle, r *rand.Rand) Maze {
|
|
|
|
if size.Dx() <= 0 || size.Dy() <= 0 {
|
|
|
|
panic("invalid maze size")
|
|
|
|
}
|
|
|
|
|
|
|
|
if r == nil {
|
|
|
|
// create a new random number generator if
|
|
|
|
// one wasn't provided.
|
|
|
|
r = rand.New(rand.NewSource(rand.Int63()))
|
|
|
|
}
|
|
|
|
|
|
|
|
w, h := size.Dx(), size.Dy()
|
|
|
|
neighborhoods := make([]*[]int, w*h)
|
|
|
|
cells := make([]cell, w*h)
|
|
|
|
|
|
|
|
// each cell begins initially disconnected, alone in its own set.
|
|
|
|
for i := range cells {
|
|
|
|
neighborhoods[i] = &[]int{i}
|
|
|
|
}
|
|
|
|
|
|
|
|
// create a list of all edges in the maze.
|
|
|
|
// there are (w-1)*h horizontal edges, and w*(h-1) vertical edges.
|
|
|
|
// the first (w-1)*h edges are horizontal, and the rest are vertical.
|
|
|
|
edges := make([]int, (w-1)*h+w*(h-1))
|
|
|
|
for i := range edges {
|
|
|
|
edges[i] = i
|
|
|
|
}
|
|
|
|
|
|
|
|
// shuffle the list of edges, this will be the order in which we attempt to join cells.
|
|
|
|
r.Shuffle(len(edges), func(i, j int) {
|
|
|
|
edges[i], edges[j] = edges[j], edges[i]
|
|
|
|
})
|
|
|
|
|
|
|
|
// repeatedly join cells until all cells are in the same set.
|
|
|
|
for _, edge := range edges {
|
|
|
|
if edge < (w-1)*h {
|
|
|
|
// join right
|
|
|
|
// we need to convert the edge index into coordinates, and then back into a cell index.
|
|
|
|
i := edge%(w-1) + edge/(w-1)*w
|
|
|
|
if tryJoinNeighborhoods(neighborhoods, i, i+1) {
|
|
|
|
cells[i] |= cellRight
|
|
|
|
} else {
|
|
|
|
// already joined, skip to the next edge.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// join down
|
|
|
|
// we can mostly use the edge index unmodified, we just need to subtract the length of the horizontal edges.
|
|
|
|
i := edge - (w-1)*h
|
|
|
|
if tryJoinNeighborhoods(neighborhoods, i, i+w) {
|
|
|
|
cells[i] |= cellDown
|
|
|
|
} else {
|
|
|
|
// already joined, skip to the next edge.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we reach this point, then two neighborhoods were joined. If all cells are in the same neighborhood,
|
|
|
|
// then we can stop trying to merge the remaining edges.
|
|
|
|
if len(*neighborhoods[0]) == w*h {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var start, end image.Point
|
|
|
|
|
|
|
|
// choose a start and end point on opposite sides of the maze.
|
|
|
|
switch r.Intn(2) {
|
|
|
|
case 0: // left/right
|
|
|
|
start = image.Pt(0, r.Intn(h)).Add(size.Min)
|
|
|
|
end = image.Pt(w-1, r.Intn(h)).Add(size.Min)
|
|
|
|
|
|
|
|
case 1: // top/bottom
|
|
|
|
start = image.Pt(r.Intn(w), 0).Add(size.Min)
|
|
|
|
end = image.Pt(r.Intn(w), h-1).Add(size.Min)
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Intn(2) == 0 {
|
|
|
|
// swap start and end points.
|
|
|
|
start, end = end, start
|
|
|
|
}
|
|
|
|
|
|
|
|
return Maze{
|
|
|
|
Size: size,
|
|
|
|
Start: start,
|
|
|
|
End: end,
|
|
|
|
cells: cells,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m Maze) coordToIndex(p image.Point) int {
|
|
|
|
return p.X - m.Size.Min.X + (p.Y-m.Size.Min.Y)*m.Size.Dx()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns true if you can move to the right from the given point.
|
|
|
|
func (m Maze) Right(p image.Point) bool {
|
|
|
|
return p.In(m.Size) && m.cells[m.coordToIndex(p)]&cellRight != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns true if you can move down from the given point.
|
|
|
|
func (m Maze) Down(p image.Point) bool {
|
|
|
|
return p.In(m.Size) && m.cells[m.coordToIndex(p)]&cellDown != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns true if you can move to the left from the given point.
|
|
|
|
func (m Maze) Left(p image.Point) bool {
|
|
|
|
return m.Right(p.Sub(image.Pt(1, 0)))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns true if you can move up from the given point.
|
|
|
|
func (m Maze) Up(p image.Point) bool {
|
|
|
|
return m.Down(p.Sub(image.Pt(0, 1)))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns a drawing of the maze, mostly for debugging purposes.
|
|
|
|
// An optional path can be drawn on top of the maze.
|
|
|
|
func (m Maze) Draw(path []image.Point) *image.RGBA {
|
|
|
|
const cellSize = 16
|
|
|
|
const markerRadius = 4 // the radius of the start and end markers.
|
|
|
|
const wallRadius = 2 // half the thickness of the wall lines.
|
|
|
|
const pathRadius = 2 // half the thickness of the path line.
|
|
|
|
|
|
|
|
backgroundColor := image.NewUniform(color.Black)
|
|
|
|
wallColor := image.NewUniform(color.White)
|
|
|
|
startColor := image.NewUniform(color.RGBA{0, 255, 0, 255})
|
|
|
|
endColor := image.NewUniform(color.RGBA{255, 0, 0, 255})
|
|
|
|
pathColor := image.NewUniform(color.RGBA{0, 0, 255, 255})
|
|
|
|
|
|
|
|
leftWall := image.Rect(0, 0, 0, cellSize).Inset(-wallRadius)
|
|
|
|
topWall := image.Rect(0, 0, cellSize, 0).Inset(-wallRadius)
|
|
|
|
rightWall := leftWall.Add(image.Pt(cellSize, 0))
|
|
|
|
bottomWall := topWall.Add(image.Pt(0, cellSize))
|
|
|
|
|
|
|
|
marker := image.Rect(cellSize/2-markerRadius, cellSize/2-markerRadius, cellSize/2+markerRadius, cellSize/2+markerRadius)
|
|
|
|
|
|
|
|
img := image.NewRGBA(image.Rect(0, 0, m.Size.Dx()*cellSize+wallRadius*2, m.Size.Dy()*cellSize+wallRadius*2))
|
|
|
|
|
|
|
|
// fill the entire image with the background color.
|
|
|
|
draw.Draw(img, img.Bounds(), backgroundColor, image.Point{}, draw.Src)
|
|
|
|
|
|
|
|
for y := m.Size.Min.Y; y < m.Size.Max.Y; y++ {
|
|
|
|
for x := m.Size.Min.X; x < m.Size.Max.X; x++ {
|
|
|
|
|
|
|
|
p := image.Pt(x, y)
|
|
|
|
cellP := p.Sub(m.Size.Min).Mul(cellSize).Add(image.Pt(wallRadius, wallRadius))
|
|
|
|
|
|
|
|
draw := func(rect image.Rectangle, color *image.Uniform) {
|
|
|
|
draw.Draw(img, rect.Add(cellP), color, image.Point{}, draw.Src)
|
|
|
|
}
|
|
|
|
|
|
|
|
if p == m.Start {
|
|
|
|
draw(marker, startColor)
|
|
|
|
}
|
|
|
|
|
|
|
|
if p == m.End {
|
|
|
|
draw(marker, endColor)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !m.Right(p) {
|
|
|
|
draw(rightWall, wallColor)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !m.Down(p) {
|
|
|
|
draw(bottomWall, wallColor)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !m.Left(p) {
|
|
|
|
draw(leftWall, wallColor)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !m.Up(p) {
|
|
|
|
draw(topWall, wallColor)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(path) > 1 {
|
|
|
|
pathDrawOffset := image.Pt(cellSize/2+wallRadius, cellSize/2+wallRadius)
|
|
|
|
|
|
|
|
draw := func(i, j int) {
|
|
|
|
// fake drawing a line between the two points by treating them as points
|
|
|
|
// of a rectangle, and drawing that rectangle instead.
|
|
|
|
//
|
|
|
|
// this only works because we're assuming there are no diagonal movements.
|
|
|
|
rect := image.Rectangle{
|
|
|
|
path[i].Sub(m.Size.Min).Mul(cellSize),
|
|
|
|
path[j].Sub(m.Size.Min).Mul(cellSize),
|
|
|
|
}.Canon().Inset(-pathRadius).Add(pathDrawOffset)
|
|
|
|
|
|
|
|
draw.Draw(img, rect, pathColor, image.Point{}, draw.Src)
|
|
|
|
}
|
|
|
|
|
|
|
|
start := 0
|
|
|
|
|
|
|
|
// cheating by assuming adjacent points in the path are adjacent cells,
|
|
|
|
// and movements are only horizontal or vertical.
|
|
|
|
for i := 1; i < len(path); i++ {
|
|
|
|
if path[start].X == path[i].X || path[start].Y == path[i].Y {
|
|
|
|
// while points are on the same horizontal or vertical line,
|
|
|
|
// skip the intermediate points.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// all the points before this one were on the same line, so draw them
|
|
|
|
// as a single line.
|
|
|
|
draw(start, i-1)
|
|
|
|
start = i - 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// any remaining points we haven't drawn yet are also on the same line.
|
|
|
|
draw(start, len(path)-1)
|
|
|
|
}
|
|
|
|
|
|
|
|
return img
|
|
|
|
}
|