mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-12-25 19:46:50 -05:00
Pull go-astar code into the repo, improve perf (#411)
Copied go-astar into d2common/d2astar, made a few optimizations. Runs roughly 30% faster according to my benchmarking. Added a `maxCost` param to prevent searching the entire map for a path. This probably needs tweaked a bit, but follows the original game more closely. Co-authored-by: Nicholas Eden <neden@zigzagame.com>
This commit is contained in:
parent
fe0fa6f14d
commit
dd21809288
153
d2common/d2astar/README.md
Normal file
153
d2common/d2astar/README.md
Normal file
@ -0,0 +1,153 @@
|
||||
D2 A*
|
||||
========
|
||||
|
||||
**A\* pathfinding implementation for OpenDiablo2**
|
||||
|
||||
***Forked from [go-astar](https://github.com/beefsack/go-astar)***
|
||||
|
||||
Changes
|
||||
-------
|
||||
* Used [sync.Pool](https://golang.org/pkg/sync/#Pool) to reuse objects created during pathfinding. This improves performance by roughly 30% by reducing allocations.
|
||||
* Added a check on the target for neighbors to identify if the user clicked an inaccessible area.
|
||||
* Added a max cost to prevent searching the entire region for a path.
|
||||
|
||||
TODO
|
||||
------
|
||||
* Evaluate bi-directional A*, specifically if it would more quickly identify if the user clicked an in inaccessible area (such as an island).
|
||||
|
||||
|
||||
The [A\* pathfinding algorithm](http://en.wikipedia.org/wiki/A*_search_algorithm) is a pathfinding algorithm noted for its performance and accuracy and is commonly used in game development. It can be used to find short paths for any weighted graph.
|
||||
|
||||
A fantastic overview of A\* can be found at [Amit Patel's Stanford website](http://theory.stanford.edu/~amitp/GameProgramming/AStarComparison.html).
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
The following crude examples were taken directly from the automated tests. Please see `path_test.go` for more examples.
|
||||
|
||||
### Key
|
||||
|
||||
* `.` - Plain (movement cost 1)
|
||||
* `~` - River (movement cost 2)
|
||||
* `M` - Mountain (movement cost 3)
|
||||
* `X` - Blocker, unable to move through
|
||||
* `F` - From / start position
|
||||
* `T` - To / goal position
|
||||
* `●` - Calculated path
|
||||
|
||||
### Straight line
|
||||
|
||||
```
|
||||
.....~...... .....~......
|
||||
.....MM..... .....MM.....
|
||||
.F........T. -> .●●●●●●●●●●.
|
||||
....MMM..... ....MMM.....
|
||||
............ ............
|
||||
```
|
||||
|
||||
### Around a mountain
|
||||
|
||||
```
|
||||
.....~...... .....~......
|
||||
.....MM..... .....MM.....
|
||||
.F..MMMM..T. -> .●●●MMMM●●●.
|
||||
....MMM..... ...●MMM●●...
|
||||
............ ...●●●●●....
|
||||
```
|
||||
|
||||
### Blocked path
|
||||
|
||||
```
|
||||
............
|
||||
.........XXX
|
||||
.F.......XTX -> No path
|
||||
.........XXX
|
||||
............
|
||||
```
|
||||
|
||||
### Maze
|
||||
|
||||
```
|
||||
FX.X........ ●X.X●●●●●●..
|
||||
.X...XXXX.X. ●X●●●XXXX●X.
|
||||
.X.X.X....X. -> ●X●X.X●●●●X.
|
||||
...X.X.XXXXX ●●●X.X●XXXXX
|
||||
.XX..X.....T .XX..X●●●●●●
|
||||
```
|
||||
|
||||
### Mountain climber
|
||||
|
||||
```
|
||||
..F..M...... ..●●●●●●●●●.
|
||||
.....MM..... .....MM...●.
|
||||
....MMMM..T. -> ....MMMM..●.
|
||||
....MMM..... ....MMM.....
|
||||
............ ............
|
||||
```
|
||||
|
||||
### River swimmer
|
||||
|
||||
```
|
||||
.....~...... .....~......
|
||||
.....~...... ....●●●.....
|
||||
.F...X...T.. -> .●●●●X●●●●..
|
||||
.....M...... .....M......
|
||||
.....M...... .....M......
|
||||
```
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
### Import the package
|
||||
|
||||
```go
|
||||
import "github.com/beefsack/go-astar"
|
||||
```
|
||||
|
||||
### Implement Pather interface
|
||||
|
||||
An example implementation is done for the tests in `path_test.go` for the Tile type.
|
||||
|
||||
The `PathNeighbors` method should return a slice of the direct neighbors.
|
||||
|
||||
The `PathNeighborCost` method should calculate an exact movement cost for direct neighbors.
|
||||
|
||||
The `PathEstimatedCost` is a heuristic method for estimating the distance between arbitrary tiles. The examples in the test files use [Manhattan distance](http://en.wikipedia.org/wiki/Taxicab_geometry) to estimate orthogonal distance between tiles.
|
||||
|
||||
```go
|
||||
type Tile struct{}
|
||||
|
||||
func (t *Tile) PathNeighbors() []astar.Pather {
|
||||
return []astar.Pather{
|
||||
t.Up(),
|
||||
t.Right(),
|
||||
t.Down(),
|
||||
t.Left(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tile) PathNeighborCost(to astar.Pather) float64 {
|
||||
return to.MovementCost
|
||||
}
|
||||
|
||||
func (t *Tile) PathEstimatedCost(to astar.Pather) float64 {
|
||||
return t.ManhattanDistance(to)
|
||||
}
|
||||
```
|
||||
|
||||
### Call Path function
|
||||
|
||||
```go
|
||||
// t1 and t2 are *Tile objects from inside the world.
|
||||
path, distance, found := astar.Path(t1, t2)
|
||||
if !found {
|
||||
log.Println("Could not find path")
|
||||
}
|
||||
// path is a slice of Pather objects which you can cast back to *Tile.
|
||||
```
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
Michael Alexander <beefsack@gmail.com>
|
||||
Robin Ranjit Chauhan <robin@pathwayi.com>
|
154
d2common/d2astar/astar.go
Normal file
154
d2common/d2astar/astar.go
Normal file
@ -0,0 +1,154 @@
|
||||
package d2astar
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var nodePool *sync.Pool
|
||||
var nodeMapPool *sync.Pool
|
||||
var priorityQueuePool *sync.Pool
|
||||
func init() {
|
||||
nodePool = &sync.Pool {
|
||||
New: func()interface{} {
|
||||
return &node{}
|
||||
},
|
||||
}
|
||||
|
||||
nodeMapPool = &sync.Pool {
|
||||
New: func()interface{} {
|
||||
return make(nodeMap, 128)
|
||||
},
|
||||
}
|
||||
|
||||
priorityQueuePool = &sync.Pool {
|
||||
New: func()interface{} {
|
||||
return priorityQueue{}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// astar is an A* pathfinding implementation.
|
||||
|
||||
// Pather is an interface which allows A* searching on arbitrary objects which
|
||||
// can represent a weighted graph.
|
||||
type Pather interface {
|
||||
// PathNeighbors returns the direct neighboring nodes of this node which
|
||||
// can be pathed to.
|
||||
PathNeighbors() []Pather
|
||||
// PathNeighborCost calculates the exact movement cost to neighbor nodes.
|
||||
PathNeighborCost(to Pather) float64
|
||||
// PathEstimatedCost is a heuristic method for estimating movement costs
|
||||
// between non-adjacent nodes.
|
||||
PathEstimatedCost(to Pather) float64
|
||||
}
|
||||
|
||||
// node is a wrapper to store A* data for a Pather node.
|
||||
type node struct {
|
||||
pather Pather
|
||||
cost float64
|
||||
rank float64
|
||||
parent *node
|
||||
open bool
|
||||
closed bool
|
||||
index int
|
||||
}
|
||||
|
||||
func (n *node) reset () {
|
||||
n.pather = nil
|
||||
n.cost = 0
|
||||
n.rank = 0
|
||||
n.parent = nil
|
||||
n.open = false
|
||||
n.closed = false
|
||||
n.index = 0
|
||||
}
|
||||
|
||||
// nodeMap is a collection of nodes keyed by Pather nodes for quick reference.
|
||||
type nodeMap map[Pather]*node
|
||||
|
||||
// get gets the Pather object wrapped in a node, instantiating if required.
|
||||
func (nm nodeMap) get(p Pather) *node {
|
||||
n, ok := nm[p]
|
||||
if !ok {
|
||||
n = nodePool.Get().(*node)
|
||||
n.pather = p
|
||||
nm[p] = n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Path calculates a short path and the distance between the two Pather nodes.
|
||||
//
|
||||
// If no path is found, found will be false.
|
||||
func Path(from, to Pather, maxCost float64) (path []Pather, distance float64, found bool) {
|
||||
// Quick escape for inaccessible areas.
|
||||
toNeighbors := to.PathNeighbors()
|
||||
if len(toNeighbors) == 0 {
|
||||
return nil, 0, false
|
||||
}
|
||||
|
||||
nm := nodeMapPool.Get().(nodeMap)
|
||||
nq := priorityQueuePool.Get().(priorityQueue)
|
||||
defer func() {
|
||||
for k, v := range nm {
|
||||
v.reset()
|
||||
nodePool.Put(v)
|
||||
delete(nm, k)
|
||||
}
|
||||
|
||||
nq = nq[0:0]
|
||||
nodeMapPool.Put(nm)
|
||||
priorityQueuePool.Put(nq)
|
||||
}()
|
||||
|
||||
heap.Init(&nq)
|
||||
fromNode := nm.get(from)
|
||||
fromNode.open = true
|
||||
heap.Push(&nq, fromNode)
|
||||
for {
|
||||
if nq.Len() == 0 {
|
||||
// There's no path, return found false.
|
||||
return
|
||||
}
|
||||
current := heap.Pop(&nq).(*node)
|
||||
current.open = false
|
||||
current.closed = true
|
||||
|
||||
if current == nm.get(to) {
|
||||
// Found a path to the goal.
|
||||
p := make([]Pather, 0, 16)
|
||||
curr := current
|
||||
for curr != nil {
|
||||
p = append(p, curr.pather)
|
||||
curr = curr.parent
|
||||
}
|
||||
return p, current.cost, true
|
||||
}
|
||||
|
||||
for _, neighbor := range current.pather.PathNeighbors() {
|
||||
cost := current.cost + current.pather.PathNeighborCost(neighbor)
|
||||
if cost > maxCost {
|
||||
fmt.Println("Canceling path")
|
||||
continue
|
||||
}
|
||||
|
||||
neighborNode := nm.get(neighbor)
|
||||
if cost < neighborNode.cost {
|
||||
if neighborNode.open {
|
||||
heap.Remove(&nq, neighborNode.index)
|
||||
}
|
||||
neighborNode.open = false
|
||||
neighborNode.closed = false
|
||||
}
|
||||
if !neighborNode.open && !neighborNode.closed {
|
||||
neighborNode.cost = cost
|
||||
neighborNode.open = true
|
||||
neighborNode.rank = cost + neighbor.PathEstimatedCost(to)
|
||||
neighborNode.parent = current
|
||||
heap.Push(&nq, neighborNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
96
d2common/d2astar/goreland_example.go
Normal file
96
d2common/d2astar/goreland_example.go
Normal file
@ -0,0 +1,96 @@
|
||||
package d2astar
|
||||
|
||||
// goreland_example.go implements implements Pather for
|
||||
// the sake of testing. This functionality forms the back end for
|
||||
// goreland_test.go, and serves as an example for how to use A* for a graph.
|
||||
|
||||
|
||||
// The Magical World of Goreland, is where Ted Stevens and Al Gore are from.
|
||||
//
|
||||
// It is composed of Big Trucks, and a Series of Tubes!
|
||||
//
|
||||
// Ok, it is basically just a Graph.
|
||||
// Nodes are called "Trucks" and they have X, Y coordinates
|
||||
// Edges are called "Tubes", they connect Trucks, and they have a cost
|
||||
//
|
||||
// The key differences between this example and the Tile world:
|
||||
// 1) There is no grid. Trucks have arbitrary coordinates.
|
||||
// 2) Edges are not implied by the grid positions. Instead edges are explicitly
|
||||
// modelled as Tubes.
|
||||
//
|
||||
// The key similarities between this example and the Tile world:
|
||||
// 1) They both use Manhattan distance as their heuristic
|
||||
// 2) Both implement Pather
|
||||
|
||||
type Goreland struct {
|
||||
// trucks map[int]*Truck // not needed really
|
||||
}
|
||||
|
||||
type Tube struct {
|
||||
from *Truck
|
||||
to *Truck
|
||||
Cost float64
|
||||
}
|
||||
|
||||
// A Truck is a Truck in a grid which implements Grapher.
|
||||
type Truck struct {
|
||||
|
||||
// X and Y are the coordinates of the truck.
|
||||
X, Y int
|
||||
|
||||
// array of tubes going to other trucks
|
||||
out_to []Tube
|
||||
|
||||
label string
|
||||
}
|
||||
|
||||
// PathNeighbors returns the neighbors of the Truck
|
||||
func (t *Truck) PathNeighbors() []Pather {
|
||||
|
||||
neighbors := []Pather{}
|
||||
|
||||
for _, tube_element := range t.out_to {
|
||||
neighbors = append(neighbors, Pather(tube_element.to))
|
||||
}
|
||||
return neighbors
|
||||
}
|
||||
|
||||
// PathNeighborCost returns the cost of the tube leading to Truck.
|
||||
func (t *Truck) PathNeighborCost(to Pather) float64 {
|
||||
|
||||
for _, tube_element := range (t).out_to {
|
||||
if Pather((tube_element.to)) == to {
|
||||
return tube_element.Cost
|
||||
}
|
||||
}
|
||||
return 10000000
|
||||
}
|
||||
|
||||
// PathEstimatedCost uses Manhattan distance to estimate orthogonal distance
|
||||
// between non-adjacent nodes.
|
||||
func (t *Truck) PathEstimatedCost(to Pather) float64 {
|
||||
|
||||
toT := to.(*Truck)
|
||||
absX := toT.X - t.X
|
||||
if absX < 0 {
|
||||
absX = -absX
|
||||
}
|
||||
absY := toT.Y - t.Y
|
||||
if absY < 0 {
|
||||
absY = -absY
|
||||
}
|
||||
r := float64(absX + absY)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// RenderPath renders a path on top of a Goreland world.
|
||||
func (w Goreland) RenderPath(path []Pather) string {
|
||||
|
||||
s := ""
|
||||
for _, p := range path {
|
||||
pT := p.(*Truck)
|
||||
s = pT.label + " " + s
|
||||
}
|
||||
return s
|
||||
}
|
86
d2common/d2astar/goreland_test.go
Normal file
86
d2common/d2astar/goreland_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
package d2astar
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func AddTruck(x int, y int, label string) *Truck {
|
||||
t1 := new(Truck)
|
||||
t1.X = x
|
||||
t1.Y = y
|
||||
t1.label = label
|
||||
return t1
|
||||
}
|
||||
|
||||
func AddTube(t1, t2 *Truck, cost float64) *Tube {
|
||||
tube1 := new(Tube)
|
||||
tube1.Cost = cost
|
||||
tube1.from = t1
|
||||
tube1.to = t2
|
||||
|
||||
t1.out_to = append(t1.out_to, *tube1)
|
||||
t2.out_to = append(t2.out_to, *tube1)
|
||||
|
||||
return tube1
|
||||
}
|
||||
|
||||
// Consider a world with Nodes (Trucks) and Edges (Tubes), Edges each having a cost
|
||||
//
|
||||
// E
|
||||
// /|
|
||||
// / |
|
||||
// S--M
|
||||
//
|
||||
// S=Start at (0,0)
|
||||
// E=End at (1,1)
|
||||
// M=Middle at (0,1)
|
||||
//
|
||||
// S-M and M-E are clean clear tubes. cost: 1
|
||||
//
|
||||
// S-E is either:
|
||||
//
|
||||
// 1) TestGraphPath_ShortDiagonal : diagonal is a nice clean clear Tube , cost: 1.9
|
||||
// Solver should traverse the bridge.
|
||||
// Expect solution: Start, End Total cost: 1.9
|
||||
//
|
||||
// 1) TestGraphPath_LongDiagonal : diagonal is a Tube plugged full of
|
||||
// "enormous amounts of material"!, cost: 10000.
|
||||
// Solver should avoid the plugged tube.
|
||||
// Expect solution Start,Middle,End Total cost: 2.0
|
||||
|
||||
func createGorelandGraphPath_Diagonal(t *testing.T, diagonal_cost float64, expectedDist float64) {
|
||||
|
||||
world := new(Goreland)
|
||||
|
||||
tr_start := AddTruck(0, 0, "Start")
|
||||
tr_mid := AddTruck(0, 1, "Middle")
|
||||
tr_end := AddTruck(1, 1, "End")
|
||||
|
||||
AddTube(tr_start, tr_end, diagonal_cost)
|
||||
AddTube(tr_start, tr_mid, 1)
|
||||
AddTube(tr_mid, tr_end, 1)
|
||||
|
||||
t.Logf("Goreland. Diagonal cost: %v\n\n", diagonal_cost)
|
||||
|
||||
p, dist, found := Path(tr_start, tr_end, math.MaxFloat64)
|
||||
|
||||
if !found {
|
||||
t.Log("Could not find a path")
|
||||
} else {
|
||||
t.Logf("Resulting path\n%s", world.RenderPath(p))
|
||||
}
|
||||
if !found && expectedDist >= 0 {
|
||||
t.Fatal("Could not find a path")
|
||||
}
|
||||
if found && dist != expectedDist {
|
||||
t.Fatalf("Expected dist to be %v but got %v", expectedDist, dist)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGraphPaths_ShortDiagonal(t *testing.T) {
|
||||
createGorelandGraphPath_Diagonal(t, 1.9, 1.9)
|
||||
}
|
||||
func TestGraphPaths_LongDiagonal(t *testing.T) {
|
||||
createGorelandGraphPath_Diagonal(t, 10000, 2.0)
|
||||
}
|
132
d2common/d2astar/path_test.go
Normal file
132
d2common/d2astar/path_test.go
Normal file
@ -0,0 +1,132 @@
|
||||
package d2astar
|
||||
|
||||
// path_test.go contains the high level tests without the testing
|
||||
// implementation. testPath is used to check the calculated path distance is
|
||||
// what we're expecting.
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// testPath takes a string encoded world, decodes it, calculates a path and
|
||||
// checks the expected distance matches. An expectedDist of -1 expects that no
|
||||
// path will be found.
|
||||
func testPath(worldInput string, t *testing.T, expectedDist float64) {
|
||||
world := ParseWorld(worldInput)
|
||||
t.Logf("Input world\n%s", world.RenderPath([]Pather{}))
|
||||
p, dist, found := Path(world.From(), world.To(), math.MaxFloat64)
|
||||
if !found {
|
||||
t.Log("Could not find a path")
|
||||
} else {
|
||||
t.Logf("Resulting path\n%s", world.RenderPath(p))
|
||||
}
|
||||
if !found && expectedDist >= 0 {
|
||||
t.Fatal("Could not find a path")
|
||||
}
|
||||
if found && dist != expectedDist {
|
||||
t.Fatalf("Expected dist to be %v but got %v", expectedDist, dist)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStraightLine checks that having no obstacles results in a straight line
|
||||
// path.
|
||||
func TestStraightLine(t *testing.T) {
|
||||
testPath(`
|
||||
.....~......
|
||||
.....MM.....
|
||||
.F........T.
|
||||
....MMM.....
|
||||
............
|
||||
`, t, 9)
|
||||
}
|
||||
|
||||
// TestPathAroundMountain checks that having a round mountain in the path
|
||||
// results in a path around the mountain.
|
||||
func TestPathAroundMountain(t *testing.T) {
|
||||
testPath(`
|
||||
.....~......
|
||||
.....MM.....
|
||||
.F..MMMM..T.
|
||||
....MMM.....
|
||||
............
|
||||
`, t, 13)
|
||||
}
|
||||
|
||||
// TestBlocked checks that no path is returned when there is no possible path.
|
||||
func TestBlocked(t *testing.T) {
|
||||
testPath(`
|
||||
............
|
||||
.........XXX
|
||||
.F.......XTX
|
||||
.........XXX
|
||||
............
|
||||
`, t, -1)
|
||||
}
|
||||
|
||||
// TestMaze checks that paths can double back on themselves to reach the goal.
|
||||
func TestMaze(t *testing.T) {
|
||||
testPath(`
|
||||
FX.X........
|
||||
.X...XXXX.X.
|
||||
.X.X.X....X.
|
||||
...X.X.XXXXX
|
||||
.XX..X.....T
|
||||
`, t, 27)
|
||||
}
|
||||
|
||||
// TestMountainClimber checks that a path will choose to go over a mountain,
|
||||
// which has a movement penalty of 3, if it's faster than going around the
|
||||
// mountain.
|
||||
func TestMountainClimber(t *testing.T) {
|
||||
testPath(`
|
||||
..F..M......
|
||||
.....MM.....
|
||||
....MMMM..T.
|
||||
....MMM.....
|
||||
............
|
||||
`, t, 12)
|
||||
}
|
||||
|
||||
// TestRiverSwimmer checks that the path will prefer to cross a river, which
|
||||
// has a movement penalty of 2, over a mountain which has a movement penalty of
|
||||
// 3.
|
||||
func TestRiverSwimmer(t *testing.T) {
|
||||
testPath(`
|
||||
.....~......
|
||||
.....~......
|
||||
.F...X...T..
|
||||
.....M......
|
||||
.....M......
|
||||
`, t, 11)
|
||||
}
|
||||
|
||||
func BenchmarkLarge(b *testing.B) {
|
||||
world := ParseWorld(`
|
||||
F............................~.................................................
|
||||
.............................~.................................................
|
||||
........M...........X........~.................................................
|
||||
.......MMM.........X.........~~................................................
|
||||
........MM........X...........~................................................
|
||||
.......MM........X............~................................................
|
||||
................X.............~................................................
|
||||
...............X..............~~...............................................
|
||||
..............X................~...............................................
|
||||
.............X.................~...X...............~...........................
|
||||
............X.......................X..............~...........................
|
||||
...........X.........................X.............~...........................
|
||||
..........X..................~........X............~...........................
|
||||
.........X...................~.........X...........~...........................
|
||||
.............................~..........X..........~...............XXXXXXXXXXXX
|
||||
............................~............X..........~..............X...X...X...
|
||||
............................~.............X.........~......MMM.....X.X.X.X.X.X.
|
||||
............................~..............X........~......MM......X.X.X.X.X.X.
|
||||
............................~...............X.......~....MMMM......X.X.X.X.X.X.
|
||||
...........................~.................X.....~......MMM......X.X.X.X.X.X.
|
||||
..............................................X....~.......MM......X.X.X.X.X.X.
|
||||
...............................................X...~.......M.........X...X...XT
|
||||
`)
|
||||
for i := 0; i < b.N; i++ {
|
||||
Path(world.From(), world.To(), math.MaxFloat64)
|
||||
}
|
||||
}
|
198
d2common/d2astar/pather_test.go
Normal file
198
d2common/d2astar/pather_test.go
Normal file
@ -0,0 +1,198 @@
|
||||
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{
|
||||
KindPlain: '.',
|
||||
KindRiver: '~',
|
||||
KindMountain: 'M',
|
||||
KindBlocker: 'X',
|
||||
KindFrom: 'F',
|
||||
KindTo: 'T',
|
||||
KindPath: '●',
|
||||
}
|
||||
|
||||
// RuneKinds map input runes to tile kinds.
|
||||
var RuneKinds = map[rune]int{
|
||||
'.': KindPlain,
|
||||
'~': KindRiver,
|
||||
'M': KindMountain,
|
||||
'X': KindBlocker,
|
||||
'F': KindFrom,
|
||||
'T': KindTo,
|
||||
}
|
||||
|
||||
// KindCosts map tile kinds to movement costs.
|
||||
var KindCosts = map[int]float64{
|
||||
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 := []Pather{}
|
||||
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
|
||||
}
|
35
d2common/d2astar/priority_queue.go
Normal file
35
d2common/d2astar/priority_queue.go
Normal file
@ -0,0 +1,35 @@
|
||||
package d2astar
|
||||
|
||||
// A priorityQueue implements heap.Interface and holds Nodes. The
|
||||
// priorityQueue is used to track open nodes by rank.
|
||||
type priorityQueue []*node
|
||||
|
||||
func (pq priorityQueue) Len() int {
|
||||
return len(pq)
|
||||
}
|
||||
|
||||
func (pq priorityQueue) Less(i, j int) bool {
|
||||
return pq[i].rank < pq[j].rank
|
||||
}
|
||||
|
||||
func (pq priorityQueue) Swap(i, j int) {
|
||||
pq[i], pq[j] = pq[j], pq[i]
|
||||
pq[i].index = i
|
||||
pq[j].index = j
|
||||
}
|
||||
|
||||
func (pq *priorityQueue) Push(x interface{}) {
|
||||
n := len(*pq)
|
||||
no := x.(*node)
|
||||
no.index = n
|
||||
*pq = append(*pq, no)
|
||||
}
|
||||
|
||||
func (pq *priorityQueue) Pop() interface{} {
|
||||
old := *pq
|
||||
n := len(old)
|
||||
no := old[n-1]
|
||||
no.index = -1
|
||||
*pq = old[0 : n-1]
|
||||
return no
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package d2common
|
||||
|
||||
import "github.com/beefsack/go-astar"
|
||||
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2astar"
|
||||
|
||||
type PathTile struct {
|
||||
Walkable bool
|
||||
@ -8,8 +8,8 @@ type PathTile struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
func (t *PathTile) PathNeighbors() []astar.Pather {
|
||||
result := make([]astar.Pather, 0)
|
||||
func (t *PathTile) PathNeighbors() []d2astar.Pather {
|
||||
result := make([]d2astar.Pather, 0, 8)
|
||||
if t.Up != nil {
|
||||
result = append(result, t.Up)
|
||||
}
|
||||
@ -38,11 +38,11 @@ func (t *PathTile) PathNeighbors() []astar.Pather {
|
||||
return result
|
||||
}
|
||||
|
||||
func (t *PathTile) PathNeighborCost(to astar.Pather) float64 {
|
||||
func (t *PathTile) PathNeighborCost(to d2astar.Pather) float64 {
|
||||
return 1 // No cost specifics currently...
|
||||
}
|
||||
|
||||
func (t *PathTile) PathEstimatedCost(to astar.Pather) float64 {
|
||||
func (t *PathTile) PathEstimatedCost(to d2astar.Pather) float64 {
|
||||
toT := to.(*PathTile)
|
||||
absX := toT.X - t.X
|
||||
if absX < 0 {
|
||||
|
@ -4,8 +4,8 @@ import (
|
||||
"math"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2astar"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
"github.com/beefsack/go-astar"
|
||||
)
|
||||
|
||||
func (m *MapEngine) RegenerateWalkPaths() {
|
||||
@ -69,7 +69,7 @@ func (m *MapEngine) RegenerateWalkPaths() {
|
||||
}
|
||||
|
||||
// Finds a walkable path between two points
|
||||
func (m *MapEngine) PathFind(startX, startY, endX, endY float64) (path []astar.Pather, distance float64, found bool) {
|
||||
func (m *MapEngine) PathFind(startX, startY, endX, endY float64) (path []d2astar.Pather, distance float64, found bool) {
|
||||
startTileX := int(math.Floor(startX))
|
||||
startTileY := int(math.Floor(startY))
|
||||
if !m.TileExists(startTileX, startTileY) {
|
||||
@ -97,7 +97,7 @@ func (m *MapEngine) PathFind(startX, startY, endX, endY float64) (path []astar.P
|
||||
}
|
||||
endNode := &m.walkMesh[endNodeIndex]
|
||||
|
||||
path, distance, found = astar.Path(endNode, startNode)
|
||||
path, distance, found = d2astar.Path(endNode, startNode, 80)
|
||||
if path != nil {
|
||||
path = path[1:]
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ import (
|
||||
"math"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2astar"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render"
|
||||
"github.com/beefsack/go-astar"
|
||||
)
|
||||
|
||||
type MapEntity interface {
|
||||
@ -25,7 +25,7 @@ type mapEntity struct {
|
||||
TargetX float64
|
||||
TargetY float64
|
||||
Speed float64
|
||||
path []astar.Pather
|
||||
path []d2astar.Pather
|
||||
|
||||
done func()
|
||||
directioner func(angle float64)
|
||||
@ -44,11 +44,11 @@ func createMapEntity(x, y int) mapEntity {
|
||||
subcellX: 1 + math.Mod(locX, 5),
|
||||
subcellY: 1 + math.Mod(locY, 5),
|
||||
Speed: 6,
|
||||
path: []astar.Pather{},
|
||||
path: []d2astar.Pather{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mapEntity) SetPath(path []astar.Pather, done func()) {
|
||||
func (m *mapEntity) SetPath(path []d2astar.Pather, done func()) {
|
||||
m.path = path
|
||||
m.done = done
|
||||
}
|
||||
@ -112,7 +112,7 @@ func (m *mapEntity) Step(tickTime float64) {
|
||||
if len(m.path) > 1 {
|
||||
m.path = m.path[1:]
|
||||
} else {
|
||||
m.path = []astar.Pather{}
|
||||
m.path = []d2astar.Pather{}
|
||||
}
|
||||
} else {
|
||||
m.LocationX = m.TargetX
|
||||
|
1
go.mod
1
go.mod
@ -6,7 +6,6 @@ require (
|
||||
github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
|
||||
github.com/beefsack/go-astar v0.0.0-20171024231011-f324bbb0d6f7
|
||||
github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1
|
||||
github.com/hajimehoshi/ebiten v1.11.2
|
||||
github.com/pkg/profile v1.5.0
|
||||
|
2
go.sum
2
go.sum
@ -5,8 +5,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/beefsack/go-astar v0.0.0-20171024231011-f324bbb0d6f7 h1:dX/NcR4V4sY+xio5sjMUUaBfmXz/7UH4R7S//oVPqhY=
|
||||
github.com/beefsack/go-astar v0.0.0-20171024231011-f324bbb0d6f7/go.mod h1:Cu3t5VeqE8kXjUBeNXWQprfuaP5UCIc5ggGjgMx9KFc=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
|
||||
|
Loading…
Reference in New Issue
Block a user