Refactor the Tile to a full Atlas
This atlas is a set of chunks and supports resizing
This commit is contained in:
parent
8586bdabd7
commit
ceca4eb7fa
6 changed files with 300 additions and 28 deletions
131
pkg/game/atlas.go
Normal file
131
pkg/game/atlas.go
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
package game
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Kind represents the type of a tile on the map
|
||||||
|
type Kind int
|
||||||
|
|
||||||
|
// Chunk represents a fixed square grid of tiles
|
||||||
|
type Chunk struct {
|
||||||
|
// Tiles represents the tiles within the chunk
|
||||||
|
Tiles []Kind `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(radius int, chunkSize int) Atlas {
|
||||||
|
size := radius * 2
|
||||||
|
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([]Kind, chunkSize*chunkSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTile sets an individual tile's kind
|
||||||
|
func (a *Atlas) SetTile(v Vector, tile Kind) 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) (Kind, 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 {
|
||||||
|
return Vector{Pmod(v.X, a.ChunkSize), Pmod(v.Y, a.ChunkSize)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChunkLocal gets a chunk local coordinate for a tile
|
||||||
|
func (a *Atlas) ToWorld(local Vector, chunk int) Vector {
|
||||||
|
return a.ChunkOrigin(chunk).Added(local)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChunkID gets the chunk ID for a position in the world
|
||||||
|
func (a *Atlas) ToChunk(v 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{a.Size / 2, 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 {
|
||||||
|
v := Vector{
|
||||||
|
X: Pmod(chunk, a.Size) - (a.Size / 2),
|
||||||
|
Y: (chunk / a.Size) - (a.Size / 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Multiplied(a.ChunkSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grown will return a grown copy of the current atlas
|
||||||
|
func (a *Atlas) Grown(newRadius int) (Atlas, error) {
|
||||||
|
delta := (newRadius * 2) - a.Size
|
||||||
|
if delta < 0 {
|
||||||
|
return Atlas{}, fmt.Errorf("Cannot shrink an atlas")
|
||||||
|
} else if delta == 0 {
|
||||||
|
return Atlas{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new atlas
|
||||||
|
newAtlas := NewAtlas(newRadius, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the new atlas
|
||||||
|
return newAtlas, nil
|
||||||
|
}
|
136
pkg/game/atlas_test.go
Normal file
136
pkg/game/atlas_test.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAtlas_NewAtlas(t *testing.T) {
|
||||||
|
a := NewAtlas(1, 1) // "radius" of 1, each chunk just one tile
|
||||||
|
assert.NotNil(t, a)
|
||||||
|
// Tiles should look like: 2 | 3
|
||||||
|
// -----
|
||||||
|
// 0 | 1
|
||||||
|
assert.Equal(t, 4, len(a.Chunks))
|
||||||
|
|
||||||
|
a = NewAtlas(2, 1) // "radius" of 2
|
||||||
|
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(1, 1)
|
||||||
|
assert.NotNil(t, a)
|
||||||
|
// Tiles should look like: 2 | 3
|
||||||
|
// -----
|
||||||
|
// 0 | 1
|
||||||
|
tile := a.ToChunk(Vector{0, 0})
|
||||||
|
assert.Equal(t, 3, tile)
|
||||||
|
tile = a.ToChunk(Vector{0, -1})
|
||||||
|
assert.Equal(t, 1, tile)
|
||||||
|
tile = a.ToChunk(Vector{-1, -1})
|
||||||
|
assert.Equal(t, 0, tile)
|
||||||
|
tile = a.ToChunk(Vector{-1, 0})
|
||||||
|
assert.Equal(t, 2, tile)
|
||||||
|
|
||||||
|
a = NewAtlas(1, 2)
|
||||||
|
assert.NotNil(t, a)
|
||||||
|
// Tiles should look like:
|
||||||
|
// 2 | 3
|
||||||
|
// -----
|
||||||
|
// 0 | 1
|
||||||
|
tile = a.ToChunk(Vector{1, 1})
|
||||||
|
assert.Equal(t, 3, tile)
|
||||||
|
tile = a.ToChunk(Vector{1, -2})
|
||||||
|
assert.Equal(t, 1, tile)
|
||||||
|
tile = a.ToChunk(Vector{-2, -2})
|
||||||
|
assert.Equal(t, 0, tile)
|
||||||
|
tile = a.ToChunk(Vector{-2, 1})
|
||||||
|
assert.Equal(t, 2, tile)
|
||||||
|
|
||||||
|
a = NewAtlas(2, 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{1, 3})
|
||||||
|
assert.Equal(t, 14, tile)
|
||||||
|
tile = a.ToChunk(Vector{1, -3})
|
||||||
|
assert.Equal(t, 2, tile)
|
||||||
|
tile = a.ToChunk(Vector{-1, -1})
|
||||||
|
assert.Equal(t, 5, tile)
|
||||||
|
tile = a.ToChunk(Vector{-2, 2})
|
||||||
|
assert.Equal(t, 13, tile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtlas_GetSetTile(t *testing.T) {
|
||||||
|
a := NewAtlas(2, 10)
|
||||||
|
assert.NotNil(t, a)
|
||||||
|
|
||||||
|
// Set the origin tile to 1 and test it
|
||||||
|
assert.NoError(t, a.SetTile(Vector{0, 0}, 1))
|
||||||
|
tile, err := a.GetTile(Vector{0, 0})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, Kind(1), tile)
|
||||||
|
|
||||||
|
// Set another tile to 1 and test it
|
||||||
|
assert.NoError(t, a.SetTile(Vector{5, -2}, 2))
|
||||||
|
tile, err = a.GetTile(Vector{5, -2})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, Kind(2), tile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtlas_Grown(t *testing.T) {
|
||||||
|
// Start with a small example
|
||||||
|
a := NewAtlas(1, 2)
|
||||||
|
assert.NotNil(t, a)
|
||||||
|
assert.Equal(t, 4, len(a.Chunks))
|
||||||
|
|
||||||
|
// Set a few tiles to values
|
||||||
|
assert.NoError(t, a.SetTile(Vector{0, 0}, 1))
|
||||||
|
assert.NoError(t, a.SetTile(Vector{-1, -1}, 2))
|
||||||
|
assert.NoError(t, a.SetTile(Vector{1, -2}, 3))
|
||||||
|
|
||||||
|
// Grow once to just double it
|
||||||
|
a, err := a.Grown(2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 16, len(a.Chunks))
|
||||||
|
|
||||||
|
tile, err := a.GetTile(Vector{0, 0})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, Kind(1), tile)
|
||||||
|
|
||||||
|
tile, err = a.GetTile(Vector{-1, -1})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, Kind(2), tile)
|
||||||
|
|
||||||
|
tile, err = a.GetTile(Vector{1, -2})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, Kind(3), tile)
|
||||||
|
|
||||||
|
// Grow it again even bigger
|
||||||
|
a, err = a.Grown(5)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 100, len(a.Chunks))
|
||||||
|
|
||||||
|
tile, err = a.GetTile(Vector{0, 0})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, Kind(1), tile)
|
||||||
|
|
||||||
|
tile, err = a.GetTile(Vector{-1, -1})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, Kind(2), tile)
|
||||||
|
|
||||||
|
tile, err = a.GetTile(Vector{1, -2})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, Kind(3), tile)
|
||||||
|
}
|
|
@ -6,6 +6,27 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vector desribes a 3D vector
|
// Vector desribes a 3D vector
|
||||||
type Vector struct {
|
type Vector struct {
|
||||||
X int `json:"x"`
|
X int `json:"x"`
|
||||||
|
@ -45,6 +66,11 @@ func (v Vector) Multiplied(val int) Vector {
|
||||||
return Vector{v.X * val, v.Y * val}
|
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
|
// Direction describes a compass direction
|
||||||
type Direction int
|
type Direction int
|
||||||
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
package game
|
|
||||||
|
|
||||||
// Tile represents a single tile on the map
|
|
||||||
type Tile struct {
|
|
||||||
// Kind represends the kind of tile this is
|
|
||||||
Kind int `json:"kind"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ChunkDimensions = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
// Chunk represents a fixed square grid of tiles
|
|
||||||
type Chunk struct {
|
|
||||||
// Tiles represents the tiles within the chunk
|
|
||||||
Tiles [ChunkDimensions][ChunkDimensions]Tile `json:"tiles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Use a fixed map dimension for now
|
|
||||||
AtlasDimensions = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
// Atlas represents a grid of Chunks
|
|
||||||
// TODO: Make this resizable
|
|
||||||
type Atlas struct {
|
|
||||||
Chunks [AtlasDimensions][AtlasDimensions]Chunk `json:"chunks"`
|
|
||||||
}
|
|
|
@ -32,6 +32,7 @@ func NewWorld() *World {
|
||||||
return &World{
|
return &World{
|
||||||
Rovers: make(map[uuid.UUID]Rover),
|
Rovers: make(map[uuid.UUID]Rover),
|
||||||
CommandQueue: make(map[uuid.UUID]CommandStream),
|
CommandQueue: make(map[uuid.UUID]CommandStream),
|
||||||
|
Atlas: NewAtlas(2, 10), // TODO: Choose an appropriate world size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ func jsonPath(name string) string {
|
||||||
|
|
||||||
// Save will serialise the interface into a json file
|
// Save will serialise the interface into a json file
|
||||||
func Save(name string, data interface{}) error {
|
func Save(name string, data interface{}) error {
|
||||||
|
path := jsonPath(name)
|
||||||
if b, err := json.MarshalIndent(data, "", "\t"); err != nil {
|
if b, err := json.MarshalIndent(data, "", "\t"); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
|
@ -36,6 +37,8 @@ func Save(name string, data interface{}) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Saved %s\n", path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,9 +57,12 @@ func Load(name string, data interface{}) error {
|
||||||
return err
|
return err
|
||||||
} else if len(b) == 0 {
|
} else if len(b) == 0 {
|
||||||
fmt.Printf("File %s was empty, loading with fresh data\n", path)
|
fmt.Printf("File %s was empty, loading with fresh data\n", path)
|
||||||
|
return nil
|
||||||
} else if err := json.Unmarshal(b, data); err != nil {
|
} else if err := json.Unmarshal(b, data); err != nil {
|
||||||
return fmt.Errorf("failed to load file %s error: %s", path, err)
|
return fmt.Errorf("failed to load file %s error: %s", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Loaded %s\n", path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue