307 lines
9.7 KiB
Go
307 lines
9.7 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package maze
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"image"
|
|
)
|
|
|
|
type problemCellState uint8
|
|
|
|
const (
|
|
// zero value, for unvisited cells.
|
|
stateUnvisited problemCellState = iota
|
|
|
|
// we considered the cell and set its heuristic, but then the movement
|
|
// test failed, we don't know how to reach it still. distance is still uninitialized.
|
|
stateUnexplored
|
|
|
|
// this is the starting cell.
|
|
// distance is 0.
|
|
stateStart
|
|
|
|
// the cell was reached via the respective direction.
|
|
// distance is the number of cells traversed to reach this cell.
|
|
stateRight
|
|
stateDown
|
|
stateLeft
|
|
stateUp
|
|
|
|
// the number of directions, for array bounds.
|
|
// not used as a state itself.
|
|
stateLen
|
|
)
|
|
|
|
var stateStrings = [stateLen]string{
|
|
"unvisited",
|
|
"unexplored",
|
|
"start",
|
|
"right",
|
|
"down",
|
|
"left",
|
|
"up",
|
|
}
|
|
|
|
// lookup table to reverse the direction.
|
|
//
|
|
// used to backtrack from the end to the start once the maze is solved.
|
|
var dirReverse = [stateLen]problemCellState{
|
|
stateUnvisited, // stateUnvisited
|
|
stateUnexplored, // stateUnexplored
|
|
stateStart, // stateStart
|
|
stateLeft, // stateRight
|
|
stateUp, // stateDown
|
|
stateRight, // stateLeft
|
|
stateDown, // stateUp
|
|
}
|
|
|
|
// lookup table to advance in the given direction.
|
|
var dirAdvance = [stateLen]image.Point{
|
|
{0, 0}, // stateUnvisited
|
|
{0, 0}, // stateUnexplored
|
|
{0, 0}, // stateStart
|
|
{1, 0}, // stateRight
|
|
{0, 1}, // stateDown
|
|
{-1, 0}, // stateLeft
|
|
{0, -1}, // stateUp
|
|
}
|
|
|
|
// lookup table to offset a point during movement tests
|
|
// for left and up, we need to test the cell to the left or above.
|
|
var dirTestOffset = [stateLen]image.Point{
|
|
{0, 0}, // stateUnvisited
|
|
{0, 0}, // stateUnexplored
|
|
{0, 0}, // stateStart
|
|
{0, 0}, // stateRight
|
|
{0, 0}, // stateDown
|
|
{-1, 0}, // stateLeft
|
|
{0, -1}, // stateUp
|
|
}
|
|
|
|
func (d problemCellState) String() string {
|
|
if d < stateLen {
|
|
return stateStrings[d]
|
|
}
|
|
|
|
// The Stringer is the only case where we do bounds checking, the other functions can just panic.
|
|
return fmt.Sprintf("unknown(%d)", d)
|
|
}
|
|
|
|
type problemCell struct {
|
|
state problemCellState
|
|
|
|
// the number of cells traversed to reach this cell.
|
|
// only valid if state for stateStart, stateRight, stateDown, stateLeft, or stateUp.
|
|
distance uint
|
|
|
|
// the optimistic distance estimate to the end point.
|
|
// valid for any state except stateUnvisited.
|
|
heuristic uint
|
|
}
|
|
|
|
// A Problem is intended to be used by the Solve function to find a path through a maze.
|
|
//
|
|
// This is more of a demonstration of how to use the Solve function, a dedicated maze solving
|
|
// algorithm would more efficient. In particular, the solver manages a max heap for discarding
|
|
// states when at capacity, which a maze solver would not need - a maze has a finite number of cells,
|
|
// which for our purposes represent a state, and we'd never exceed (and thus need to keep track
|
|
// of and discard) that many states.
|
|
type Problem struct {
|
|
// the bounds of the maze.
|
|
size image.Rectangle
|
|
|
|
// a lookup table for the bounds that a cell can be transverse in.
|
|
//
|
|
// for the movement directions, this will be identical to size, but contracted by one cell.
|
|
// for example, legalPositions[stateRight] will have the rightmost column removed, as you can't
|
|
// move further right from the rightmost column.
|
|
//
|
|
// for other possible states, the rectangle will be empty (and realistically, never used).
|
|
legalPositions [stateLen]image.Rectangle
|
|
|
|
// the functions that test if movement is allowed in the given direction.
|
|
// this needs to be combined with dirTestOffset to get the correct cell to test.
|
|
testFuncs [stateLen]func(image.Point) bool
|
|
|
|
// the start and end points of the maze.
|
|
start, end image.Point
|
|
|
|
// the cells in the maze, starting at size.Min, going left to right, top to bottom.
|
|
cells []problemCell
|
|
}
|
|
|
|
// convert a coordinate to an index in the cells slice.
|
|
//
|
|
// it is assumed that c.In(p.size) is true, otherwise this
|
|
// will yield a wrong, and possibly out of bounds index.
|
|
func (p Problem) coordToIndex(c image.Point) int {
|
|
return (c.X - p.size.Min.X) + (c.Y-p.size.Min.Y)*p.size.Dx()
|
|
}
|
|
|
|
func absDelta(a, b int) uint {
|
|
if a > b {
|
|
return uint(a - b)
|
|
}
|
|
return uint(b - a)
|
|
}
|
|
|
|
// an optimistic distance estimate based on the Manhattan distance to the end point.
|
|
func (p Problem) heuristic(c image.Point) uint {
|
|
return absDelta(c.X, p.end.X) + absDelta(c.Y, p.end.Y)
|
|
}
|
|
|
|
// Initializes the maze search problem, and appends the start point to the given slice.
|
|
func (p *Problem) Initialize(out []image.Point) ([]image.Point, error) {
|
|
p.cells = make([]problemCell, p.size.Dx()*p.size.Dy())
|
|
|
|
p.cells[p.coordToIndex(p.start)] = problemCell{
|
|
state: stateStart,
|
|
heuristic: p.heuristic(p.end),
|
|
}
|
|
|
|
return append(out, p.start), nil
|
|
}
|
|
|
|
// Appends the next positions to consider from the given position.
|
|
func (p Problem) Next(c image.Point, out []image.Point) ([]image.Point, error) {
|
|
distance := p.cells[p.coordToIndex(c)].distance + 1
|
|
|
|
for _, d := range [...]problemCellState{stateRight, stateDown, stateLeft, stateUp} {
|
|
if !c.In(p.legalPositions[d]) {
|
|
// can't move in this direction, stop immediately.
|
|
continue
|
|
}
|
|
|
|
next := c.Add(dirAdvance[d])
|
|
nextIndex := p.coordToIndex(next)
|
|
|
|
switch {
|
|
case p.cells[nextIndex].state == stateUnvisited:
|
|
// calculate the heuristic for the first time and change its state to unexplored.
|
|
p.cells[nextIndex].heuristic = p.heuristic(next)
|
|
p.cells[nextIndex].state = stateUnexplored
|
|
fallthrough
|
|
|
|
case p.cells[nextIndex].state == stateUnexplored:
|
|
// similar to the unvisited case, but don't calculate the heuristic again.
|
|
fallthrough
|
|
|
|
// if the cell has been visited, but we found a shorter path to it, then
|
|
// update the direction and distance.
|
|
case distance < p.cells[nextIndex].distance:
|
|
// if we reach this point, then one of the above cases has occurred, and
|
|
// this is a cell we're interested in exploring further.
|
|
//
|
|
// test to make sure we can actually move there.
|
|
if p.testFuncs[d](c.Add(dirTestOffset[d])) {
|
|
p.cells[nextIndex].state = d
|
|
p.cells[nextIndex].distance = distance
|
|
out = append(out, next)
|
|
}
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// Returns true if the given position is the end of the maze.
|
|
//
|
|
// We're also assuming that the point was returned by Initialize or Next,
|
|
// otherwise we wouldn't know how to reach the start position, and this
|
|
// wouldn't actually be solved.
|
|
func (p Problem) Solved(c image.Point) bool {
|
|
return c == p.end
|
|
}
|
|
|
|
// Converts the given position, the one that passed the Solved test above,
|
|
// into a list of points that form a path from the start to the end.
|
|
func (p Problem) Finish(c image.Point) ([]image.Point, error) {
|
|
if c != p.end {
|
|
return nil, fmt.Errorf("not at the end: %v", c)
|
|
}
|
|
|
|
switch p.cells[p.coordToIndex(c)].state {
|
|
case stateUnvisited, stateUnexplored:
|
|
return nil, fmt.Errorf("no path to the end")
|
|
}
|
|
|
|
length := p.cells[p.coordToIndex(c)].distance
|
|
path := make([]image.Point, length+1)
|
|
path[length] = c
|
|
|
|
for ; length > 0; length-- {
|
|
c = c.Add(dirAdvance[dirReverse[p.cells[p.coordToIndex(c)].state]])
|
|
path[length-1] = c
|
|
}
|
|
|
|
return path, nil
|
|
}
|
|
|
|
// This function would be used to release any resources associated with a given search state,
|
|
// but since a state for this problem is just a point, and all the actual state information
|
|
// is stored in the cells slice, there's nothing to do here.
|
|
func (p Problem) Discard(image.Point) {
|
|
// nop
|
|
}
|
|
|
|
// Returns true if state a should be explored before b.
|
|
func (p Problem) OptimisticLess(a, b image.Point) bool {
|
|
ca := p.cells[p.coordToIndex(a)]
|
|
cb := p.cells[p.coordToIndex(b)]
|
|
|
|
if r := cmp.Compare(ca.distance+ca.heuristic, cb.distance+cb.heuristic); r != 0 {
|
|
return r < 0
|
|
}
|
|
|
|
// If the estimated total path length for both states seems to be equal,
|
|
// prefer the one with the lower heuristic (closest to the end).
|
|
//
|
|
// since distance+heuristic for both states is equal, this conversely means that
|
|
// we prefer the one with the higher distance traveled so far.
|
|
return ca.heuristic < cb.heuristic
|
|
}
|
|
|
|
// Similar to OptimisticLess, but we scale the heuristic to penalize uncertainty
|
|
// about the actual remaining distance.
|
|
//
|
|
// Note that the solver uses this with a max heap to determine which states to prune when at capacity.
|
|
//
|
|
// This will typically be similar to OptimisticLess, but with a penalty for uncertainty.
|
|
//
|
|
// For our purposes, we're just going to use the same heuristic as OptimisticLess. Assuming that you give
|
|
// the solver a capacity equal to the number of cells in the maze, it won't need to prune any states.
|
|
func (p Problem) PessimisticLess(a, b image.Point) bool {
|
|
return p.OptimisticLess(a, b)
|
|
}
|
|
|
|
// NewMazeProblem creates a new maze problem with the given size, start and end points,
|
|
// and well as two functions that test if movement to the right and down is allowed from a given point.
|
|
func NewProblem(size image.Rectangle, start, end image.Point, rightTest func(image.Point) bool, downTest func(image.Point) bool) *Problem {
|
|
var legalPositions [stateLen]image.Rectangle
|
|
|
|
legalPositions[stateRight] = image.Rect(size.Min.X, size.Min.Y, size.Max.X-1, size.Max.Y)
|
|
legalPositions[stateDown] = image.Rect(size.Min.X, size.Min.Y, size.Max.X, size.Max.Y-1)
|
|
legalPositions[stateLeft] = image.Rect(size.Min.X+1, size.Min.Y, size.Max.X, size.Max.Y)
|
|
legalPositions[stateUp] = image.Rect(size.Min.X, size.Min.Y+1, size.Max.X, size.Max.Y)
|
|
|
|
var testFuncs [stateLen]func(image.Point) bool
|
|
|
|
testFuncs[stateRight] = rightTest
|
|
testFuncs[stateDown] = downTest
|
|
|
|
// note that the dirTestOffset lookup table has non-zero values for left and up,
|
|
// to compensate for the fact that we need to test the cell to the left or above.
|
|
testFuncs[stateLeft] = rightTest
|
|
testFuncs[stateUp] = downTest
|
|
|
|
return &Problem{
|
|
size: size,
|
|
legalPositions: legalPositions,
|
|
testFuncs: testFuncs,
|
|
start: start,
|
|
end: end,
|
|
}
|
|
}
|