From be0f4f1affb054b53247193a527e8fcab2f2bf65 Mon Sep 17 00:00:00 2001 From: Marc Di Luzio Date: Fri, 5 Jun 2020 16:37:52 +0100 Subject: [PATCH] Convert bearings to compass points and locations to int coords --- docs/poc.md | 4 +- pkg/game/command.go | 2 +- pkg/game/command_test.go | 6 +-- pkg/game/geom.go | 85 +++++++++++++++++++++++++++++++++++++-- pkg/game/geom_test.go | 58 ++++++++++++++++++++++++++ pkg/game/world.go | 16 +++----- pkg/game/world_test.go | 6 +-- pkg/server/api.go | 15 ++++++- pkg/server/routes.go | 28 ++++++++----- pkg/server/routes_test.go | 2 +- 10 files changed, 187 insertions(+), 35 deletions(-) diff --git a/docs/poc.md b/docs/poc.md index 6e098ab..1a4e90e 100644 --- a/docs/poc.md +++ b/docs/poc.md @@ -6,8 +6,8 @@ For a proof of concept we'll implement a very small subset of the final features ### Core Features * Create an account (done) -* Control an entity in a 2D map (done) -* Query for what the entity can see +* Control an rover in a 2D map (done) +* Query for what the rover can see (done) * Persistent accounts and world (done) * Multiple users (done) * Populate map with locations/objects diff --git a/pkg/game/command.go b/pkg/game/command.go index 4a196c4..d3af43b 100644 --- a/pkg/game/command.go +++ b/pkg/game/command.go @@ -6,7 +6,7 @@ import "github.com/google/uuid" type Command func() error // CommandMove will move the rover in question -func (w *World) CommandMove(id uuid.UUID, bearing float64, duration float64) Command { +func (w *World) CommandMove(id uuid.UUID, bearing Direction, duration int) Command { return func() error { _, err := w.MoveRover(id, bearing, duration) return err diff --git a/pkg/game/command_test.go b/pkg/game/command_test.go index 99cb296..c946981 100644 --- a/pkg/game/command_test.go +++ b/pkg/game/command_test.go @@ -20,14 +20,14 @@ func TestCommand_Move(t *testing.T) { err = world.WarpRover(a, pos) assert.NoError(t, err, "Failed to set position for rover") - bearing := 0.0 - duration := 1.0 + bearing := North + duration := 1 // Try the move command moveCommand := world.CommandMove(a, bearing, duration) assert.NoError(t, world.Execute(moveCommand), "Failed to execute move command") newpos, err := world.RoverPosition(a) assert.NoError(t, err, "Failed to set position for rover") - pos.Add(Vector{0.0, float64(duration) * attribs.Speed}) // We should have moved duration*speed north + pos.Add(Vector{0.0, duration * attribs.Speed}) // We should have moved duration*speed north assert.Equal(t, pos, newpos, "Failed to correctly set position for rover") } diff --git a/pkg/game/geom.go b/pkg/game/geom.go index f65680b..846555a 100644 --- a/pkg/game/geom.go +++ b/pkg/game/geom.go @@ -1,11 +1,15 @@ package game -import "math" +import ( + "fmt" + "math" + "strings" +) // Vector desribes a 3D vector type Vector struct { - X float64 `json:"x"` - Y float64 `json:"y"` + X int `json:"x"` + Y int `json:"y"` } // Add adds one vector to another @@ -27,7 +31,7 @@ func (v Vector) Negated() Vector { // Length returns the length of the vector func (v Vector) Length() float64 { - return math.Sqrt(v.X*v.X + v.Y*v.Y) + return math.Sqrt(float64(v.X*v.X + v.Y*v.Y)) } // Distance returns the distance between two vectors @@ -35,3 +39,76 @@ func (v Vector) Distance(v2 Vector) float64 { // Negate the two vectors and calciate the length return v.Added(v2.Negated()).Length() } + +// Multiplied returns the vector multiplied by an int +func (v Vector) Multiplied(val int) Vector { + return Vector{v.X * val, v.Y * val} +} + +// Direction describes a compass direction +type Direction int + +const ( + North Direction = iota + NorthEast + East + SouthEast + South + SouthWest + West + NorthWest +) + +// DirectionString simply describes the strings associated with a direction +type DirectionString struct { + Long string + Short string +} + +// DirectionStrings is the set of strings for each direction +var DirectionStrings = []DirectionString{ + {"North", "N"}, + {"NorthEast", "NE"}, + {"East", "E"}, + {"SouthEast", "SE"}, + {"South", "S"}, + {"SouthWest", "SW"}, + {"West", "W"}, + {"NorthWest", "NW"}, +} + +// String converts a Direction to a String +func (d Direction) String() string { + return DirectionStrings[d].Long +} + +// ShortString converts a Direction to a short string version +func (d Direction) ShortString() string { + return DirectionStrings[d].Short +} + +// DirectionFromString gets the Direction from a string +func DirectionFromString(s string) (Direction, error) { + for i, d := range DirectionStrings { + if strings.ToLower(d.Long) == strings.ToLower(s) || strings.ToLower(d.Short) == strings.ToLower(s) { + return Direction(i), nil + } + } + return -1, fmt.Errorf("Unknown direction: %s", s) +} + +var DirectionVectors = []Vector{ + {0, 1}, // N + {1, 1}, // NE + {1, 0}, // E + {1, -1}, // SE + {0, -1}, // S + {-1, 1}, // SW + {-1, 0}, // W + {-1, 1}, // NW +} + +// Vector converts a Direction to a Vector +func (d Direction) Vector() Vector { + return DirectionVectors[d] +} diff --git a/pkg/game/geom_test.go b/pkg/game/geom_test.go index af24cc5..440dd23 100644 --- a/pkg/game/geom_test.go +++ b/pkg/game/geom_test.go @@ -169,3 +169,61 @@ func TestVector_Distance(t *testing.T) { }) } } + +func TestVector_Multiplied(t *testing.T) { + + tests := []struct { + name string + vec Vector + arg int + want Vector + }{ + { + name: "Basic multiply 1", + vec: North.Vector(), + arg: 2, + want: Vector{0, 2}, + }, + { + name: "Basic multiply 2", + vec: NorthWest.Vector(), + arg: -1, + want: SouthEast.Vector(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := Vector{ + X: tt.vec.X, + Y: tt.vec.Y, + } + if got := v.Multiplied(tt.arg); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Vector.Multiplied() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDirection(t *testing.T) { + dir := North + + assert.Equal(t, "North", dir.String()) + assert.Equal(t, "N", dir.ShortString()) + assert.Equal(t, Vector{0, 1}, dir.Vector()) + + dir, err := DirectionFromString("N") + assert.NoError(t, err) + assert.Equal(t, North, dir) + + dir, err = DirectionFromString("n") + assert.NoError(t, err) + assert.Equal(t, North, dir) + + dir, err = DirectionFromString("north") + assert.NoError(t, err) + assert.Equal(t, North, dir) + + dir, err = DirectionFromString("NorthWest") + assert.NoError(t, err) + assert.Equal(t, NorthWest, dir) +} diff --git a/pkg/game/world.go b/pkg/game/world.go index 3470864..3b050a8 100644 --- a/pkg/game/world.go +++ b/pkg/game/world.go @@ -2,7 +2,6 @@ package game import ( "fmt" - "math" "github.com/google/uuid" ) @@ -16,10 +15,10 @@ type World struct { // RoverAttributes contains attributes of a rover type RoverAttributes struct { // Speed represents the Speed that the rover will move per second - Speed float64 `json:"speed"` + Speed int `json:"speed"` // Range represents the distance the unit's radar can see - Range float64 `json:"range"` + Range int `json:"range"` } // Rover describes a single rover in the world @@ -103,16 +102,13 @@ func (w *World) WarpRover(id uuid.UUID, pos Vector) error { } // SetPosition sets an rovers position -func (w *World) MoveRover(id uuid.UUID, bearing float64, duration float64) (Vector, error) { +func (w *World) MoveRover(id uuid.UUID, bearing Direction, duration int) (Vector, error) { if i, ok := w.Rovers[id]; ok { // Calculate the distance - distance := i.Attributes.Speed * float64(duration) + distance := i.Attributes.Speed * duration // Calculate the full movement based on the bearing - move := Vector{ - X: math.Sin(bearing) * distance, - Y: math.Cos(bearing) * distance, - } + move := bearing.Vector().Multiplied(distance) // Increment the position by the movement i.Pos.Add(move) @@ -138,7 +134,7 @@ func (w World) RadarFromRover(id uuid.UUID) (RadarDescription, error) { // Gather nearby rovers within the range for _, r2 := range w.Rovers { - if r1.Id != r2.Id && r1.Pos.Distance(r2.Pos) < r1.Attributes.Range { + if r1.Id != r2.Id && r1.Pos.Distance(r2.Pos) < float64(r1.Attributes.Range) { desc.Rovers = append(desc.Rovers, r2.Pos) } } diff --git a/pkg/game/world_test.go b/pkg/game/world_test.go index ddb8bd0..7672706 100644 --- a/pkg/game/world_test.go +++ b/pkg/game/world_test.go @@ -71,11 +71,11 @@ func TestWorld_GetSetMovePosition(t *testing.T) { assert.NoError(t, err, "Failed to set position for rover") assert.Equal(t, pos, newpos, "Failed to correctly set position for rover") - bearing := 0.0 - duration := 1.0 + bearing := North + duration := 1 newpos, err = world.MoveRover(a, bearing, duration) assert.NoError(t, err, "Failed to set position for rover") - pos.Add(Vector{0, attribs.Speed * float64(duration)}) // We should have move one unit of the speed north + pos.Add(Vector{0, attribs.Speed * duration}) // We should have move one unit of the speed north assert.Equal(t, pos, newpos, "Failed to correctly move position for rover") } diff --git a/pkg/server/api.go b/pkg/server/api.go index 6ceb479..c2e4e9a 100644 --- a/pkg/server/api.go +++ b/pkg/server/api.go @@ -70,6 +70,17 @@ const ( CommandMove = "move" ) +const ( + BearingNorth = "North" + BearingNorthEast + BearingEast + BearingSouthEast + BearingSouth + BearingSouthWest + BearingWest + BearingNorthWest +) + // Command describes a single command to execute // it contains the type, and then any members used for each command type type Command struct { @@ -77,8 +88,8 @@ type Command struct { Command string `json:"command"` // Used for CommandMove - Bearing float64 `json:"bearing"` // The direction to move in degrees - Duration float64 `json:"duration"` // The duration of the move in seconds + Bearing string `json:"bearing"` // The direction to move on a compass in short (NW) or long (NorthWest) form + Duration int `json:"duration"` // The duration of the move in ticks } // ================ diff --git a/pkg/server/routes.go b/pkg/server/routes.go index d572b19..b228855 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -148,6 +148,22 @@ func HandleSpawn(s *Server, b io.ReadCloser, w io.Writer) error { return nil } +// ConvertCommands converts server commands to game commands +func (s *Server) ConvertCommands(commands []Command, inst uuid.UUID) ([]game.Command, error) { + var cmds []game.Command + for _, c := range commands { + switch c.Command { + case CommandMove: + if bearing, err := game.DirectionFromString(c.Bearing); err != nil { + return nil, err + } else { + cmds = append(cmds, s.world.CommandMove(inst, bearing, c.Duration)) + } + } + } + return cmds, nil +} + // HandleSpawn will spawn the player entity for the associated account func HandleCommands(s *Server, b io.ReadCloser, w io.Writer) error { // Set up the response @@ -170,19 +186,13 @@ func HandleCommands(s *Server, b io.ReadCloser, w io.Writer) error { } else if inst, err := s.accountant.GetRover(id); err != nil { response.Error = fmt.Sprintf("Provided account has no rover: %s", err) + } else if cmds, err := s.ConvertCommands(data.Commands, inst); err != nil { + response.Error = fmt.Sprintf("Couldn't convert commands: %s", err) + } else { // log the data sent fmt.Printf("\tcommands data: %v\n", data) - // Iterate through the commands to generate all game commands - var cmds []game.Command - for _, c := range data.Commands { - switch c.Command { - case CommandMove: - cmds = append(cmds, s.world.CommandMove(inst, c.Bearing, c.Duration)) - } - } - // Execute the commands if err := s.world.Execute(cmds...); err != nil { response.Error = fmt.Sprintf("Failed to execute commands: %s", err) diff --git a/pkg/server/routes_test.go b/pkg/server/routes_test.go index b2b63f2..42b7125 100644 --- a/pkg/server/routes_test.go +++ b/pkg/server/routes_test.go @@ -89,7 +89,7 @@ func TestHandleCommands(t *testing.T) { Commands: []Command{ { Command: CommandMove, - Bearing: 0.0, + Bearing: "N", Duration: 1, }, },