Split out maths functions into maths, vector and bearing

This commit is contained in:
Marc Di Luzio 2020-06-09 18:08:07 +01:00
parent aae668fb57
commit 51fe918090
14 changed files with 335 additions and 273 deletions

76
pkg/bearing/bearing.go Normal file
View file

@ -0,0 +1,76 @@
package bearing
import (
"fmt"
"strings"
"github.com/mdiluz/rove/pkg/vector"
)
// Direction describes a compass direction
type Direction int
const (
North Direction = iota
NorthEast
East
SouthEast
South
SouthWest
West
NorthWest
)
// DirectionString simply describes the strings associated with a direction
type DirectionString struct {
Long string
Short string
}
// DirectionStrings is the set of strings for each direction
var DirectionStrings = []DirectionString{
{"North", "N"},
{"NorthEast", "NE"},
{"East", "E"},
{"SouthEast", "SE"},
{"South", "S"},
{"SouthWest", "SW"},
{"West", "W"},
{"NorthWest", "NW"},
}
// String converts a Direction to a String
func (d Direction) String() string {
return DirectionStrings[d].Long
}
// ShortString converts a Direction to a short string version
func (d Direction) ShortString() string {
return DirectionStrings[d].Short
}
// DirectionFromString gets the Direction from a string
func DirectionFromString(s string) (Direction, error) {
for i, d := range DirectionStrings {
if strings.ToLower(d.Long) == strings.ToLower(s) || strings.ToLower(d.Short) == strings.ToLower(s) {
return Direction(i), nil
}
}
return -1, fmt.Errorf("Unknown direction: %s", s)
}
var DirectionVectors = []vector.Vector{
{X: 0, Y: 1}, // N
{X: 1, Y: 1}, // NE
{X: 1, Y: 0}, // E
{X: 1, Y: -1}, // SE
{X: 0, Y: -1}, // S
{X: -1, Y: 1}, // SW
{X: -1, Y: 0}, // W
{X: -1, Y: 1}, // NW
}
// Vector converts a Direction to a Vector
func (d Direction) Vector() vector.Vector {
return DirectionVectors[d]
}

View file

@ -0,0 +1,32 @@
package bearing
import (
"testing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
)
func TestDirection(t *testing.T) {
dir := North
assert.Equal(t, "North", dir.String())
assert.Equal(t, "N", dir.ShortString())
assert.Equal(t, vector.Vector{X: 0, Y: 1}, dir.Vector())
dir, err := DirectionFromString("N")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = DirectionFromString("n")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = DirectionFromString("north")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = DirectionFromString("NorthWest")
assert.NoError(t, err)
assert.Equal(t, NorthWest, dir)
}

View file

@ -4,6 +4,9 @@ import (
"fmt" "fmt"
"log" "log"
"math/rand" "math/rand"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/pkg/vector"
) )
// Chunk represents a fixed square grid of tiles // Chunk represents a fixed square grid of tiles
@ -55,7 +58,7 @@ func (a *Atlas) SpawnRocks() error {
for i := -extent; i < extent; i++ { for i := -extent; i < extent; i++ {
for j := -extent; j < extent; j++ { for j := -extent; j < extent; j++ {
if rand.Intn(16) == 0 { if rand.Intn(16) == 0 {
if err := a.SetTile(Vector{i, j}, TileRock); err != nil { if err := a.SetTile(vector.Vector{X: i, Y: j}, TileRock); err != nil {
return err return err
} }
} }
@ -72,13 +75,13 @@ func (a *Atlas) SpawnWalls() error {
// Surround the atlas in walls // Surround the atlas in walls
for i := -extent; i < extent; i++ { for i := -extent; i < extent; i++ {
if err := a.SetTile(Vector{i, extent - 1}, TileWall); err != nil { // N if err := a.SetTile(vector.Vector{X: i, Y: extent - 1}, TileWall); err != nil { // N
return err return err
} else if a.SetTile(Vector{extent - 1, i}, TileWall); err != nil { // E } else if a.SetTile(vector.Vector{X: extent - 1, Y: i}, TileWall); err != nil { // E
return err return err
} else if a.SetTile(Vector{i, -extent}, TileWall); err != nil { // S } else if a.SetTile(vector.Vector{X: i, Y: -extent}, TileWall); err != nil { // S
return err return err
} else if a.SetTile(Vector{-extent, i}, TileWall); err != nil { // W } else if a.SetTile(vector.Vector{X: -extent, Y: i}, TileWall); err != nil { // W
return err return err
} }
} }
@ -87,7 +90,7 @@ func (a *Atlas) SpawnWalls() error {
} }
// SetTile sets an individual tile's kind // SetTile sets an individual tile's kind
func (a *Atlas) SetTile(v Vector, tile Tile) error { func (a *Atlas) SetTile(v vector.Vector, tile Tile) error {
chunk := a.ToChunk(v) chunk := a.ToChunk(v)
if chunk >= len(a.Chunks) { if chunk >= len(a.Chunks) {
return fmt.Errorf("location outside of allocated atlas") return fmt.Errorf("location outside of allocated atlas")
@ -103,7 +106,7 @@ func (a *Atlas) SetTile(v Vector, tile Tile) error {
} }
// GetTile will return an individual tile // GetTile will return an individual tile
func (a *Atlas) GetTile(v Vector) (Tile, error) { func (a *Atlas) GetTile(v vector.Vector) (Tile, error) {
chunk := a.ToChunk(v) chunk := a.ToChunk(v)
if chunk >= len(a.Chunks) { if chunk >= len(a.Chunks) {
return 0, fmt.Errorf("location outside of allocated atlas") return 0, fmt.Errorf("location outside of allocated atlas")
@ -119,32 +122,32 @@ func (a *Atlas) GetTile(v Vector) (Tile, error) {
} }
// ToChunkLocal gets a chunk local coordinate for a tile // ToChunkLocal gets a chunk local coordinate for a tile
func (a *Atlas) ToChunkLocal(v Vector) Vector { func (a *Atlas) ToChunkLocal(v vector.Vector) vector.Vector {
return Vector{Pmod(v.X, a.ChunkSize), Pmod(v.Y, a.ChunkSize)} return vector.Vector{X: maths.Pmod(v.X, a.ChunkSize), Y: maths.Pmod(v.Y, a.ChunkSize)}
} }
// GetChunkLocal gets a chunk local coordinate for a tile // GetChunkLocal gets a chunk local coordinate for a tile
func (a *Atlas) ToWorld(local Vector, chunk int) Vector { func (a *Atlas) ToWorld(local vector.Vector, chunk int) vector.Vector {
return a.ChunkOrigin(chunk).Added(local) return a.ChunkOrigin(chunk).Added(local)
} }
// GetChunkID gets the chunk ID for a position in the world // GetChunkID gets the chunk ID for a position in the world
func (a *Atlas) ToChunk(v Vector) int { func (a *Atlas) ToChunk(v vector.Vector) int {
local := a.ToChunkLocal(v) local := a.ToChunkLocal(v)
// Get the chunk origin itself // Get the chunk origin itself
origin := v.Added(local.Negated()) origin := v.Added(local.Negated())
// Divided it by the number of chunks // Divided it by the number of chunks
origin = origin.Divided(a.ChunkSize) origin = origin.Divided(a.ChunkSize)
// Shift it by our size (our origin is in the middle) // Shift it by our size (our origin is in the middle)
origin = origin.Added(Vector{a.Size / 2, a.Size / 2}) origin = origin.Added(vector.Vector{X: a.Size / 2, Y: a.Size / 2})
// Get the ID based on the final values // Get the ID based on the final values
return (a.Size * origin.Y) + origin.X return (a.Size * origin.Y) + origin.X
} }
// ChunkOrigin gets the chunk origin for a given chunk index // ChunkOrigin gets the chunk origin for a given chunk index
func (a *Atlas) ChunkOrigin(chunk int) Vector { func (a *Atlas) ChunkOrigin(chunk int) vector.Vector {
v := Vector{ v := vector.Vector{
X: Pmod(chunk, a.Size) - (a.Size / 2), X: maths.Pmod(chunk, a.Size) - (a.Size / 2),
Y: (chunk / a.Size) - (a.Size / 2), Y: (chunk / a.Size) - (a.Size / 2),
} }
@ -152,14 +155,14 @@ func (a *Atlas) ChunkOrigin(chunk int) Vector {
} }
// GetWorldExtent gets the min and max valid coordinates of world // GetWorldExtent gets the min and max valid coordinates of world
func (a *Atlas) GetWorldExtents() (min Vector, max Vector) { func (a *Atlas) GetWorldExtents() (min vector.Vector, max vector.Vector) {
min = Vector{ min = vector.Vector{
-(a.Size / 2) * a.ChunkSize, X: -(a.Size / 2) * a.ChunkSize,
-(a.Size / 2) * a.ChunkSize, Y: -(a.Size / 2) * a.ChunkSize,
} }
max = Vector{ max = vector.Vector{
-min.X - 1, X: -min.X - 1,
-min.Y - 1, Y: -min.Y - 1,
} }
return return
} }

View file

@ -3,6 +3,7 @@ package game
import ( import (
"testing" "testing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -28,13 +29,13 @@ func TestAtlas_ToChunk(t *testing.T) {
// Tiles should look like: 2 | 3 // Tiles should look like: 2 | 3
// ----- // -----
// 0 | 1 // 0 | 1
tile := a.ToChunk(Vector{0, 0}) tile := a.ToChunk(vector.Vector{X: 0, Y: 0})
assert.Equal(t, 3, tile) assert.Equal(t, 3, tile)
tile = a.ToChunk(Vector{0, -1}) tile = a.ToChunk(vector.Vector{X: 0, Y: -1})
assert.Equal(t, 1, tile) assert.Equal(t, 1, tile)
tile = a.ToChunk(Vector{-1, -1}) tile = a.ToChunk(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 0, tile) assert.Equal(t, 0, tile)
tile = a.ToChunk(Vector{-1, 0}) tile = a.ToChunk(vector.Vector{X: -1, Y: 0})
assert.Equal(t, 2, tile) assert.Equal(t, 2, tile)
a = NewAtlas(2, 2) a = NewAtlas(2, 2)
@ -43,13 +44,13 @@ func TestAtlas_ToChunk(t *testing.T) {
// 2 | 3 // 2 | 3
// ----- // -----
// 0 | 1 // 0 | 1
tile = a.ToChunk(Vector{1, 1}) tile = a.ToChunk(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 3, tile) assert.Equal(t, 3, tile)
tile = a.ToChunk(Vector{1, -2}) tile = a.ToChunk(vector.Vector{X: 1, Y: -2})
assert.Equal(t, 1, tile) assert.Equal(t, 1, tile)
tile = a.ToChunk(Vector{-2, -2}) tile = a.ToChunk(vector.Vector{X: -2, Y: -2})
assert.Equal(t, 0, tile) assert.Equal(t, 0, tile)
tile = a.ToChunk(Vector{-2, 1}) tile = a.ToChunk(vector.Vector{X: -2, Y: 1})
assert.Equal(t, 2, tile) assert.Equal(t, 2, tile)
a = NewAtlas(4, 2) a = NewAtlas(4, 2)
@ -62,13 +63,13 @@ func TestAtlas_ToChunk(t *testing.T) {
// 4 | 5 || 6 | 7 // 4 | 5 || 6 | 7
// ---------------- // ----------------
// 0 | 1 || 2 | 3 // 0 | 1 || 2 | 3
tile = a.ToChunk(Vector{1, 3}) tile = a.ToChunk(vector.Vector{X: 1, Y: 3})
assert.Equal(t, 14, tile) assert.Equal(t, 14, tile)
tile = a.ToChunk(Vector{1, -3}) tile = a.ToChunk(vector.Vector{X: 1, Y: -3})
assert.Equal(t, 2, tile) assert.Equal(t, 2, tile)
tile = a.ToChunk(Vector{-1, -1}) tile = a.ToChunk(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 5, tile) assert.Equal(t, 5, tile)
tile = a.ToChunk(Vector{-2, 2}) tile = a.ToChunk(vector.Vector{X: -2, Y: 2})
assert.Equal(t, 13, tile) assert.Equal(t, 13, tile)
} }
@ -77,14 +78,14 @@ func TestAtlas_GetSetTile(t *testing.T) {
assert.NotNil(t, a) assert.NotNil(t, a)
// Set the origin tile to 1 and test it // Set the origin tile to 1 and test it
assert.NoError(t, a.SetTile(Vector{0, 0}, 1)) assert.NoError(t, a.SetTile(vector.Vector{X: 0, Y: 0}, 1))
tile, err := a.GetTile(Vector{0, 0}) tile, err := a.GetTile(vector.Vector{X: 0, Y: 0})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, Tile(1), tile) assert.Equal(t, Tile(1), tile)
// Set another tile to 1 and test it // Set another tile to 1 and test it
assert.NoError(t, a.SetTile(Vector{5, -2}, 2)) assert.NoError(t, a.SetTile(vector.Vector{X: 5, Y: -2}, 2))
tile, err = a.GetTile(Vector{5, -2}) tile, err = a.GetTile(vector.Vector{X: 5, Y: -2})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, Tile(2), tile) assert.Equal(t, Tile(2), tile)
} }
@ -96,24 +97,24 @@ func TestAtlas_Grown(t *testing.T) {
assert.Equal(t, 4, len(a.Chunks)) assert.Equal(t, 4, len(a.Chunks))
// Set a few tiles to values // Set a few tiles to values
assert.NoError(t, a.SetTile(Vector{0, 0}, 1)) assert.NoError(t, a.SetTile(vector.Vector{X: 0, Y: 0}, 1))
assert.NoError(t, a.SetTile(Vector{-1, -1}, 2)) assert.NoError(t, a.SetTile(vector.Vector{X: -1, Y: -1}, 2))
assert.NoError(t, a.SetTile(Vector{1, -2}, 3)) assert.NoError(t, a.SetTile(vector.Vector{X: 1, Y: -2}, 3))
// Grow once to just double it // Grow once to just double it
err := a.Grow(4) err := a.Grow(4)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 16, len(a.Chunks)) assert.Equal(t, 16, len(a.Chunks))
tile, err := a.GetTile(Vector{0, 0}) tile, err := a.GetTile(vector.Vector{X: 0, Y: 0})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, Tile(1), tile) assert.Equal(t, Tile(1), tile)
tile, err = a.GetTile(Vector{-1, -1}) tile, err = a.GetTile(vector.Vector{X: -1, Y: -1})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, Tile(2), tile) assert.Equal(t, Tile(2), tile)
tile, err = a.GetTile(Vector{1, -2}) tile, err = a.GetTile(vector.Vector{X: 1, Y: -2})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, Tile(3), tile) assert.Equal(t, Tile(3), tile)
@ -122,15 +123,15 @@ func TestAtlas_Grown(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 100, len(a.Chunks)) assert.Equal(t, 100, len(a.Chunks))
tile, err = a.GetTile(Vector{0, 0}) tile, err = a.GetTile(vector.Vector{X: 0, Y: 0})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, Tile(1), tile) assert.Equal(t, Tile(1), tile)
tile, err = a.GetTile(Vector{-1, -1}) tile, err = a.GetTile(vector.Vector{X: -1, Y: -1})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, Tile(2), tile) assert.Equal(t, Tile(2), tile)
tile, err = a.GetTile(Vector{1, -2}) tile, err = a.GetTile(vector.Vector{X: 1, Y: -2})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, Tile(3), tile) assert.Equal(t, Tile(3), tile)
} }
@ -143,25 +144,25 @@ func TestAtlas_SpawnWorld(t *testing.T) {
assert.NoError(t, a.SpawnWalls()) assert.NoError(t, a.SpawnWalls())
for i := -4; i < 4; i++ { for i := -4; i < 4; i++ {
tile, err := a.GetTile(Vector{i, -4}) tile, err := a.GetTile(vector.Vector{X: i, Y: -4})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, TileWall, tile) assert.Equal(t, TileWall, tile)
} }
for i := -4; i < 4; i++ { for i := -4; i < 4; i++ {
tile, err := a.GetTile(Vector{-4, i}) tile, err := a.GetTile(vector.Vector{X: -4, Y: i})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, TileWall, tile) assert.Equal(t, TileWall, tile)
} }
for i := -4; i < 4; i++ { for i := -4; i < 4; i++ {
tile, err := a.GetTile(Vector{3, i}) tile, err := a.GetTile(vector.Vector{X: 3, Y: i})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, TileWall, tile) assert.Equal(t, TileWall, tile)
} }
for i := -4; i < 4; i++ { for i := -4; i < 4; i++ {
tile, err := a.GetTile(Vector{i, 3}) tile, err := a.GetTile(vector.Vector{X: i, Y: 3})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, TileWall, tile) assert.Equal(t, TileWall, tile)
} }

View file

@ -3,6 +3,8 @@ package game
import ( import (
"testing" "testing"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -10,7 +12,7 @@ func TestCommand_Move(t *testing.T) {
world := NewWorld(2, 8) world := NewWorld(2, 8)
a, err := world.SpawnRover() a, err := world.SpawnRover()
assert.NoError(t, err) assert.NoError(t, err)
pos := Vector{ pos := vector.Vector{
X: 1.0, X: 1.0,
Y: 2.0, Y: 2.0,
} }
@ -21,7 +23,7 @@ func TestCommand_Move(t *testing.T) {
err = world.WarpRover(a, pos) err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover") assert.NoError(t, err, "Failed to set position for rover")
bearing := North bearing := bearing.North
duration := 1 duration := 1
// Try the move command // Try the move command
moveCommand := Command{Command: CommandMove, Bearing: bearing.String(), Duration: duration} moveCommand := Command{Command: CommandMove, Bearing: bearing.String(), Duration: duration}
@ -32,6 +34,6 @@ func TestCommand_Move(t *testing.T) {
newatributes, err := world.RoverAttributes(a) newatributes, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to set position for rover") assert.NoError(t, err, "Failed to set position for rover")
pos.Add(Vector{0.0, duration * attribs.Speed}) // We should have moved duration*speed north pos.Add(vector.Vector{X: 0.0, Y: duration * attribs.Speed}) // We should have moved duration*speed north
assert.Equal(t, pos, newatributes.Pos, "Failed to correctly set position for rover") assert.Equal(t, pos, newatributes.Pos, "Failed to correctly set position for rover")
} }

View file

@ -1,158 +0,0 @@
package game
import (
"fmt"
"math"
"strings"
)
// TODO: Pull this out into math package and get more test coverage
// Abs gets the absolute value of an int
func Abs(x int) int {
if x < 0 {
return -x
}
return x
}
// pmod is a mositive modulo
// golang's % is a "remainder" function si misbehaves for negative modulus inputs
func Pmod(x, d int) int {
x = x % d
if x >= 0 {
return x
} else if d < 0 {
return x - d
} else {
return x + d
}
}
// Max returns the highest int
func Max(x int, y int) int {
if x < y {
return y
}
return x
}
// Min returns the lowest int
func Min(x int, y int) int {
if x > y {
return y
}
return x
}
// Vector desribes a 3D vector
type Vector struct {
X int `json:"x"`
Y int `json:"y"`
}
// Add adds one vector to another
func (v *Vector) Add(v2 Vector) {
v.X += v2.X
v.Y += v2.Y
}
// Added calculates a new vector
func (v Vector) Added(v2 Vector) Vector {
v.Add(v2)
return v
}
// Negated returns a negated vector
func (v Vector) Negated() Vector {
return Vector{-v.X, -v.Y}
}
// Length returns the length of the vector
func (v Vector) Length() float64 {
return math.Sqrt(float64(v.X*v.X + v.Y*v.Y))
}
// Distance returns the distance between two vectors
func (v Vector) Distance(v2 Vector) float64 {
// Negate the two vectors and calciate the length
return v.Added(v2.Negated()).Length()
}
// Multiplied returns the vector multiplied by an int
func (v Vector) Multiplied(val int) Vector {
return Vector{v.X * val, v.Y * val}
}
// Divided returns the vector divided by an int
func (v Vector) Divided(val int) Vector {
return Vector{v.X / val, v.Y / val}
}
// Direction describes a compass direction
type Direction int
const (
North Direction = iota
NorthEast
East
SouthEast
South
SouthWest
West
NorthWest
)
// DirectionString simply describes the strings associated with a direction
type DirectionString struct {
Long string
Short string
}
// DirectionStrings is the set of strings for each direction
var DirectionStrings = []DirectionString{
{"North", "N"},
{"NorthEast", "NE"},
{"East", "E"},
{"SouthEast", "SE"},
{"South", "S"},
{"SouthWest", "SW"},
{"West", "W"},
{"NorthWest", "NW"},
}
// String converts a Direction to a String
func (d Direction) String() string {
return DirectionStrings[d].Long
}
// ShortString converts a Direction to a short string version
func (d Direction) ShortString() string {
return DirectionStrings[d].Short
}
// DirectionFromString gets the Direction from a string
func DirectionFromString(s string) (Direction, error) {
for i, d := range DirectionStrings {
if strings.ToLower(d.Long) == strings.ToLower(s) || strings.ToLower(d.Short) == strings.ToLower(s) {
return Direction(i), nil
}
}
return -1, fmt.Errorf("Unknown direction: %s", s)
}
var DirectionVectors = []Vector{
{0, 1}, // N
{1, 1}, // NE
{1, 0}, // E
{1, -1}, // SE
{0, -1}, // S
{-1, 1}, // SW
{-1, 0}, // W
{-1, 1}, // NW
}
// Vector converts a Direction to a Vector
func (d Direction) Vector() Vector {
return DirectionVectors[d]
}

View file

@ -1,6 +1,9 @@
package game package game
import "github.com/google/uuid" import (
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/vector"
)
// RoverAttributes contains attributes of a rover // RoverAttributes contains attributes of a rover
type RoverAttributes struct { type RoverAttributes struct {
@ -14,7 +17,7 @@ type RoverAttributes struct {
Name string `json:"name"` Name string `json:"name"`
// Pos represents where this rover is in the world // Pos represents where this rover is in the world
Pos Vector `json:"pos"` Pos vector.Vector `json:"pos"`
} }
// Rover describes a single rover in the world // Rover describes a single rover in the world

View file

@ -8,6 +8,9 @@ import (
"sync" "sync"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/pkg/vector"
"github.com/tjarratt/babble" "github.com/tjarratt/babble"
) )
@ -70,9 +73,9 @@ func (w *World) SpawnRover() (uuid.UUID, error) {
strings.ReplaceAll(rover.Attributes.Name, "'s", "") strings.ReplaceAll(rover.Attributes.Name, "'s", "")
// Spawn in a random place near the origin // Spawn in a random place near the origin
rover.Attributes.Pos = Vector{ rover.Attributes.Pos = vector.Vector{
w.Atlas.ChunkSize/2 - rand.Intn(w.Atlas.ChunkSize), X: w.Atlas.ChunkSize/2 - rand.Intn(w.Atlas.ChunkSize),
w.Atlas.ChunkSize/2 - rand.Intn(w.Atlas.ChunkSize), Y: w.Atlas.ChunkSize/2 - rand.Intn(w.Atlas.ChunkSize),
} }
// Seach until we error (run out of world) // Seach until we error (run out of world)
@ -84,7 +87,7 @@ func (w *World) SpawnRover() (uuid.UUID, error) {
break break
} else { } else {
// Try and spawn to the east of the blockage // Try and spawn to the east of the blockage
rover.Attributes.Pos.Add(Vector{1, 0}) rover.Attributes.Pos.Add(vector.Vector{X: 1, Y: 0})
} }
} }
} }
@ -146,7 +149,7 @@ func (w *World) SetRoverAttributes(id uuid.UUID, attributes RoverAttributes) err
} }
// WarpRover sets an rovers position // WarpRover sets an rovers position
func (w *World) WarpRover(id uuid.UUID, pos Vector) error { func (w *World) WarpRover(id uuid.UUID, pos vector.Vector) error {
w.worldMutex.Lock() w.worldMutex.Lock()
defer w.worldMutex.Unlock() defer w.worldMutex.Unlock()
@ -176,7 +179,7 @@ func (w *World) WarpRover(id uuid.UUID, pos Vector) error {
} }
// SetPosition sets an rovers position // SetPosition sets an rovers position
func (w *World) MoveRover(id uuid.UUID, bearing Direction) (RoverAttributes, error) { func (w *World) MoveRover(id uuid.UUID, bearing bearing.Direction) (RoverAttributes, error) {
w.worldMutex.Lock() w.worldMutex.Lock()
defer w.worldMutex.Unlock() defer w.worldMutex.Unlock()
@ -224,31 +227,31 @@ func (w *World) RadarFromRover(id uuid.UUID) ([]Tile, error) {
roverPos := r.Attributes.Pos roverPos := r.Attributes.Pos
// Get the radar min and max values // Get the radar min and max values
radarMin := Vector{ radarMin := vector.Vector{
X: roverPos.X - r.Attributes.Range, X: roverPos.X - r.Attributes.Range,
Y: roverPos.Y - r.Attributes.Range, Y: roverPos.Y - r.Attributes.Range,
} }
radarMax := Vector{ radarMax := vector.Vector{
X: roverPos.X + r.Attributes.Range, X: roverPos.X + r.Attributes.Range,
Y: roverPos.Y + r.Attributes.Range, Y: roverPos.Y + r.Attributes.Range,
} }
// Make sure we only query within the actual world // Make sure we only query within the actual world
worldMin, worldMax := w.Atlas.GetWorldExtents() worldMin, worldMax := w.Atlas.GetWorldExtents()
scanMin := Vector{ scanMin := vector.Vector{
X: Max(radarMin.X, worldMin.X), X: maths.Max(radarMin.X, worldMin.X),
Y: Max(radarMin.Y, worldMin.Y), Y: maths.Max(radarMin.Y, worldMin.Y),
} }
scanMax := Vector{ scanMax := vector.Vector{
X: Min(radarMax.X, worldMax.X), X: maths.Min(radarMax.X, worldMax.X),
Y: Min(radarMax.Y, worldMax.Y), Y: maths.Min(radarMax.Y, worldMax.Y),
} }
// Gather up all tiles within the range // Gather up all tiles within the range
var radar = make([]Tile, radarSpan*radarSpan) var radar = make([]Tile, radarSpan*radarSpan)
for j := scanMin.Y; j <= scanMax.Y; j++ { for j := scanMin.Y; j <= scanMax.Y; j++ {
for i := scanMin.X; i <= scanMax.X; i++ { for i := scanMin.X; i <= scanMax.X; i++ {
q := Vector{i, j} q := vector.Vector{X: i, Y: j}
if tile, err := w.Atlas.GetTile(q); err != nil { if tile, err := w.Atlas.GetTile(q); err != nil {
return nil, fmt.Errorf("failed to query tile: %s", err) return nil, fmt.Errorf("failed to query tile: %s", err)
@ -275,7 +278,7 @@ func (w *World) Enqueue(rover uuid.UUID, commands ...Command) error {
for _, c := range commands { for _, c := range commands {
switch c.Command { switch c.Command {
case "move": case "move":
if _, err := DirectionFromString(c.Bearing); err != nil { if _, err := bearing.DirectionFromString(c.Bearing); err != nil {
return fmt.Errorf("unknown direction: %s", c.Bearing) return fmt.Errorf("unknown direction: %s", c.Bearing)
} }
default: default:
@ -330,7 +333,7 @@ func (w *World) ExecuteCommand(c *Command, rover uuid.UUID) (finished bool, err
switch c.Command { switch c.Command {
case "move": case "move":
if dir, err := DirectionFromString(c.Bearing); err != nil { if dir, err := bearing.DirectionFromString(c.Bearing); err != nil {
return true, fmt.Errorf("unknown direction in command %+v, skipping: %s\n", c, err) return true, fmt.Errorf("unknown direction in command %+v, skipping: %s\n", c, err)
} else if _, err := w.MoveRover(rover, dir); err != nil { } else if _, err := w.MoveRover(rover, dir); err != nil {

View file

@ -3,6 +3,8 @@ package game
import ( import (
"testing" "testing"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -65,7 +67,7 @@ func TestWorld_GetSetMovePosition(t *testing.T) {
attribs, err := world.RoverAttributes(a) attribs, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to get rover attribs") assert.NoError(t, err, "Failed to get rover attribs")
pos := Vector{ pos := vector.Vector{
X: 0.0, X: 0.0,
Y: 0.0, Y: 0.0,
} }
@ -77,15 +79,15 @@ func TestWorld_GetSetMovePosition(t *testing.T) {
assert.NoError(t, err, "Failed to set position for rover") assert.NoError(t, err, "Failed to set position for rover")
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly set position for rover") assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly set position for rover")
bearing := North bearing := bearing.North
duration := 1 duration := 1
newAttribs, err = world.MoveRover(a, bearing) newAttribs, err = world.MoveRover(a, bearing)
assert.NoError(t, err, "Failed to set position for rover") assert.NoError(t, err, "Failed to set position for rover")
pos.Add(Vector{0, attribs.Speed * duration}) // We should have move one unit of the speed north pos.Add(vector.Vector{X: 0, Y: attribs.Speed * duration}) // We should have move one unit of the speed north
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly move position for rover") assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly move position for rover")
// Place a tile in front of the rover // Place a tile in front of the rover
assert.NoError(t, world.Atlas.SetTile(Vector{0, 2}, TileWall)) assert.NoError(t, world.Atlas.SetTile(vector.Vector{X: 0, Y: 2}, TileWall))
newAttribs, err = world.MoveRover(a, bearing) newAttribs, err = world.MoveRover(a, bearing)
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly not move position for rover into wall") assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly not move position for rover into wall")
} }
@ -106,9 +108,9 @@ func TestWorld_RadarFromRover(t *testing.T) {
assert.NoError(t, err, "Failed to set rover attribs") assert.NoError(t, err, "Failed to set rover attribs")
// Warp the rovers into position // Warp the rovers into position
bpos := Vector{-3, -3} bpos := vector.Vector{X: -3, Y: -3}
assert.NoError(t, world.WarpRover(b, bpos), "Failed to warp rover") assert.NoError(t, world.WarpRover(b, bpos), "Failed to warp rover")
assert.NoError(t, world.WarpRover(a, Vector{0, 0}), "Failed to warp rover") assert.NoError(t, world.WarpRover(a, vector.Vector{X: 0, Y: 0}), "Failed to warp rover")
// Spawn the world wall // Spawn the world wall
err = world.Atlas.SpawnWalls() err = world.Atlas.SpawnWalls()

41
pkg/maths/maths.go Normal file
View file

@ -0,0 +1,41 @@
package maths
// Abs gets the absolute value of an int
func Abs(x int) int {
if x < 0 {
return -x
}
return x
}
// pmod is a mositive modulo
// golang's % is a "remainder" function si misbehaves for negative modulus inputs
func Pmod(x, d int) int {
if x == 0 || d == 0 {
return 0
}
x = x % d
if x >= 0 {
return x
} else if d < 0 {
return x - d
} else {
return x + d
}
}
// Max returns the highest int
func Max(x int, y int) int {
if x < y {
return y
}
return x
}
// Min returns the lowest int
func Min(x int, y int) int {
if x > y {
return y
}
return x
}

33
pkg/maths/maths_test.go Normal file
View file

@ -0,0 +1,33 @@
package maths
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAbs(t *testing.T) {
assert.Equal(t, 0, Abs(0))
assert.Equal(t, 1, Abs(1))
assert.Equal(t, 1, Abs(-1))
}
func TestPmod(t *testing.T) {
assert.Equal(t, 0, Pmod(0, 0))
assert.Equal(t, 2, Pmod(6, 4))
assert.Equal(t, 2, Pmod(-6, 4))
assert.Equal(t, 4, Pmod(-6, 10))
}
func TestMax(t *testing.T) {
assert.Equal(t, 500, Max(100, 500))
assert.Equal(t, 1, Max(-4, 1))
assert.Equal(t, -2, Max(-4, -2))
}
func TestMin(t *testing.T) {
assert.Equal(t, 100, Min(100, 500))
assert.Equal(t, -4, Min(-4, 1))
assert.Equal(t, -4, Min(-4, -2))
}

View file

@ -10,6 +10,7 @@ import (
"github.com/mdiluz/rove/pkg/game" "github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/rove" "github.com/mdiluz/rove/pkg/rove"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -91,7 +92,7 @@ func TestHandleCommand(t *testing.T) {
// Spawn the rover rover for the account // Spawn the rover rover for the account
_, inst, err := s.SpawnRoverForAccount(a.Id) _, inst, err := s.SpawnRoverForAccount(a.Id)
assert.NoError(t, s.world.WarpRover(inst, game.Vector{})) assert.NoError(t, s.world.WarpRover(inst, vector.Vector{}))
attribs, err := s.world.RoverAttributes(inst) attribs, err := s.world.RoverAttributes(inst)
assert.NoError(t, err, "Couldn't get rover position") assert.NoError(t, err, "Couldn't get rover position")
@ -130,7 +131,7 @@ func TestHandleCommand(t *testing.T) {
attribs2, err := s.world.RoverAttributes(inst) attribs2, err := s.world.RoverAttributes(inst)
assert.NoError(t, err, "Couldn't get rover position") assert.NoError(t, err, "Couldn't get rover position")
attribs.Pos.Add(game.Vector{X: 0.0, Y: attrib.Speed * 1}) // Should have moved north by the speed and duration attribs.Pos.Add(vector.Vector{X: 0.0, Y: attrib.Speed * 1}) // Should have moved north by the speed and duration
assert.Equal(t, attribs.Pos, attribs2.Pos, "Rover should have moved by bearing") assert.Equal(t, attribs.Pos, attribs2.Pos, "Rover should have moved by bearing")
} }
@ -145,13 +146,13 @@ func TestHandleRadar(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Warp this rover to 0,0 // Warp this rover to 0,0
assert.NoError(t, s.world.WarpRover(id, game.Vector{})) assert.NoError(t, s.world.WarpRover(id, vector.Vector{}))
// Explicity set a few nearby tiles // Explicity set a few nearby tiles
wallPos1 := game.Vector{X: 0, Y: -1} wallPos1 := vector.Vector{X: 0, Y: -1}
wallPos2 := game.Vector{X: 1, Y: 1} wallPos2 := vector.Vector{X: 1, Y: 1}
rockPos := game.Vector{X: 1, Y: 3} rockPos := vector.Vector{X: 1, Y: 3}
emptyPos := game.Vector{X: -2, Y: -3} emptyPos := vector.Vector{X: -2, Y: -3}
assert.NoError(t, s.world.Atlas.SetTile(wallPos1, game.TileWall)) assert.NoError(t, s.world.Atlas.SetTile(wallPos1, game.TileWall))
assert.NoError(t, s.world.Atlas.SetTile(wallPos2, game.TileWall)) assert.NoError(t, s.world.Atlas.SetTile(wallPos2, game.TileWall))
assert.NoError(t, s.world.Atlas.SetTile(rockPos, game.TileRock)) assert.NoError(t, s.world.Atlas.SetTile(rockPos, game.TileRock))
@ -171,7 +172,7 @@ func TestHandleRadar(t *testing.T) {
} }
scope := attrib.Range*2 + 1 scope := attrib.Range*2 + 1
radarOrigin := game.Vector{X: -attrib.Range, Y: -attrib.Range} radarOrigin := vector.Vector{X: -attrib.Range, Y: -attrib.Range}
// Make sure the rover tile is correct // Make sure the rover tile is correct
assert.Equal(t, game.TileRover, status.Tiles[len(status.Tiles)/2]) assert.Equal(t, game.TileRover, status.Tiles[len(status.Tiles)/2])

47
pkg/vector/vector.go Normal file
View file

@ -0,0 +1,47 @@
package vector
import "math"
// Vector desribes a 3D vector
type Vector struct {
X int `json:"x"`
Y int `json:"y"`
}
// Add adds one vector to another
func (v *Vector) Add(v2 Vector) {
v.X += v2.X
v.Y += v2.Y
}
// Added calculates a new vector
func (v Vector) Added(v2 Vector) Vector {
v.Add(v2)
return v
}
// Negated returns a negated vector
func (v Vector) Negated() Vector {
return Vector{-v.X, -v.Y}
}
// Length returns the length of the vector
func (v Vector) Length() float64 {
return math.Sqrt(float64(v.X*v.X + v.Y*v.Y))
}
// Distance returns the distance between two vectors
func (v Vector) Distance(v2 Vector) float64 {
// Negate the two vectors and calciate the length
return v.Added(v2.Negated()).Length()
}
// Multiplied returns the vector multiplied by an int
func (v Vector) Multiplied(val int) Vector {
return Vector{v.X * val, v.Y * val}
}
// Divided returns the vector divided by an int
func (v Vector) Divided(val int) Vector {
return Vector{v.X / val, v.Y / val}
}

View file

@ -1,4 +1,4 @@
package game package vector
import ( import (
"math" "math"
@ -180,15 +180,15 @@ func TestVector_Multiplied(t *testing.T) {
}{ }{
{ {
name: "Basic multiply 1", name: "Basic multiply 1",
vec: North.Vector(), vec: Vector{0, 1},
arg: 2, arg: 2,
want: Vector{0, 2}, want: Vector{0, 2},
}, },
{ {
name: "Basic multiply 2", name: "Basic multiply 2",
vec: NorthWest.Vector(), vec: Vector{-1, 1},
arg: -1, arg: -1,
want: SouthEast.Vector(), want: Vector{1, -1},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -203,27 +203,3 @@ func TestVector_Multiplied(t *testing.T) {
}) })
} }
} }
func TestDirection(t *testing.T) {
dir := North
assert.Equal(t, "North", dir.String())
assert.Equal(t, "N", dir.ShortString())
assert.Equal(t, Vector{0, 1}, dir.Vector())
dir, err := DirectionFromString("N")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = DirectionFromString("n")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = DirectionFromString("north")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = DirectionFromString("NorthWest")
assert.NoError(t, err)
assert.Equal(t, NorthWest, dir)
}