Remove star, update refs. (#625)

* Added patreon supports to credits

* Remove astar path finding code.
This commit is contained in:
Tim Sarbin 2020-07-26 12:55:10 -04:00 committed by GitHub
parent ab012b8f70
commit 8c8ab94f8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 4 additions and 983 deletions

View File

@ -1,153 +0,0 @@
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 path-finding. This improves performance by roughly 30% by reducing allocations.
* Added a max cost to prevent searching the entire region for a path.
* If there is no path the target within the max cost, the path found that gets closest to target will be returned. This allows the player to click in inaccessible areas causing the character to run along the edge.
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>

View File

@ -1,176 +0,0 @@
package d2astar
import (
"container/heap"
"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 and path will be the closest node to the target with a valid path.
func Path(from, to Pather, maxCost float64) (path []Pather, distance float64, found bool) {
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, fallback to closest.
break
}
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 {
// Out of range, tweak maxCost if this is cutting off too soon.
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)
}
}
}
// No path found, use closest node available, with found false to indicate the path doesn't reach the target.
closestNode := nm.get(from)
closestNode.rank = closestNode.pather.PathEstimatedCost(to)
for _, current := range nm {
if current.parent == nil {
// This node wasn't evaluated while path finding, probably isn't a good option.
continue
}
current.rank = current.pather.PathEstimatedCost(to)
if current.rank < closestNode.rank {
closestNode = current
}
}
p := make([]Pather, 0, 16)
curr := closestNode
for curr != nil {
p = append(p, curr.pather)
curr = curr.parent
}
return p, closestNode.cost, false
}

View File

@ -1,2 +0,0 @@
// Package d2astar provides an a* pathing algorithm
package d2astar

View File

@ -1,104 +0,0 @@
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
//Goreland represents a world of trucks and tubes.
type Goreland struct {
// trucks map[int]*Truck // not needed really
}
const (
_tenFuckingMillion = 10000000
)
// Tube is an edge. They connect Trucks, and have a cost.
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
outTo []Tube
label string
}
// PathNeighbors returns the neighbors of the Truck
func (t *Truck) PathNeighbors() []Pather {
neighbors := []Pather{}
for _, tubeElement := range t.outTo {
neighbors = append(neighbors, Pather(tubeElement.to))
}
return neighbors
}
// PathNeighborCost returns the cost of the tube leading to Truck.
func (t *Truck) PathNeighborCost(to Pather) float64 {
for _, tubeElement := range (t).outTo {
if Pather((tubeElement.to)) == to {
return tubeElement.Cost
}
}
return _tenFuckingMillion
}
// 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
}

View File

@ -1,88 +0,0 @@
package d2astar
import (
"math"
"testing"
)
func addTruck(x, 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.outTo = append(t1.outTo, *tube1)
t2.outTo = append(t2.outTo, *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 createGorelandGraphPathDiagonal(t *testing.T, diagonalCost, expectedDist float64) {
world := new(Goreland)
trStart := addTruck(0, 0, "Start")
trMid := addTruck(0, 1, "Middle")
trEnd := addTruck(1, 1, "End")
addTube(trStart, trEnd, diagonalCost)
addTube(trStart, trMid, 1)
addTube(trMid, trEnd, 1)
t.Logf("Goreland. Diagonal cost: %v\n\n", diagonalCost)
p, dist, found := Path(trStart, trEnd, 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) {
createGorelandGraphPathDiagonal(t, 1.9, 1.9)
}
func TestGraphPaths_LongDiagonal(t *testing.T) {
createGorelandGraphPathDiagonal(t, 10000, 2.0)
}

View File

@ -1,136 +0,0 @@
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)
}
}

View File

@ -1,215 +0,0 @@
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
}

View File

@ -1,36 +0,0 @@
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
}

View File

@ -1,68 +0,0 @@
package d2common
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2astar"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
)
// PathTile represents a node in path finding
type PathTile struct {
Walkable bool
Up, Down *PathTile
Left, Right *PathTile
UpLeft, UpRight *PathTile
DownLeft, DownRight *PathTile
Position d2vector.Position
}
// PathNeighbors returns the direct neighboring nodes of this node which can be pathed to
func (t *PathTile) PathNeighbors() []d2astar.Pather {
result := make([]d2astar.Pather, 0, 8)
if t.Up != nil {
result = append(result, t.Up)
}
if t.Right != nil {
result = append(result, t.Right)
}
if t.Down != nil {
result = append(result, t.Down)
}
if t.Left != nil {
result = append(result, t.Left)
}
if t.UpLeft != nil {
result = append(result, t.UpLeft)
}
if t.UpRight != nil {
result = append(result, t.UpRight)
}
if t.DownLeft != nil {
result = append(result, t.DownLeft)
}
if t.DownRight != nil {
result = append(result, t.DownRight)
}
return result
}
// PathNeighborCost calculates the exact movement cost to neighbor nodes
func (t *PathTile) PathNeighborCost(_ d2astar.Pather) float64 {
return 1 // No cost specifics currently...
}
// PathEstimatedCost is a heuristic method for estimating movement costs between non-adjacent nodes
func (t *PathTile) PathEstimatedCost(to d2astar.Pather) float64 {
delta := to.(*PathTile).Position.Clone()
delta.Subtract(&t.Position.Vector)
delta.Abs()
return delta.X() + delta.Y()
}

7
go.mod
View File

@ -3,18 +3,15 @@ module github.com/OpenDiablo2/OpenDiablo2
go 1.14
require (
4d63.com/gochecknoinits v0.0.0-20200108094044-eb73b47b9fc4 // indirect
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/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1
github.com/hajimehoshi/ebiten v1.12.0-alpha.7.0.20200703165837-6c33ed107f28
github.com/hajimehoshi/ebiten v1.11.4
github.com/pkg/profile v1.5.0
github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff
github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.4.0
github.com/veandco/go-sdl2 v0.4.4 // indirect
golang.org/x/image v0.0.0-20200119044424-58c23975cae1
golang.org/x/image v0.0.0-20200618115811-c13761719519
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/sourcemap.v1 v1.0.5 // indirect
)

2
go.sum
View File

@ -25,6 +25,8 @@ github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:
github.com/hajimehoshi/bitmapfont v1.2.0/go.mod h1:h9QrPk6Ktb2neObTlAbma6Ini1xgMjbJ3w7ysmD7IOU=
github.com/hajimehoshi/ebiten v1.11.2 h1:4oixxPh5DuSNlmL/Q4LIOGFXCY8uLC+pcxOnqAGbwEY=
github.com/hajimehoshi/ebiten v1.11.2/go.mod h1:aDEhx0K9gSpXw3Cxf2hCXDxPSoF8vgjNqKxrZa/B4Dg=
github.com/hajimehoshi/ebiten v1.11.4 h1:ngYF0NxKjFBsY/Bol6V0X/b0hoCCTi9nJRg7Dv8+ePc=
github.com/hajimehoshi/ebiten v1.11.4/go.mod h1:aDEhx0K9gSpXw3Cxf2hCXDxPSoF8vgjNqKxrZa/B4Dg=
github.com/hajimehoshi/ebiten v1.12.0-alpha.5.0.20200627174955-aea4630b5f84 h1:BRvyD5kF4r9JGWd3xwBgQNpRhBDzvmAEXP2tv1MGjHA=
github.com/hajimehoshi/ebiten v1.12.0-alpha.5.0.20200627174955-aea4630b5f84/go.mod h1:8vzUI4e0fBkbONYOY4WJN/qikY2zv/VG6kFTzJ0B//o=
github.com/hajimehoshi/ebiten v1.12.0-alpha.6.0.20200629133528-780465b702ce h1:cEKWqbtxFremkIRhJxz0Z80wXqNNe8ZNk6ra8XASC1I=