From 43648926ca0ce8851d1c1fd2102b4ed37b045fb6 Mon Sep 17 00:00:00 2001 From: Marc Di Luzio Date: Sun, 7 Jun 2020 22:30:03 +0100 Subject: [PATCH] Large refactor to properly implement radar /radar now returns a set of non-empty tile blips --- cmd/rove/main.go | 2 +- pkg/game/atlas.go | 9 +++- pkg/game/command_test.go | 3 +- pkg/game/math.go | 16 +++++++ pkg/game/tile.go | 2 + pkg/game/world.go | 99 +++++++++++++++++++++++++++++++-------- pkg/game/world_test.go | 46 ++++++++++++------ pkg/rove/api.go | 4 +- pkg/server/routes.go | 2 +- pkg/server/routes_test.go | 39 ++++++++++++--- pkg/server/server.go | 10 ++-- 11 files changed, 182 insertions(+), 50 deletions(-) diff --git a/cmd/rove/main.go b/cmd/rove/main.go index 0a5ea23..2e46736 100644 --- a/cmd/rove/main.go +++ b/cmd/rove/main.go @@ -172,7 +172,7 @@ func InnerMain(command string) error { return fmt.Errorf("Server returned failure: %s", response.Error) } else { - fmt.Printf("nearby rovers: %+v\n", response.Rovers) + fmt.Printf("radar blips: %+v\n", response.Blips) } case "rover": diff --git a/pkg/game/atlas.go b/pkg/game/atlas.go index 8f1bfcd..4f9f684 100644 --- a/pkg/game/atlas.go +++ b/pkg/game/atlas.go @@ -85,13 +85,13 @@ func (a *Atlas) SetTile(v Vector, tile Tile) error { // GetTile will return an individual tile func (a *Atlas) GetTile(v Vector) (Tile, error) { chunk := a.ToChunk(v) - if chunk > len(a.Chunks) { + 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) { + if tileId >= len(a.Chunks[chunk].Tiles) { return 0, fmt.Errorf("location outside of allocated chunk") } @@ -131,6 +131,11 @@ func (a *Atlas) ChunkOrigin(chunk int) Vector { return v.Multiplied(a.ChunkSize) } +// GetWorldExtent gets the extent of the world +func (a *Atlas) GetWorldExtent() int { + return (a.Size / 2) * a.ChunkSize +} + // Grow will return a grown copy of the current atlas func (a *Atlas) Grow(size int) error { if size%2 != 0 { diff --git a/pkg/game/command_test.go b/pkg/game/command_test.go index fcdb9ce..f221b66 100644 --- a/pkg/game/command_test.go +++ b/pkg/game/command_test.go @@ -8,7 +8,8 @@ import ( func TestCommand_Move(t *testing.T) { world := NewWorld() - a := world.SpawnRover() + a, err := world.SpawnRover() + assert.NoError(t, err) pos := Vector{ X: 1.0, Y: 2.0, diff --git a/pkg/game/math.go b/pkg/game/math.go index bb6bec6..3c9c1b2 100644 --- a/pkg/game/math.go +++ b/pkg/game/math.go @@ -27,6 +27,22 @@ func Pmod(x, d int) int { } } +// 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"` diff --git a/pkg/game/tile.go b/pkg/game/tile.go index 41cf648..b0d1fb8 100644 --- a/pkg/game/tile.go +++ b/pkg/game/tile.go @@ -7,4 +7,6 @@ const ( TileEmpty = Tile(0) TileWall = Tile(1) + + TileRover = Tile(2) ) diff --git a/pkg/game/world.go b/pkg/game/world.go index d0ccb5c..f6a3965 100644 --- a/pkg/game/world.go +++ b/pkg/game/world.go @@ -42,7 +42,7 @@ func (w *World) SpawnWorldBorder() error { } // SpawnRover adds an rover to the game -func (w *World) SpawnRover() uuid.UUID { +func (w *World) SpawnRover() (uuid.UUID, error) { w.worldMutex.Lock() defer w.worldMutex.Unlock() @@ -52,7 +52,7 @@ func (w *World) SpawnRover() uuid.UUID { Attributes: RoverAttributes{ Speed: 1.0, - Range: 20.0, + Range: 5.0, // Set the name randomly Name: babble.NewBabbler().Babble(), @@ -65,12 +65,29 @@ func (w *World) SpawnRover() uuid.UUID { w.Atlas.ChunkSize - (rand.Int() % (w.Atlas.ChunkSize * 2)), } - // TODO: Verify no blockages in this area + // Seach until we error (run out of world) + for { + if tile, err := w.Atlas.GetTile(rover.Attributes.Pos); err != nil { + return uuid.Nil, err + } else { + if tile == TileEmpty { + break + } else { + // Try and spawn to the east of the blockage + rover.Attributes.Pos.Add(Vector{1, 0}) + } + } + } + + // Set the world tile to a rover + if err := w.Atlas.SetTile(rover.Attributes.Pos, TileRover); err != nil { + return uuid.Nil, err + } // Append the rover to the list w.Rovers[rover.Id] = rover - return rover.Id + return rover.Id, nil } // Removes an rover from the game @@ -78,7 +95,11 @@ func (w *World) DestroyRover(id uuid.UUID) error { w.worldMutex.Lock() defer w.worldMutex.Unlock() - if _, ok := w.Rovers[id]; ok { + if i, ok := w.Rovers[id]; ok { + // Clear the tile + if err := w.Atlas.SetTile(i.Attributes.Pos, TileEmpty); err != nil { + return fmt.Errorf("coudln't clear old rover tile: %s", err) + } delete(w.Rovers, id) } else { return fmt.Errorf("no rover matching id") @@ -104,6 +125,14 @@ func (w *World) WarpRover(id uuid.UUID, pos Vector) error { defer w.worldMutex.Unlock() if i, ok := w.Rovers[id]; ok { + // Update the world tile + // TODO: Make this (and other things) transactional + if err := w.Atlas.SetTile(pos, 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 { + return fmt.Errorf("coudln't clear old rover tile: %s", err) + } + i.Attributes.Pos = pos w.Rovers[id] = i return nil @@ -129,8 +158,16 @@ func (w *World) MoveRover(id uuid.UUID, bearing Direction) (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") + return i.Attributes, fmt.Errorf("couldn't get tile for new position: %s", err) } else if tile == TileEmpty { + // Set the world tiles + // TODO: Make this (and other things) transactional + if err := w.Atlas.SetTile(newPos, 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 { + return i.Attributes, fmt.Errorf("coudln't clear old rover tile: %s", err) + } + // Perform the move i.Attributes.Pos = newPos w.Rovers[id] = i @@ -142,30 +179,54 @@ func (w *World) MoveRover(id uuid.UUID, bearing Direction) (RoverAttributes, err } } -// RadarDescription describes what a rover can see -type RadarDescription struct { - // Rovers is the set of rovers that this radar can see - Rovers []Vector `json:"rovers"` +// RadarBlip represents a single blip on the radar +type RadarBlip struct { + Position Vector `json:"position"` + Tile Tile `json:"tile"` } // RadarFromRover can be used to query what a rover can currently see -func (w *World) RadarFromRover(id uuid.UUID) (RadarDescription, error) { +func (w *World) RadarFromRover(id uuid.UUID) ([]RadarBlip, error) { w.worldMutex.RLock() defer w.worldMutex.RUnlock() - if r1, ok := w.Rovers[id]; ok { - var desc RadarDescription + if r, ok := w.Rovers[id]; ok { + var blips []RadarBlip - // Gather nearby rovers within the range - for _, r2 := range w.Rovers { - if r1.Id != r2.Id && r1.Attributes.Pos.Distance(r2.Attributes.Pos) < float64(r1.Attributes.Range) { - desc.Rovers = append(desc.Rovers, r2.Attributes.Pos) + extent := w.Atlas.GetWorldExtent() + + // Get min and max extents to query + min := Vector{ + Max(-extent, r.Attributes.Pos.X-r.Attributes.Range), + Max(-extent, r.Attributes.Pos.Y-r.Attributes.Range), + } + max := Vector{ + Min(extent-1, r.Attributes.Pos.X+r.Attributes.Range), + Min(extent-1, r.Attributes.Pos.Y+r.Attributes.Range), + } + + // Gather up all tiles within the range + for i := min.X; i < max.X; i++ { + for j := min.Y; j < max.Y; j++ { + + // Skip this rover + q := Vector{i, j} + if q == r.Attributes.Pos { + continue + } + + if tile, err := w.Atlas.GetTile(q); err != nil { + return blips, fmt.Errorf("failed to query tile: %s", err) + + } else if tile != TileEmpty { + blips = append(blips, RadarBlip{Position: q, Tile: tile}) + } } } - return desc, nil + return blips, nil } else { - return RadarDescription{}, fmt.Errorf("no rover matching id") + return nil, fmt.Errorf("no rover matching id") } } diff --git a/pkg/game/world_test.go b/pkg/game/world_test.go index cb88d21..dc39a24 100644 --- a/pkg/game/world_test.go +++ b/pkg/game/world_test.go @@ -16,8 +16,10 @@ func TestNewWorld(t *testing.T) { func TestWorld_CreateRover(t *testing.T) { world := NewWorld() - a := world.SpawnRover() - b := world.SpawnRover() + a, err := world.SpawnRover() + assert.NoError(t, err) + b, err := world.SpawnRover() + assert.NoError(t, err) // Basic duplicate check if a == b { @@ -29,7 +31,8 @@ func TestWorld_CreateRover(t *testing.T) { func TestWorld_RoverAttributes(t *testing.T) { world := NewWorld() - a := world.SpawnRover() + a, err := world.SpawnRover() + assert.NoError(t, err) attribs, err := world.RoverAttributes(a) assert.NoError(t, err, "Failed to get rover attribs") @@ -39,10 +42,12 @@ func TestWorld_RoverAttributes(t *testing.T) { func TestWorld_DestroyRover(t *testing.T) { world := NewWorld() - a := world.SpawnRover() - b := world.SpawnRover() + a, err := world.SpawnRover() + assert.NoError(t, err) + b, err := world.SpawnRover() + assert.NoError(t, err) - err := world.DestroyRover(a) + err = world.DestroyRover(a) assert.NoError(t, err, "Error returned from rover destroy") // Basic duplicate check @@ -55,7 +60,8 @@ func TestWorld_DestroyRover(t *testing.T) { func TestWorld_GetSetMovePosition(t *testing.T) { world := NewWorld() - a := world.SpawnRover() + a, err := world.SpawnRover() + assert.NoError(t, err) attribs, err := world.RoverAttributes(a) assert.NoError(t, err, "Failed to get rover attribs") @@ -86,22 +92,34 @@ func TestWorld_GetSetMovePosition(t *testing.T) { func TestWorld_RadarFromRover(t *testing.T) { world := NewWorld() - a := world.SpawnRover() - b := world.SpawnRover() - c := world.SpawnRover() + a, err := world.SpawnRover() + assert.NoError(t, err) + b, err := world.SpawnRover() + assert.NoError(t, err) + c, err := world.SpawnRover() + assert.NoError(t, err) // Get a's attributes attrib, err := world.RoverAttributes(a) assert.NoError(t, err, "Failed to get rover attribs") // Warp the rovers so a can see b but not c + bpos := Vector{attrib.Range - 1, 0} + cpos := Vector{attrib.Range + 1, 0} assert.NoError(t, world.WarpRover(a, Vector{0, 0}), "Failed to warp rover") - assert.NoError(t, world.WarpRover(b, Vector{attrib.Range - 1, 0}), "Failed to warp rover") - assert.NoError(t, world.WarpRover(c, Vector{attrib.Range + 1, 0}), "Failed to warp rover") + assert.NoError(t, world.WarpRover(b, bpos), "Failed to warp rover") + assert.NoError(t, world.WarpRover(c, cpos), "Failed to warp rover") radar, err := world.RadarFromRover(a) assert.NoError(t, err, "Failed to get radar from rover") - assert.Equal(t, 1, len(radar.Rovers), "Radar returned wrong number of rovers") - assert.Equal(t, Vector{attrib.Range - 1, 0}, radar.Rovers[0], "Rover on radar in wrong position") + assert.Equal(t, 1, len(radar), "Radar returned wrong number of rovers") + + found := false + for _, blip := range radar { + if blip.Position == bpos && blip.Tile == TileRover { + found = true + } + } + assert.True(t, found, "Rover not found on radar in expected position") } diff --git a/pkg/rove/api.go b/pkg/rove/api.go index d47f1c6..0163a01 100644 --- a/pkg/rove/api.go +++ b/pkg/rove/api.go @@ -104,8 +104,8 @@ type RadarResponse struct { Success bool `json:"success"` Error string `json:"error,omitempty"` - // The set of positions for nearby rovers - Rovers []game.Vector `json:"rovers"` + // The set of positions for nearby non-empty tiles + Blips []game.RadarBlip `json:"blips"` } // ================ diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 7824b5b..29a2bfd 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -197,7 +197,7 @@ func HandleRadar(s *Server, vars map[string]string, b io.ReadCloser, w io.Writer } else { fmt.Printf("Responded with radar\taccount:%s\tradar:%+v\n", id, radar) - response.Rovers = radar.Rovers + response.Blips = radar response.Success = true } diff --git a/pkg/server/routes_test.go b/pkg/server/routes_test.go index ab21bf1..4fa85fd 100644 --- a/pkg/server/routes_test.go +++ b/pkg/server/routes_test.go @@ -140,7 +140,22 @@ func TestHandleRadar(t *testing.T) { assert.NoError(t, err, "Error registering account") // Spawn the rover rover for the account - _, _, err = s.SpawnRoverForAccount(a.Id) + _, id, err := s.SpawnRoverForAccount(a.Id) + assert.NoError(t, err) + + // Warp this rover to 0 + assert.NoError(t, s.world.WarpRover(id, game.Vector{})) + + // Spawn another rover + id, err = s.world.SpawnRover() + assert.NoError(t, err) + // Warp this rover to just above the other one + roverPos := game.Vector{X: 0, Y: 1} + assert.NoError(t, s.world.WarpRover(id, roverPos)) + + // Set a tile to wall below this rover + wallPos := game.Vector{X: 0, Y: -1} + assert.NoError(t, s.world.Atlas.SetTile(wallPos, game.TileWall)) request, _ := http.NewRequest(http.MethodGet, path.Join("/", a.Id.String(), "/radar"), nil) response := httptest.NewRecorder() @@ -155,7 +170,18 @@ func TestHandleRadar(t *testing.T) { t.Errorf("got false for /radar: %s", status.Error) } - // TODO: Verify the radar information + foundWall := false + foundRover := false + for _, b := range status.Blips { + if b.Position == wallPos && b.Tile == game.TileWall { + foundWall = true + } else if b.Position == roverPos && b.Tile == game.TileRover { + foundRover = true + } + } + assert.True(t, foundWall) + assert.True(t, foundRover) + } func TestHandleRover(t *testing.T) { @@ -164,8 +190,9 @@ func TestHandleRover(t *testing.T) { a, err := s.accountant.RegisterAccount("test") assert.NoError(t, err, "Error registering account") - // Spawn the rover rover for the account - _, _, err = s.SpawnRoverForAccount(a.Id) + // Spawn one rover for the account + attribs, _, err := s.SpawnRoverForAccount(a.Id) + assert.NoError(t, err) request, _ := http.NewRequest(http.MethodGet, path.Join("/", a.Id.String(), "/rover"), nil) response := httptest.NewRecorder() @@ -178,7 +205,7 @@ func TestHandleRover(t *testing.T) { if status.Success != true { t.Errorf("got false for /rover: %s", status.Error) + } else if attribs != status.Attributes { + t.Errorf("Missmatched attributes: %+v, !=%+v", attribs, status.Attributes) } - - // TODO: Verify the rover information } diff --git a/pkg/server/server.go b/pkg/server/server.go index d045983..0f06c62 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -269,7 +269,7 @@ func (s *Server) wrapHandler(method string, handler Handler) func(w http.Respons w.WriteHeader(http.StatusInternalServerError) } else if err := json.NewEncoder(w).Encode(val); err != nil { - fmt.Printf("Failed to encode return to json: %s", err) + fmt.Printf("Failed to encode reply to json: %s", err) w.WriteHeader(http.StatusInternalServerError) } else { @@ -280,9 +280,11 @@ func (s *Server) wrapHandler(method string, handler Handler) func(w http.Respons // SpawnRoverForAccount spawns the rover rover for an account func (s *Server) SpawnRoverForAccount(accountid uuid.UUID) (game.RoverAttributes, uuid.UUID, error) { - inst := s.world.SpawnRover() - if attribs, err := s.world.RoverAttributes(inst); err != nil { - return game.RoverAttributes{}, uuid.UUID{}, fmt.Errorf("No attributes found for created rover") + if inst, err := s.world.SpawnRover(); err != nil { + return game.RoverAttributes{}, uuid.UUID{}, err + + } else if attribs, err := s.world.RoverAttributes(inst); err != nil { + return game.RoverAttributes{}, uuid.UUID{}, fmt.Errorf("No attributes found for created rover: %s", err) } else { if err := s.accountant.AssignRover(accountid, inst); err != nil {