Move the Atlas code into it's own package

This commit is contained in:
Marc Di Luzio 2020-06-11 20:42:59 +01:00
parent 8cd7b06c0c
commit faaa556ad0
8 changed files with 44 additions and 35 deletions

View file

@ -1,191 +0,0 @@
package game
import (
"fmt"
"log"
"math/rand"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/pkg/vector"
)
// Chunk represents a fixed square grid of tiles
type Chunk struct {
// Tiles represents the tiles within the chunk
Tiles []Tile `json:"tiles"`
}
// Atlas represents a grid of Chunks
type Atlas struct {
// Chunks represents all chunks in the world
// This is intentionally not a 2D array so it can be expanded in all directions
Chunks []Chunk `json:"chunks"`
// size is the current width/height of the given atlas
Size int `json:"size"`
// ChunkSize is the dimensions of each chunk
ChunkSize int `json:"chunksize"`
}
// NewAtlas creates a new empty atlas
func NewAtlas(size, chunkSize int) Atlas {
if size%2 != 0 {
log.Fatal("atlas size must always be even")
}
a := Atlas{
Size: size,
Chunks: make([]Chunk, size*size),
ChunkSize: chunkSize,
}
// Initialise all the chunks
for i := range a.Chunks {
a.Chunks[i] = Chunk{
Tiles: make([]Tile, chunkSize*chunkSize),
}
}
return a
}
// SpawnRocks peppers the world with rocks
func (a *Atlas) SpawnRocks() error {
extent := a.ChunkSize * (a.Size / 2)
// Pepper the current world with rocks
for i := -extent; i < extent; i++ {
for j := -extent; j < extent; j++ {
if rand.Intn(16) == 0 {
if err := a.SetTile(vector.Vector{X: i, Y: j}, TileRock); err != nil {
return err
}
}
}
}
return nil
}
// SpawnWalls spawns the around the world
func (a *Atlas) SpawnWalls() error {
extent := a.ChunkSize * (a.Size / 2)
// Surround the atlas in walls
for i := -extent; i < extent; i++ {
if err := a.SetTile(vector.Vector{X: i, Y: extent - 1}, TileWall); err != nil { // N
return err
} else if err := a.SetTile(vector.Vector{X: extent - 1, Y: i}, TileWall); err != nil { // E
return err
} else if err := a.SetTile(vector.Vector{X: i, Y: -extent}, TileWall); err != nil { // S
return err
} else if err := a.SetTile(vector.Vector{X: -extent, Y: i}, TileWall); err != nil { // W
return err
}
}
return nil
}
// SetTile sets an individual tile's kind
func (a *Atlas) SetTile(v vector.Vector, tile Tile) error {
chunk := a.toChunk(v)
if chunk >= len(a.Chunks) {
return fmt.Errorf("location outside of allocated atlas")
}
local := a.toChunkLocal(v)
tileId := local.X + local.Y*a.ChunkSize
if tileId >= len(a.Chunks[chunk].Tiles) {
return fmt.Errorf("location outside of allocated chunk")
}
a.Chunks[chunk].Tiles[tileId] = tile
return nil
}
// GetTile will return an individual tile
func (a *Atlas) GetTile(v vector.Vector) (Tile, error) {
chunk := a.toChunk(v)
if chunk >= len(a.Chunks) {
return 0, fmt.Errorf("location outside of allocated atlas")
}
local := a.toChunkLocal(v)
tileId := local.X + local.Y*a.ChunkSize
if tileId >= len(a.Chunks[chunk].Tiles) {
return 0, fmt.Errorf("location outside of allocated chunk")
}
return a.Chunks[chunk].Tiles[tileId], nil
}
// toChunkLocal gets a chunk local coordinate for a tile
func (a *Atlas) toChunkLocal(v vector.Vector) vector.Vector {
return vector.Vector{X: maths.Pmod(v.X, a.ChunkSize), Y: maths.Pmod(v.Y, a.ChunkSize)}
}
// GetChunkID gets the chunk ID for a position in the world
func (a *Atlas) toChunk(v vector.Vector) int {
local := a.toChunkLocal(v)
// Get the chunk origin itself
origin := v.Added(local.Negated())
// Divided it by the number of chunks
origin = origin.Divided(a.ChunkSize)
// Shift it by our size (our origin is in the middle)
origin = origin.Added(vector.Vector{X: a.Size / 2, Y: a.Size / 2})
// Get the ID based on the final values
return (a.Size * origin.Y) + origin.X
}
// chunkOrigin gets the chunk origin for a given chunk index
func (a *Atlas) chunkOrigin(chunk int) vector.Vector {
v := vector.Vector{
X: maths.Pmod(chunk, a.Size) - (a.Size / 2),
Y: (chunk / a.Size) - (a.Size / 2),
}
return v.Multiplied(a.ChunkSize)
}
// GetWorldExtent gets the min and max valid coordinates of world
func (a *Atlas) GetWorldExtents() (min, max vector.Vector) {
min = vector.Vector{
X: -(a.Size / 2) * a.ChunkSize,
Y: -(a.Size / 2) * a.ChunkSize,
}
max = vector.Vector{
X: -min.X - 1,
Y: -min.Y - 1,
}
return
}
// Grow will return a grown copy of the current atlas
func (a *Atlas) Grow(size int) error {
if size%2 != 0 {
return fmt.Errorf("atlas size must always be even")
}
delta := size - a.Size
if delta < 0 {
return fmt.Errorf("cannot shrink an atlas")
} else if delta == 0 {
return nil
}
// Create a new atlas
newAtlas := NewAtlas(size, a.ChunkSize)
// Copy old chunks into new chunks
for index, chunk := range a.Chunks {
// Calculate the new chunk location and copy over the data
newAtlas.Chunks[newAtlas.toChunk(a.chunkOrigin(index))] = chunk
}
// Copy the new atlas data into this one
*a = newAtlas
// Return the new atlas
return nil
}

View file

@ -1,169 +0,0 @@
package game
import (
"testing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
)
func TestAtlas_NewAtlas(t *testing.T) {
a := NewAtlas(2, 1)
assert.NotNil(t, a)
// Tiles should look like: 2 | 3
// -----
// 0 | 1
assert.Equal(t, 4, len(a.Chunks))
a = NewAtlas(4, 1)
assert.NotNil(t, a)
// Tiles should look like: 2 | 3
// -----
// 0 | 1
assert.Equal(t, 16, len(a.Chunks))
}
func TestAtlas_toChunk(t *testing.T) {
a := NewAtlas(2, 1)
assert.NotNil(t, a)
// Tiles should look like: 2 | 3
// -----
// 0 | 1
tile := a.toChunk(vector.Vector{X: 0, Y: 0})
assert.Equal(t, 3, tile)
tile = a.toChunk(vector.Vector{X: 0, Y: -1})
assert.Equal(t, 1, tile)
tile = a.toChunk(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 0, tile)
tile = a.toChunk(vector.Vector{X: -1, Y: 0})
assert.Equal(t, 2, tile)
a = NewAtlas(2, 2)
assert.NotNil(t, a)
// Tiles should look like:
// 2 | 3
// -----
// 0 | 1
tile = a.toChunk(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 3, tile)
tile = a.toChunk(vector.Vector{X: 1, Y: -2})
assert.Equal(t, 1, tile)
tile = a.toChunk(vector.Vector{X: -2, Y: -2})
assert.Equal(t, 0, tile)
tile = a.toChunk(vector.Vector{X: -2, Y: 1})
assert.Equal(t, 2, tile)
a = NewAtlas(4, 2)
assert.NotNil(t, a)
// Tiles should look like:
// 12| 13|| 14| 15
// ----------------
// 8 | 9 || 10| 11
// ================
// 4 | 5 || 6 | 7
// ----------------
// 0 | 1 || 2 | 3
tile = a.toChunk(vector.Vector{X: 1, Y: 3})
assert.Equal(t, 14, tile)
tile = a.toChunk(vector.Vector{X: 1, Y: -3})
assert.Equal(t, 2, tile)
tile = a.toChunk(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 5, tile)
tile = a.toChunk(vector.Vector{X: -2, Y: 2})
assert.Equal(t, 13, tile)
}
func TestAtlas_GetSetTile(t *testing.T) {
a := NewAtlas(4, 10)
assert.NotNil(t, a)
// Set the origin tile to 1 and test it
assert.NoError(t, a.SetTile(vector.Vector{X: 0, Y: 0}, 1))
tile, err := a.GetTile(vector.Vector{X: 0, Y: 0})
assert.NoError(t, err)
assert.Equal(t, Tile(1), tile)
// Set another tile to 1 and test it
assert.NoError(t, a.SetTile(vector.Vector{X: 5, Y: -2}, 2))
tile, err = a.GetTile(vector.Vector{X: 5, Y: -2})
assert.NoError(t, err)
assert.Equal(t, Tile(2), tile)
}
func TestAtlas_Grown(t *testing.T) {
// Start with a small example
a := NewAtlas(2, 2)
assert.NotNil(t, a)
assert.Equal(t, 4, len(a.Chunks))
// Set a few tiles to values
assert.NoError(t, a.SetTile(vector.Vector{X: 0, Y: 0}, 1))
assert.NoError(t, a.SetTile(vector.Vector{X: -1, Y: -1}, 2))
assert.NoError(t, a.SetTile(vector.Vector{X: 1, Y: -2}, 3))
// Grow once to just double it
err := a.Grow(4)
assert.NoError(t, err)
assert.Equal(t, 16, len(a.Chunks))
tile, err := a.GetTile(vector.Vector{X: 0, Y: 0})
assert.NoError(t, err)
assert.Equal(t, Tile(1), tile)
tile, err = a.GetTile(vector.Vector{X: -1, Y: -1})
assert.NoError(t, err)
assert.Equal(t, Tile(2), tile)
tile, err = a.GetTile(vector.Vector{X: 1, Y: -2})
assert.NoError(t, err)
assert.Equal(t, Tile(3), tile)
// Grow it again even bigger
err = a.Grow(10)
assert.NoError(t, err)
assert.Equal(t, 100, len(a.Chunks))
tile, err = a.GetTile(vector.Vector{X: 0, Y: 0})
assert.NoError(t, err)
assert.Equal(t, Tile(1), tile)
tile, err = a.GetTile(vector.Vector{X: -1, Y: -1})
assert.NoError(t, err)
assert.Equal(t, Tile(2), tile)
tile, err = a.GetTile(vector.Vector{X: 1, Y: -2})
assert.NoError(t, err)
assert.Equal(t, Tile(3), tile)
}
func TestAtlas_SpawnWorld(t *testing.T) {
// Start with a small example
a := NewAtlas(2, 4)
assert.NotNil(t, a)
assert.Equal(t, 4, len(a.Chunks))
assert.NoError(t, a.SpawnWalls())
for i := -4; i < 4; i++ {
tile, err := a.GetTile(vector.Vector{X: i, Y: -4})
assert.NoError(t, err)
assert.Equal(t, TileWall, tile)
}
for i := -4; i < 4; i++ {
tile, err := a.GetTile(vector.Vector{X: -4, Y: i})
assert.NoError(t, err)
assert.Equal(t, TileWall, tile)
}
for i := -4; i < 4; i++ {
tile, err := a.GetTile(vector.Vector{X: 3, Y: i})
assert.NoError(t, err)
assert.Equal(t, TileWall, tile)
}
for i := -4; i < 4; i++ {
tile, err := a.GetTile(vector.Vector{X: i, Y: 3})
assert.NoError(t, err)
assert.Equal(t, TileWall, tile)
}
}

View file

@ -1,12 +0,0 @@
package game
// Tile represents the type of a tile on the map
type Tile byte
const (
TileEmpty = Tile(0)
TileRover = Tile(1)
TileWall = Tile(2)
TileRock = Tile(3)
)

View file

@ -9,6 +9,7 @@ import (
"sync"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/pkg/vector"
@ -21,7 +22,7 @@ type World struct {
Rovers map[uuid.UUID]Rover `json:"rovers"`
// Atlas represends the world map of chunks and tiles
Atlas Atlas `json:"atlas"`
Atlas atlas.Atlas `json:"atlas"`
// Mutex to lock around all world operations
worldMutex sync.RWMutex
@ -42,7 +43,7 @@ func NewWorld(size, chunkSize int) *World {
Rovers: make(map[uuid.UUID]Rover),
CommandQueue: make(map[uuid.UUID]CommandStream),
Incoming: make(map[uuid.UUID]CommandStream),
Atlas: NewAtlas(size, chunkSize),
Atlas: atlas.NewAtlas(size, chunkSize),
}
}
@ -88,7 +89,7 @@ func (w *World) SpawnRover() (uuid.UUID, error) {
if tile, err := w.Atlas.GetTile(rover.Attributes.Pos); err != nil {
return uuid.Nil, err
} else {
if tile == TileEmpty {
if tile == atlas.TileEmpty {
break
} else {
// Try and spawn to the east of the blockage
@ -98,7 +99,7 @@ func (w *World) SpawnRover() (uuid.UUID, error) {
}
// Set the world tile to a rover
if err := w.Atlas.SetTile(rover.Attributes.Pos, TileRover); err != nil {
if err := w.Atlas.SetTile(rover.Attributes.Pos, atlas.TileRover); err != nil {
return uuid.Nil, err
}
@ -117,7 +118,7 @@ func (w *World) DestroyRover(id uuid.UUID) error {
if i, ok := w.Rovers[id]; ok {
// Clear the tile
if err := w.Atlas.SetTile(i.Attributes.Pos, TileEmpty); err != nil {
if err := w.Atlas.SetTile(i.Attributes.Pos, atlas.TileEmpty); err != nil {
return fmt.Errorf("coudln't clear old rover tile: %s", err)
}
delete(w.Rovers, id)
@ -167,11 +168,11 @@ func (w *World) WarpRover(id uuid.UUID, pos vector.Vector) error {
// Update the world tile
if tile, err := w.Atlas.GetTile(pos); err != nil {
return fmt.Errorf("coudln't get state of destination rover tile: %s", err)
} else if tile == TileRover {
} else if tile == atlas.TileRover {
return fmt.Errorf("can't warp rover to occupied tile, check before warping")
} else if err := w.Atlas.SetTile(pos, TileRover); err != nil {
} else if err := w.Atlas.SetTile(pos, atlas.TileRover); err != nil {
return fmt.Errorf("coudln't set rover tile: %s", err)
} else if err := w.Atlas.SetTile(i.Attributes.Pos, TileEmpty); err != nil {
} else if err := w.Atlas.SetTile(i.Attributes.Pos, atlas.TileEmpty); err != nil {
return fmt.Errorf("coudln't clear old rover tile: %s", err)
}
@ -201,11 +202,11 @@ func (w *World) MoveRover(id uuid.UUID, b bearing.Bearing) (RoverAttributes, err
// Get the tile and verify it's empty
if tile, err := w.Atlas.GetTile(newPos); err != nil {
return i.Attributes, fmt.Errorf("couldn't get tile for new position: %s", err)
} else if tile == TileEmpty {
} else if tile == atlas.TileEmpty {
// Set the world tiles
if err := w.Atlas.SetTile(newPos, TileRover); err != nil {
if err := w.Atlas.SetTile(newPos, atlas.TileRover); err != nil {
return i.Attributes, fmt.Errorf("coudln't set rover tile: %s", err)
} else if err := w.Atlas.SetTile(i.Attributes.Pos, TileEmpty); err != nil {
} else if err := w.Atlas.SetTile(i.Attributes.Pos, atlas.TileEmpty); err != nil {
return i.Attributes, fmt.Errorf("coudln't clear old rover tile: %s", err)
}
@ -221,7 +222,7 @@ func (w *World) MoveRover(id uuid.UUID, b bearing.Bearing) (RoverAttributes, err
}
// RadarFromRover can be used to query what a rover can currently see
func (w *World) RadarFromRover(id uuid.UUID) ([]Tile, error) {
func (w *World) RadarFromRover(id uuid.UUID) ([]atlas.Tile, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
@ -252,7 +253,7 @@ func (w *World) RadarFromRover(id uuid.UUID) ([]Tile, error) {
}
// Gather up all tiles within the range
var radar = make([]Tile, radarSpan*radarSpan)
var radar = make([]atlas.Tile, radarSpan*radarSpan)
for j := scanMin.Y; j <= scanMax.Y; j++ {
for i := scanMin.X; i <= scanMax.X; i++ {
q := vector.Vector{X: i, Y: j}
@ -377,7 +378,7 @@ func (w *World) ExecuteCommand(c *Command, rover uuid.UUID) (finished bool, err
}
// PrintTiles simply prints the input tiles directly for debug
func PrintTiles(tiles []Tile) {
func PrintTiles(tiles []atlas.Tile) {
num := int(math.Sqrt(float64(len(tiles))))
for j := num - 1; j >= 0; j-- {
for i := 0; i < num; i++ {

View file

@ -3,6 +3,7 @@ package game
import (
"testing"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
@ -87,7 +88,7 @@ func TestWorld_GetSetMovePosition(t *testing.T) {
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly move position for rover")
// Place a tile in front of the rover
assert.NoError(t, world.Atlas.SetTile(vector.Vector{X: 0, Y: 2}, TileWall))
assert.NoError(t, world.Atlas.SetTile(vector.Vector{X: 0, Y: 2}, atlas.TileWall))
newAttribs, err = world.MoveRover(a, b)
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly not move position for rover into wall")
}
@ -134,12 +135,12 @@ func TestWorld_RadarFromRover(t *testing.T) {
PrintTiles(radar)
// Test all expected values
assert.Equal(t, TileRover, radar[1+fullRange])
assert.Equal(t, TileRover, radar[4+4*fullRange])
assert.Equal(t, atlas.TileRover, radar[1+fullRange])
assert.Equal(t, atlas.TileRover, radar[4+4*fullRange])
for i := 0; i < 8; i++ {
assert.Equal(t, TileWall, radar[i])
assert.Equal(t, TileWall, radar[i+(7*9)])
assert.Equal(t, TileWall, radar[i*9])
assert.Equal(t, TileWall, radar[(i*9)+7])
assert.Equal(t, atlas.TileWall, radar[i])
assert.Equal(t, atlas.TileWall, radar[i+(7*9)])
assert.Equal(t, atlas.TileWall, radar[i*9])
assert.Equal(t, atlas.TileWall, radar[(i*9)+7])
}
}