From dd218092883fb31da2a0dbc357ab9528f05e8491 Mon Sep 17 00:00:00 2001 From: nicholas-eden Date: Mon, 22 Jun 2020 23:04:17 -0700 Subject: [PATCH] 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 --- d2common/d2astar/README.md | 153 +++++++++++++++++++ d2common/d2astar/astar.go | 154 +++++++++++++++++++ d2common/d2astar/goreland_example.go | 96 ++++++++++++ d2common/d2astar/goreland_test.go | 86 +++++++++++ d2common/d2astar/path_test.go | 132 +++++++++++++++++ d2common/d2astar/pather_test.go | 198 +++++++++++++++++++++++++ d2common/d2astar/priority_queue.go | 35 +++++ d2common/path_tile.go | 10 +- d2core/d2map/d2mapengine/walk_mesh.go | 6 +- d2core/d2map/d2mapentity/map_entity.go | 10 +- go.mod | 1 - go.sum | 2 - 12 files changed, 867 insertions(+), 16 deletions(-) create mode 100644 d2common/d2astar/README.md create mode 100644 d2common/d2astar/astar.go create mode 100644 d2common/d2astar/goreland_example.go create mode 100644 d2common/d2astar/goreland_test.go create mode 100644 d2common/d2astar/path_test.go create mode 100644 d2common/d2astar/pather_test.go create mode 100644 d2common/d2astar/priority_queue.go diff --git a/d2common/d2astar/README.md b/d2common/d2astar/README.md new file mode 100644 index 00000000..7919d490 --- /dev/null +++ b/d2common/d2astar/README.md @@ -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 +Robin Ranjit Chauhan diff --git a/d2common/d2astar/astar.go b/d2common/d2astar/astar.go new file mode 100644 index 00000000..ea828086 --- /dev/null +++ b/d2common/d2astar/astar.go @@ -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) + } + } + } +} diff --git a/d2common/d2astar/goreland_example.go b/d2common/d2astar/goreland_example.go new file mode 100644 index 00000000..e255dbec --- /dev/null +++ b/d2common/d2astar/goreland_example.go @@ -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 +} diff --git a/d2common/d2astar/goreland_test.go b/d2common/d2astar/goreland_test.go new file mode 100644 index 00000000..81955b02 --- /dev/null +++ b/d2common/d2astar/goreland_test.go @@ -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) +} diff --git a/d2common/d2astar/path_test.go b/d2common/d2astar/path_test.go new file mode 100644 index 00000000..bacf37e0 --- /dev/null +++ b/d2common/d2astar/path_test.go @@ -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) + } +} diff --git a/d2common/d2astar/pather_test.go b/d2common/d2astar/pather_test.go new file mode 100644 index 00000000..25649009 --- /dev/null +++ b/d2common/d2astar/pather_test.go @@ -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 +} diff --git a/d2common/d2astar/priority_queue.go b/d2common/d2astar/priority_queue.go new file mode 100644 index 00000000..acb762a4 --- /dev/null +++ b/d2common/d2astar/priority_queue.go @@ -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 +} diff --git a/d2common/path_tile.go b/d2common/path_tile.go index cabad35a..abe4dcc9 100644 --- a/d2common/path_tile.go +++ b/d2common/path_tile.go @@ -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 { diff --git a/d2core/d2map/d2mapengine/walk_mesh.go b/d2core/d2map/d2mapengine/walk_mesh.go index 3482e9b1..d0666fc0 100644 --- a/d2core/d2map/d2mapengine/walk_mesh.go +++ b/d2core/d2map/d2mapengine/walk_mesh.go @@ -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:] } diff --git a/d2core/d2map/d2mapentity/map_entity.go b/d2core/d2map/d2mapentity/map_entity.go index 76a2aa47..84532978 100644 --- a/d2core/d2map/d2mapentity/map_entity.go +++ b/d2core/d2map/d2mapentity/map_entity.go @@ -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 diff --git a/go.mod b/go.mod index 39eb27c4..bf5476a1 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 24f0b553..79fb411d 100644 --- a/go.sum +++ b/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=