diff --git a/pkg/game/atlas.go b/pkg/game/atlas.go new file mode 100644 index 0000000..fefcaf2 --- /dev/null +++ b/pkg/game/atlas.go @@ -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 +} diff --git a/pkg/game/atlas_test.go b/pkg/game/atlas_test.go new file mode 100644 index 0000000..c5591e8 --- /dev/null +++ b/pkg/game/atlas_test.go @@ -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) +} diff --git a/pkg/game/math.go b/pkg/game/math.go index 846555a..bb6bec6 100644 --- a/pkg/game/math.go +++ b/pkg/game/math.go @@ -6,6 +6,27 @@ import ( "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 type Vector struct { X int `json:"x"` @@ -45,6 +66,11 @@ 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 diff --git a/pkg/game/tile.go b/pkg/game/tile.go deleted file mode 100644 index a590076..0000000 --- a/pkg/game/tile.go +++ /dev/null @@ -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"` -} diff --git a/pkg/game/world.go b/pkg/game/world.go index 09cdf61..398d728 100644 --- a/pkg/game/world.go +++ b/pkg/game/world.go @@ -32,6 +32,7 @@ func NewWorld() *World { return &World{ Rovers: make(map[uuid.UUID]Rover), CommandQueue: make(map[uuid.UUID]CommandStream), + Atlas: NewAtlas(2, 10), // TODO: Choose an appropriate world size } } diff --git a/pkg/persistence/persistence.go b/pkg/persistence/persistence.go index 7d90272..85ffa18 100644 --- a/pkg/persistence/persistence.go +++ b/pkg/persistence/persistence.go @@ -29,6 +29,7 @@ func jsonPath(name string) string { // Save will serialise the interface into a json file func Save(name string, data interface{}) error { + path := jsonPath(name) if b, err := json.MarshalIndent(data, "", "\t"); err != nil { return err } else { @@ -36,6 +37,8 @@ func Save(name string, data interface{}) error { return err } } + + fmt.Printf("Saved %s\n", path) return nil } @@ -54,9 +57,12 @@ func Load(name string, data interface{}) error { return err } else if len(b) == 0 { fmt.Printf("File %s was empty, loading with fresh data\n", path) + return nil } else if err := json.Unmarshal(b, data); err != nil { return fmt.Errorf("failed to load file %s error: %s", path, err) } + + fmt.Printf("Loaded %s\n", path) return nil }