OpenDiablo2/d2common/d2astar/pather_test.go

216 lines
4.6 KiB
Go

package d2astar
// pather_test.go implements a basic world and tiles that implement Pather for
// the sake of testing. This functionality forms the back end for
// path_test.go, and serves as an example for how to use A* for a grid.
import (
"fmt"
"strings"
)
// Kind* constants refer to tile kinds for input and output.
const (
// KindPlain (.) is a plain tile with a movement cost of 1.
KindPlain = iota
// KindRiver (~) is a river tile with a movement cost of 2.
KindRiver
// KindMountain (M) is a mountain tile with a movement cost of 3.
KindMountain
// KindBlocker (X) is a tile which blocks movement.
KindBlocker
// KindFrom (F) is a tile which marks where the path should be calculated
// from.
KindFrom
// KindTo (T) is a tile which marks the goal of the path.
KindTo
// KindPath (●) is a tile to represent where the path is in the output.
KindPath
)
// KindRunes map tile kinds to output runes.
var KindRunes = map[int]rune{ // nolint:gochecknoglobals // it's a test
KindPlain: '.',
KindRiver: '~',
KindMountain: 'M',
KindBlocker: 'X',
KindFrom: 'F',
KindTo: 'T',
KindPath: '●',
}
// RuneKinds map input runes to tile kinds.
var RuneKinds = map[rune]int{ // nolint:gochecknoglobals // it's a test
'.': KindPlain,
'~': KindRiver,
'M': KindMountain,
'X': KindBlocker,
'F': KindFrom,
'T': KindTo,
}
// KindCosts map tile kinds to movement costs.
var KindCosts = map[int]float64{ // nolint:gochecknoglobals // it's a test
KindPlain: 1.0,
KindFrom: 1.0,
KindTo: 1.0,
KindRiver: 2.0,
KindMountain: 3.0,
}
// A Tile is a tile in a grid which implements Pather.
type Tile struct {
// Kind is the kind of tile, potentially affecting movement.
Kind int
// X and Y are the coordinates of the tile.
X, Y int
// W is a reference to the World that the tile is a part of.
W World
}
// PathNeighbors returns the neighbors of the tile, excluding blockers and
// tiles off the edge of the board.
func (t *Tile) PathNeighbors() []Pather {
neighbors := make([]Pather, 0)
for _, offset := range [][]int{
{-1, 0},
{1, 0},
{0, -1},
{0, 1},
} {
if n := t.W.Tile(t.X+offset[0], t.Y+offset[1]); n != nil &&
n.Kind != KindBlocker {
neighbors = append(neighbors, n)
}
}
return neighbors
}
// PathNeighborCost returns the movement cost of the directly neighboring tile.
func (t *Tile) PathNeighborCost(to Pather) float64 {
toT := to.(*Tile)
return KindCosts[toT.Kind]
}
// PathEstimatedCost uses Manhattan distance to estimate orthogonal distance
// between non-adjacent nodes.
func (t *Tile) PathEstimatedCost(to Pather) float64 {
toT := to.(*Tile)
absX := toT.X - t.X
if absX < 0 {
absX = -absX
}
absY := toT.Y - t.Y
if absY < 0 {
absY = -absY
}
return float64(absX + absY)
}
// World is a two dimensional map of Tiles.
type World map[int]map[int]*Tile
// Tile gets the tile at the given coordinates in the world.
func (w World) Tile(x, y int) *Tile {
if w[x] == nil {
return nil
}
return w[x][y]
}
// SetTile sets a tile at the given coordinates in the world.
func (w World) SetTile(t *Tile, x, y int) {
if w[x] == nil {
w[x] = map[int]*Tile{}
}
w[x][y] = t
t.X = x
t.Y = y
t.W = w
}
// FirstOfKind gets the first tile on the board of a kind, used to get the from
// and to tiles as there should only be one of each.
func (w World) FirstOfKind(kind int) *Tile {
for _, row := range w {
for _, t := range row {
if t.Kind == kind {
return t
}
}
}
return nil
}
// From gets the from tile from the world.
func (w World) From() *Tile {
return w.FirstOfKind(KindFrom)
}
// To gets the to tile from the world.
func (w World) To() *Tile {
return w.FirstOfKind(KindTo)
}
// RenderPath renders a path on top of a world.
func (w World) RenderPath(path []Pather) string {
width := len(w)
if width == 0 {
return ""
}
height := len(w[0])
pathLocs := map[string]bool{}
for _, p := range path {
pT := p.(*Tile)
pathLocs[fmt.Sprintf("%d,%d", pT.X, pT.Y)] = true
}
rows := make([]string, height)
for x := 0; x < width; x++ {
for y := 0; y < height; y++ {
t := w.Tile(x, y)
r := ' '
if pathLocs[fmt.Sprintf("%d,%d", x, y)] {
r = KindRunes[KindPath]
} else if t != nil {
r = KindRunes[t.Kind]
}
rows[y] += string(r)
}
}
return strings.Join(rows, "\n")
}
// ParseWorld parses a textual representation of a world into a world map.
func ParseWorld(input string) World {
w := World{}
for y, row := range strings.Split(strings.TrimSpace(input), "\n") {
for x, raw := range row {
kind, ok := RuneKinds[raw]
if !ok {
kind = KindBlocker
}
w.SetTile(&Tile{Kind: kind}, x, y)
}
}
return w
}