Compare commits

..

No commits in common. "master" and "v0.34.0" have entirely different histories.

26 changed files with 1027 additions and 2090 deletions

View file

@ -18,9 +18,12 @@ gen:
protoc --proto_path proto --go_out=plugins=grpc,paths=source_relative:proto/ proto/roveapi/roveapi.proto
test:
@echo Run unit and integration tests
docker-compose -f docker-compose-test.yml up --build --exit-code-from=rove-tests --abort-on-container-exit rove-tests
docker-compose -f docker-compose-test.yml down
@echo Unit tests
go test -v ./...
@echo Integration tests
docker-compose up --build --exit-code-from=rove-tests --abort-on-container-exit rove-tests
docker-compose down
go tool cover -html=/tmp/coverage-data/c.out -o /tmp/coverage.html
@echo Done, coverage data can be found in /tmp/coverage.html

View file

@ -1,8 +1,9 @@
Rove
====
![Tests](https://github.com/mdiluz/rove/workflows/Tests/badge.svg) ![Docker](https://github.com/mdiluz/rove/workflows/Docker/badge.svg) [![rove](https://snapcraft.io//rove/badge.svg)](https://snapcraft.io/rove)
![Rove](data/icon.svg)
![Rove](https://github.com/mdiluz/rove/blob/master/data/icon.svg)
Rove is an asynchronous nomadic game about exploring as part of a loose community.
This repository contains the source code for the `rove-server` deployment and the `rove` command line client. See [mdiluz.github.io/rove](https://mdiluz.github.io/rove/) for game details, and [roveapi.proto](proto/roveapi/roveapi.proto) for the current server-client API.
This repository contains the source code for the `rove-server` deployment and the `rove` command line client. See [mdiluz.github.io/rove](https://mdiluz.github.io/rove/) for game details, and [roveapi.proto](https://github.com/mdiluz/rove/blob/master/proto/roveapi/roveapi.proto) for the current server-client API.

View file

@ -1,4 +1,4 @@
package accounts
package internal
// Accountant decribes something that stores accounts and account values
type Accountant interface {
@ -21,8 +21,8 @@ type Accountant interface {
// Account represents a registered user
type Account struct {
// Name simply describes the account and must be unique
Name string
Name string `json:"name"`
// Data represents internal account data
Data map[string]string
Data map[string]string `json:"data"`
}

View file

@ -1,4 +1,4 @@
package accounts
package internal
import (
"testing"

View file

@ -5,6 +5,7 @@ import (
"fmt"
"log"
"github.com/mdiluz/rove/pkg/rove"
"github.com/mdiluz/rove/pkg/version"
"github.com/mdiluz/rove/proto/roveapi"
)
@ -34,7 +35,7 @@ func (s *Server) Register(ctx context.Context, req *roveapi.RegisterRequest) (*r
return nil, fmt.Errorf("empty account name")
}
if acc, err := s.world.Accountant.RegisterAccount(req.Name); err != nil {
if acc, err := s.accountant.RegisterAccount(req.Name); err != nil {
return nil, err
} else if _, err := s.SpawnRoverForAccount(req.Name); err != nil {
@ -57,13 +58,13 @@ func (s *Server) Register(ctx context.Context, req *roveapi.RegisterRequest) (*r
func (s *Server) Status(ctx context.Context, req *roveapi.StatusRequest) (response *roveapi.StatusResponse, err error) {
log.Printf("Handling status request: %s\n", req.Account.Name)
if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
if valid, err := s.accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
return nil, err
} else if !valid {
return nil, fmt.Errorf("Secret incorrect for account %s", req.Account.Name)
} else if resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover"); err != nil {
} else if resp, err := s.accountant.GetValue(req.Account.Name, "rover"); err != nil {
return nil, err
} else if rover, err := s.world.GetRover(resp); err != nil {
@ -75,7 +76,40 @@ func (s *Server) Status(ctx context.Context, req *roveapi.StatusRequest) (respon
inv = append(inv, byte(i.Type))
}
queued := s.world.RoverCommands(resp)
i, q := s.world.RoverCommands(resp)
var incoming, queued []*roveapi.Command
for _, i := range i {
c := &roveapi.Command{
Command: i.Command,
}
switch i.Command {
case roveapi.CommandType_move:
c.Data = &roveapi.Command_Bearing{
Bearing: i.Bearing,
}
case roveapi.CommandType_broadcast:
c.Data = &roveapi.Command_Message{
Message: i.Message,
}
}
incoming = append(incoming, c)
}
for _, q := range q {
c := &roveapi.Command{
Command: q.Command,
}
switch q.Command {
case roveapi.CommandType_move:
c.Data = &roveapi.Command_Bearing{
Bearing: q.Bearing,
}
case roveapi.CommandType_broadcast:
c.Data = &roveapi.Command_Message{
Message: q.Message,
}
}
queued = append(queued, c)
}
var logs []*roveapi.Log
for _, log := range rover.Logs {
logs = append(logs, &roveapi.Log{
@ -85,29 +119,21 @@ func (s *Server) Status(ctx context.Context, req *roveapi.StatusRequest) (respon
}
response = &roveapi.StatusResponse{
Readings: &roveapi.RoverReadings{
Name: rover.Name,
Position: &roveapi.Vector{
X: int32(rover.Pos.X),
Y: int32(rover.Pos.Y),
},
Logs: logs,
Wind: s.world.Wind,
},
Spec: &roveapi.RoverSpecifications{
Name: rover.Name,
Range: int32(rover.Range),
Capacity: int32(rover.Capacity),
MaximumIntegrity: int32(rover.MaximumIntegrity),
MaximumCharge: int32(rover.MaximumCharge),
},
Status: &roveapi.RoverStatus{
Bearing: rover.Bearing,
Inventory: inv,
Capacity: int32(rover.Capacity),
Integrity: int32(rover.Integrity),
MaximumIntegrity: int32(rover.MaximumIntegrity),
Charge: int32(rover.Charge),
MaximumCharge: int32(rover.MaximumCharge),
IncomingCommands: incoming,
QueuedCommands: queued,
SailPosition: rover.SailPosition,
},
Logs: logs,
}
}
return response, nil
@ -117,7 +143,7 @@ func (s *Server) Status(ctx context.Context, req *roveapi.StatusRequest) (respon
func (s *Server) Radar(ctx context.Context, req *roveapi.RadarRequest) (*roveapi.RadarResponse, error) {
log.Printf("Handling radar request: %s\n", req.Account.Name)
if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
if valid, err := s.accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
return nil, err
} else if !valid {
@ -126,7 +152,7 @@ func (s *Server) Radar(ctx context.Context, req *roveapi.RadarRequest) (*roveapi
response := &roveapi.RadarResponse{}
resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover")
resp, err := s.accountant.GetValue(req.Account.Name, "rover")
if err != nil {
return nil, err
@ -149,19 +175,33 @@ func (s *Server) Radar(ctx context.Context, req *roveapi.RadarRequest) (*roveapi
func (s *Server) Command(ctx context.Context, req *roveapi.CommandRequest) (*roveapi.CommandResponse, error) {
log.Printf("Handling command request: %s and %+v\n", req.Account.Name, req.Commands)
if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
if valid, err := s.accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
return nil, err
} else if !valid {
return nil, fmt.Errorf("Secret incorrect for account %s", req.Account.Name)
}
resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover")
resp, err := s.accountant.GetValue(req.Account.Name, "rover")
if err != nil {
return nil, err
}
if err := s.world.Enqueue(resp, req.Commands...); err != nil {
var cmds []rove.Command
for _, c := range req.Commands {
n := rove.Command{
Command: c.Command,
}
switch c.Command {
case roveapi.CommandType_move:
n.Bearing = c.GetBearing()
case roveapi.CommandType_broadcast:
n.Message = c.GetMessage()
}
cmds = append(cmds, n)
}
if err := s.world.Enqueue(resp, cmds...); err != nil {
return nil, err
}

View file

@ -4,8 +4,6 @@ import (
"fmt"
"log"
"net"
"os"
"path"
"sync"
"github.com/mdiluz/rove/pkg/persistence"
@ -13,12 +11,8 @@ import (
"github.com/mdiluz/rove/proto/roveapi"
"github.com/robfig/cron"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/reflection"
)
var cert = os.Getenv("CERT_NAME")
const (
// PersistentData will allow the server to load and save it's state
PersistentData = iota
@ -33,6 +27,9 @@ type Server struct {
// Internal state
world *rove.World
// Accountant
accountant Accountant
// gRPC server
netListener net.Listener
grpcServ *grpc.Server
@ -83,6 +80,7 @@ func NewServer(opts ...ServerOption) *Server {
persistence: EphemeralData,
schedule: cron.New(),
world: rove.NewWorld(32),
accountant: NewSimpleAccountant(),
}
// Apply all options
@ -109,22 +107,8 @@ func (s *Server) Initialise(fillWorld bool) (err error) {
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Load TLS
var opts []grpc.ServerOption
if len(os.Getenv("NO_TLS")) == 0 {
pem := path.Join("/etc/letsencrypt/live/", cert, "fullchain.pem")
key := path.Join("/etc/letsencrypt/live/", cert, "privkey.pem")
creds, err := credentials.NewServerTLSFromFile(pem, key)
if err != nil {
log.Fatalf("failed to setup TLS: %v", err)
}
opts = append(opts, grpc.Creds(creds))
}
s.grpcServ = grpc.NewServer(opts...)
s.grpcServ = grpc.NewServer()
roveapi.RegisterRoveServer(s.grpcServ, s)
reflection.Register(s.grpcServ)
return nil
}
@ -147,8 +131,8 @@ func (s *Server) Run() {
log.Println("Executing server tick")
// Tick the world
s.world.Tick()
// Run the command queues
s.world.ExecuteCommandQueues()
// Save out the new world state
if err := s.SaveWorld(); err != nil {
@ -204,7 +188,7 @@ func (s *Server) SaveWorld() error {
if s.persistence == PersistentData {
s.world.RLock()
defer s.world.RUnlock()
if err := persistence.SaveAll("world", s.world); err != nil {
if err := persistence.SaveAll("world", s.world, "accounts", s.accountant); err != nil {
return fmt.Errorf("failed to save out persistent data: %s", err)
}
}
@ -216,7 +200,7 @@ func (s *Server) LoadWorld() error {
if s.persistence == PersistentData {
s.world.Lock()
defer s.world.Unlock()
if err := persistence.LoadAll("world", &s.world); err != nil {
if err := persistence.LoadAll("world", &s.world, "accounts", &s.accountant); err != nil {
return err
}
}
@ -225,10 +209,22 @@ func (s *Server) LoadWorld() error {
// SpawnRoverForAccount spawns the rover rover for an account
func (s *Server) SpawnRoverForAccount(account string) (string, error) {
inst, err := s.world.SpawnRover(account)
inst, err := s.world.SpawnRover()
if err != nil {
return "", err
}
err = s.accountant.AssignData(account, "rover", inst)
if err != nil {
log.Printf("Failed to assign rover to account, %s", err)
// Try and clear up the rover
if err := s.world.DestroyRover(inst); err != nil {
log.Printf("Failed to destroy rover after failed rover assign: %s", err)
}
return "", err
}
return inst, nil
}

View file

@ -1,7 +1,6 @@
package internal
import (
"os"
"testing"
)
@ -31,7 +30,6 @@ func TestNewServer_OptionPersistentData(t *testing.T) {
}
func TestServer_Run(t *testing.T) {
os.Setenv("NO_TLS", "1")
server := NewServer()
if server == nil {
t.Error("Failed to create server")
@ -47,7 +45,6 @@ func TestServer_Run(t *testing.T) {
}
func TestServer_RunPersistentData(t *testing.T) {
os.Setenv("NO_TLS", "1")
server := NewServer(OptionPersistentData())
if server == nil {
t.Error("Failed to create server")

View file

@ -1,4 +1,4 @@
package accounts
package internal
import (
"fmt"
@ -9,7 +9,7 @@ import (
// SimpleAccountant manages a set of accounts
type SimpleAccountant struct {
Accounts map[string]Account
Accounts map[string]Account `json:"accounts"`
}
// NewSimpleAccountant creates a new accountant

View file

@ -22,12 +22,6 @@ const (
// GlyphRoverLive represents a live rover
GlyphRoverLive = Glyph('R')
// GlyphRoverDormant represents a dormant rover
GlyphRoverDormant = Glyph('r')
// GlyphRoverParts represents spare rover parts
GlyphRoverParts = Glyph('*')
// GlyphRockSmall is a small stashable rock
GlyphRockSmall = Glyph('o')
@ -57,12 +51,8 @@ func ObjectGlyph(o roveapi.Object) Glyph {
return GlyphRoverLive
case roveapi.Object_RockSmall:
return GlyphRockSmall
case roveapi.Object_RoverDormant:
return GlyphRoverDormant
case roveapi.Object_RockLarge:
return GlyphRockLarge
case roveapi.Object_RoverParts:
return GlyphRoverParts
}
log.Fatalf("Unknown object type: %c", o)

View file

@ -1,7 +1,6 @@
package main
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
@ -9,7 +8,6 @@ import (
"os"
"path"
"path/filepath"
"strconv"
"time"
"github.com/mdiluz/rove/cmd/rove/internal"
@ -17,7 +15,6 @@ import (
"github.com/mdiluz/rove/proto/roveapi"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var home = os.Getenv("HOME")
@ -25,29 +22,23 @@ var defaultDataPath = path.Join(home, ".local/share/")
// Command usage
func printUsage() {
fmt.Fprintln(os.Stderr, "Usage: rove ARG [OPT...]")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "Arguments:")
fmt.Fprintln(os.Stderr, "\tversion outputs version")
fmt.Fprintln(os.Stderr, "\thelp outputs this usage text")
fmt.Fprintln(os.Stderr, "\tconfig [HOST] outputs the local config, optionally sets host")
fmt.Fprintf(os.Stderr, "Usage: rove COMMAND [ARGS...]\n")
fmt.Fprintln(os.Stderr, "\nCommands")
fmt.Fprintln(os.Stderr, "\tserver-status prints the server status")
fmt.Fprintln(os.Stderr, "\tregister NAME registers an account and spawns a rover")
fmt.Fprintln(os.Stderr, "\tradar prints radar data in ASCII form")
fmt.Fprintln(os.Stderr, "\tstatus gets rover status")
fmt.Fprintln(os.Stderr, "\tcommand CMD [VAL...] [REPEAT] sets the command queue, accepts multiple in sequence")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "Rover commands:")
fmt.Fprintln(os.Stderr, "\ttoggle toggles the current sail mode")
fmt.Fprintln(os.Stderr, "\tregister NAME registers an account and stores it (use with -name)")
fmt.Fprintln(os.Stderr, "\tcommand COMMAND [VAL...] issue commands to rover, accepts multiple, see below")
fmt.Fprintln(os.Stderr, "\tradar gathers radar data for the current rover")
fmt.Fprintln(os.Stderr, "\tstatus gets status info for current rover")
fmt.Fprintln(os.Stderr, "\tconfig [HOST] outputs the local config info, optionally sets host")
fmt.Fprintln(os.Stderr, "\thelp outputs this usage information")
fmt.Fprintln(os.Stderr, "\tversion outputs version info")
fmt.Fprintln(os.Stderr, "\nRover commands:")
fmt.Fprintln(os.Stderr, "\tmove BEARING moves the rover in the chosen direction")
fmt.Fprintln(os.Stderr, "\tstash stores the object at the rover location in the inventory")
fmt.Fprintln(os.Stderr, "\trepair repairs the rover using inventory item")
fmt.Fprintln(os.Stderr, "\trepair uses an inventory object to repair the rover")
fmt.Fprintln(os.Stderr, "\trecharge wait a tick to recharge the rover")
fmt.Fprintln(os.Stderr, "\tbroadcast MSG broadcast a simple ASCII triplet to nearby rovers")
fmt.Fprintln(os.Stderr, "\tsalvage salvages a dormant rover for parts")
fmt.Fprintln(os.Stderr, "\ttransfer transfer's control into a dormant rover")
fmt.Fprintln(os.Stderr, "\tupgrade SPEC spends rover parts to upgrade one rover spec (capacity, range, integrity, charge")
fmt.Fprintln(os.Stderr, "\twait waits before performing the next command")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "Environment")
fmt.Fprintln(os.Stderr, "\nEnvironment")
fmt.Fprintln(os.Stderr, "\tROVE_USER_DATA path to user data, defaults to "+defaultDataPath)
}
@ -55,14 +46,14 @@ const gRPCport = 9090
// Account stores data for an account
type Account struct {
Name string
Secret string
Name string `json:"name"`
Secret string `json:"secret"`
}
// Config is used to store internal data
type Config struct {
Host string
Account Account
Host string `json:"host,omitempty"`
Account Account `json:"account,omitempty"`
}
// ConfigPath returns the configuration path
@ -137,20 +128,12 @@ func BearingFromString(s string) roveapi.Bearing {
switch s {
case "N":
return roveapi.Bearing_North
case "NE":
return roveapi.Bearing_NorthEast
case "E":
return roveapi.Bearing_East
case "SE":
return roveapi.Bearing_SouthEast
case "S":
return roveapi.Bearing_South
case "SW":
return roveapi.Bearing_SouthWest
case "W":
return roveapi.Bearing_West
case "NW":
return roveapi.Bearing_NorthWest
}
return roveapi.Bearing_BearingUnknown
}
@ -188,15 +171,8 @@ func InnerMain(command string, args ...string) error {
return fmt.Errorf("no host set in %s, set one with '%s config {HOST}'", ConfigPath(), os.Args[0])
}
var opts []grpc.DialOption
if len(os.Getenv("NO_TLS")) == 0 {
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
} else {
opts = append(opts, grpc.WithInsecure())
}
// Set up the server
clientConn, err := grpc.Dial(fmt.Sprintf("%s:%d", config.Host, gRPCport), opts...)
clientConn, err := grpc.Dial(fmt.Sprintf("%s:%d", config.Host, gRPCport), grpc.WithInsecure())
if err != nil {
return err
}
@ -248,22 +224,22 @@ func InnerMain(command string, args ...string) error {
// Iterate through each command
var commands []*roveapi.Command
for i := 0; i < len(args); i++ {
var cmd *roveapi.Command
switch args[i] {
case "turn":
case "move":
i++
if len(args) == i {
return fmt.Errorf("turn command must be passed a compass bearing")
return fmt.Errorf("move command must be passed bearing")
}
b := BearingFromString(args[i])
if b == roveapi.Bearing_BearingUnknown {
return fmt.Errorf("turn command must be given a valid bearing %s", args[i])
}
cmd = &roveapi.Command{
Command: roveapi.CommandType_turn,
Bearing: b,
var b roveapi.Bearing
if b = BearingFromString(args[i]); b == roveapi.Bearing_BearingUnknown {
return fmt.Errorf("unrecognised bearing: %s", args[i])
}
commands = append(commands,
&roveapi.Command{
Command: roveapi.CommandType_move,
Data: &roveapi.Command_Bearing{Bearing: b},
},
)
case "broadcast":
i++
if len(args) == i {
@ -271,53 +247,22 @@ func InnerMain(command string, args ...string) error {
} else if len(args[i]) > 3 {
return fmt.Errorf("broadcast command must be given ASCII triplet of 3 or less: %s", args[i])
}
cmd = &roveapi.Command{
commands = append(commands,
&roveapi.Command{
Command: roveapi.CommandType_broadcast,
Data: []byte(args[i]),
}
case "upgrade":
i++
if len(args) == i {
return fmt.Errorf("upgrade command must be passed a spec to upgrade")
}
var u roveapi.RoverUpgrade
switch args[i] {
case "capacity":
u = roveapi.RoverUpgrade_Capacity
case "range":
u = roveapi.RoverUpgrade_Range
case "integrity":
u = roveapi.RoverUpgrade_MaximumIntegrity
case "charge":
u = roveapi.RoverUpgrade_MaximumCharge
default:
return fmt.Errorf("upgrade command must be passed a known upgrade spec")
}
cmd = &roveapi.Command{
Command: roveapi.CommandType_upgrade,
Upgrade: u,
}
Data: &roveapi.Command_Message{Message: []byte(args[i])},
},
)
default:
// By default just use the command literally
cmd = &roveapi.Command{
commands = append(commands,
&roveapi.Command{
Command: roveapi.CommandType(roveapi.CommandType_value[args[i]]),
},
)
}
}
// Try and convert the next command to a number
number := 0
if len(args) > i+1 {
num, err := strconv.Atoi(args[i+1])
if err == nil {
number = num
i++
}
}
cmd.Repeat = int32(number)
commands = append(commands, cmd)
}
_, err := client.Command(ctx, &roveapi.CommandRequest{
Account: &roveapi.Account{
Name: config.Account.Name,
@ -403,15 +348,6 @@ func InnerMain(command string, args ...string) error {
func main() {
// Bail without any args
if len(os.Args) == 1 {
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "m mm mmm m m mmm")
fmt.Fprintln(os.Stderr, "#\" \" #\" \"# \"m m\" #\" #")
fmt.Fprintln(os.Stderr, "# # # #m# #\"\"\"\"")
fmt.Fprintln(os.Stderr, "# \"#m#\" # \"#mm\"")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "Rove is an asychronous nomadic game about exploring a planet as part of a loose community.")
fmt.Fprintln(os.Stderr, "Visit https://mdiluz.github.io/rove/ for more information.")
fmt.Fprintf(os.Stderr, "\n")
printUsage()
os.Exit(1)
}

View file

@ -13,7 +13,6 @@ import (
)
func Test_InnerMain(t *testing.T) {
os.Setenv("NO_TLS", "1")
// Use temporary local user data
tmp, err := ioutil.TempDir(os.TempDir(), "rove-")
@ -51,17 +50,12 @@ func Test_InnerMain(t *testing.T) {
assert.Error(t, InnerMain("command"))
// Give it commands
assert.NoError(t, InnerMain("command", "toggle"))
assert.NoError(t, InnerMain("command", "move", "N"))
assert.NoError(t, InnerMain("command", "stash"))
assert.NoError(t, InnerMain("command", "repair"))
assert.NoError(t, InnerMain("command", "upgrade", "capacity"))
assert.NoError(t, InnerMain("command", "broadcast", "abc"))
assert.NoError(t, InnerMain("command", "wait", "10"))
assert.NoError(t, InnerMain("command", "wait", "1", "turn", "NW", "toggle", "broadcast", "zyx"))
// Give it malformed commands
assert.Error(t, InnerMain("command", "unknown"))
assert.Error(t, InnerMain("command", "move", "stash"))
assert.Error(t, InnerMain("command", "broadcast"))
assert.Error(t, InnerMain("command", "upgrade"))
assert.Error(t, InnerMain("command", "1"))
}

View file

@ -1,32 +0,0 @@
version: '3'
services:
rove-test-server:
build:
context: .
dockerfile: Dockerfile
image: rove:latest
ports:
- "9090:9090"
environment:
- PORT=9090
- DATA_PATH=/tmp/
- WORDS_FILE=data/words_alpha.txt
- TICK_RATE=10
- NO_TLS=1
command: [ "./rove-server"]
rove-tests:
depends_on: [ rove-test-server ]
build:
context: .
dockerfile: Dockerfile
image: rove:latest
environment:
- ROVE_GRPC=rove-test-server
command: [ "./script/wait-for-it.sh", "rove-test-server:9090", "--", "go", "test", "-v", "./...", "--tags=integration", "-cover", "-coverprofile=/mnt/coverage-data/c.out", "-count", "1" ]
volumes:
- /tmp/coverage-data:/mnt/coverage-data:rw

View file

@ -15,11 +15,22 @@ services:
- PORT=9090
- DATA_PATH=/mnt/rove-server
- WORDS_FILE=data/words_alpha.txt
- TICK_RATE=3
- CERT_NAME=${CERT_NAME}
- TICK_RATE=5
volumes:
- persistent-data:/mnt/rove-server:rw
- /etc/letsencrypt/:/etc/letsencrypt/
command: [ "./rove-server"]
rove-tests:
depends_on: [ rove-server ]
build:
context: .
dockerfile: Dockerfile
image: rove:latest
environment:
- ROVE_GRPC=rove-server
command: [ "./script/wait-for-it.sh", "rove-server:9090", "--", "go", "test", "-v", "./...", "--tags=integration", "-cover", "-coverprofile=/mnt/coverage-data/c.out", "-count", "1" ]
volumes:
- /tmp/coverage-data:/mnt/coverage-data:rw

View file

@ -1,4 +1,4 @@
package rove
package atlas
import (
"github.com/mdiluz/rove/pkg/maths"

View file

@ -1,4 +1,4 @@
package rove
package atlas
import (
"testing"

View file

@ -1,43 +1,50 @@
package rove
package atlas
import (
"log"
"math/rand"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/ojrac/opensimplex-go"
)
// chunk represents a fixed square grid of tiles
type chunk struct {
// Tiles represents the tiles within the chunk
Tiles []byte
Tiles []byte `json:"tiles"`
// Objects represents the objects within the chunk
// only one possible object per tile for now
Objects map[int]Object
Objects map[int]Object `json:"objects"`
}
// chunkBasedAtlas represents a grid of Chunks
type chunkBasedAtlas 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
Chunks []chunk `json:"chunks"`
// LowerBound is the origin of the bottom left corner of the current chunks in world space (current chunks cover >= this value)
LowerBound maths.Vector
LowerBound maths.Vector `json:"lowerBound"`
// UpperBound is the top left corner of the current chunks (curent chunks cover < this value)
UpperBound maths.Vector
UpperBound maths.Vector `json:"upperBound"`
// ChunkSize is the x/y dimensions of each square chunk
ChunkSize int
ChunkSize int `json:"chunksize"`
// worldGen is the internal world generator
worldGen WorldGen
// terrainNoise describes the noise function for the terrain
terrainNoise opensimplex.Noise
// terrainNoise describes the noise function for the terrain
objectNoise opensimplex.Noise
}
const (
noiseSeed = 1024
terrainNoiseScale = 6
objectNoiseScale = 3
)
// NewChunkAtlas creates a new empty atlas
@ -48,7 +55,8 @@ func NewChunkAtlas(chunkSize int) Atlas {
Chunks: make([]chunk, 1),
LowerBound: maths.Vector{X: 0, Y: 0},
UpperBound: maths.Vector{X: chunkSize, Y: chunkSize},
worldGen: NewNoiseWorldGen(noiseSeed),
terrainNoise: opensimplex.New(noiseSeed),
objectNoise: opensimplex.New(noiseSeed),
}
// Initialise the first chunk
a.populate(0)
@ -97,16 +105,41 @@ func (a *chunkBasedAtlas) populate(chunk int) {
origin := a.chunkOriginInWorldSpace(chunk)
for i := 0; i < a.ChunkSize; i++ {
for j := 0; j < a.ChunkSize; j++ {
loc := maths.Vector{X: origin.X + i, Y: origin.Y + j}
// Set the tile
c.Tiles[j*a.ChunkSize+i] = byte(a.worldGen.GetTile(loc))
// Set the object
obj := a.worldGen.GetObject(loc)
if obj.Type != roveapi.Object_ObjectUnknown {
c.Objects[j*a.ChunkSize+i] = obj
// Get the terrain noise value for this location
t := a.terrainNoise.Eval2(float64(origin.X+i)/terrainNoiseScale, float64(origin.Y+j)/terrainNoiseScale)
var tile roveapi.Tile
switch {
case t > 0.5:
tile = roveapi.Tile_Gravel
case t > 0.05:
tile = roveapi.Tile_Sand
default:
tile = roveapi.Tile_Rock
}
c.Tiles[j*a.ChunkSize+i] = byte(tile)
// Get the object noise value for this location
o := a.objectNoise.Eval2(float64(origin.X+i)/objectNoiseScale, float64(origin.Y+j)/objectNoiseScale)
var obj = roveapi.Object_ObjectUnknown
switch {
case o > 0.6:
obj = roveapi.Object_RockLarge
case o > 0.5:
obj = roveapi.Object_RockSmall
}
if obj != roveapi.Object_ObjectUnknown {
c.Objects[j*a.ChunkSize+i] = Object{Type: roveapi.Object(obj)}
}
}
}
// Set up any objects
for i := 0; i < len(c.Tiles); i++ {
if rand.Intn(16) == 0 {
c.Objects[i] = Object{Type: roveapi.Object_RockLarge}
} else if rand.Intn(32) == 0 {
c.Objects[i] = Object{Type: roveapi.Object_RockSmall}
}
}
@ -207,7 +240,8 @@ func (a *chunkBasedAtlas) worldSpaceToChunkWithGrow(v maths.Vector) int {
LowerBound: lower,
UpperBound: upper,
Chunks: make([]chunk, size.X*size.Y),
worldGen: a.worldGen,
terrainNoise: a.terrainNoise,
objectNoise: a.objectNoise,
}
// Log that we're resizing

View file

@ -1,4 +1,4 @@
package rove
package atlas
import (
"github.com/mdiluz/rove/proto/roveapi"
@ -7,10 +7,7 @@ import (
// Object represents an object in the world
type Object struct {
// The type of the object
Type roveapi.Object
// Data is an internal type used for certain types of object
Data []byte
Type roveapi.Object `json:"type"`
}
// IsBlocking checks if an object is a blocking object
@ -32,7 +29,6 @@ func (o *Object) IsBlocking() bool {
func (o *Object) IsStashable() bool {
var stashable = [...]roveapi.Object{
roveapi.Object_RockSmall,
roveapi.Object_RoverParts,
}
for _, t := range stashable {

View file

@ -8,8 +8,8 @@ import (
// Vector desribes a 3D vector
type Vector struct {
X int
Y int
X int `json:"x"`
Y int `json:"y"`
}
// Add adds one vector to another
@ -89,31 +89,13 @@ func BearingToVector(b roveapi.Bearing) Vector {
switch b {
case roveapi.Bearing_North:
return Vector{Y: 1}
case roveapi.Bearing_NorthEast:
return Vector{X: 1, Y: 1}
case roveapi.Bearing_East:
return Vector{X: 1}
case roveapi.Bearing_SouthEast:
return Vector{X: 1, Y: -1}
case roveapi.Bearing_South:
return Vector{Y: -1}
case roveapi.Bearing_SouthWest:
return Vector{X: -1, Y: -1}
case roveapi.Bearing_West:
return Vector{X: -1}
case roveapi.Bearing_NorthWest:
return Vector{X: -1, Y: 1}
}
return Vector{}
}
// Dot returns the dot product of two vectors
func Dot(a Vector, b Vector) int {
return a.X*b.X + a.Y*b.Y
}
// AngleCos returns the cosine of the angle between two vectors
func AngleCos(a Vector, b Vector) float64 {
return float64(Dot(a, b)) / a.Length() * b.Length()
}

17
pkg/rove/command.go Normal file
View file

@ -0,0 +1,17 @@
package rove
import "github.com/mdiluz/rove/proto/roveapi"
// Command represends a single command to execute
type Command struct {
Command roveapi.CommandType `json:"command"`
// Used in the move command
Bearing roveapi.Bearing `json:"bearing,omitempty"`
// Used in the broadcast command
Message []byte `json:"message,omitempty"`
}
// CommandStream is a list of commands to execute in order
type CommandStream []Command

View file

@ -1,7 +1,6 @@
package rove
import (
"encoding/json"
"testing"
"github.com/mdiluz/rove/pkg/maths"
@ -9,297 +8,63 @@ import (
"github.com/stretchr/testify/assert"
)
func TestCommand_Invalid(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
func TestCommand_Move(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover()
assert.NoError(t, err)
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_none})
assert.Error(t, err)
pos := maths.Vector{
X: 1.0,
Y: 2.0,
}
func TestCommand_Toggle(t *testing.T) {
w := NewWorld(8)
a, err := w.SpawnRover("")
assert.NoError(t, err)
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
r, err := w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
// Try the move command
moveCommand := Command{Command: roveapi.CommandType_move, Bearing: roveapi.Bearing_North}
assert.NoError(t, world.Enqueue(a, moveCommand), "Failed to execute move command")
err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_toggle})
assert.NoError(t, err)
w.Tick()
// Tick the world
world.EnqueueAllIncoming()
world.ExecuteCommandQueues()
r, err = w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_CatchingWind, r.SailPosition)
err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_toggle})
assert.NoError(t, err)
w.Tick()
r, err = w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
newPos, err := world.RoverPosition(a)
assert.NoError(t, err, "Failed to set position for rover")
pos.Add(maths.Vector{X: 0.0, Y: 1})
assert.Equal(t, pos, newPos, "Failed to correctly set position for rover")
}
func TestCommand_Turn(t *testing.T) {
w := NewWorld(8)
a, err := w.SpawnRover("")
func TestCommand_Recharge(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover()
assert.NoError(t, err)
err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_turn, Bearing: roveapi.Bearing_NorthWest})
assert.NoError(t, err)
w.Tick()
r, err := w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.Bearing_NorthWest, r.Bearing)
pos := maths.Vector{
X: 1.0,
Y: 2.0,
}
func TestCommand_Stash(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
info, err := w.GetRover(name)
assert.NoError(t, err)
assert.Empty(t, info.Inventory)
// Move to use up some charge
moveCommand := Command{Command: roveapi.CommandType_move, Bearing: roveapi.Bearing_North}
assert.NoError(t, world.Enqueue(a, moveCommand), "Failed to queue move command")
// Drop a pickup below us
w.Atlas.SetObject(info.Pos, Object{Type: roveapi.Object_RockSmall})
// Tick the world
world.EnqueueAllIncoming()
world.ExecuteCommandQueues()
// Try and stash it
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_stash})
assert.NoError(t, err)
w.Tick()
rover, _ := world.GetRover(a)
assert.Equal(t, rover.MaximumCharge-1, rover.Charge)
// Check we now have it in the inventory
info, err = w.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, 1, len(info.Inventory))
assert.Equal(t, Object{Type: roveapi.Object_RockSmall}, info.Inventory[0])
chargeCommand := Command{Command: roveapi.CommandType_recharge}
assert.NoError(t, world.Enqueue(a, chargeCommand), "Failed to queue recharge command")
// Check it's no longer on the atlas
_, obj := w.Atlas.QueryPosition(info.Pos)
assert.Equal(t, Object{Type: roveapi.Object_ObjectUnknown}, obj)
}
// Tick the world
world.EnqueueAllIncoming()
world.ExecuteCommandQueues()
func TestCommand_Repair(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
info, err := w.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, info.MaximumIntegrity, info.Integrity)
// Put a blocking rock to the north
w.Atlas.SetObject(info.Pos.Added(maths.Vector{X: 0, Y: 1}), Object{Type: roveapi.Object_RockLarge})
// Try and move and make sure we're blocked
newpos, err := w.TryMoveRover(name, roveapi.Bearing_North)
assert.NoError(t, err)
assert.Equal(t, info.Pos, newpos)
// Check we're damaged
info, err = w.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, info.MaximumIntegrity-1, info.Integrity)
// Stash a repair object
w.Atlas.SetObject(info.Pos, Object{Type: roveapi.Object_RoverParts})
obj, err := w.RoverStash(name)
assert.NoError(t, err)
assert.Equal(t, roveapi.Object_RoverParts, obj)
// Enqueue the repair and tick
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_repair})
assert.NoError(t, err)
w.Tick()
// Check we're repaired
info, err = w.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, info.MaximumIntegrity, info.Integrity)
assert.Equal(t, 0, len(info.Inventory))
}
func TestCommand_Broadcast(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
// Enqueue the broadcast and tick
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_broadcast, Data: []byte("ABC")})
assert.NoError(t, err)
w.Tick()
info, err := w.GetRover(name)
assert.NoError(t, err)
assert.Contains(t, info.Logs[len(info.Logs)-1].Text, "ABC")
}
func TestCommand_Salvage(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
info, err := w.GetRover(name)
assert.NoError(t, err)
w.Atlas.SetObject(info.Pos, Object{Type: roveapi.Object_RoverDormant})
// Enqueue the broadcast and tick
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_salvage})
assert.NoError(t, err)
w.Tick()
// Check we now have some rover parts
info, err = w.GetRover(name)
assert.NoError(t, err)
assert.NotEmpty(t, info.Inventory)
for _, i := range info.Inventory {
assert.Equal(t, roveapi.Object_RoverParts, i.Type)
}
// Check the dormant rover is gone
_, obj := w.Atlas.QueryPosition(info.Pos)
assert.Equal(t, roveapi.Object_ObjectUnknown, obj.Type)
}
func TestCommand_Transfer(t *testing.T) {
w := NewWorld(8)
acc, err := w.Accountant.RegisterAccount("tmp")
assert.NoError(t, err)
nameA, err := w.SpawnRover(acc.Name)
assert.NoError(t, err)
infoA, err := w.GetRover(nameA)
assert.NoError(t, err)
// Drop a dormant rover on the current position
infoB := DefaultRover()
infoB.Name = "abc"
infoB.Pos = infoA.Pos
data, err := json.Marshal(infoB)
assert.NoError(t, err)
w.Atlas.SetObject(infoA.Pos, Object{Type: roveapi.Object_RoverDormant, Data: data})
// Enqueue a transfer as well as a dud command
err = w.Enqueue(nameA,
&roveapi.Command{Command: roveapi.CommandType_transfer},
&roveapi.Command{Command: roveapi.CommandType_broadcast, Data: []byte("xyz")})
assert.NoError(t, err)
w.Tick()
// Ensure both command queues are empty
assert.Empty(t, w.CommandQueue[nameA])
assert.Empty(t, w.CommandQueue[infoB.Name])
// Verify the account now controls the new rover
accountRover, err := w.Accountant.GetValue(acc.Name, "rover")
assert.NoError(t, err)
assert.Equal(t, infoB.Name, accountRover)
// Verify the position now has a dormant rover
_, obj := w.Atlas.QueryPosition(infoA.Pos)
assert.Equal(t, roveapi.Object_RoverDormant, obj.Type)
// Verify the stored data matches
var stored Rover
err = json.Unmarshal(obj.Data, &stored)
assert.NoError(t, err)
assert.Equal(t, infoA.Name, stored.Name)
// Verify the new rover data matches what we put in
infoB2, err := w.GetRover(infoB.Name)
assert.NoError(t, err)
assert.Equal(t, infoB.Name, infoB2.Name)
rover, _ = world.GetRover(a)
assert.Equal(t, rover.MaximumCharge, rover.Charge)
}
func TestCommand_Wait(t *testing.T) {
w := NewWorld(8)
a, err := w.SpawnRover("")
assert.NoError(t, err)
r, err := w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_wait, Repeat: 4}, &roveapi.Command{Command: roveapi.CommandType_toggle})
assert.NoError(t, err)
// Tick 5 times during the wait (1 normal execute + 4)
for i := 0; i < 5; i++ {
w.Tick()
r, err = w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
}
// One last tick to do the toggle
w.Tick()
r, err = w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_CatchingWind, r.SailPosition)
}
func TestCommand_Upgrade(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
rover, ok := w.Rovers[name]
assert.True(t, ok)
// Try an invalid upgrade
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_upgrade})
assert.Error(t, err)
// Try a valid command but without the parts
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_upgrade, Upgrade: roveapi.RoverUpgrade_Capacity})
assert.NoError(t, err)
// Ensure nothing changed and we logged the attempt
pre := rover.Capacity
w.Tick()
assert.Equal(t, pre, rover.Capacity)
assert.Contains(t, rover.Logs[len(rover.Logs)-1].Text, "tried")
// One non-part item
rover.Inventory = []Object{
{
Type: roveapi.Object_RoverParts,
},
{
Type: roveapi.Object_RoverParts,
},
{
Type: roveapi.Object_RockSmall,
},
{
Type: roveapi.Object_RoverParts,
},
{
Type: roveapi.Object_RoverParts,
},
{
Type: roveapi.Object_RoverParts,
},
}
// Try a valid command again
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_upgrade, Upgrade: roveapi.RoverUpgrade_Capacity})
assert.NoError(t, err)
// Check that the capacity increases on the tick and all the parts are used
pre = rover.Capacity
w.Tick()
assert.Equal(t, pre+1, rover.Capacity)
assert.Equal(t, 1, len(rover.Inventory))
assert.Equal(t, roveapi.Object_RockSmall, rover.Inventory[0].Type)
}

View file

@ -1,89 +1,54 @@
package rove
import (
"bufio"
"fmt"
"log"
"math/rand"
"os"
"time"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
)
const (
maxLogEntries = 16
)
// RoverLogEntry describes a single log entry for the rover
type RoverLogEntry struct {
// Time is the timestamp of the entry
Time time.Time
Time time.Time `json:"time"`
// Text contains the information in this log entry
Text string
Text string `json:"text"`
}
// Rover describes a single rover in the world
type Rover struct {
// Unique name of this rover
Name string
Name string `json:"name"`
// Pos represents where this rover is in the world
Pos maths.Vector
// Bearing is the current direction the rover is facing
Bearing roveapi.Bearing
Pos maths.Vector `json:"pos"`
// Range represents the distance the unit's radar can see
Range int
Range int `json:"range"`
// Inventory represents any items the rover is carrying
Inventory []Object
Inventory []atlas.Object `json:"inventory"`
// Capacity is the maximum number of inventory items
Capacity int
Capacity int `json:"capacity"`
// Integrity represents current rover health
Integrity int
Integrity int `json:"integrity"`
// MaximumIntegrity is the full integrity of the rover
MaximumIntegrity int
MaximumIntegrity int `json:"maximum-integrity"`
// Charge is the amount of energy the rover has
Charge int
Charge int `json:"charge"`
// MaximumCharge is the maximum charge able to be stored
MaximumCharge int
// SailPosition is the current position of the sails
SailPosition roveapi.SailPosition
// Current number of ticks in this move, used for sailing speeds
MoveTicks int
MaximumCharge int `json:"maximum-Charge"`
// Logs Stores log of information
Logs []RoverLogEntry
// The account that owns this rover
Owner string
}
// DefaultRover returns a default rover object with default settings
func DefaultRover() *Rover {
return &Rover{
Range: 10,
Integrity: 10,
MaximumIntegrity: 10,
Capacity: 10,
Charge: 10,
MaximumCharge: 10,
Bearing: roveapi.Bearing_North,
SailPosition: roveapi.SailPosition_SolarCharging,
Name: GenerateRoverName(),
}
Logs []RoverLogEntry `json:"logs"`
}
// AddLogEntryf adds an entry to the rovers log
@ -96,42 +61,4 @@ func (r *Rover) AddLogEntryf(format string, args ...interface{}) {
Text: text,
},
)
// Limit the number of logs
if len(r.Logs) > maxLogEntries {
r.Logs = r.Logs[len(r.Logs)-maxLogEntries:]
}
}
var wordsFile = os.Getenv("WORDS_FILE")
var roverWords []string
// GenerateRoverName generates a new rover name
func GenerateRoverName() string {
// Try and load the rover words file
if len(roverWords) == 0 {
// Try and load the words file
if file, err := os.Open(wordsFile); err != nil {
log.Printf("Couldn't read words file [%s], running without words: %s\n", wordsFile, err)
} else {
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
roverWords = append(roverWords, scanner.Text())
}
if scanner.Err() != nil {
log.Printf("Failure during word file scan: %s\n", scanner.Err())
}
}
}
// Assign a random name if we have words
if len(roverWords) > 0 {
// Loop until we find a unique name
return fmt.Sprintf("%s-%s", roverWords[rand.Intn(len(roverWords))], roverWords[rand.Intn(len(roverWords))])
}
// Default to a unique string
return uuid.New().String()
}

View file

@ -1,81 +1,104 @@
package rove
import (
"encoding/json"
"bufio"
"fmt"
"log"
"math/rand"
"os"
"sync"
"github.com/mdiluz/rove/pkg/accounts"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
)
const (
// ticksPerNormalMove defines the number of ticks it should take for a "normal" speed move
ticksPerNormalMove = 4
// upgradeCost is the cost in rover parts needed to upgrade a rover specification
upgradeCost = 5
)
// CommandStream is a list of commands to execute in order
type CommandStream []*roveapi.Command
// World describes a self contained universe and everything in it
type World struct {
// TicksPerDay is the amount of ticks in a single day
TicksPerDay int
TicksPerDay int `json:"ticks-per-day"`
// Current number of ticks from the start
CurrentTicks int
CurrentTicks int `json:"current-ticks"`
// Rovers is a id->data map of all the rovers in the game
Rovers map[string]*Rover
Rovers map[string]Rover `json:"rovers"`
// Atlas represends the world map of chunks and tiles
Atlas Atlas
// Wind is the current wind direction
Wind roveapi.Bearing
Atlas atlas.Atlas `json:"atlas"`
// Commands is the set of currently executing command streams per rover
CommandQueue map[string]CommandStream
// Accountant
Accountant accounts.Accountant
CommandQueue map[string]CommandStream `json:"commands"`
// Incoming represents the set of commands to add to the queue at the end of the current tick
CommandIncoming map[string]CommandStream `json:"incoming"`
// Mutex to lock around all world operations
worldMutex sync.RWMutex
// Mutex to lock around command operations
cmdMutex sync.RWMutex
// Set of possible words to use for names
words []string
}
var wordsFile = os.Getenv("WORDS_FILE")
// NewWorld creates a new world object
func NewWorld(chunkSize int) *World {
return &World{
Rovers: make(map[string]*Rover),
CommandQueue: make(map[string]CommandStream),
Atlas: NewChunkAtlas(chunkSize),
TicksPerDay: 24,
CurrentTicks: 0,
Accountant: accounts.NewSimpleAccountant(),
Wind: roveapi.Bearing_North,
// Try and load the words file
var lines []string
if file, err := os.Open(wordsFile); err != nil {
log.Printf("Couldn't read words file [%s], running without words: %s\n", wordsFile, err)
} else {
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if scanner.Err() != nil {
log.Printf("Failure during word file scan: %s\n", scanner.Err())
}
}
// SpawnRover adds an rover to the game (without lock)
func (w *World) SpawnRover(account string) (string, error) {
return &World{
Rovers: make(map[string]Rover),
CommandQueue: make(map[string]CommandStream),
CommandIncoming: make(map[string]CommandStream),
Atlas: atlas.NewChunkAtlas(chunkSize),
words: lines,
TicksPerDay: 24,
CurrentTicks: 0,
}
}
// SpawnRover adds an rover to the game
func (w *World) SpawnRover() (string, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
// Initialise the rover
rover := DefaultRover()
rover := Rover{
Range: 4,
Integrity: 10,
MaximumIntegrity: 10,
Capacity: 10,
Charge: 10,
MaximumCharge: 10,
Name: uuid.New().String(),
}
// Assign the owner
rover.Owner = account
// Assign a random name if we have words
if len(w.words) > 0 {
for {
// Loop until we find a unique name
name := fmt.Sprintf("%s-%s", w.words[rand.Intn(len(w.words))], w.words[rand.Intn(len(w.words))])
if _, ok := w.Rovers[name]; !ok {
rover.Name = name
break
}
}
}
// Spawn in a random place near the origin
rover.Pos = maths.Vector{
@ -101,13 +124,7 @@ func (w *World) SpawnRover(account string) (string, error) {
// Append the rover to the list
w.Rovers[rover.Name] = rover
var err error
// Only assign if we've been given an account
if len(account) > 0 {
err = w.Accountant.AssignData(account, "rover", rover.Name)
}
return rover.Name, err
return rover.Name, nil
}
// GetRover gets a specific rover by name
@ -119,7 +136,7 @@ func (w *World) GetRover(rover string) (Rover, error) {
if !ok {
return Rover{}, fmt.Errorf("Failed to find rover with name: %s", rover)
}
return *i, nil
return i, nil
}
// RoverRecharge charges up a rover
@ -142,6 +159,7 @@ func (w *World) RoverRecharge(rover string) (int, error) {
i.Charge++
i.AddLogEntryf("recharged to %d", i.Charge)
}
w.Rovers[rover] = i
return i.Charge, nil
}
@ -176,6 +194,7 @@ func (w *World) RoverBroadcast(rover string, message []byte) (err error) {
}
i.AddLogEntryf("broadcasted %s", string(message))
w.Rovers[rover] = i
return
}
@ -184,26 +203,12 @@ func (w *World) DestroyRover(rover string) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
_, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("no rover matching id")
}
// Remove this rover from tracked rovers
delete(w.Rovers, rover)
r.Owner = ""
r.AddLogEntryf("rover destroyed")
// Marshal the rover data
data, err := json.Marshal(r)
if err != nil {
return err
}
// Place the dormant rover down
w.Atlas.SetObject(r.Pos, Object{Type: roveapi.Object_RoverDormant, Data: data})
return nil
}
@ -230,11 +235,12 @@ func (w *World) SetRoverPosition(rover string, pos maths.Vector) error {
}
i.Pos = pos
w.Rovers[rover] = i
return nil
}
// RoverInventory returns the inventory of a requested rover
func (w *World) RoverInventory(rover string) ([]Object, error) {
func (w *World) RoverInventory(rover string) ([]atlas.Object, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
@ -266,11 +272,12 @@ func (w *World) WarpRover(rover string, pos maths.Vector) error {
}
i.Pos = pos
w.Rovers[rover] = i
return nil
}
// TryMoveRover attempts to move a rover in a specific direction
func (w *World) TryMoveRover(rover string, b roveapi.Bearing) (maths.Vector, error) {
// MoveRover attempts to move a rover in a specific direction
func (w *World) MoveRover(rover string, b roveapi.Bearing) (maths.Vector, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
@ -279,6 +286,12 @@ func (w *World) TryMoveRover(rover string, b roveapi.Bearing) (maths.Vector, err
return maths.Vector{}, fmt.Errorf("no rover matching id")
}
// Ensure the rover has energy
if i.Charge <= 0 {
return i.Pos, nil
}
i.Charge--
// Try the new move position
newPos := i.Pos.Added(maths.BearingToVector(b))
@ -288,11 +301,17 @@ func (w *World) TryMoveRover(rover string, b roveapi.Bearing) (maths.Vector, err
i.AddLogEntryf("moved %s to %+v", b.String(), newPos)
// Perform the move
i.Pos = newPos
w.Rovers[rover] = i
} else {
// If it is a blocking tile, reduce the rover integrity
i.AddLogEntryf("tried to move %s to %+v", b.String(), newPos)
i.Integrity = i.Integrity - 1
i.AddLogEntryf("had a collision, new integrity %d", i.Integrity)
if i.Integrity == 0 {
// TODO: The rover needs to be left dormant with the player
} else {
w.Rovers[rover] = i
}
}
return i.Pos, nil
@ -310,13 +329,11 @@ func (w *World) RoverStash(rover string) (roveapi.Object, error) {
// Can't pick up when full
if len(r.Inventory) >= r.Capacity {
r.AddLogEntryf("tried to stash object but inventory was full")
return roveapi.Object_ObjectUnknown, nil
}
// Ensure the rover has energy
if r.Charge <= 0 {
r.AddLogEntryf("tried to stash object but had no charge")
return roveapi.Object_ObjectUnknown, nil
}
r.Charge--
@ -328,237 +345,11 @@ func (w *World) RoverStash(rover string) (roveapi.Object, error) {
r.AddLogEntryf("stashed %c", obj.Type)
r.Inventory = append(r.Inventory, obj)
w.Atlas.SetObject(r.Pos, Object{Type: roveapi.Object_ObjectUnknown})
w.Rovers[rover] = r
w.Atlas.SetObject(r.Pos, atlas.Object{Type: roveapi.Object_ObjectUnknown})
return obj.Type, nil
}
// RoverSalvage will salvage a rover for parts
func (w *World) RoverSalvage(rover string) (roveapi.Object, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return roveapi.Object_ObjectUnknown, fmt.Errorf("no rover matching id")
}
// Can't pick up when full
if len(r.Inventory) >= r.Capacity {
r.AddLogEntryf("tried to salvage dormant rover but inventory was full")
return roveapi.Object_ObjectUnknown, nil
}
// Ensure the rover has energy
if r.Charge <= 0 {
r.AddLogEntryf("tried to salvage dormant rover but had no charge")
return roveapi.Object_ObjectUnknown, nil
}
r.Charge--
_, obj := w.Atlas.QueryPosition(r.Pos)
if obj.Type != roveapi.Object_RoverDormant {
r.AddLogEntryf("tried to salvage dormant rover but found no rover to salvage")
return roveapi.Object_ObjectUnknown, nil
}
r.AddLogEntryf("salvaged dormant rover")
for i := 0; i < 5; i++ {
if len(r.Inventory) == r.Capacity {
break
}
r.Inventory = append(r.Inventory, Object{Type: roveapi.Object_RoverParts})
}
w.Atlas.SetObject(r.Pos, Object{Type: roveapi.Object_ObjectUnknown})
return obj.Type, nil
}
// RoverTransfer will transfer rover control to dormant rover
func (w *World) RoverTransfer(rover string) (string, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
oldRover, ok := w.Rovers[rover]
if !ok {
return "", fmt.Errorf("no rover matching id")
}
_, obj := w.Atlas.QueryPosition(oldRover.Pos)
if obj.Type != roveapi.Object_RoverDormant {
oldRover.AddLogEntryf("tried to transfer to dormant rover but found no rover")
return "", nil
}
// Unmarshal the dormant rover
var newRover Rover
err := json.Unmarshal(obj.Data, &newRover)
if err != nil {
return "", err
}
// Add logs
oldRover.AddLogEntryf("transferring to dormant rover %s", newRover.Name)
newRover.AddLogEntryf("transferred from rover %s", oldRover.Name)
// Transfer the ownership
err = w.Accountant.AssignData(oldRover.Owner, "rover", newRover.Name)
if err != nil {
return "", err
}
newRover.Owner = oldRover.Owner
oldRover.Owner = ""
// Place the old rover in the world
oldRoverData, err := json.Marshal(oldRover)
if err != nil {
return "", err
}
w.Atlas.SetObject(oldRover.Pos, Object{Type: roveapi.Object_RoverDormant, Data: oldRoverData})
// Swap the rovers in the tracking
w.Rovers[newRover.Name] = &newRover
delete(w.Rovers, oldRover.Name)
// Clear the command queues for both rovers
delete(w.CommandQueue, oldRover.Name)
delete(w.CommandQueue, newRover.Name)
return newRover.Name, nil
}
// RoverToggle will toggle the sail position
func (w *World) RoverToggle(rover string) (roveapi.SailPosition, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return roveapi.SailPosition_UnknownSailPosition, fmt.Errorf("no rover matching id")
}
// Swap the sail position
switch r.SailPosition {
case roveapi.SailPosition_CatchingWind:
r.SailPosition = roveapi.SailPosition_SolarCharging
case roveapi.SailPosition_SolarCharging:
r.SailPosition = roveapi.SailPosition_CatchingWind
}
// Reset the movement ticks
r.MoveTicks = 0
return r.SailPosition, nil
}
// RoverUpgrade will try to upgrade the rover
func (w *World) RoverUpgrade(rover string, upgrade roveapi.RoverUpgrade) (int, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return 0, fmt.Errorf("no rover matching id")
}
cost := upgradeCost
num := 0
for i := range r.Inventory {
if r.Inventory[i].Type == roveapi.Object_RoverParts {
num++
}
}
if num < cost {
r.AddLogEntryf("tried to upgrade but lacked rover parts")
return 0, nil
}
// Apply the upgrade
var ret int
switch upgrade {
case roveapi.RoverUpgrade_Capacity:
r.Capacity++
ret = r.Capacity
case roveapi.RoverUpgrade_Range:
r.Range++
ret = r.Range
case roveapi.RoverUpgrade_MaximumCharge:
r.MaximumCharge++
ret = r.MaximumCharge
case roveapi.RoverUpgrade_MaximumIntegrity:
r.MaximumIntegrity++
ret = r.MaximumIntegrity
default:
return 0, fmt.Errorf("unknown upgrade: %s", upgrade)
}
// Remove the cost in rover parts
var n []Object
for _, o := range r.Inventory {
if o.Type == roveapi.Object_RoverParts && cost > 0 {
cost--
} else {
n = append(n, o)
}
}
// Assign back the inventory
r.Inventory = n
r.AddLogEntryf("upgraded %s to %d", upgrade, ret)
return ret, nil
}
// RoverTurn will turn the rover
func (w *World) RoverTurn(rover string, bearing roveapi.Bearing) (roveapi.Bearing, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return roveapi.Bearing_BearingUnknown, fmt.Errorf("no rover matching id")
}
// Set the new bearing
r.Bearing = bearing
// Reset the movement ticks
r.MoveTicks = 0
return r.Bearing, nil
}
// RoverRepair will turn the rover
func (w *World) RoverRepair(rover string) (int, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return 0, fmt.Errorf("no rover matching id")
}
// Can't repair past max
if r.Integrity >= r.MaximumIntegrity {
return r.Integrity, nil
}
// Find rover parts in inventory
for i, o := range r.Inventory {
if o.Type == roveapi.Object_RoverParts {
// Copy-erase from slice
r.Inventory[i] = r.Inventory[len(r.Inventory)-1]
r.Inventory = r.Inventory[:len(r.Inventory)-1]
// Repair
r.Integrity = r.Integrity + 1
r.AddLogEntryf("repaired self to %d", r.Integrity)
break
}
}
return r.Integrity, nil
}
// RadarFromRover can be used to query what a rover can currently see
func (w *World) RadarFromRover(rover string) (radar []roveapi.Tile, objs []roveapi.Object, err error) {
w.worldMutex.RLock()
@ -618,7 +409,10 @@ func (w *World) RadarFromRover(rover string) (radar []roveapi.Tile, objs []rovea
}
// RoverCommands returns current commands for the given rover
func (w *World) RoverCommands(rover string) (queued CommandStream) {
func (w *World) RoverCommands(rover string) (incoming []Command, queued []Command) {
if c, ok := w.CommandIncoming[rover]; ok {
incoming = c
}
if c, ok := w.CommandQueue[rover]; ok {
queued = c
}
@ -626,34 +420,27 @@ func (w *World) RoverCommands(rover string) (queued CommandStream) {
}
// Enqueue will queue the commands given
func (w *World) Enqueue(rover string, commands ...*roveapi.Command) error {
func (w *World) Enqueue(rover string, commands ...Command) error {
// First validate the commands
for _, c := range commands {
switch c.Command {
case roveapi.CommandType_broadcast:
if len(c.GetData()) > 3 {
return fmt.Errorf("too many characters in message (limit 3): %d", len(c.GetData()))
case roveapi.CommandType_move:
if c.Bearing == roveapi.Bearing_BearingUnknown {
return fmt.Errorf("bearing must be valid")
}
for _, b := range c.GetData() {
case roveapi.CommandType_broadcast:
if len(c.Message) > 3 {
return fmt.Errorf("too many characters in message (limit 3): %d", len(c.Message))
}
for _, b := range c.Message {
if b < 37 || b > 126 {
return fmt.Errorf("invalid message character: %c", b)
}
}
case roveapi.CommandType_turn:
if c.GetBearing() == roveapi.Bearing_BearingUnknown {
return fmt.Errorf("turn command given unknown bearing")
}
case roveapi.CommandType_upgrade:
if c.GetUpgrade() == roveapi.RoverUpgrade_RoverUpgradeUnknown {
return fmt.Errorf("upgrade command given unknown upgrade")
}
case roveapi.CommandType_wait:
case roveapi.CommandType_toggle:
case roveapi.CommandType_stash:
case roveapi.CommandType_repair:
case roveapi.CommandType_salvage:
case roveapi.CommandType_transfer:
case roveapi.CommandType_recharge:
// Nothing to verify
default:
return fmt.Errorf("unknown command: %s", c.Command)
@ -664,31 +451,39 @@ func (w *World) Enqueue(rover string, commands ...*roveapi.Command) error {
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
w.CommandQueue[rover] = commands
// Override the incoming command set
w.CommandIncoming[rover] = commands
return nil
}
// Tick will execute any commands in the current command queue and tick the world
func (w *World) Tick() {
// EnqueueAllIncoming will enqueue the incoming commands
func (w *World) EnqueueAllIncoming() {
// Add any incoming commands from this tick and clear that queue
for id, incoming := range w.CommandIncoming {
commands := w.CommandQueue[id]
commands = append(commands, incoming...)
w.CommandQueue[id] = commands
}
w.CommandIncoming = make(map[string]CommandStream)
}
// ExecuteCommandQueues will execute any commands in the current command queue
func (w *World) ExecuteCommandQueues() {
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
// Iterate through all the current commands
for rover, cmds := range w.CommandQueue {
if len(cmds) != 0 {
// Extract the first command in the queue
c := cmds[0]
w.CommandQueue[rover] = cmds[1:]
// Execute the command
if done, err := w.ExecuteCommand(cmds[0], rover); err != nil {
if err := w.ExecuteCommand(&c, rover); err != nil {
log.Println(err)
// TODO: Report this error somehow
} else if done {
// Extract the first command in the queue
// Only if the command queue still has entries (the command may have modified this queue)
if _, ok := w.CommandQueue[rover]; ok {
w.CommandQueue[rover] = cmds[1:]
}
}
} else {
@ -697,115 +492,54 @@ func (w *World) Tick() {
}
}
// Move all the rovers based on current wind and sails
for n, r := range w.Rovers {
// Skip if we're not catching the wind
if r.SailPosition != roveapi.SailPosition_CatchingWind {
continue
}
// Increment the current move ticks
r.MoveTicks++
// Get the difference between the two bearings
// Normalise, we don't care about clockwise/anticlockwise
diff := maths.Abs(int(w.Wind - r.Bearing))
if diff > 4 {
diff = 8 - diff
}
// Calculate the travel "ticks"
var ticksToMove int
switch diff {
case 0:
// Going with the wind, travel at base speed of once every 4 ticks
ticksToMove = ticksPerNormalMove
case 1:
// At a slight angle, we can go a little faster
ticksToMove = ticksPerNormalMove / 2
case 2:
// Perpendicular to wind, max speed
ticksToMove = 1
case 3:
// Heading at 45 degrees into the wind, back to min speed
ticksToMove = ticksPerNormalMove
case 4:
// Heading durectly into the wind, no movement at all
default:
log.Fatalf("bearing difference of %d should be impossible", diff)
}
// If we've incremented over the current move ticks on the rover, we can try and make the move
if ticksToMove != 0 && r.MoveTicks >= ticksToMove {
_, err := w.TryMoveRover(n, r.Bearing)
if err != nil {
log.Println(err)
// TODO: Report this error somehow
}
// Reset the move ticks
r.MoveTicks = 0
}
}
// Check all rover integrities
for _, r := range w.Rovers {
if r.Integrity <= 0 {
// The rover has died destroy it
err := w.DestroyRover(r.Name)
if err != nil {
log.Println(err)
// TODO: Report this error somehow
}
// Spawn a new one for this account
_, err = w.SpawnRover(r.Owner)
if err != nil {
log.Println(err)
// TODO: Report this error somehow
}
}
}
// Add any incoming commands from this tick and clear that queue
w.EnqueueAllIncoming()
// Increment the current tick count
w.CurrentTicks++
// Change the wind every day
if (w.CurrentTicks % w.TicksPerDay) == 0 {
w.Wind = roveapi.Bearing((rand.Int() % 8) + 1) // Random cardinal bearing
}
}
// ExecuteCommand will execute a single command
func (w *World) ExecuteCommand(c *roveapi.Command, rover string) (done bool, err error) {
log.Printf("Executing command: %+v for %s\n", c.Command, rover)
func (w *World) ExecuteCommand(c *Command, rover string) (err error) {
log.Printf("Executing command: %+v for %s\n", *c, rover)
switch c.Command {
case roveapi.CommandType_toggle:
_, err = w.RoverToggle(rover)
case roveapi.CommandType_stash:
_, err = w.RoverStash(rover)
case roveapi.CommandType_repair:
_, err = w.RoverRepair(rover)
case roveapi.CommandType_broadcast:
err = w.RoverBroadcast(rover, c.GetData())
case roveapi.CommandType_turn:
_, err = w.RoverTurn(rover, c.GetBearing())
case roveapi.CommandType_salvage:
_, err = w.RoverSalvage(rover)
case roveapi.CommandType_transfer:
_, err = w.RoverTransfer(rover)
case roveapi.CommandType_upgrade:
_, err = w.RoverUpgrade(rover, c.GetUpgrade())
case roveapi.CommandType_wait:
// Nothing to do
default:
return true, fmt.Errorf("unknown command: %s", c.Command)
case roveapi.CommandType_move:
if _, err := w.MoveRover(rover, c.Bearing); err != nil {
return err
}
// Decrement the repeat number
c.Repeat--
return c.Repeat < 0, err
case roveapi.CommandType_stash:
if _, err := w.RoverStash(rover); err != nil {
return err
}
case roveapi.CommandType_repair:
r, err := w.GetRover(rover)
if err != nil {
return err
}
// Consume an inventory item to repair if possible
if len(r.Inventory) > 0 && r.Integrity < r.MaximumIntegrity {
r.Inventory = r.Inventory[:len(r.Inventory)-1]
r.Integrity = r.Integrity + 1
r.AddLogEntryf("repaired self to %d", r.Integrity)
w.Rovers[rover] = r
}
case roveapi.CommandType_recharge:
_, err := w.RoverRecharge(rover)
if err != nil {
return err
}
case roveapi.CommandType_broadcast:
if err := w.RoverBroadcast(rover, c.Message); err != nil {
return err
}
default:
return fmt.Errorf("unknown command: %s", c.Command)
}
return
}
// Daytime returns if it's currently daytime

View file

@ -3,6 +3,7 @@ package rove
import (
"testing"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/stretchr/testify/assert"
@ -18,9 +19,9 @@ func TestNewWorld(t *testing.T) {
func TestWorld_CreateRover(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover("")
a, err := world.SpawnRover()
assert.NoError(t, err)
b, err := world.SpawnRover("")
b, err := world.SpawnRover()
assert.NoError(t, err)
// Basic duplicate check
@ -33,7 +34,7 @@ func TestWorld_CreateRover(t *testing.T) {
func TestWorld_GetRover(t *testing.T) {
world := NewWorld(4)
a, err := world.SpawnRover("")
a, err := world.SpawnRover()
assert.NoError(t, err)
rover, err := world.GetRover(a)
@ -44,9 +45,9 @@ func TestWorld_GetRover(t *testing.T) {
func TestWorld_DestroyRover(t *testing.T) {
world := NewWorld(1)
a, err := world.SpawnRover("")
a, err := world.SpawnRover()
assert.NoError(t, err)
b, err := world.SpawnRover("")
b, err := world.SpawnRover()
assert.NoError(t, err)
err = world.DestroyRover(a)
@ -62,7 +63,7 @@ func TestWorld_DestroyRover(t *testing.T) {
func TestWorld_GetSetMovePosition(t *testing.T) {
world := NewWorld(4)
a, err := world.SpawnRover("")
a, err := world.SpawnRover()
assert.NoError(t, err)
pos := maths.Vector{
@ -78,47 +79,49 @@ func TestWorld_GetSetMovePosition(t *testing.T) {
assert.Equal(t, pos, newPos, "Failed to correctly set position for rover")
b := roveapi.Bearing_North
newPos, err = world.TryMoveRover(a, b)
newPos, err = world.MoveRover(a, b)
assert.NoError(t, err, "Failed to set position for rover")
pos.Add(maths.Vector{X: 0, Y: 1})
assert.Equal(t, pos, newPos, "Failed to correctly move position for rover")
rover, err := world.GetRover(a)
assert.NoError(t, err, "Failed to get rover information")
assert.Equal(t, rover.MaximumCharge-1, rover.Charge, "Rover should have lost charge for moving")
assert.Contains(t, rover.Logs[len(rover.Logs)-1].Text, "moved", "Rover logs should contain the move")
// Place a tile in front of the rover
world.Atlas.SetObject(maths.Vector{X: 0, Y: 2}, Object{Type: roveapi.Object_RockLarge})
newPos, err = world.TryMoveRover(a, b)
world.Atlas.SetObject(maths.Vector{X: 0, Y: 2}, atlas.Object{Type: roveapi.Object_RockLarge})
newPos, err = world.MoveRover(a, b)
assert.NoError(t, err, "Failed to move rover")
assert.Equal(t, pos, newPos, "Failed to correctly not move position for rover into wall")
rover, err = world.GetRover(a)
assert.NoError(t, err, "Failed to get rover information")
assert.Equal(t, rover.MaximumCharge-2, rover.Charge, "Rover should have lost charge for move attempt")
}
func TestWorld_RadarFromRover(t *testing.T) {
// Create world that should have visible walls on the radar
world := NewWorld(2)
a, err := world.SpawnRover("")
a, err := world.SpawnRover()
assert.NoError(t, err)
b, err := world.SpawnRover("")
b, err := world.SpawnRover()
assert.NoError(t, err)
// Warp the rovers into position
bpos := maths.Vector{X: -3, Y: -3}
world.Atlas.SetObject(bpos, Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(b, bpos), "Failed to warp rover")
world.Atlas.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(a, maths.Vector{X: 0, Y: 0}), "Failed to warp rover")
r, err := world.GetRover(a)
assert.NoError(t, err)
radar, objs, err := world.RadarFromRover(a)
assert.NoError(t, err, "Failed to get radar from rover")
fullRange := r.Range + r.Range + 1
fullRange := 4 + 4 + 1
assert.Equal(t, fullRange*fullRange, len(radar), "Radar returned wrong length")
assert.Equal(t, fullRange*fullRange, len(objs), "Radar returned wrong length")
// TODO: Verify the other rover is on the radar
// Test the expected values
assert.Equal(t, roveapi.Object_RoverLive, objs[1+fullRange])
assert.Equal(t, roveapi.Object_RoverLive, objs[4+4*fullRange])
// Check the radar results are stable
radar1, objs1, err := world.RadarFromRover(a)
@ -129,11 +132,82 @@ func TestWorld_RadarFromRover(t *testing.T) {
assert.Equal(t, objs1, objs2)
}
func TestWorld_RoverDamage(t *testing.T) {
func TestWorld_RoverStash(t *testing.T) {
world := NewWorld(2)
acc, err := world.Accountant.RegisterAccount("tmp")
assert.NoError(t, err)
a, err := world.SpawnRover(acc.Name)
a, err := world.SpawnRover()
assert.NoError(t, err)
pos := maths.Vector{
X: 0.0,
Y: 0.0,
}
world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_ObjectUnknown})
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
rover, err := world.GetRover(a)
assert.NoError(t, err, "Failed to get rover")
for i := 0; i < rover.Capacity; i++ {
// Place an object
world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_RockSmall})
// Pick it up
o, err := world.RoverStash(a)
assert.NoError(t, err, "Failed to stash")
assert.Equal(t, roveapi.Object_RockSmall, o, "Failed to get correct object")
// Check it's gone
_, obj := world.Atlas.QueryPosition(pos)
assert.Equal(t, roveapi.Object_ObjectUnknown, obj.Type, "Stash failed to remove object from atlas")
// Check we have it
inv, err := world.RoverInventory(a)
assert.NoError(t, err, "Failed to get inventory")
assert.Equal(t, i+1, len(inv))
assert.Equal(t, atlas.Object{Type: roveapi.Object_RockSmall}, inv[i])
// Check that this did reduce the charge
info, err := world.GetRover(a)
assert.NoError(t, err, "Failed to get rover")
assert.Equal(t, info.MaximumCharge-(i+1), info.Charge, "Rover lost charge for stash")
assert.Contains(t, info.Logs[len(info.Logs)-1].Text, "stashed", "Rover logs should contain the move")
}
// Recharge the rover
for i := 0; i < rover.MaximumCharge; i++ {
_, err = world.RoverRecharge(a)
assert.NoError(t, err)
}
// Place an object
world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_RockSmall})
// Try to pick it up
o, err := world.RoverStash(a)
assert.NoError(t, err, "Failed to stash")
assert.Equal(t, roveapi.Object_ObjectUnknown, o, "Failed to get correct object")
// Check it's still there
_, obj := world.Atlas.QueryPosition(pos)
assert.Equal(t, roveapi.Object_RockSmall, obj.Type, "Stash failed to remove object from atlas")
// Check we don't have it
inv, err := world.RoverInventory(a)
assert.NoError(t, err, "Failed to get inventory")
assert.Equal(t, rover.Capacity, len(inv))
// Check that this didn't reduce the charge
info, err := world.GetRover(a)
assert.NoError(t, err, "Failed to get rover")
assert.Equal(t, info.MaximumCharge, info.Charge, "Rover lost charge for non-stash")
}
func TestWorld_RoverDamage(t *testing.T) {
world := NewWorld(2)
a, err := world.SpawnRover()
assert.NoError(t, err)
pos := maths.Vector{
@ -141,16 +215,15 @@ func TestWorld_RoverDamage(t *testing.T) {
Y: 0.0,
}
world.Atlas.SetObject(pos, Object{Type: roveapi.Object_ObjectUnknown})
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
info, err := world.GetRover(a)
assert.NoError(t, err, "couldn't get rover info")
world.Atlas.SetObject(maths.Vector{X: 0.0, Y: 1.0}, Object{Type: roveapi.Object_RockLarge})
world.Atlas.SetObject(maths.Vector{X: 0.0, Y: 1.0}, atlas.Object{Type: roveapi.Object_RockLarge})
vec, err := world.TryMoveRover(a, roveapi.Bearing_North)
vec, err := world.MoveRover(a, roveapi.Bearing_North)
assert.NoError(t, err, "Failed to move rover")
assert.Equal(t, pos, vec, "Rover managed to move into large rock")
@ -158,29 +231,102 @@ func TestWorld_RoverDamage(t *testing.T) {
assert.NoError(t, err, "couldn't get rover info")
assert.Equal(t, info.Integrity-1, newinfo.Integrity, "rover should have lost integrity")
assert.Contains(t, newinfo.Logs[len(newinfo.Logs)-1].Text, "collision", "Rover logs should contain the collision")
// Keep moving to damage the rover
for i := 0; i < info.Integrity-1; i++ {
vec, err := world.TryMoveRover(a, roveapi.Bearing_North)
assert.NoError(t, err, "Failed to move rover")
assert.Equal(t, pos, vec, "Rover managed to move into large rock")
}
// Tick the world to check for rover deaths
world.Tick()
func TestWorld_RoverRepair(t *testing.T) {
world := NewWorld(2)
a, err := world.SpawnRover()
assert.NoError(t, err)
// Rover should have been destroyed now
_, err = world.GetRover(a)
assert.Error(t, err)
pos := maths.Vector{
X: 0.0,
Y: 0.0,
}
world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_ObjectUnknown})
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
originalInfo, err := world.GetRover(a)
assert.NoError(t, err, "couldn't get rover info")
// Pick up something to repair with
world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_RockSmall})
o, err := world.RoverStash(a)
assert.NoError(t, err, "Failed to stash")
assert.Equal(t, roveapi.Object_RockSmall, o, "Failed to get correct object")
world.Atlas.SetObject(maths.Vector{X: 0.0, Y: 1.0}, atlas.Object{Type: roveapi.Object_RockLarge})
// Try and bump into the rock
vec, err := world.MoveRover(a, roveapi.Bearing_North)
assert.NoError(t, err, "Failed to move rover")
assert.Equal(t, pos, vec, "Rover managed to move into large rock")
newinfo, err := world.GetRover(a)
assert.NoError(t, err, "couldn't get rover info")
assert.Equal(t, originalInfo.Integrity-1, newinfo.Integrity, "rover should have lost integrity")
err = world.ExecuteCommand(&Command{Command: roveapi.CommandType_repair}, a)
assert.NoError(t, err, "Failed to repair rover")
newinfo, err = world.GetRover(a)
assert.NoError(t, err, "couldn't get rover info")
assert.Equal(t, originalInfo.Integrity, newinfo.Integrity, "rover should have gained integrity")
assert.Contains(t, newinfo.Logs[len(newinfo.Logs)-1].Text, "repair", "Rover logs should contain the repair")
// Check again that it can't repair past the max
world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_RockSmall})
o, err = world.RoverStash(a)
assert.NoError(t, err, "Failed to stash")
assert.Equal(t, roveapi.Object_RockSmall, o, "Failed to get correct object")
err = world.ExecuteCommand(&Command{Command: roveapi.CommandType_repair}, a)
assert.NoError(t, err, "Failed to repair rover")
newinfo, err = world.GetRover(a)
assert.NoError(t, err, "couldn't get rover info")
assert.Equal(t, originalInfo.Integrity, newinfo.Integrity, "rover should have kept the same integrity")
}
func TestWorld_Charge(t *testing.T) {
world := NewWorld(4)
a, err := world.SpawnRover()
assert.NoError(t, err)
// Get the rover information
rover, err := world.GetRover(a)
assert.NoError(t, err, "Failed to get rover information")
assert.Equal(t, rover.MaximumCharge, rover.Charge, "Rover should start with maximum charge")
// Use up all the charge
for i := 0; i < rover.MaximumCharge; i++ {
// Get the initial position
initialPos, err := world.RoverPosition(a)
assert.NoError(t, err, "Failed to get position for rover")
// Ensure the path ahead is empty
world.Atlas.SetTile(initialPos.Added(maths.BearingToVector(roveapi.Bearing_North)), roveapi.Tile_Rock)
world.Atlas.SetObject(initialPos.Added(maths.BearingToVector(roveapi.Bearing_North)), atlas.Object{Type: roveapi.Object_ObjectUnknown})
// Try and move north (along unblocked path)
newPos, err := world.MoveRover(a, roveapi.Bearing_North)
assert.NoError(t, err, "Failed to set position for rover")
assert.Equal(t, initialPos.Added(maths.BearingToVector(roveapi.Bearing_North)), newPos, "Failed to correctly move position for rover")
// Ensure rover lost charge
rover, err := world.GetRover(a)
assert.NoError(t, err, "Failed to get rover information")
assert.Equal(t, rover.MaximumCharge-(i+1), rover.Charge, "Rover should have lost charge")
}
_, obj := world.Atlas.QueryPosition(info.Pos)
assert.Equal(t, roveapi.Object_RoverDormant, obj.Type)
}
func TestWorld_Daytime(t *testing.T) {
world := NewWorld(1)
a, err := world.SpawnRover("")
a, err := world.SpawnRover()
assert.NoError(t, err)
// Remove rover charge
@ -196,7 +342,7 @@ func TestWorld_Daytime(t *testing.T) {
// Loop for half the day
for i := 0; i < world.TicksPerDay/2; i++ {
assert.True(t, world.Daytime())
world.Tick()
world.ExecuteCommandQueues()
}
// Remove rover charge again
@ -212,22 +358,20 @@ func TestWorld_Daytime(t *testing.T) {
// Loop for half the day
for i := 0; i < world.TicksPerDay/2; i++ {
assert.False(t, world.Daytime())
world.Tick()
world.ExecuteCommandQueues()
}
}
func TestWorld_Broadcast(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover("")
a, err := world.SpawnRover()
assert.NoError(t, err)
b, err := world.SpawnRover("")
b, err := world.SpawnRover()
assert.NoError(t, err)
// Warp rovers near to eachother
world.Atlas.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
world.Atlas.SetObject(maths.Vector{X: 1, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(a, maths.Vector{X: 0, Y: 0}))
assert.NoError(t, world.WarpRover(b, maths.Vector{X: 1, Y: 0}))
@ -246,7 +390,7 @@ func TestWorld_Broadcast(t *testing.T) {
assert.Contains(t, rb.Logs[len(rb.Logs)-1].Text, "ABC", "Rover A should have logged it's broadcast")
// Warp B outside of the range of A
world.Atlas.SetObject(maths.Vector{X: ra.Range, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
world.Atlas.SetObject(maths.Vector{X: ra.Range, Y: 0}, atlas.Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(b, maths.Vector{X: ra.Range, Y: 0}))
// Broadcast from a again
@ -263,7 +407,7 @@ func TestWorld_Broadcast(t *testing.T) {
assert.Contains(t, rb.Logs[len(rb.Logs)-1].Text, "XYZ", "Rover A should have logged it's broadcast")
// Warp B outside of the range of A
world.Atlas.SetObject(maths.Vector{X: ra.Range + 1, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
world.Atlas.SetObject(maths.Vector{X: ra.Range + 1, Y: 0}, atlas.Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(b, maths.Vector{X: ra.Range + 1, Y: 0}))
// Broadcast from a again
@ -279,81 +423,3 @@ func TestWorld_Broadcast(t *testing.T) {
assert.NoError(t, err)
assert.Contains(t, rb.Logs[len(rb.Logs)-1].Text, "HJK", "Rover A should have logged it's broadcast")
}
func TestWorld_Sailing(t *testing.T) {
world := NewWorld(8)
world.Tick() // One initial tick to set the wind direction the first time
world.Wind = roveapi.Bearing_North // Set the wind direction to north
name, err := world.SpawnRover("")
assert.NoError(t, err)
// Warp the rover to 0,0 after clearing it
world.Atlas.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(name, maths.Vector{X: 0, Y: 0}))
s, err := world.RoverToggle(name)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_CatchingWind, s)
b, err := world.RoverTurn(name, roveapi.Bearing_North)
assert.NoError(t, err)
assert.Equal(t, roveapi.Bearing_North, b)
// Clear the space to the north
world.Atlas.SetObject(maths.Vector{X: 0, Y: 1}, Object{Type: roveapi.Object_ObjectUnknown})
// Tick the world and check we've moved not moved
world.Tick()
info, err := world.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, maths.Vector{Y: 0}, info.Pos)
// Loop a few more times
for i := 0; i < ticksPerNormalMove-2; i++ {
world.Tick()
info, err := world.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, maths.Vector{Y: 0}, info.Pos)
}
// Now check we've moved (after the TicksPerNormalMove number of ticks)
world.Tick()
info, err = world.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, maths.Vector{Y: 1}, info.Pos)
// Reset the world ticks back to stop any wind changes etc.
world.CurrentTicks = 1
// Face the rover south, into the wind
b, err = world.RoverTurn(name, roveapi.Bearing_South)
assert.NoError(t, err)
assert.Equal(t, roveapi.Bearing_South, b)
// Tick a bunch, we should never move
for i := 0; i < ticksPerNormalMove*2; i++ {
world.Tick()
info, err := world.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, maths.Vector{Y: 1}, info.Pos)
}
// Reset the world ticks back to stop any wind changes etc.
world.CurrentTicks = 1
world.Wind = roveapi.Bearing_SouthEast // Set up a south easternly wind
// Turn the rover perpendicular
b, err = world.RoverTurn(name, roveapi.Bearing_NorthEast)
assert.NoError(t, err)
assert.Equal(t, roveapi.Bearing_NorthEast, b)
// Clear a space
world.Atlas.SetObject(maths.Vector{X: 1, Y: 2}, Object{Type: roveapi.Object_ObjectUnknown})
// Now check we've moved immediately
world.Tick()
info, err = world.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, maths.Vector{X: 1, Y: 2}, info.Pos)
}

View file

@ -1,70 +0,0 @@
package rove
import (
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/ojrac/opensimplex-go"
)
// WorldGen describes a world gen algorythm
type WorldGen interface {
// GetTile generates a tile for a location
GetTile(v maths.Vector) roveapi.Tile
// GetObject generates an object for a location
GetObject(v maths.Vector) Object
}
// NoiseWorldGen returns a noise based world generator
type NoiseWorldGen struct {
// noise describes the noise function
noise opensimplex.Noise
}
// NewNoiseWorldGen creates a new noise based world generator
func NewNoiseWorldGen(seed int64) WorldGen {
return &NoiseWorldGen{
noise: opensimplex.New(seed),
}
}
const (
terrainNoiseScale = 15
rockNoiseScale = 3
partsNoiseScale = 2
)
// GetTile returns the chosen tile at a location
func (g *NoiseWorldGen) GetTile(v maths.Vector) roveapi.Tile {
t := g.noise.Eval2(float64(v.X)/terrainNoiseScale, float64(v.Y)/terrainNoiseScale)
switch {
case t > 0.5:
return roveapi.Tile_Gravel
case t > 0.05:
return roveapi.Tile_Sand
default:
return roveapi.Tile_Rock
}
}
// GetObject returns the chosen object at a location
func (g *NoiseWorldGen) GetObject(v maths.Vector) (obj Object) {
r := g.noise.Eval2(float64(v.X)/rockNoiseScale, float64(v.Y)/rockNoiseScale)
switch {
// Prioritise rocks
case r > 0.6:
obj.Type = roveapi.Object_RockLarge
case r > 0.5:
obj.Type = roveapi.Object_RockSmall
default:
// Otherwise, try some rover parts
p := g.noise.Eval2(float64(v.X)/partsNoiseScale, float64(v.Y)/partsNoiseScale)
switch {
case p > 0.7:
obj.Type = roveapi.Object_RoverParts
}
}
return obj
}

File diff suppressed because it is too large Load diff

View file

@ -89,47 +89,25 @@ message RegisterResponse {
// CommandType defines the type of a command to give to the rover
enum CommandType {
none = 0;
// Waits before performing the next command
wait = 1;
// Toggles the sails, either catching the wind, or charging from the sun
toggle = 2;
// Turns the rover in the specified bearing (requires bearing)
turn = 3;
// Move the rover in a direction, requires bearing
move = 1;
// Stashes item at current location in rover inventory
stash = 4;
stash = 2;
// Repairs the rover using an inventory object
repair = 5;
// Broadcasts a message to nearby rovers (requires data)
broadcast = 6;
// Salvages a neighboring dormant rover for parts
salvage = 7;
// Transfers remote control into dormant rover
transfer = 8;
// Upgrades a chosen rover specification using 5 rover parts
upgrade = 9;
repair = 3;
// Waits a tick to add more charge to the rover
recharge = 4;
// Broadcasts a message to nearby rovers
broadcast = 5;
}
// Bearing represents a compass direction
enum Bearing {
// BearingUnknown an unknown invalid bearing
BearingUnknown = 0;
North = 1;
NorthEast = 2;
East = 3;
SouthEast = 4;
South = 5;
SouthWest = 6;
West = 7;
NorthWest = 8;
}
// Describes the type of upgrade
enum RoverUpgrade {
RoverUpgradeUnknown = 0;
Range = 1;
Capacity = 2;
MaximumIntegrity = 3;
MaximumCharge = 4;
East = 2;
South = 3;
West = 4;
}
// Command is a single command for a rover
@ -137,18 +115,16 @@ message Command {
// The command type
CommandType command = 1;
// The number of times to repeat the command after the first
int32 repeat = 2;
oneof data {
// A bearing
// Used with MOVE
Bearing bearing = 2;
// broadcast - a simple message, must be composed of up to 3 printable ASCII
// glyphs (32-126)
bytes data = 3;
// move - the bearing for the rover to turn to
Bearing bearing = 4;
// upgrade - the upgrade to apply to the rover
RoverUpgrade upgrade = 5;
// A simple message, must be composed of printable ASCII glyphs (32-126)
// maximum of three characters
// Used with BROADCAST
bytes message = 3;
}
}
// CommandRequest describes a set of commands to be requested for the rover
@ -175,18 +151,11 @@ enum Object {
// RoverLive represents a live rover
RoverLive = 1;
// RoverDormant describes a dormant rover
RoverDormant = 2;
// RockSmall is a small stashable rock
RockSmall = 3;
RockSmall = 2;
// RockLarge is a large blocking rock
RockLarge = 4;
// RoverParts is one unit of rover parts, used for repairing and fixing the
// rover
RoverParts = 5;
RockLarge = 3;
}
enum Tile {
@ -247,76 +216,41 @@ message Vector {
int32 y = 2;
}
// SailPosition represents the position of the sola sail
enum SailPosition {
UnknownSailPosition = 0;
// CatchingWind means the sail is catching the wind and moving the rover
CatchingWind = 1;
// SolarCharging means the sail is facing the sun and charging
SolarCharging = 2;
}
message RoverSpecifications {
// StatusResponse is the response given to a status request
message StatusResponse {
// The name of the rover
string name = 1;
// Position of the rover in world coordinates
Vector position = 2;
// The range of this rover's radar and broadcasting
int32 range = 2;
// The capacity of the inventory
int32 capacity = 3;
// The maximum health of the rover
int32 maximumIntegrity = 4;
// The max energy the rover can store
int32 maximumCharge = 5;
}
message RoverStatus {
// The current direction of the rover
Bearing bearing = 1;
// The current position of the sails
SailPosition sailPosition = 2;
int32 range = 3;
// The items in the rover inventory
bytes inventory = 3;
bytes inventory = 4;
// The capacity of the inventory
int32 capacity = 5;
// The current health of the rover
int32 integrity = 4;
int32 integrity = 6;
// The maximum health of the rover
int32 maximumIntegrity = 7;
// The energy stored in the rover
int32 charge = 5;
int32 charge = 8;
// The max energy the rover can store
int32 maximumCharge = 9;
// The set of currently incoming commands for this tick
repeated Command incomingCommands = 10;
// The set of currently queued commands
repeated Command queuedCommands = 6;
}
message RoverReadings {
// Position of the rover in world coordinates
Vector position = 1;
// The current wind direction
Bearing wind = 2;
repeated Command queuedCommands = 11;
// The most recent logs
repeated Log logs = 3;
}
// StatusResponse is the response given to a status request
message StatusResponse {
// The static rover information
RoverSpecifications spec = 1;
// Current rover status
RoverStatus status = 2;
// Current rover readings
RoverReadings readings = 3;
repeated Log logs = 12;
}