Large refactor to properly implement radar

/radar now returns a set of non-empty tile blips
This commit is contained in:
Marc Di Luzio 2020-06-07 22:30:03 +01:00
parent fc54775df9
commit 43648926ca
11 changed files with 182 additions and 50 deletions

View file

@ -172,7 +172,7 @@ func InnerMain(command string) error {
return fmt.Errorf("Server returned failure: %s", response.Error) return fmt.Errorf("Server returned failure: %s", response.Error)
} else { } else {
fmt.Printf("nearby rovers: %+v\n", response.Rovers) fmt.Printf("radar blips: %+v\n", response.Blips)
} }
case "rover": case "rover":

View file

@ -85,13 +85,13 @@ 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) (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")
} }
local := a.ToChunkLocal(v) local := a.ToChunkLocal(v)
tileId := local.X + local.Y*a.ChunkSize 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") 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) 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 // Grow will return a grown copy of the current atlas
func (a *Atlas) Grow(size int) error { func (a *Atlas) Grow(size int) error {
if size%2 != 0 { if size%2 != 0 {

View file

@ -8,7 +8,8 @@ import (
func TestCommand_Move(t *testing.T) { func TestCommand_Move(t *testing.T) {
world := NewWorld() world := NewWorld()
a := world.SpawnRover() a, err := world.SpawnRover()
assert.NoError(t, err)
pos := Vector{ pos := Vector{
X: 1.0, X: 1.0,
Y: 2.0, Y: 2.0,

View file

@ -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 // Vector desribes a 3D vector
type Vector struct { type Vector struct {
X int `json:"x"` X int `json:"x"`

View file

@ -7,4 +7,6 @@ const (
TileEmpty = Tile(0) TileEmpty = Tile(0)
TileWall = Tile(1) TileWall = Tile(1)
TileRover = Tile(2)
) )

View file

@ -42,7 +42,7 @@ func (w *World) SpawnWorldBorder() error {
} }
// SpawnRover adds an rover to the game // SpawnRover adds an rover to the game
func (w *World) SpawnRover() uuid.UUID { func (w *World) SpawnRover() (uuid.UUID, error) {
w.worldMutex.Lock() w.worldMutex.Lock()
defer w.worldMutex.Unlock() defer w.worldMutex.Unlock()
@ -52,7 +52,7 @@ func (w *World) SpawnRover() uuid.UUID {
Attributes: RoverAttributes{ Attributes: RoverAttributes{
Speed: 1.0, Speed: 1.0,
Range: 20.0, Range: 5.0,
// Set the name randomly // Set the name randomly
Name: babble.NewBabbler().Babble(), Name: babble.NewBabbler().Babble(),
@ -65,12 +65,29 @@ func (w *World) SpawnRover() uuid.UUID {
w.Atlas.ChunkSize - (rand.Int() % (w.Atlas.ChunkSize * 2)), 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 // Append the rover to the list
w.Rovers[rover.Id] = rover w.Rovers[rover.Id] = rover
return rover.Id return rover.Id, nil
} }
// Removes an rover from the game // Removes an rover from the game
@ -78,7 +95,11 @@ func (w *World) DestroyRover(id uuid.UUID) error {
w.worldMutex.Lock() w.worldMutex.Lock()
defer w.worldMutex.Unlock() 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) delete(w.Rovers, id)
} else { } else {
return fmt.Errorf("no rover matching id") 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() defer w.worldMutex.Unlock()
if i, ok := w.Rovers[id]; ok { 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 i.Attributes.Pos = pos
w.Rovers[id] = i w.Rovers[id] = i
return nil 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 // Get the tile and verify it's empty
if tile, err := w.Atlas.GetTile(newPos); err != nil { 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 { } 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 // Perform the move
i.Attributes.Pos = newPos i.Attributes.Pos = newPos
w.Rovers[id] = i 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 // RadarBlip represents a single blip on the radar
type RadarDescription struct { type RadarBlip struct {
// Rovers is the set of rovers that this radar can see Position Vector `json:"position"`
Rovers []Vector `json:"rovers"` Tile Tile `json:"tile"`
} }
// RadarFromRover can be used to query what a rover can currently see // 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() w.worldMutex.RLock()
defer w.worldMutex.RUnlock() defer w.worldMutex.RUnlock()
if r1, ok := w.Rovers[id]; ok { if r, ok := w.Rovers[id]; ok {
var desc RadarDescription var blips []RadarBlip
// Gather nearby rovers within the range extent := w.Atlas.GetWorldExtent()
for _, r2 := range w.Rovers {
if r1.Id != r2.Id && r1.Attributes.Pos.Distance(r2.Attributes.Pos) < float64(r1.Attributes.Range) { // Get min and max extents to query
desc.Rovers = append(desc.Rovers, r2.Attributes.Pos) 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 { } else {
return RadarDescription{}, fmt.Errorf("no rover matching id") return nil, fmt.Errorf("no rover matching id")
} }
} }

View file

@ -16,8 +16,10 @@ func TestNewWorld(t *testing.T) {
func TestWorld_CreateRover(t *testing.T) { func TestWorld_CreateRover(t *testing.T) {
world := NewWorld() world := NewWorld()
a := world.SpawnRover() a, err := world.SpawnRover()
b := world.SpawnRover() assert.NoError(t, err)
b, err := world.SpawnRover()
assert.NoError(t, err)
// Basic duplicate check // Basic duplicate check
if a == b { if a == b {
@ -29,7 +31,8 @@ func TestWorld_CreateRover(t *testing.T) {
func TestWorld_RoverAttributes(t *testing.T) { func TestWorld_RoverAttributes(t *testing.T) {
world := NewWorld() world := NewWorld()
a := world.SpawnRover() a, err := world.SpawnRover()
assert.NoError(t, err)
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")
@ -39,10 +42,12 @@ func TestWorld_RoverAttributes(t *testing.T) {
func TestWorld_DestroyRover(t *testing.T) { func TestWorld_DestroyRover(t *testing.T) {
world := NewWorld() world := NewWorld()
a := world.SpawnRover() a, err := world.SpawnRover()
b := 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") assert.NoError(t, err, "Error returned from rover destroy")
// Basic duplicate check // Basic duplicate check
@ -55,7 +60,8 @@ func TestWorld_DestroyRover(t *testing.T) {
func TestWorld_GetSetMovePosition(t *testing.T) { func TestWorld_GetSetMovePosition(t *testing.T) {
world := NewWorld() world := NewWorld()
a := world.SpawnRover() a, err := world.SpawnRover()
assert.NoError(t, err)
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")
@ -86,22 +92,34 @@ func TestWorld_GetSetMovePosition(t *testing.T) {
func TestWorld_RadarFromRover(t *testing.T) { func TestWorld_RadarFromRover(t *testing.T) {
world := NewWorld() world := NewWorld()
a := world.SpawnRover() a, err := world.SpawnRover()
b := world.SpawnRover() assert.NoError(t, err)
c := world.SpawnRover() b, err := world.SpawnRover()
assert.NoError(t, err)
c, err := world.SpawnRover()
assert.NoError(t, err)
// Get a's attributes // Get a's attributes
attrib, err := world.RoverAttributes(a) attrib, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to get rover attribs") assert.NoError(t, err, "Failed to get rover attribs")
// Warp the rovers so a can see b but not c // 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(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(b, bpos), "Failed to warp rover")
assert.NoError(t, world.WarpRover(c, Vector{attrib.Range + 1, 0}), "Failed to warp rover") assert.NoError(t, world.WarpRover(c, cpos), "Failed to warp rover")
radar, err := world.RadarFromRover(a) radar, err := world.RadarFromRover(a)
assert.NoError(t, err, "Failed to get radar from rover") 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, 1, len(radar), "Radar returned wrong number of rovers")
assert.Equal(t, Vector{attrib.Range - 1, 0}, radar.Rovers[0], "Rover on radar in wrong position")
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")
} }

View file

@ -104,8 +104,8 @@ type RadarResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
// The set of positions for nearby rovers // The set of positions for nearby non-empty tiles
Rovers []game.Vector `json:"rovers"` Blips []game.RadarBlip `json:"blips"`
} }
// ================ // ================

View file

@ -197,7 +197,7 @@ func HandleRadar(s *Server, vars map[string]string, b io.ReadCloser, w io.Writer
} else { } else {
fmt.Printf("Responded with radar\taccount:%s\tradar:%+v\n", id, radar) fmt.Printf("Responded with radar\taccount:%s\tradar:%+v\n", id, radar)
response.Rovers = radar.Rovers response.Blips = radar
response.Success = true response.Success = true
} }

View file

@ -140,7 +140,22 @@ func TestHandleRadar(t *testing.T) {
assert.NoError(t, err, "Error registering account") assert.NoError(t, err, "Error registering account")
// Spawn the rover rover for the 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) request, _ := http.NewRequest(http.MethodGet, path.Join("/", a.Id.String(), "/radar"), nil)
response := httptest.NewRecorder() response := httptest.NewRecorder()
@ -155,7 +170,18 @@ func TestHandleRadar(t *testing.T) {
t.Errorf("got false for /radar: %s", status.Error) 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) { func TestHandleRover(t *testing.T) {
@ -164,8 +190,9 @@ func TestHandleRover(t *testing.T) {
a, err := s.accountant.RegisterAccount("test") a, err := s.accountant.RegisterAccount("test")
assert.NoError(t, err, "Error registering account") assert.NoError(t, err, "Error registering account")
// Spawn the rover rover for the account // Spawn one rover for the account
_, _, err = s.SpawnRoverForAccount(a.Id) attribs, _, err := s.SpawnRoverForAccount(a.Id)
assert.NoError(t, err)
request, _ := http.NewRequest(http.MethodGet, path.Join("/", a.Id.String(), "/rover"), nil) request, _ := http.NewRequest(http.MethodGet, path.Join("/", a.Id.String(), "/rover"), nil)
response := httptest.NewRecorder() response := httptest.NewRecorder()
@ -178,7 +205,7 @@ func TestHandleRover(t *testing.T) {
if status.Success != true { if status.Success != true {
t.Errorf("got false for /rover: %s", status.Error) 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
} }

View file

@ -269,7 +269,7 @@ func (s *Server) wrapHandler(method string, handler Handler) func(w http.Respons
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} else if err := json.NewEncoder(w).Encode(val); err != nil { } 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) w.WriteHeader(http.StatusInternalServerError)
} else { } 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 // SpawnRoverForAccount spawns the rover rover for an account
func (s *Server) SpawnRoverForAccount(accountid uuid.UUID) (game.RoverAttributes, uuid.UUID, error) { func (s *Server) SpawnRoverForAccount(accountid uuid.UUID) (game.RoverAttributes, uuid.UUID, error) {
inst := s.world.SpawnRover() if inst, err := s.world.SpawnRover(); err != nil {
if attribs, err := s.world.RoverAttributes(inst); err != nil { return game.RoverAttributes{}, uuid.UUID{}, err
return game.RoverAttributes{}, uuid.UUID{}, fmt.Errorf("No attributes found for created rover")
} 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 { } else {
if err := s.accountant.AssignRover(accountid, inst); err != nil { if err := s.accountant.AssignRover(accountid, inst); err != nil {