Compare commits

...

170 commits

Author SHA1 Message Date
f29b189a42 Update README.md
Some checks failed
Docker / build (push) Has been cancelled
Tests / Build and Test (push) Has been cancelled
2024-09-27 10:43:28 +01:00
Marc Di Luzio
fe8029a4b3
Merge pull request #43 from mdiluz/gameplay-improvements
Assorted gameplay improvements from play testing
2020-08-02 12:52:29 +01:00
35b25dde98 Add the upgrade command to the cmdline client 2020-08-02 12:49:15 +01:00
804f82dd20 Fix up commandline interface for repeat commands to go after the command 2020-08-02 12:43:44 +01:00
4e4af1a1be Add a test for the upgrade command 2020-08-02 12:15:49 +01:00
b114b68ff7 Add upgrade command code 2020-08-02 12:03:12 +01:00
1200b0a2a2 Add "upgrade" command to use the rover parts 2020-08-02 11:39:28 +01:00
6a44633d40 Add rover parts to the cmdline pretty printer 2020-08-02 11:28:14 +01:00
636f0ed773 Spawn rover parts a little more frequently 2020-08-01 11:26:10 +01:00
018c122861 Stop spawning dormant rovers in the world 2020-08-01 11:24:53 +01:00
e66b899e2a Increase the base rover range to 10 2020-08-01 11:15:34 +01:00
70f041ae5d Spawn rover parts in the world 2020-08-01 11:09:15 +01:00
d7bda3f607
Merge pull request #42 from mdiluz/tls
Add TLS to server-client communications
2020-07-26 23:57:35 +01:00
94767f06d3 Fix to disable TLS in tests 2020-07-26 23:53:29 +01:00
500e0f9557 Skip the tls verify on the client side for now 2020-07-26 23:46:42 +01:00
4f2a7edeb1 Skip local tests, removing duplicate test runs 2020-07-26 23:42:23 +01:00
cf1dff2814 Make sure the client verifies the TLS 2020-07-26 23:41:52 +01:00
71a0ef9920 Use the fullchain.pem not the cert.pem as explained by letsencrypt 2020-07-26 23:36:34 +01:00
9b03ffb7f1 Add skip verify on the client for now 2020-07-26 23:30:09 +01:00
4821a90143 Pass the cert name into the docker deployment 2020-07-26 23:29:58 +01:00
ac3844fe7a Mount letsencrypt in the docker to read the certs 2020-07-26 23:26:36 +01:00
70d92c2d5e Add TLS to gRPC 2020-07-26 23:10:39 +01:00
bb50fae00b
Merge pull request #41 from mdiluz/add-reflection
Add gRPC reflection to the server
2020-07-26 22:53:59 +01:00
a321e5d72f Add gRPC reflection to the server 2020-07-26 22:48:48 +01:00
49ffa18f23
Merge pull request #40 from mdiluz/add-wait-command
Add wait command
2020-07-26 18:19:30 +01:00
e542999b91 Move test deployment out to it's own file 2020-07-26 18:11:01 +01:00
74e1cd4564 Convert number to repeat to avoid confusion 2020-07-26 18:02:06 +01:00
c0d4a809c9 Update command line client to allow specifying command number 2020-07-26 17:31:09 +01:00
1514603517 Allow number to be used in all commands 2020-07-26 17:19:04 +01:00
9dcbbee1a2
Merge pull request #39 from mdiluz/fix-help-text
Big update to help text and add a simple description
2020-07-26 17:12:22 +01:00
bcf71f0bf9 Add a "wait" command with a number 2020-07-26 17:09:47 +01:00
cec61a9db7 Big update to help text and add a simple description 2020-07-26 16:57:43 +01:00
47e2e13c49
Merge pull request #38 from mdiluz/world-gen-improve
World gen improve
2020-07-25 23:48:59 +01:00
6891ec8439 Adjust the terrain scale to be much larger 2020-07-25 23:39:32 +01:00
113090fbcb Fix bug where we were still placing psuedo-random objects down 2020-07-25 23:39:13 +01:00
a235f6a5f5
Merge pull request #37 from mdiluz/destroy-rovers
Destroy rovers
2020-07-25 23:24:46 +01:00
cd97220a11 Perform rover destruction during the main server tick 2020-07-25 23:18:21 +01:00
f9b3ce3edb Destroy the rover when it has 0 integrity 2020-07-25 23:13:05 +01:00
abf67f7f37
Merge pull request #35 from mdiluz/fix-starting-wind
Add the starting wind as north and ensure it's only updated the next day
2020-07-24 23:27:07 +01:00
5d4fd801c1 Add the starting wind as north and ensure it's only updated the next day 2020-07-24 23:22:46 +01:00
fa05e7f253
Merge pull request #34 from mdiluz/implement-salvage
Add rover salvage mechanics
2020-07-24 23:08:27 +01:00
1f5aa765f4 Merge remote-tracking branch 'origin/master' into implement-salvage 2020-07-24 23:05:02 +01:00
7be0f83c5e Fix golanglint missing error check 2020-07-24 22:58:59 +01:00
a0e04b7e3a Placed dormant world rovers randomly have better base stats 2020-07-24 22:56:35 +01:00
a93ce97b0b Only assign rovers to accounts if given an account 2020-07-24 22:54:06 +01:00
57621d169a Implement a test for transfer and fix bugs 2020-07-24 22:50:47 +01:00
fdfcc88540 Move the account registration into the world 2020-07-24 22:50:35 +01:00
6f2d67bd7c Tag rovers by the controlling account 2020-07-24 22:22:32 +01:00
e840b3e47b Move accountant into world 2020-07-24 20:06:06 +01:00
1e4d642038 Add rover transfer command and implementation
Need to swap the accounts
2020-07-24 20:01:35 +01:00
5b2ea533f4 Remove incorrect proto comment 2020-07-24 19:46:32 +01:00
edd3e5a6cb Fix test by removing object before warping rover 2020-07-24 19:42:34 +01:00
b0ff3eb6ea Remove redundant tests (covered in command_tests) 2020-07-24 19:39:33 +01:00
be36f0631b Add logs for failed stashes 2020-07-24 19:39:25 +01:00
c321f88d96 Add code for salvage command 2020-07-24 19:39:14 +01:00
7cccb4394f Fix the help text comment 2020-07-24 19:28:44 +01:00
524487ce14 Stop the dormant rover from being a blocking object 2020-07-24 19:27:54 +01:00
2f1ccdfdb9 Make repair require rover parts 2020-07-24 19:08:39 +01:00
ce6e10afbb Add salvage command to main.go man page 2020-07-24 19:08:03 +01:00
2c1bb80779 Add salvage command
Slight refactor to re-use command variables

	Also fixes the cmdline client turn command
2020-07-23 20:57:36 +01:00
a5ac809387
Merge pull request #33 from mdiluz/dev
General dev
2020-07-23 18:50:31 +01:00
41cd93e986 Add command test for no command as error 2020-07-23 18:41:12 +01:00
8cc3b9155e Implement broadcast command test 2020-07-23 18:40:32 +01:00
8a8a27ab47 Add a test for the repair command 2020-07-23 18:37:54 +01:00
3bfc91b8f6 Add command test for stashing 2020-07-23 16:58:17 +01:00
8279a08a37 Limit the log entries to a max number 2020-07-23 16:47:39 +01:00
46d904acc6 Rename and comment ticksPerNormalMove 2020-07-23 16:44:15 +01:00
be74183878
Merge pull request #32 from mdiluz/simplifystatus
Organise the status response into sub-sections
2020-07-23 16:33:10 +01:00
13e4d6a5e6 Remove an old log left in 2020-07-23 00:34:57 +01:00
f7192b3997 Organise the status response into sub-sections 2020-07-23 00:32:19 +01:00
4cbf4a9ab5
Merge pull request #31 from mdiluz/remove-doublequeue
Remove the incoming command streams, de-scopes and simplifies
2020-07-23 00:18:51 +01:00
2bc2477128 Remove the incoming command streams, de-scopes and simplifies 2020-07-23 00:13:28 +01:00
d49d034f0e
Merge pull request #30 from mdiluz/sailing
Swap out movement mechanics for sailing mechanics
2020-07-23 00:03:26 +01:00
6adc652cea Fix lint errors 2020-07-22 23:59:28 +01:00
1e18610c5c Set tick rate to 3 2020-07-22 23:57:22 +01:00
5d80cb2596 Implement sailing tests and fix into-the-wind bug 2020-07-22 23:50:42 +01:00
c89c5f6e74 Implement current wind direction and rover wind movement 2020-07-22 23:36:13 +01:00
c94ac68f44 Remove all json tags, simply not needed 2020-07-22 19:55:38 +01:00
075a502103 Pull the repair function out 2020-07-22 19:25:47 +01:00
9e42764398 Update the rover list to a list of pointers 2020-07-22 19:25:32 +01:00
447dbe3582 Fix a test comment 2020-07-22 19:24:55 +01:00
6b5d5abea1 Rename the world tick function and set the tick rate back to default 2020-07-22 19:24:36 +01:00
8667f55143 Simplify by making command streams pointer lists like in proto 2020-07-21 23:52:14 +01:00
f78efd1223 Add SailPosition to the rover and implement toggle command
This also converts the commands to use the proto type for simplicity
2020-07-21 23:44:06 +01:00
6f30b665c7 Make the bearings 8 directional 2020-07-21 22:58:59 +01:00
6c75f07aff Remove move and recharge commands in favor of toggle command for the sails 2020-07-21 22:57:43 +01:00
89123394cd
Merge pull request #28 from mdiluz/dormant-rovers
Dormant rovers
2020-07-19 19:20:03 +01:00
77212c7258 Fix logic for rover marshal test 2020-07-19 18:57:22 +01:00
bffad84181 Don't use noise for rover spawns for now 2020-07-19 18:57:12 +01:00
04d7a5a4ca Fill in the dormant rover log 2020-07-19 18:47:54 +01:00
211771121f Extract rover naming to rover.go 2020-07-19 18:47:44 +01:00
d3c480cb04 Add dormant rover data marshalled into obj data 2020-07-19 18:39:16 +01:00
1281713211 Clear locations before warp in tests 2020-07-19 18:35:12 +01:00
9130cf2517 Move atlas package into rove 2020-07-19 18:30:07 +01:00
c48274eb23 Small refactor in GetObject 2020-07-19 18:27:58 +01:00
ddbbdce1f8 Move default rover params to function 2020-07-19 18:23:11 +01:00
faa1271c5a Try and very rarely spawn a dormant rover 2020-07-19 13:57:45 +01:00
37d828c457 Rename the rock noise 2020-07-19 13:53:38 +01:00
959cbfa15a Combine the two noise functions, we only need one 2020-07-19 13:51:49 +01:00
c637ed37b9 Make the dormat rover blocking 2020-07-19 13:49:43 +01:00
87a9abcd12 Add a glyph for the dormant rover 2020-07-19 13:49:34 +01:00
1eba9a8652 Pull world gen out into interface 2020-07-19 13:41:47 +01:00
4f1a9c2c2b Re-order object types 2020-07-19 13:27:59 +01:00
713699687f Add a dormat rover data type 2020-07-19 13:27:38 +01:00
fd0992353d Add data to objects 2020-07-19 13:27:29 +01:00
e27398dbc0
Merge pull request #27 from mdiluz/bearings
Refactor bearing out to proto file
2020-07-19 13:16:59 +01:00
4b7510ffa1 Merge remote-tracking branch 'origin/master' into bearings 2020-07-19 13:13:36 +01:00
57f668ae54 Reinstate BearingFromString function 2020-07-19 13:13:09 +01:00
c13151b60f
Merge pull request #26 from mdiluz/glyphs
Refactor to move object/tile types into the proto file
2020-07-19 13:02:47 +01:00
db8ed0302d Merge glyphs branch 2020-07-19 13:01:25 +01:00
cd6a275bb9 Move code to internal cmd/main 2020-07-19 12:59:36 +01:00
4e0e55af88 Move bearing into proto file 2020-07-19 12:54:41 +01:00
3796ee09a3 Merge remote-tracking branch 'origin/master' into glyphs 2020-07-19 12:38:46 +01:00
0a8b8d5979
Merge pull request #25 from mdiluz/remove-diagonal-moves
Remove diagonal moves
2020-07-19 12:37:59 +01:00
e9188dbbf6 Auto-format proto file 2020-07-19 12:37:36 +01:00
da91d31649 MOve glyph code into client 2020-07-19 12:36:48 +01:00
4a89cb9d6e Move glyph functions out to the glyph file 2020-07-19 12:34:54 +01:00
7bdfa44fb6 Fix up the concept of "None" tiles and objects
Replace with "Unknown" which is effectively an invalid value
2020-07-19 12:33:11 +01:00
305f64ec38 Large refactor, move object and tile types out into the proto 2020-07-19 12:26:57 +01:00
24d4fe9273 Convert tiles and object types to typed consts 2020-07-19 11:59:14 +01:00
7e41ac0028 Rename the glyphs 2020-07-19 11:57:41 +01:00
f665436007 Convert objects and tiles to base ints 2020-07-19 11:54:11 +01:00
acdd019093 Add Glyph methods to convert to a glyph 2020-07-19 11:50:19 +01:00
53d6ad08d9 Rename Type to ObjectType 2020-07-19 11:47:19 +01:00
a0b811a659 Move glyph definitions into a central type 2020-07-19 11:46:37 +01:00
5814ac95b8 Make sure we fallthrough for the NES cases 2020-07-19 11:29:28 +01:00
c2e3c9f090 Reject move commands in non-cardinal directions 2020-07-19 11:26:08 +01:00
0e731df1a3
Merge pull request #24 from mdiluz/fix-southwest
Fix SW direction to go south rather than north
2020-07-11 17:31:40 +01:00
105d69bd7c Fix SW direction to go south rather than north 2020-07-11 17:25:38 +01:00
79598c3373
Merge pull request #23 from mdiluz/update-page
Small clean up
2020-07-10 21:43:25 +01:00
9593602dc5 Move flatpak file to data 2020-07-10 21:38:16 +01:00
327c246c8e Remove vscode viles 2020-07-10 21:37:19 +01:00
346bc940e2
Merge pull request #22 from mdiluz/update-page
Update the github page markdown
2020-07-10 21:36:00 +01:00
427db95b5b Fix grammar 2020-07-10 21:32:02 +01:00
a0946f9e58
Merge pull request #21 from mdiluz/mdiluz-patch-1
Update README.md
2020-07-10 21:30:46 +01:00
5c4d7469bb Update the github page markdown 2020-07-10 21:30:15 +01:00
3bad9f0122
Update README.md
Fix proto link and amend text
2020-07-10 20:59:26 +01:00
3f2ae97048
Merge pull request #20 from mdiluz/more-improvements
Various refactors and improvements
2020-07-10 20:39:40 +01:00
737534f739 Move roveapi into the proto dir 2020-07-10 19:01:41 +01:00
46f81abbd7 Move accounts into rove-server.internal 2020-07-10 18:57:57 +01:00
9ccb7ac019 Remove google proto files, no longer needed 2020-07-10 18:48:03 +01:00
f0ab2abf6e Move object into atlas 2020-07-10 18:39:33 +01:00
f40f7123d4 Move bearing into maths 2020-07-10 18:24:54 +01:00
5b1fe61097 Move vector into maths package 2020-07-10 18:22:59 +01:00
97d3be000b Re-order some World members 2020-07-10 18:14:32 +01:00
065f79cbb3 Fix warping to non-empty space 2020-07-10 18:11:38 +01:00
b534ac0516 Rename generated rove package to roveapi and the game package to rove 2020-07-10 18:09:51 +01:00
b451ea519d Make sure the accounts are saved as well 2020-07-10 17:21:59 +01:00
dc2800fa54 Move Accountant behind an interface 2020-07-10 17:09:47 +01:00
c1267829ac Clear out genproto mod require 2020-07-10 17:00:30 +01:00
d6349d081e Clear the tile before warping to it 2020-07-10 16:59:55 +01:00
9a7c48ae78 Make chunkBasedAtlas private 2020-07-10 16:56:17 +01:00
a0be8a463c Pull out chunk based atlas into new file 2020-07-10 16:54:43 +01:00
655e00b41f Don't expose Chunk externally 2020-07-10 16:52:31 +01:00
fb2ffc5252 Convert Atlas to an interface 2020-07-10 16:52:00 +01:00
5ac44d85cb Add a warning to missing DATA_PATH env 2020-07-10 16:38:49 +01:00
3665a62c6e
Merge pull request #19 from mdiluz/clean-proto
De-scope and proto clean
2020-07-10 09:38:33 +01:00
6c1ee311cd Delete unused files 2020-07-10 00:29:06 +01:00
fe6dae4c52 Update the generated file for rove.pb.go 2020-07-10 00:27:14 +01:00
0be6aa7c12 Clean, format and comment the rove.proto file 2020-07-10 00:26:49 +01:00
96a137ad2f Simplify - remove duplicate command types in favor of a better defined Command type in proto 2020-07-10 00:12:54 +01:00
7d780d05bd De-scope - remove swagger docs and http proxy
HTTP proxy was becoming annoying to maintain, and gRPC is easier to use anyway

swagger docs are just part of the fallout
2020-07-10 00:12:35 +01:00
59a1bdc14b
Update launch.json
Remove old comments
2020-07-09 23:15:33 +01:00
bffe539d77
Merge pull request #18 from mdiluz/fix-missing-message-in-reply
Fix missing broadcast message in status reply
2020-07-09 22:57:42 +01:00
b032fdbfe2 Fix missing broadcast message in status reply 2020-07-09 22:52:58 +01:00
23764a3fc3
Merge pull request #17 from mdiluz/add-broadcast-to-cmdline
Add broadcast command to the cmdline client
2020-07-09 22:44:51 +01:00
091469dd91 Add broadcast command to the cmdline client 2020-07-09 22:37:55 +01:00
db19e4a657
Merge pull request #16 from mdiluz/rover-logs-and-communication
Add "broadcast" command
2020-07-09 22:19:42 +01:00
e21023ec25 Update generated files 2020-07-09 22:12:13 +01:00
d4d82c38e0 Add "broadcast" command
This will send a readable ascii triplet to all rovers in range
2020-07-09 22:05:12 +01:00
57 changed files with 5154 additions and 5796 deletions

30
.vscode/launch.json vendored
View file

@ -1,30 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Rove Server",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/rove-server/main.go",
"cwd": "${workspaceFolder}",
"env": {
"WORDS_FILE": "${workspaceFolder}/data/words_alpha.txt",
},
"args": [],
},
{
"name": "Launch Rove",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/rove/main.go",
"cwd": "${workspaceFolder}",
"env": {},
"args": ["radar"],
}
]
}

View file

@ -1,5 +0,0 @@
{
"gopls": {
"buildFlags": ["-tags=integration"]
},
}

View file

@ -8,7 +8,6 @@ RUN go mod download
# Build the executables
RUN go build -o rove -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=$(git describe --always --long --dirty --tags)'" cmd/rove/main.go
RUN go build -o rove-server -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=$(git describe --always --long --dirty --tags)'" cmd/rove-server/main.go
RUN go build -o rove-server-rest-proxy cmd/rove-server-rest-proxy/main.go
CMD [ "./rove-server" ]

View file

@ -1,8 +0,0 @@
FROM quay.io/goswagger/swagger:latest
LABEL maintainer="Marc Di Luzio <marc.diluzio@gmail.com>"
WORKDIR /app
COPY . .
CMD [ "serve", "pkg/rove/rove.swagger.json", "--no-open" ]

View file

@ -3,7 +3,7 @@ VERSION := $(shell git describe --always --long --dirty --tags)
build:
@echo Running no-output build
go mod download
go build -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=${VERSION}'" ./...
go build -ldflags="-X 'github.com/mdiluz/rove/cmd/version.Version=${VERSION}'" ./...
install:
@echo Installing to GOPATH
@ -12,24 +12,15 @@ install:
gen:
@echo Installing go dependencies
go install \
github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger \
github.com/golang/protobuf/protoc-gen-go
go install github.com/golang/protobuf/protoc-gen-go
go mod download
@echo Generating rove server gRPC and gateway
protoc --proto_path proto --go_out=plugins=grpc,paths=source_relative:pkg/ proto/rove/rove.proto
protoc --proto_path proto --grpc-gateway_out=paths=source_relative:pkg/ proto/rove/rove.proto
@echo Generating rove server swagger
protoc --proto_path proto --swagger_out=logtostderr=true:pkg/ proto/rove/rove.proto
@echo Generating rove server gRPC
protoc --proto_path proto --go_out=plugins=grpc,paths=source_relative:proto/ proto/roveapi/roveapi.proto
test:
@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
@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
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,11 +1,8 @@
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](https://github.com/mdiluz/rove/blob/master/data/icon.svg)
![Rove](data/icon.svg)
Rove is an asynchronous nomadic game about exploring as part of a loose community.
This repository is a [living document](https://github.com/mdiluz/rove/tree/master/docs) of current game design, as well as source code for the `rove-server` deployment and the `rove` command line client.
See [rove.proto](https://github.com/mdiluz/rove/blob/master/proto/rove/rove.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](proto/roveapi/roveapi.proto) for the current server-client API.

View file

@ -1,150 +0,0 @@
// +build integration
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"testing"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/rove"
"github.com/stretchr/testify/assert"
)
// Server is a simple wrapper to a server path
type Server string
// Request performs a HTTP
func (s Server) Request(method, path string, in, out interface{}) error {
u := url.URL{
Scheme: "http",
Host: fmt.Sprintf("%s:8080", string(s)),
Path: path,
}
client := &http.Client{}
// Marshal the input
marshalled, err := json.Marshal(in)
if err != nil {
return err
}
// Set up the request
req, err := http.NewRequest(method, u.String(), bytes.NewReader(marshalled))
if err != nil {
return err
}
// Do the POST
req.Header.Set("Content-Type", "application/json")
if resp, err := client.Do(req); err != nil {
return err
} else if resp.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body to code %d", resp.StatusCode)
}
return fmt.Errorf("http returned status %d: %s", resp.StatusCode, string(body))
} else {
return json.NewDecoder(resp.Body).Decode(out)
}
}
var serv = Server(os.Getenv("ROVE_HTTP"))
func TestServer_ServerStatus(t *testing.T) {
req := &rove.ServerStatusRequest{}
resp := &rove.ServerStatusResponse{}
if err := serv.Request("GET", "server-status", req, resp); err != nil {
log.Fatal(err)
}
}
func TestServer_Register(t *testing.T) {
req := &rove.RegisterRequest{Name: uuid.New().String()}
resp := &rove.RegisterResponse{}
err := serv.Request("POST", "register", req, resp)
assert.NoError(t, err, "First register attempt should pass")
err = serv.Request("POST", "register", req, resp)
assert.Error(t, err, "Second identical register attempt should fail")
}
func TestServer_Command(t *testing.T) {
acc := uuid.New().String()
var resp rove.RegisterResponse
err := serv.Request("POST", "register", &rove.RegisterRequest{Name: acc}, &resp)
assert.NoError(t, err, "First register attempt should pass")
req := &rove.CommandRequest{
Account: &rove.Account{
Name: resp.Account.Name,
},
Commands: []*rove.Command{
{
Command: "move",
Bearing: "NE",
},
},
}
assert.Error(t, serv.Request("POST", "command", req, &rove.CommandResponse{}), "Commands should fail with no secret")
req.Account.Secret = resp.Account.Secret
assert.NoError(t, serv.Request("POST", "command", req, &rove.CommandResponse{}), "Commands should pass")
}
func TestServer_Radar(t *testing.T) {
acc := uuid.New().String()
var reg rove.RegisterResponse
err := serv.Request("POST", "register", &rove.RegisterRequest{Name: acc}, &reg)
assert.NoError(t, err, "First register attempt should pass")
resp := &rove.RadarResponse{}
req := &rove.RadarRequest{
Account: &rove.Account{
Name: reg.Account.Name,
},
}
assert.Error(t, serv.Request("POST", "radar", req, resp), "Radar should fail without secret")
req.Account.Secret = reg.Account.Secret
assert.NoError(t, serv.Request("POST", "radar", req, resp), "Radar should pass")
assert.NotZero(t, resp.Range, "Radar should return valid range")
w := int(resp.Range*2 + 1)
assert.Equal(t, w*w, len(resp.Tiles), "radar should return correct number of tiles")
assert.Equal(t, w*w, len(resp.Objects), "radar should return correct number of objects")
}
func TestServer_Status(t *testing.T) {
acc := uuid.New().String()
var reg rove.RegisterResponse
err := serv.Request("POST", "register", &rove.RegisterRequest{Name: acc}, &reg)
assert.NoError(t, err, "First register attempt should pass")
resp := &rove.StatusResponse{}
req := &rove.StatusRequest{
Account: &rove.Account{
Name: reg.Account.Name,
},
}
assert.Error(t, serv.Request("POST", "status", req, resp), "Status should fail without secret")
req.Account.Secret = reg.Account.Secret
assert.NoError(t, serv.Request("POST", "status", req, resp), "Status should pass")
assert.NotZero(t, resp.Range, "Rover should return valid range")
assert.NotZero(t, len(resp.Name), "Rover should return valid name")
assert.NotZero(t, resp.Position, "Rover should return valid position")
assert.NotZero(t, resp.Integrity, "Rover should have positive integrity")
}

View file

@ -1,51 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strconv"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
"github.com/mdiluz/rove/pkg/rove"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var endpoint = os.Getenv("ROVE_GRPC")
if len(endpoint) == 0 {
endpoint = "localhost:9090"
}
var iport int
var port = os.Getenv("PORT")
if len(port) == 0 {
iport = 8080
} else {
var err error
iport, err = strconv.Atoi(port)
if err != nil {
log.Fatal("$PORT not valid int")
}
}
// Create a new mux and register it with the gRPC endpoint
fmt.Printf("Hosting reverse-proxy on %d for %s\n", iport, endpoint)
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
if err := rove.RegisterRoveHandlerFromEndpoint(ctx, mux, endpoint, opts); err != nil {
log.Fatal(err)
}
// Start the HTTP server and proxy calls to gRPC endpoint when needed
if err := http.ListenAndServe(fmt.Sprintf(":%d", iport), mux); err != nil {
log.Fatal(err)
}
}

View file

@ -5,14 +5,13 @@ import (
"fmt"
"log"
"github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/rove"
"github.com/mdiluz/rove/pkg/version"
"github.com/mdiluz/rove/proto/roveapi"
)
// ServerStatus returns the status of the current server to a gRPC request
func (s *Server) ServerStatus(context.Context, *rove.ServerStatusRequest) (*rove.ServerStatusResponse, error) {
response := &rove.ServerStatusResponse{
func (s *Server) ServerStatus(context.Context, *roveapi.ServerStatusRequest) (*roveapi.ServerStatusResponse, error) {
response := &roveapi.ServerStatusResponse{
Ready: true,
Version: version.Version,
TickRate: int32(s.minutesPerTick),
@ -28,14 +27,14 @@ func (s *Server) ServerStatus(context.Context, *rove.ServerStatusRequest) (*rove
}
// Register registers a new account for a gRPC request
func (s *Server) Register(ctx context.Context, req *rove.RegisterRequest) (*rove.RegisterResponse, error) {
func (s *Server) Register(ctx context.Context, req *roveapi.RegisterRequest) (*roveapi.RegisterResponse, error) {
log.Printf("Handling register request: %s\n", req.Name)
if len(req.Name) == 0 {
return nil, fmt.Errorf("empty account name")
}
if acc, err := s.accountant.RegisterAccount(req.Name); err != nil {
if acc, err := s.world.Accountant.RegisterAccount(req.Name); err != nil {
return nil, err
} else if _, err := s.SpawnRoverForAccount(req.Name); err != nil {
@ -45,8 +44,8 @@ func (s *Server) Register(ctx context.Context, req *rove.RegisterRequest) (*rove
return nil, fmt.Errorf("internal server error when saving world: %s", err)
} else {
return &rove.RegisterResponse{
Account: &rove.Account{
return &roveapi.RegisterResponse{
Account: &roveapi.Account{
Name: acc.Name,
Secret: acc.Data["secret"],
},
@ -55,16 +54,16 @@ func (s *Server) Register(ctx context.Context, req *rove.RegisterRequest) (*rove
}
// Status returns rover information for a gRPC request
func (s *Server) Status(ctx context.Context, req *rove.StatusRequest) (response *rove.StatusResponse, err error) {
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.accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
if valid, err := s.world.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.accountant.GetValue(req.Account.Name, "rover"); err != nil {
} else if resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover"); err != nil {
return nil, err
} else if rover, err := s.world.GetRover(resp); err != nil {
@ -76,63 +75,58 @@ func (s *Server) Status(ctx context.Context, req *rove.StatusRequest) (response
inv = append(inv, byte(i.Type))
}
i, q := s.world.RoverCommands(resp)
var incoming, queued []*rove.Command
for _, i := range i {
incoming = append(incoming, &rove.Command{
Command: i.Command,
Bearing: i.Bearing,
})
}
for _, q := range q {
queued = append(queued, &rove.Command{
Command: q.Command,
Bearing: q.Bearing,
})
}
var logs []*rove.Log
queued := s.world.RoverCommands(resp)
var logs []*roveapi.Log
for _, log := range rover.Logs {
logs = append(logs, &rove.Log{
logs = append(logs, &roveapi.Log{
Text: log.Text,
Time: fmt.Sprintf("%d", log.Time.Unix()), // proto uses strings under the hood for 64bit ints anyway
})
}
response = &rove.StatusResponse{
Name: rover.Name,
Position: &rove.Vector{
X: int32(rover.Pos.X),
Y: int32(rover.Pos.Y),
response = &roveapi.StatusResponse{
Readings: &roveapi.RoverReadings{
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,
Integrity: int32(rover.Integrity),
Charge: int32(rover.Charge),
QueuedCommands: queued,
SailPosition: rover.SailPosition,
},
Range: int32(rover.Range),
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,
Logs: logs,
}
}
return response, nil
}
// Radar returns the radar information for a rover
func (s *Server) Radar(ctx context.Context, req *rove.RadarRequest) (*rove.RadarResponse, error) {
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.accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
if valid, err := s.world.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)
}
response := &rove.RadarResponse{}
response := &roveapi.RadarResponse{}
resp, err := s.accountant.GetValue(req.Account.Name, "rover")
resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover")
if err != nil {
return nil, err
@ -152,31 +146,24 @@ func (s *Server) Radar(ctx context.Context, req *rove.RadarRequest) (*rove.Radar
}
// Command issues commands to the world based on a gRPC request
func (s *Server) Command(ctx context.Context, req *rove.CommandRequest) (*rove.CommandResponse, error) {
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.accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
if valid, err := s.world.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.accountant.GetValue(req.Account.Name, "rover")
resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover")
if err != nil {
return nil, err
}
var cmds []game.Command
for _, c := range req.Commands {
cmds = append(cmds, game.Command{
Bearing: c.Bearing,
Command: c.Command})
}
if err := s.world.Enqueue(resp, cmds...); err != nil {
if err := s.world.Enqueue(resp, req.Commands...); err != nil {
return nil, err
}
return &rove.CommandResponse{}, nil
return &roveapi.CommandResponse{}, nil
}

View file

@ -4,16 +4,21 @@ import (
"fmt"
"log"
"net"
"os"
"path"
"sync"
"github.com/mdiluz/rove/pkg/accounts"
"github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/persistence"
"github.com/mdiluz/rove/pkg/rove"
"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
@ -26,10 +31,7 @@ const (
type Server struct {
// Internal state
world *game.World
// Accountant
accountant *accounts.Accountant
world *rove.World
// gRPC server
netListener net.Listener
@ -80,8 +82,7 @@ func NewServer(opts ...ServerOption) *Server {
address: "",
persistence: EphemeralData,
schedule: cron.New(),
world: game.NewWorld(32),
accountant: accounts.NewAccountant(),
world: rove.NewWorld(32),
}
// Apply all options
@ -108,8 +109,22 @@ func (s *Server) Initialise(fillWorld bool) (err error) {
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s.grpcServ = grpc.NewServer()
rove.RegisterRoveServer(s.grpcServ, s)
// 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...)
roveapi.RegisterRoveServer(s.grpcServ, s)
reflection.Register(s.grpcServ)
return nil
}
@ -132,8 +147,8 @@ func (s *Server) Run() {
log.Println("Executing server tick")
// Run the command queues
s.world.ExecuteCommandQueues()
// Tick the world
s.world.Tick()
// Save out the new world state
if err := s.SaveWorld(); err != nil {
@ -210,22 +225,10 @@ 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()
inst, err := s.world.SpawnRover(account)
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,6 +1,7 @@
package internal
import (
"os"
"testing"
)
@ -30,6 +31,7 @@ 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")
@ -45,6 +47,7 @@ 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

@ -52,7 +52,9 @@ func InnerMain() {
log.Printf("Initialising version %s...\n", version.Version)
// Set the persistence path
if err := persistence.SetPath(data); err != nil {
if len(data) == 0 {
log.Fatal("DATA_PATH not set")
} else if err := persistence.SetPath(data); err != nil {
log.Fatal(err)
}

View file

@ -0,0 +1,70 @@
package internal
import (
"log"
"github.com/mdiluz/rove/proto/roveapi"
)
// Glyph represents the text representation of something in the game
type Glyph byte
const (
// GlyphGroundRock is solid rock ground
GlyphGroundRock = Glyph('-')
// GlyphGroundGravel is loose rocks
GlyphGroundGravel = Glyph(':')
// GlyphGroundSand is sand
GlyphGroundSand = Glyph('~')
// 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')
// GlyphRockLarge is a large blocking rock
GlyphRockLarge = Glyph('O')
)
// TileGlyph returns the glyph for this tile type
func TileGlyph(t roveapi.Tile) Glyph {
switch t {
case roveapi.Tile_Rock:
return GlyphGroundRock
case roveapi.Tile_Gravel:
return GlyphGroundGravel
case roveapi.Tile_Sand:
return GlyphGroundSand
}
log.Fatalf("Unknown tile type: %c", t)
return 0
}
// ObjectGlyph returns the glyph for this object type
func ObjectGlyph(o roveapi.Object) Glyph {
switch o {
case roveapi.Object_RoverLive:
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)
return 0
}

View file

@ -1,6 +1,7 @@
package main
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
@ -8,16 +9,15 @@ import (
"os"
"path"
"path/filepath"
"strconv"
"time"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/objects"
"github.com/mdiluz/rove/pkg/rove"
"github.com/mdiluz/rove/cmd/rove/internal"
"github.com/mdiluz/rove/pkg/version"
"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,37 +25,44 @@ var defaultDataPath = path.Join(home, ".local/share/")
// Command usage
func printUsage() {
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 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 uses an inventory object to repair the rover")
fmt.Fprintln(os.Stderr, "\trecharge wait a tick to recharge the rover")
fmt.Fprintln(os.Stderr, "\nEnvironment")
fmt.Fprintln(os.Stderr, "\tROVE_USER_DATA path to user data, defaults to "+defaultDataPath)
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.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, "\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, "\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, "\tROVE_USER_DATA path to user data, defaults to "+defaultDataPath)
}
const gRPCport = 9090
// Account stores data for an account
type Account struct {
Name string `json:"name"`
Secret string `json:"secret"`
Name string
Secret string
}
// Config is used to store internal data
type Config struct {
Host string `json:"host,omitempty"`
Account Account `json:"account,omitempty"`
Host string
Account Account
}
// ConfigPath returns the configuration path
@ -66,7 +73,7 @@ func ConfigPath() string {
if len(override) > 0 {
datapath = override
}
datapath = path.Join(datapath, "rove.json")
datapath = path.Join(datapath, "roveapi.json")
return datapath
}
@ -125,6 +132,29 @@ func checkAccount(a Account) error {
return nil
}
// BearingFromString converts a string to a bearing
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
}
// InnerMain wraps the main function so we can test it
func InnerMain(command string, args ...string) error {
@ -158,19 +188,26 @@ 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), grpc.WithInsecure())
clientConn, err := grpc.Dial(fmt.Sprintf("%s:%d", config.Host, gRPCport), opts...)
if err != nil {
return err
}
var client = rove.NewRoveClient(clientConn)
var client = roveapi.NewRoveClient(clientConn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Handle all the commands
switch command {
case "server-status":
response, err := client.ServerStatus(ctx, &rove.ServerStatusRequest{})
response, err := client.ServerStatus(ctx, &roveapi.ServerStatusRequest{})
switch {
case err != nil:
return err
@ -188,7 +225,7 @@ func InnerMain(command string, args ...string) error {
return fmt.Errorf("must pass name to 'register'")
}
resp, err := client.Register(ctx, &rove.RegisterRequest{
resp, err := client.Register(ctx, &roveapi.RegisterRequest{
Name: args[0],
})
switch {
@ -209,34 +246,80 @@ func InnerMain(command string, args ...string) error {
}
// Iterate through each command
var commands []*rove.Command
var commands []*roveapi.Command
for i := 0; i < len(args); i++ {
var cmd *roveapi.Command
switch args[i] {
case "move":
case "turn":
i++
if len(args) == i {
return fmt.Errorf("move command must be passed bearing")
} else if _, err := bearing.FromString(args[i]); err != nil {
return err
return fmt.Errorf("turn command must be passed a compass 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,
}
case "broadcast":
i++
if len(args) == i {
return fmt.Errorf("broadcast command must be passed an ASCII triplet")
} 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{
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,
}
commands = append(commands,
&rove.Command{
Command: game.CommandMove,
Bearing: args[i],
},
)
default:
// By default just use the command literally
commands = append(commands,
&rove.Command{
Command: args[i],
},
)
cmd = &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, &rove.CommandRequest{
Account: &rove.Account{
_, err := client.Command(ctx, &roveapi.CommandRequest{
Account: &roveapi.Account{
Name: config.Account.Name,
Secret: config.Account.Secret,
},
@ -256,8 +339,8 @@ func InnerMain(command string, args ...string) error {
return err
}
response, err := client.Radar(ctx, &rove.RadarRequest{
Account: &rove.Account{
response, err := client.Radar(ctx, &roveapi.RadarRequest{
Account: &roveapi.Account{
Name: config.Account.Name,
Secret: config.Account.Secret,
},
@ -275,12 +358,10 @@ func InnerMain(command string, args ...string) error {
for i := 0; i < num; i++ {
t := response.Tiles[i+num*j]
o := response.Objects[i+num*j]
if o != byte(objects.None) {
fmt.Printf("%c", o)
} else if t != byte(atlas.TileNone) {
fmt.Printf("%c", t)
if o != roveapi.Object_ObjectUnknown {
fmt.Printf("%c", internal.ObjectGlyph(o))
} else {
fmt.Printf(" ")
fmt.Printf("%c", internal.TileGlyph(t))
}
}
@ -293,8 +374,8 @@ func InnerMain(command string, args ...string) error {
return err
}
response, err := client.Status(ctx, &rove.StatusRequest{
Account: &rove.Account{
response, err := client.Status(ctx, &roveapi.StatusRequest{
Account: &roveapi.Account{
Name: config.Account.Name,
Secret: config.Account.Secret,
},
@ -322,6 +403,15 @@ 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,6 +13,7 @@ import (
)
func Test_InnerMain(t *testing.T) {
os.Setenv("NO_TLS", "1")
// Use temporary local user data
tmp, err := ioutil.TempDir(os.TempDir(), "rove-")
@ -50,10 +51,17 @@ func Test_InnerMain(t *testing.T) {
assert.Error(t, InnerMain("command"))
// Give it commands
assert.NoError(t, InnerMain("command", "move", "N"))
assert.NoError(t, InnerMain("command", "toggle"))
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", "move", "stash"))
assert.Error(t, InnerMain("command", "unknown"))
assert.Error(t, InnerMain("command", "broadcast"))
assert.Error(t, InnerMain("command", "upgrade"))
assert.Error(t, InnerMain("command", "1"))
}

32
docker-compose-test.yml Normal file
View file

@ -0,0 +1,32 @@
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

@ -4,16 +4,6 @@ volumes:
persistent-data:
services:
rove-docs:
build:
context: .
dockerfile: Dockerfile.docs
image: rove-docs:latest
ports:
- "80:80"
environment:
- PORT=80
rove-server:
build:
context: .
@ -25,36 +15,11 @@ services:
- PORT=9090
- DATA_PATH=/mnt/rove-server
- WORDS_FILE=data/words_alpha.txt
- TICK_RATE=5
- TICK_RATE=3
- CERT_NAME=${CERT_NAME}
volumes:
- persistent-data:/mnt/rove-server:rw
- /etc/letsencrypt/:/etc/letsencrypt/
command: [ "./rove-server"]
rove:
depends_on: [ rove-server, rove-docs ]
build:
context: .
dockerfile: Dockerfile
image: rove:latest
ports:
- "8080:8080"
environment:
- PORT=8080
- ROVE_GRPC=rove-server:9090
command: [ "./script/wait-for-it.sh", "rove-server:9090", "--", "./rove-server-rest-proxy" ]
rove-tests:
depends_on: [ rove ]
build:
context: .
dockerfile: Dockerfile
image: rove:latest
environment:
- ROVE_HTTP=rove
- ROVE_GRPC=rove-server
command: [ "./script/wait-for-it.sh", "rove:8080", "--", "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,67 +1,61 @@
Rove
=====
An asynchronous nomadic game about exploring a planet as part of a loose community.
Rove is an asynchronous nomadic game about exploring a planet as part of a loose community.
-------------------------------------------
## The Basics
## Core gameplay
### Core
Remotely explore the surface of a planet with an upgradable and customisable rover. Send commands to be executed asynchronously, view the rover's radar, and communicate and coordinate with other nearby rovers.
Control a rover on the surface of the planet using a remote control interface.
### Key Components
Commands are sent and happen asynchronously, and the rover feeds back information about position and surroundings, as well as photos.
### General
Movement is slow and sometimes dangerous.
Resources can be collected, and rovers recharge power during the day.
Hazards damage the rover. Resources can be spent to repair.
Spend resources to create and spawn a new improved rover a significant distance away, leaving the current one dormant.
"Dying" leaves the current rover dormant and assigns the users a new rover.
Players can repair dormant rovers to gain control of them, taking on their improvements and inventory.
### Multiplayer
Players can see each other and use very rudimentary signals.
Dormant rovers store full history of travel and owners, as well as their improvements and resources.
* Navigate an expansive world
* Collect resources to repair and upgrade
* Keep the rover batteries charged as you explore
* Help other players on their journey
* Explore north to discover more
-------------------------------------------
### Implementation
## Installing
* A server that receives the commands, sends out data, and handles interactions between players.
On Ubuntu:
```
$ snap install rove
```
* An app, or apps, that interface with the server to let you control and view rover information
Elsewhere (with [go](https://golang.org/doc/install) installed)
```
go get github.com/mdiluz/rove
cd $GOPATH/src/github.com/mdiluz/rove/
make install
```
-------------------------------------------
### To Solve
### Implementation Details
`rove-server` hosts the game world and a gRPC server to allow users to interact from any client.
`rove` is a basic example command-line client that allows for simple play, to explore it's usage, see the output of `rove help`
-------------------------------------------
### "Find the fun" issues to solve
* What kinds of progression/upgrades exist?
* How does the game encourage cooperation?
* How would the gameplay prevent griefing?
* What drives the exploration?
-------------------------------------------
### Key ideas left to integrate
Feeling “known for” something - the person who did X thing. Limit number of X things that can be done, possibly over time.
A significant aspect of failure - failing must be a focus of the game. Winning the game might actually be failing in some specific way.
A clear and well defined investment vs. payoff curve.
Not an infinite game, let the game have a point where youre done and can move on.
* Feeling “known for” something - the person who did X thing. Limit number of X things that can be done, possibly over time.
* A significant aspect of failure - failing must be a focus of the game. Winning the game might actually be failing in some specific way.
* A clear and well defined investment vs. payoff curve.
* Not an infinite game, let the game have a point where youre done and can move on.

1
go.mod
View file

@ -14,7 +14,6 @@ require (
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
golang.org/x/text v0.3.3 // indirect
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013
google.golang.org/grpc v1.30.0
google.golang.org/protobuf v1.25.0
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect

View file

@ -1,89 +1,28 @@
package accounts
import (
"fmt"
"time"
// Accountant decribes something that stores accounts and account values
type Accountant interface {
// RegisterAccount will register a new account and return it's info
RegisterAccount(name string) (acc Account, err error)
"github.com/google/uuid"
)
// AssignData stores a custom account key value pair
AssignData(account string, key string, value string) error
// GetValue returns custom account data for a specific key
GetValue(account string, key string) (string, error)
// VerifySecret will verify whether the account secret matches with the
VerifySecret(account string, secret string) (bool, error)
// GetSecret gets the secret associated with an account
GetSecret(account string) (string, error)
}
// Account represents a registered user
type Account struct {
// Name simply describes the account and must be unique
Name string `json:"name"`
Name string
// Data represents internal account data
Data map[string]string `json:"data"`
}
// Accountant manages a set of accounts
type Accountant struct {
Accounts map[string]Account `json:"accounts"`
}
// NewAccountant creates a new accountant
func NewAccountant() *Accountant {
return &Accountant{
Accounts: make(map[string]Account),
}
}
// RegisterAccount adds an account to the set of internal accounts
func (a *Accountant) RegisterAccount(name string) (acc Account, err error) {
// Set up the account info
acc.Name = name
acc.Data = make(map[string]string)
// Verify this acount isn't already registered
for _, a := range a.Accounts {
if a.Name == acc.Name {
return Account{}, fmt.Errorf("account name already registered: %s", a.Name)
}
}
// Set the creation time
acc.Data["created"] = time.Now().String()
// Create a secret
acc.Data["secret"] = uuid.New().String()
// Simply add the account to the map
a.Accounts[acc.Name] = acc
return
}
// VerifySecret verifies if an account secret is correct
func (a *Accountant) VerifySecret(account string, secret string) (bool, error) {
// Find the account matching the ID
if this, ok := a.Accounts[account]; ok {
return this.Data["secret"] == secret, nil
}
return false, fmt.Errorf("no account found for id: %s", account)
}
// AssignData assigns data to an account
func (a *Accountant) AssignData(account string, key string, value string) error {
// Find the account matching the ID
if this, ok := a.Accounts[account]; ok {
this.Data[key] = value
a.Accounts[account] = this
} else {
return fmt.Errorf("no account found for id: %s", account)
}
return nil
}
// GetValue gets the rover rover for the account
func (a *Accountant) GetValue(account string, key string) (string, error) {
// Find the account matching the ID
this, ok := a.Accounts[account]
if !ok {
return "", fmt.Errorf("no account found for id: %s", account)
}
return this.Data[key], nil
Data map[string]string
}

View file

@ -8,7 +8,7 @@ import (
func TestNewAccountant(t *testing.T) {
// Very basic verify here for now
accountant := NewAccountant()
accountant := NewSimpleAccountant()
if accountant == nil {
t.Error("Failed to create accountant")
}
@ -16,7 +16,7 @@ func TestNewAccountant(t *testing.T) {
func TestAccountant_RegisterAccount(t *testing.T) {
accountant := NewAccountant()
accountant := NewSimpleAccountant()
// Start by making two accounts
@ -44,10 +44,7 @@ func TestAccountant_RegisterAccount(t *testing.T) {
}
func TestAccountant_AssignGetData(t *testing.T) {
accountant := NewAccountant()
if len(accountant.Accounts) != 0 {
t.Error("New accountant created with non-zero account number")
}
accountant := NewSimpleAccountant()
name := uuid.New().String()
a, err := accountant.RegisterAccount(name)

View file

@ -0,0 +1,90 @@
package accounts
import (
"fmt"
"time"
"github.com/google/uuid"
)
// SimpleAccountant manages a set of accounts
type SimpleAccountant struct {
Accounts map[string]Account
}
// NewSimpleAccountant creates a new accountant
func NewSimpleAccountant() Accountant {
return &SimpleAccountant{
Accounts: make(map[string]Account),
}
}
// RegisterAccount adds an account to the set of internal accounts
func (a *SimpleAccountant) RegisterAccount(name string) (acc Account, err error) {
// Set up the account info
acc.Name = name
acc.Data = make(map[string]string)
// Verify this acount isn't already registered
for _, a := range a.Accounts {
if a.Name == acc.Name {
return Account{}, fmt.Errorf("account name already registered: %s", a.Name)
}
}
// Set the creation time
acc.Data["created"] = time.Now().String()
// Create a secret
acc.Data["secret"] = uuid.New().String()
// Simply add the account to the map
a.Accounts[acc.Name] = acc
return
}
// VerifySecret verifies if an account secret is correct
func (a *SimpleAccountant) VerifySecret(account string, secret string) (bool, error) {
// Find the account matching the ID
if this, ok := a.Accounts[account]; ok {
return this.Data["secret"] == secret, nil
}
return false, fmt.Errorf("no account found for id: %s", account)
}
// GetSecret gets the internal secret
func (a *SimpleAccountant) GetSecret(account string) (string, error) {
// Find the account matching the ID
if this, ok := a.Accounts[account]; ok {
return this.Data["secret"], nil
}
return "", fmt.Errorf("no account found for id: %s", account)
}
// AssignData assigns data to an account
func (a *SimpleAccountant) AssignData(account string, key string, value string) error {
// Find the account matching the ID
if this, ok := a.Accounts[account]; ok {
this.Data[key] = value
a.Accounts[account] = this
} else {
return fmt.Errorf("no account found for id: %s", account)
}
return nil
}
// GetValue gets the rover rover for the account
func (a *SimpleAccountant) GetValue(account string, key string) (string, error) {
// Find the account matching the ID
this, ok := a.Accounts[account]
if !ok {
return "", fmt.Errorf("no account found for id: %s", account)
}
return this.Data[key], nil
}

View file

@ -1,274 +0,0 @@
package atlas
import (
"fmt"
"testing"
"github.com/mdiluz/rove/pkg/objects"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
)
func TestAtlas_NewAtlas(t *testing.T) {
a := NewAtlas(1)
assert.NotNil(t, a)
assert.Equal(t, 1, a.ChunkSize)
assert.Equal(t, 1, len(a.Chunks)) // Should start empty
}
func TestAtlas_toChunk(t *testing.T) {
a := NewAtlas(1)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(vector.Vector{X: -1, Y: -1})
a.QueryPosition(vector.Vector{X: 0, Y: 0})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
chunkID := a.worldSpaceToChunkIndex(vector.Vector{X: 0, Y: 0})
assert.Equal(t, 3, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 0, Y: -1})
assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -1, Y: 0})
assert.Equal(t, 2, chunkID)
a = NewAtlas(2)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(vector.Vector{X: -2, Y: -2})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 3, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: -2})
assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -2, Y: -2})
assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -2, Y: 1})
assert.Equal(t, 2, chunkID)
a = NewAtlas(2)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(vector.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(vector.Vector{X: -3, Y: -3})
assert.Equal(t, 4*4, len(a.Chunks))
// Chunks should look like:
// 12| 13|| 14| 15
// ----------------
// 8 | 9 || 10| 11
// ================
// 4 | 5 || 6 | 7
// ----------------
// 0 | 1 || 2 | 3
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: 3})
assert.Equal(t, 14, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: -3})
assert.Equal(t, 2, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 5, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -2, Y: 2})
assert.Equal(t, 13, chunkID)
a = NewAtlas(3)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(vector.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// || 2| 3
// -------
// || 0| 1
// =======
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 3, Y: 1})
assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: 4})
assert.Equal(t, 2, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 5, Y: 5})
assert.Equal(t, 3, chunkID)
}
func TestAtlas_toWorld(t *testing.T) {
a := NewAtlas(1)
assert.NotNil(t, a)
// Get a tile to spawn some chunks
a.QueryPosition(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
assert.Equal(t, vector.Vector{X: -1, Y: -1}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, vector.Vector{X: 0, Y: -1}, a.chunkOriginInWorldSpace(1))
a = NewAtlas(2)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(vector.Vector{X: -2, Y: -2})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
assert.Equal(t, vector.Vector{X: -2, Y: -2}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, vector.Vector{X: -2, Y: 0}, a.chunkOriginInWorldSpace(2))
a = NewAtlas(2)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(vector.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(vector.Vector{X: -3, Y: -3})
assert.Equal(t, 4*4, len(a.Chunks))
// Chunks should look like:
// 12| 13|| 14| 15
// ----------------
// 8 | 9 || 10| 11
// ================
// 4 | 5 || 6 | 7
// ----------------
// 0 | 1 || 2 | 3
assert.Equal(t, vector.Vector{X: -4, Y: -4}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, vector.Vector{X: 2, Y: -2}, a.chunkOriginInWorldSpace(7))
a = NewAtlas(3)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(vector.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// || 2| 3
// -------
// || 0| 1
// =======
assert.Equal(t, vector.Vector{X: 0, Y: 0}, a.chunkOriginInWorldSpace(0))
}
func TestAtlas_GetSetTile(t *testing.T) {
a := NewAtlas(10)
assert.NotNil(t, a)
// Set the origin tile to 1 and test it
a.SetTile(vector.Vector{X: 0, Y: 0}, 1)
tile, _ := a.QueryPosition(vector.Vector{X: 0, Y: 0})
assert.Equal(t, byte(1), tile)
// Set another tile to 1 and test it
a.SetTile(vector.Vector{X: 5, Y: -2}, 2)
tile, _ = a.QueryPosition(vector.Vector{X: 5, Y: -2})
assert.Equal(t, byte(2), tile)
}
func TestAtlas_GetSetObject(t *testing.T) {
a := NewAtlas(10)
assert.NotNil(t, a)
// Set the origin tile to 1 and test it
a.SetObject(vector.Vector{X: 0, Y: 0}, objects.Object{Type: objects.LargeRock})
_, obj := a.QueryPosition(vector.Vector{X: 0, Y: 0})
assert.Equal(t, objects.Object{Type: objects.LargeRock}, obj)
// Set another tile to 1 and test it
a.SetObject(vector.Vector{X: 5, Y: -2}, objects.Object{Type: objects.SmallRock})
_, obj = a.QueryPosition(vector.Vector{X: 5, Y: -2})
assert.Equal(t, objects.Object{Type: objects.SmallRock}, obj)
}
func TestAtlas_Grown(t *testing.T) {
// Start with a small example
a := NewAtlas(2)
assert.NotNil(t, a)
assert.Equal(t, 1, len(a.Chunks))
// Set a few tiles to values
a.SetTile(vector.Vector{X: 0, Y: 0}, 1)
a.SetTile(vector.Vector{X: -1, Y: -1}, 2)
a.SetTile(vector.Vector{X: 1, Y: -2}, 3)
// Check tile values
tile, _ := a.QueryPosition(vector.Vector{X: 0, Y: 0})
assert.Equal(t, byte(1), tile)
tile, _ = a.QueryPosition(vector.Vector{X: -1, Y: -1})
assert.Equal(t, byte(2), tile)
tile, _ = a.QueryPosition(vector.Vector{X: 1, Y: -2})
assert.Equal(t, byte(3), tile)
tile, _ = a.QueryPosition(vector.Vector{X: 0, Y: 0})
assert.Equal(t, byte(1), tile)
tile, _ = a.QueryPosition(vector.Vector{X: -1, Y: -1})
assert.Equal(t, byte(2), tile)
tile, _ = a.QueryPosition(vector.Vector{X: 1, Y: -2})
assert.Equal(t, byte(3), tile)
}
func TestAtlas_GetSetCorrect(t *testing.T) {
// Big stress test to ensure we do actually properly expand for all reasonable values
for i := 1; i <= 4; i++ {
for x := -i * 2; x < i*2; x++ {
for y := -i * 2; y < i*2; y++ {
a := NewAtlas(i)
assert.NotNil(t, a)
assert.Equal(t, 1, len(a.Chunks))
pos := vector.Vector{X: x, Y: y}
a.SetTile(pos, TileRock)
a.SetObject(pos, objects.Object{Type: objects.LargeRock})
tile, obj := a.QueryPosition(pos)
assert.Equal(t, TileRock, Tile(tile))
assert.Equal(t, objects.Object{Type: objects.LargeRock}, obj)
}
}
}
}
func TestAtlas_WorldGen(t *testing.T) {
a := NewAtlas(8)
// Spawn a large world
_, _ = a.QueryPosition(vector.Vector{X: 20, Y: 20})
// Print out the world for manual evaluation
num := 20
for j := num - 1; j >= 0; j-- {
for i := 0; i < num; i++ {
t, o := a.QueryPosition(vector.Vector{X: i, Y: j})
if o.Type != objects.None {
fmt.Printf("%c", o.Type)
} else if t != byte(TileNone) {
fmt.Printf("%c", t)
} else {
fmt.Printf(" ")
}
}
fmt.Print("\n")
}
}

View file

@ -1,84 +0,0 @@
package bearing
import (
"fmt"
"strings"
"github.com/mdiluz/rove/pkg/vector"
)
// Bearing describes a compass direction
type Bearing int
const (
// North describes a 0,1 vector
North Bearing = iota
// NorthEast describes a 1,1 vector
NorthEast
// East describes a 1,0 vector
East
// SouthEast describes a 1,-1 vector
SouthEast
// South describes a 0,-1 vector
South
// SouthWest describes a -1,-1 vector
SouthWest
// West describes a -1,0 vector
West
// NorthWest describes a -1,1 vector
NorthWest
)
// bearingString simply describes the strings associated with a direction
type bearingString struct {
Long string
Short string
}
// bearingStrings is the set of strings for each direction
var bearingStrings = []bearingString{
{"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 Bearing) String() string {
return bearingStrings[d].Long
}
// ShortString converts a Direction to a short string version
func (d Bearing) ShortString() string {
return bearingStrings[d].Short
}
// FromString gets the Direction from a string
func FromString(s string) (Bearing, error) {
for i, d := range bearingStrings {
if strings.EqualFold(d.Long, s) || strings.EqualFold(d.Short, s) {
return Bearing(i), nil
}
}
return -1, fmt.Errorf("unknown bearing: %s", s)
}
var bearingVectors = []vector.Vector{
{X: 0, Y: 1}, // N
{X: 1, Y: 1}, // NE
{X: 1, Y: 0}, // E
{X: 1, Y: -1}, // SE
{X: 0, Y: -1}, // S
{X: -1, Y: 1}, // SW
{X: -1, Y: 0}, // W
{X: -1, Y: 1}, // NW
}
// Vector converts a Direction to a Vector
func (d Bearing) Vector() vector.Vector {
return bearingVectors[d]
}

View file

@ -1,32 +0,0 @@
package bearing
import (
"testing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
)
func TestDirection(t *testing.T) {
dir := North
assert.Equal(t, "North", dir.String())
assert.Equal(t, "N", dir.ShortString())
assert.Equal(t, vector.Vector{X: 0, Y: 1}, dir.Vector())
dir, err := FromString("N")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = FromString("n")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = FromString("north")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = FromString("NorthWest")
assert.NoError(t, err)
assert.Equal(t, NorthWest, dir)
}

View file

@ -1,26 +0,0 @@
package game
const (
// CommandMove Moves the rover in the chosen bearing
CommandMove = "move"
// CommandStash Will attempt to stash the object at the current location
CommandStash = "stash"
// CommandRepair Will attempt to repair the rover with an inventory object
CommandRepair = "repair"
// CommandRecharge Will use one tick to charge the rover
CommandRecharge = "recharge"
)
// Command represends a single command to execute
type Command struct {
Command string `json:"command"`
// Used in the move command
Bearing string `json:"bearing,omitempty"`
}
// CommandStream is a list of commands to execute in order
type CommandStream []Command

View file

@ -1,69 +0,0 @@
package game
import (
"testing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
)
func TestCommand_Move(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover()
assert.NoError(t, err)
pos := vector.Vector{
X: 1.0,
Y: 2.0,
}
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
// Try the move command
moveCommand := Command{Command: CommandMove, Bearing: "N"}
assert.NoError(t, world.Enqueue(a, moveCommand), "Failed to execute move command")
// Tick the world
world.EnqueueAllIncoming()
world.ExecuteCommandQueues()
newPos, err := world.RoverPosition(a)
assert.NoError(t, err, "Failed to set position for rover")
pos.Add(vector.Vector{X: 0.0, Y: 1})
assert.Equal(t, pos, newPos, "Failed to correctly set position for rover")
}
func TestCommand_Recharge(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover()
assert.NoError(t, err)
pos := vector.Vector{
X: 1.0,
Y: 2.0,
}
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
// Move to use up some charge
moveCommand := Command{Command: CommandMove, Bearing: "N"}
assert.NoError(t, world.Enqueue(a, moveCommand), "Failed to queue move command")
// Tick the world
world.EnqueueAllIncoming()
world.ExecuteCommandQueues()
rover, _ := world.GetRover(a)
assert.Equal(t, rover.MaximumCharge-1, rover.Charge)
chargeCommand := Command{Command: CommandRecharge}
assert.NoError(t, world.Enqueue(a, chargeCommand), "Failed to queue recharge command")
// Tick the world
world.EnqueueAllIncoming()
world.ExecuteCommandQueues()
rover, _ = world.GetRover(a)
assert.Equal(t, rover.MaximumCharge, rover.Charge)
}

View file

@ -1,64 +0,0 @@
package game
import (
"fmt"
"log"
"time"
"github.com/mdiluz/rove/pkg/objects"
"github.com/mdiluz/rove/pkg/vector"
)
// RoverLogEntry describes a single log entry for the rover
type RoverLogEntry struct {
// Time is the timestamp of the entry
Time time.Time `json:"time"`
// Text contains the information in this log entry
Text string `json:"text"`
}
// Rover describes a single rover in the world
type Rover struct {
// Unique name of this rover
Name string `json:"name"`
// Pos represents where this rover is in the world
Pos vector.Vector `json:"pos"`
// Range represents the distance the unit's radar can see
Range int `json:"range"`
// Inventory represents any items the rover is carrying
Inventory []objects.Object `json:"inventory"`
// Capacity is the maximum number of inventory items
Capacity int `json:"capacity"`
// Integrity represents current rover health
Integrity int `json:"integrity"`
// MaximumIntegrity is the full integrity of the rover
MaximumIntegrity int `json:"maximum-integrity"`
// Charge is the amount of energy the rover has
Charge int `json:"charge"`
// MaximumCharge is the maximum charge able to be stored
MaximumCharge int `json:"maximum-Charge"`
// Logs Stores log of information
Logs []RoverLogEntry `json:"logs"`
}
// AddLogEntryf adds an entry to the rovers log
func (r *Rover) AddLogEntryf(format string, args ...interface{}) {
text := fmt.Sprintf(format, args...)
log.Printf("%s log entry: %s", r.Name, text)
r.Logs = append(r.Logs,
RoverLogEntry{
Time: time.Now(),
Text: text,
},
)
}

View file

@ -1,533 +0,0 @@
package game
import (
"bufio"
"fmt"
"log"
"math/rand"
"os"
"sync"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/objects"
"github.com/mdiluz/rove/pkg/vector"
)
// World describes a self contained universe and everything in it
type World struct {
// Rovers is a id->data map of all the rovers in the game
Rovers map[string]Rover `json:"rovers"`
// Atlas represends the world map of chunks and tiles
Atlas atlas.Atlas `json:"atlas"`
// Mutex to lock around all world operations
worldMutex sync.RWMutex
// Commands is the set of currently executing command streams per rover
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 command operations
cmdMutex sync.RWMutex
// Set of possible words to use for names
words []string
// TicksPerDay is the amount of ticks in a single day
TicksPerDay int `json:"ticks-per-day"`
// Current number of ticks from the start
CurrentTicks int `json:"current-ticks"`
}
var wordsFile = os.Getenv("WORDS_FILE")
// NewWorld creates a new world object
func NewWorld(chunkSize int) *World {
// 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())
}
}
return &World{
Rovers: make(map[string]Rover),
CommandQueue: make(map[string]CommandStream),
CommandIncoming: make(map[string]CommandStream),
Atlas: atlas.NewAtlas(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 := Rover{
Range: 4,
Integrity: 10,
MaximumIntegrity: 10,
Capacity: 10,
Charge: 10,
MaximumCharge: 10,
Name: uuid.New().String(),
}
// 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 = vector.Vector{
X: w.Atlas.ChunkSize/2 - rand.Intn(w.Atlas.ChunkSize),
Y: w.Atlas.ChunkSize/2 - rand.Intn(w.Atlas.ChunkSize),
}
// Seach until we error (run out of world)
for {
_, obj := w.Atlas.QueryPosition(rover.Pos)
if !obj.IsBlocking() {
break
} else {
// Try and spawn to the east of the blockage
rover.Pos.Add(vector.Vector{X: 1, Y: 0})
}
}
// Add a log entry for robot creation
rover.AddLogEntryf("created at %+v", rover.Pos)
// Append the rover to the list
w.Rovers[rover.Name] = rover
return rover.Name, nil
}
// GetRover gets a specific rover by name
func (w *World) GetRover(rover string) (Rover, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
i, ok := w.Rovers[rover]
if !ok {
return Rover{}, fmt.Errorf("Failed to find rover with name: %s", rover)
}
return i, nil
}
// RoverRecharge charges up a rover
func (w *World) RoverRecharge(rover string) (int, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return 0, fmt.Errorf("Failed to find rover with name: %s", rover)
}
// We can only recharge during the day
if !w.Daytime() {
return i.Charge, nil
}
// Add one charge
if i.Charge < i.MaximumCharge {
i.Charge++
i.AddLogEntryf("recharged to %d", i.Charge)
}
w.Rovers[rover] = i
return i.Charge, nil
}
// DestroyRover Removes an rover from the game
func (w *World) DestroyRover(rover string) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
_, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("no rover matching id")
}
delete(w.Rovers, rover)
return nil
}
// RoverPosition returns the position of the rover
func (w *World) RoverPosition(rover string) (vector.Vector, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
i, ok := w.Rovers[rover]
if !ok {
return vector.Vector{}, fmt.Errorf("no rover matching id")
}
return i.Pos, nil
}
// SetRoverPosition sets the position of the rover
func (w *World) SetRoverPosition(rover string, pos vector.Vector) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("no rover matching id")
}
i.Pos = pos
w.Rovers[rover] = i
return nil
}
// RoverInventory returns the inventory of a requested rover
func (w *World) RoverInventory(rover string) ([]objects.Object, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
i, ok := w.Rovers[rover]
if !ok {
return nil, fmt.Errorf("no rover matching id")
}
return i.Inventory, nil
}
// WarpRover sets an rovers position
func (w *World) WarpRover(rover string, pos vector.Vector) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("no rover matching id")
}
// Nothing to do if these positions match
if i.Pos == pos {
return nil
}
// Check the tile is not blocked
_, obj := w.Atlas.QueryPosition(pos)
if obj.IsBlocking() {
return fmt.Errorf("can't warp rover to occupied tile, check before warping")
}
i.Pos = pos
w.Rovers[rover] = i
return nil
}
// MoveRover attempts to move a rover in a specific direction
func (w *World) MoveRover(rover string, b bearing.Bearing) (vector.Vector, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return vector.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(b.Vector())
// Get the tile and verify it's empty
_, obj := w.Atlas.QueryPosition(newPos)
if !obj.IsBlocking() {
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
}
// RoverStash will stash an item at the current rovers position
func (w *World) RoverStash(rover string) (objects.Type, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return objects.None, fmt.Errorf("no rover matching id")
}
// Can't pick up when full
if len(r.Inventory) >= r.Capacity {
return objects.None, nil
}
// Ensure the rover has energy
if r.Charge <= 0 {
return objects.None, nil
}
r.Charge--
_, obj := w.Atlas.QueryPosition(r.Pos)
if !obj.IsStashable() {
return objects.None, nil
}
r.AddLogEntryf("stashed %c", obj.Type)
r.Inventory = append(r.Inventory, obj)
w.Rovers[rover] = r
w.Atlas.SetObject(r.Pos, objects.Object{Type: objects.None})
return obj.Type, nil
}
// RadarFromRover can be used to query what a rover can currently see
func (w *World) RadarFromRover(rover string) (radar []byte, objs []byte, err error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
r, ok := w.Rovers[rover]
if !ok {
err = fmt.Errorf("no rover matching id")
return
}
// The radar should span in range direction on each axis, plus the row/column the rover is currently on
radarSpan := (r.Range * 2) + 1
roverPos := r.Pos
// Get the radar min and max values
radarMin := vector.Vector{
X: roverPos.X - r.Range,
Y: roverPos.Y - r.Range,
}
radarMax := vector.Vector{
X: roverPos.X + r.Range,
Y: roverPos.Y + r.Range,
}
// Gather up all tiles within the range
radar = make([]byte, radarSpan*radarSpan)
objs = make([]byte, radarSpan*radarSpan)
for j := radarMin.Y; j <= radarMax.Y; j++ {
for i := radarMin.X; i <= radarMax.X; i++ {
q := vector.Vector{X: i, Y: j}
tile, obj := w.Atlas.QueryPosition(q)
// Get the position relative to the bottom left of the radar
relative := q.Added(radarMin.Negated())
index := relative.X + relative.Y*radarSpan
radar[index] = tile
objs[index] = byte(obj.Type)
}
}
// Add all rovers to the radar
for _, r := range w.Rovers {
// If the rover is in range
dist := r.Pos.Added(roverPos.Negated())
dist = dist.Abs()
if dist.X <= r.Range && dist.Y <= r.Range {
relative := r.Pos.Added(radarMin.Negated())
index := relative.X + relative.Y*radarSpan
objs[index] = byte(objects.Rover)
}
}
return radar, objs, nil
}
// RoverCommands returns current commands for the given rover
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
}
return
}
// Enqueue will queue the commands given
func (w *World) Enqueue(rover string, commands ...Command) error {
// First validate the commands
for _, c := range commands {
switch c.Command {
case CommandMove:
if _, err := bearing.FromString(c.Bearing); err != nil {
return fmt.Errorf("unknown bearing: %s", c.Bearing)
}
case CommandStash:
case CommandRepair:
case CommandRecharge:
// Nothing to verify
default:
return fmt.Errorf("unknown command: %s", c.Command)
}
}
// Lock our commands edit
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
// Override the incoming command set
w.CommandIncoming[rover] = commands
return nil
}
// 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 err := w.ExecuteCommand(&c, rover); err != nil {
log.Println(err)
// TODO: Report this error somehow
}
} else {
// Clean out the empty entry
delete(w.CommandQueue, rover)
}
}
// Add any incoming commands from this tick and clear that queue
w.EnqueueAllIncoming()
// Increment the current tick count
w.CurrentTicks++
}
// ExecuteCommand will execute a single command
func (w *World) ExecuteCommand(c *Command, rover string) (err error) {
log.Printf("Executing command: %+v for %s\n", *c, rover)
switch c.Command {
case CommandMove:
if dir, err := bearing.FromString(c.Bearing); err != nil {
return err
} else if _, err := w.MoveRover(rover, dir); err != nil {
return err
}
case CommandStash:
if _, err := w.RoverStash(rover); err != nil {
return err
}
case CommandRepair:
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 CommandRecharge:
_, err := w.RoverRecharge(rover)
if err != nil {
return err
}
default:
return fmt.Errorf("unknown command: %s", c.Command)
}
return
}
// Daytime returns if it's currently daytime
// for simplicity this uses the 1st half of the day as daytime, the 2nd half as nighttime
func (w *World) Daytime() bool {
tickInDay := w.CurrentTicks % w.TicksPerDay
return tickInDay < w.TicksPerDay/2
}
// RLock read locks the world
func (w *World) RLock() {
w.worldMutex.RLock()
w.cmdMutex.RLock()
}
// RUnlock read unlocks the world
func (w *World) RUnlock() {
w.worldMutex.RUnlock()
w.cmdMutex.RUnlock()
}
// Lock locks the world
func (w *World) Lock() {
w.worldMutex.Lock()
w.cmdMutex.Lock()
}
// Unlock unlocks the world
func (w *World) Unlock() {
w.worldMutex.Unlock()
w.cmdMutex.Unlock()
}

View file

@ -1,367 +0,0 @@
package game
import (
"testing"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/objects"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
)
func TestNewWorld(t *testing.T) {
// Very basic for now, nothing to verify
world := NewWorld(4)
if world == nil {
t.Error("Failed to create world")
}
}
func TestWorld_CreateRover(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover()
assert.NoError(t, err)
b, err := world.SpawnRover()
assert.NoError(t, err)
// Basic duplicate check
if a == b {
t.Errorf("Created identical rovers")
} else if len(world.Rovers) != 2 {
t.Errorf("Incorrect number of rovers created")
}
}
func TestWorld_GetRover(t *testing.T) {
world := NewWorld(4)
a, err := world.SpawnRover()
assert.NoError(t, err)
rover, err := world.GetRover(a)
assert.NoError(t, err, "Failed to get rover attribs")
assert.NotZero(t, rover.Range, "Rover should not be spawned blind")
assert.Contains(t, rover.Logs[len(rover.Logs)-1].Text, "created", "Rover logs should contain the creation")
}
func TestWorld_DestroyRover(t *testing.T) {
world := NewWorld(1)
a, err := world.SpawnRover()
assert.NoError(t, err)
b, err := world.SpawnRover()
assert.NoError(t, err)
err = world.DestroyRover(a)
assert.NoError(t, err, "Error returned from rover destroy")
// Basic duplicate check
if len(world.Rovers) != 1 {
t.Error("Too many rovers left in world")
} else if _, ok := world.Rovers[b]; !ok {
t.Error("Remaining rover is incorrect")
}
}
func TestWorld_GetSetMovePosition(t *testing.T) {
world := NewWorld(4)
a, err := world.SpawnRover()
assert.NoError(t, err)
pos := vector.Vector{
X: 0.0,
Y: 0.0,
}
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
newPos, err := world.RoverPosition(a)
assert.NoError(t, err, "Failed to set position for rover")
assert.Equal(t, pos, newPos, "Failed to correctly set position for rover")
b := bearing.North
newPos, err = world.MoveRover(a, b)
assert.NoError(t, err, "Failed to set position for rover")
pos.Add(vector.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(vector.Vector{X: 0, Y: 2}, objects.Object{Type: objects.LargeRock})
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()
assert.NoError(t, err)
b, err := world.SpawnRover()
assert.NoError(t, err)
// Warp the rovers into position
bpos := vector.Vector{X: -3, Y: -3}
assert.NoError(t, world.WarpRover(b, bpos), "Failed to warp rover")
assert.NoError(t, world.WarpRover(a, vector.Vector{X: 0, Y: 0}), "Failed to warp rover")
radar, objs, err := world.RadarFromRover(a)
assert.NoError(t, err, "Failed to get radar from rover")
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")
// Test the expected values
assert.Equal(t, byte(objects.Rover), objs[1+fullRange])
assert.Equal(t, byte(objects.Rover), objs[4+4*fullRange])
// Check the radar results are stable
radar1, objs1, err := world.RadarFromRover(a)
assert.NoError(t, err)
radar2, objs2, err := world.RadarFromRover(a)
assert.NoError(t, err)
assert.Equal(t, radar1, radar2)
assert.Equal(t, objs1, objs2)
}
func TestWorld_RoverStash(t *testing.T) {
world := NewWorld(2)
a, err := world.SpawnRover()
assert.NoError(t, err)
pos := vector.Vector{
X: 0.0,
Y: 0.0,
}
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
// Set to a traversible tile
world.Atlas.SetTile(pos, atlas.TileRock)
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, objects.Object{Type: objects.SmallRock})
// Pick it up
o, err := world.RoverStash(a)
assert.NoError(t, err, "Failed to stash")
assert.Equal(t, objects.SmallRock, o, "Failed to get correct object")
// Check it's gone
_, obj := world.Atlas.QueryPosition(pos)
assert.Equal(t, objects.None, 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, objects.Object{Type: objects.SmallRock}, 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, objects.Object{Type: objects.SmallRock})
// Try to pick it up
o, err := world.RoverStash(a)
assert.NoError(t, err, "Failed to stash")
assert.Equal(t, objects.None, o, "Failed to get correct object")
// Check it's still there
_, obj := world.Atlas.QueryPosition(pos)
assert.Equal(t, objects.SmallRock, 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 := vector.Vector{
X: 0.0,
Y: 0.0,
}
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(vector.Vector{X: 0.0, Y: 1.0}, objects.Object{Type: objects.LargeRock})
vec, err := world.MoveRover(a, 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, 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")
}
func TestWorld_RoverRepair(t *testing.T) {
world := NewWorld(2)
a, err := world.SpawnRover()
assert.NoError(t, err)
pos := vector.Vector{
X: 0.0,
Y: 0.0,
}
world.Atlas.SetTile(pos, atlas.TileNone)
world.Atlas.SetObject(pos, objects.Object{Type: objects.None})
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, objects.Object{Type: objects.SmallRock})
o, err := world.RoverStash(a)
assert.NoError(t, err, "Failed to stash")
assert.Equal(t, objects.SmallRock, o, "Failed to get correct object")
world.Atlas.SetObject(vector.Vector{X: 0.0, Y: 1.0}, objects.Object{Type: objects.LargeRock})
// Try and bump into the rock
vec, err := world.MoveRover(a, 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: CommandRepair}, 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, objects.Object{Type: objects.SmallRock})
o, err = world.RoverStash(a)
assert.NoError(t, err, "Failed to stash")
assert.Equal(t, objects.SmallRock, o, "Failed to get correct object")
err = world.ExecuteCommand(&Command{Command: CommandRepair}, 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(bearing.North.Vector()), atlas.TileRock)
world.Atlas.SetObject(initialPos.Added(bearing.North.Vector()), objects.Object{Type: objects.None})
// Try and move north (along unblocked path)
newPos, err := world.MoveRover(a, bearing.North)
assert.NoError(t, err, "Failed to set position for rover")
assert.Equal(t, initialPos.Added(bearing.North.Vector()), 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")
}
}
func TestWorld_Daytime(t *testing.T) {
world := NewWorld(1)
a, err := world.SpawnRover()
assert.NoError(t, err)
// Remove rover charge
rover := world.Rovers[a]
rover.Charge = 0
world.Rovers[a] = rover
// Try and recharge, should work
_, err = world.RoverRecharge(a)
assert.NoError(t, err)
assert.Equal(t, 1, world.Rovers[a].Charge)
// Loop for half the day
for i := 0; i < world.TicksPerDay/2; i++ {
assert.True(t, world.Daytime())
world.ExecuteCommandQueues()
}
// Remove rover charge again
rover = world.Rovers[a]
rover.Charge = 0
world.Rovers[a] = rover
// Try and recharge, should fail
_, err = world.RoverRecharge(a)
assert.NoError(t, err)
assert.Equal(t, 0, world.Rovers[a].Charge)
// Loop for half the day
for i := 0; i < world.TicksPerDay/2; i++ {
assert.False(t, world.Daytime())
world.ExecuteCommandQueues()
}
}

View file

@ -1,15 +1,15 @@
package vector
package maths
import (
"math"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
)
// Vector desribes a 3D vector
type Vector struct {
X int `json:"x"`
Y int `json:"y"`
X int
Y int
}
// Add adds one vector to another
@ -71,15 +71,49 @@ func (v Vector) DividedFloor(val int) Vector {
// Abs returns an absolute version of the vector
func (v Vector) Abs() Vector {
return Vector{maths.Abs(v.X), maths.Abs(v.Y)}
return Vector{Abs(v.X), Abs(v.Y)}
}
// Min returns the minimum values in both vectors
func Min(v1 Vector, v2 Vector) Vector {
return Vector{maths.Min(v1.X, v2.X), maths.Min(v1.Y, v2.Y)}
// Min2 returns the minimum values in both vectors
func Min2(v1 Vector, v2 Vector) Vector {
return Vector{Min(v1.X, v2.X), Min(v1.Y, v2.Y)}
}
// Max returns the max values in both vectors
func Max(v1 Vector, v2 Vector) Vector {
return Vector{maths.Max(v1.X, v2.X), maths.Max(v1.Y, v2.Y)}
// Max2 returns the max values in both vectors
func Max2(v1 Vector, v2 Vector) Vector {
return Vector{Max(v1.X, v2.X), Max(v1.Y, v2.Y)}
}
// BearingToVector converts a bearing to a vector
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()
}

View file

@ -1,4 +1,4 @@
package vector
package maths
import (
"math"

View file

@ -1,53 +0,0 @@
package objects
// Type represents an object type
type Type byte
// Types of objects
const (
// None represents no object at all
None = Type(0)
// Rover represents a live rover
Rover = Type('R')
// SmallRock is a small stashable rock
SmallRock = Type('o')
// LargeRock is a large blocking rock
LargeRock = Type('O')
)
// Object represents an object in the world
type Object struct {
Type Type `json:"type"`
}
// IsBlocking checks if an object is a blocking object
func (o *Object) IsBlocking() bool {
var blocking = [...]Type{
Rover,
LargeRock,
}
for _, t := range blocking {
if o.Type == t {
return true
}
}
return false
}
// IsStashable checks if an object is stashable
func (o *Object) IsStashable() bool {
var stashable = [...]Type{
SmallRock,
}
for _, t := range stashable {
if o.Type == t {
return true
}
}
return false
}

18
pkg/rove/atlas.go Normal file
View file

@ -0,0 +1,18 @@
package rove
import (
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
)
// Atlas represents a 2D world atlas of tiles and objects
type Atlas interface {
// SetTile sets a location on the Atlas to a type of tile
SetTile(v maths.Vector, tile roveapi.Tile)
// SetObject will set a location on the Atlas to contain an object
SetObject(v maths.Vector, obj Object)
// QueryPosition queries a position on the atlas
QueryPosition(v maths.Vector) (roveapi.Tile, Object)
}

257
pkg/rove/atlas_test.go Normal file
View file

@ -0,0 +1,257 @@
package rove
import (
"testing"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/stretchr/testify/assert"
)
func TestAtlas_NewAtlas(t *testing.T) {
a := NewChunkAtlas(1).(*chunkBasedAtlas)
assert.NotNil(t, a)
assert.Equal(t, 1, a.ChunkSize)
assert.Equal(t, 1, len(a.Chunks)) // Should start empty
}
func TestAtlas_toChunk(t *testing.T) {
a := NewChunkAtlas(1).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(maths.Vector{X: -1, Y: -1})
a.QueryPosition(maths.Vector{X: 0, Y: 0})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
chunkID := a.worldSpaceToChunkIndex(maths.Vector{X: 0, Y: 0})
assert.Equal(t, 3, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 0, Y: -1})
assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -1, Y: -1})
assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -1, Y: 0})
assert.Equal(t, 2, chunkID)
a = NewChunkAtlas(2).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(maths.Vector{X: -2, Y: -2})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(maths.Vector{X: 1, Y: 1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: 1})
assert.Equal(t, 3, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: -2})
assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -2, Y: -2})
assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -2, Y: 1})
assert.Equal(t, 2, chunkID)
a = NewChunkAtlas(2).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(maths.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(maths.Vector{X: -3, Y: -3})
assert.Equal(t, 4*4, len(a.Chunks))
// Chunks should look like:
// 12| 13|| 14| 15
// ----------------
// 8 | 9 || 10| 11
// ================
// 4 | 5 || 6 | 7
// ----------------
// 0 | 1 || 2 | 3
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: 3})
assert.Equal(t, 14, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: -3})
assert.Equal(t, 2, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -1, Y: -1})
assert.Equal(t, 5, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -2, Y: 2})
assert.Equal(t, 13, chunkID)
a = NewChunkAtlas(3).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(maths.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// || 2| 3
// -------
// || 0| 1
// =======
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: 1})
assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 3, Y: 1})
assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: 4})
assert.Equal(t, 2, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 5, Y: 5})
assert.Equal(t, 3, chunkID)
}
func TestAtlas_toWorld(t *testing.T) {
a := NewChunkAtlas(1).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn some chunks
a.QueryPosition(maths.Vector{X: -1, Y: -1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
assert.Equal(t, maths.Vector{X: -1, Y: -1}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, maths.Vector{X: 0, Y: -1}, a.chunkOriginInWorldSpace(1))
a = NewChunkAtlas(2).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(maths.Vector{X: -2, Y: -2})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(maths.Vector{X: 1, Y: 1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
assert.Equal(t, maths.Vector{X: -2, Y: -2}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, maths.Vector{X: -2, Y: 0}, a.chunkOriginInWorldSpace(2))
a = NewChunkAtlas(2).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(maths.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(maths.Vector{X: -3, Y: -3})
assert.Equal(t, 4*4, len(a.Chunks))
// Chunks should look like:
// 12| 13|| 14| 15
// ----------------
// 8 | 9 || 10| 11
// ================
// 4 | 5 || 6 | 7
// ----------------
// 0 | 1 || 2 | 3
assert.Equal(t, maths.Vector{X: -4, Y: -4}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, maths.Vector{X: 2, Y: -2}, a.chunkOriginInWorldSpace(7))
a = NewChunkAtlas(3).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(maths.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// || 2| 3
// -------
// || 0| 1
// =======
assert.Equal(t, maths.Vector{X: 0, Y: 0}, a.chunkOriginInWorldSpace(0))
}
func TestAtlas_GetSetTile(t *testing.T) {
a := NewChunkAtlas(10)
assert.NotNil(t, a)
// Set the origin tile and test it
a.SetTile(maths.Vector{X: 0, Y: 0}, roveapi.Tile_Gravel)
tile, _ := a.QueryPosition(maths.Vector{X: 0, Y: 0})
assert.Equal(t, roveapi.Tile_Gravel, tile)
// Set another tile and test it
a.SetTile(maths.Vector{X: 5, Y: -2}, roveapi.Tile_Rock)
tile, _ = a.QueryPosition(maths.Vector{X: 5, Y: -2})
assert.Equal(t, roveapi.Tile_Rock, tile)
}
func TestAtlas_GetSetObject(t *testing.T) {
a := NewChunkAtlas(10)
assert.NotNil(t, a)
// Set the origin tile to 1 and test it
a.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_RockLarge})
_, obj := a.QueryPosition(maths.Vector{X: 0, Y: 0})
assert.Equal(t, Object{Type: roveapi.Object_RockLarge}, obj)
// Set another tile to 1 and test it
a.SetObject(maths.Vector{X: 5, Y: -2}, Object{Type: roveapi.Object_RockSmall})
_, obj = a.QueryPosition(maths.Vector{X: 5, Y: -2})
assert.Equal(t, Object{Type: roveapi.Object_RockSmall}, obj)
}
func TestAtlas_Grown(t *testing.T) {
// Start with a small example
a := NewChunkAtlas(2).(*chunkBasedAtlas)
assert.NotNil(t, a)
assert.Equal(t, 1, len(a.Chunks))
// Set a few tiles to values
a.SetTile(maths.Vector{X: 0, Y: 0}, roveapi.Tile_Gravel)
a.SetTile(maths.Vector{X: -1, Y: -1}, roveapi.Tile_Rock)
a.SetTile(maths.Vector{X: 1, Y: -2}, roveapi.Tile_Sand)
// Check tile values
tile, _ := a.QueryPosition(maths.Vector{X: 0, Y: 0})
assert.Equal(t, roveapi.Tile_Gravel, tile)
tile, _ = a.QueryPosition(maths.Vector{X: -1, Y: -1})
assert.Equal(t, roveapi.Tile_Rock, tile)
tile, _ = a.QueryPosition(maths.Vector{X: 1, Y: -2})
assert.Equal(t, roveapi.Tile_Sand, tile)
tile, _ = a.QueryPosition(maths.Vector{X: 0, Y: 0})
assert.Equal(t, roveapi.Tile_Gravel, tile)
tile, _ = a.QueryPosition(maths.Vector{X: -1, Y: -1})
assert.Equal(t, roveapi.Tile_Rock, tile)
tile, _ = a.QueryPosition(maths.Vector{X: 1, Y: -2})
assert.Equal(t, roveapi.Tile_Sand, tile)
}
func TestAtlas_GetSetCorrect(t *testing.T) {
// Big stress test to ensure we do actually properly expand for all reasonable values
for i := 1; i <= 4; i++ {
for x := -i * 2; x < i*2; x++ {
for y := -i * 2; y < i*2; y++ {
a := NewChunkAtlas(i).(*chunkBasedAtlas)
assert.NotNil(t, a)
assert.Equal(t, 1, len(a.Chunks))
pos := maths.Vector{X: x, Y: y}
a.SetTile(pos, roveapi.Tile_Rock)
a.SetObject(pos, Object{Type: roveapi.Object_RockLarge})
tile, obj := a.QueryPosition(pos)
assert.Equal(t, roveapi.Tile_Rock, roveapi.Tile(tile))
assert.Equal(t, Object{Type: roveapi.Object_RockLarge}, obj)
}
}
}
}
func TestAtlas_WorldGen(t *testing.T) {
a := NewChunkAtlas(8)
// Spawn a large world
_, _ = a.QueryPosition(maths.Vector{X: 20, Y: 20})
}

View file

@ -1,163 +1,112 @@
package atlas
package rove
import (
"log"
"math/rand"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/pkg/objects"
"github.com/mdiluz/rove/pkg/vector"
"github.com/ojrac/opensimplex-go"
"github.com/mdiluz/rove/proto/roveapi"
)
// Tile describes the type of terrain
type Tile byte
const (
// TileNone is a keyword for nothing
TileNone = Tile(0)
// TileRock is solid rock ground
TileRock = Tile('-')
// TileGravel is loose rocks
TileGravel = Tile(':')
// TileSand is sand
TileSand = Tile('~')
)
// Chunk represents a fixed square grid of tiles
type Chunk struct {
// chunk represents a fixed square grid of tiles
type chunk struct {
// Tiles represents the tiles within the chunk
Tiles []byte `json:"tiles"`
Tiles []byte
// Objects represents the objects within the chunk
// only one possible object per tile for now
Objects map[int]objects.Object `json:"objects"`
Objects map[int]Object
}
// Atlas represents a grid of Chunks
type Atlas struct {
// 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 `json:"chunks"`
Chunks []chunk
// LowerBound is the origin of the bottom left corner of the current chunks in world space (current chunks cover >= this value)
LowerBound vector.Vector `json:"lowerBound"`
LowerBound maths.Vector
// UpperBound is the top left corner of the current chunks (curent chunks cover < this value)
UpperBound vector.Vector `json:"upperBound"`
UpperBound maths.Vector
// ChunkSize is the x/y dimensions of each square chunk
ChunkSize int `json:"chunksize"`
ChunkSize int
// terrainNoise describes the noise function for the terrain
terrainNoise opensimplex.Noise
// terrainNoise describes the noise function for the terrain
objectNoise opensimplex.Noise
// worldGen is the internal world generator
worldGen WorldGen
}
const (
noiseSeed = 1024
terrainNoiseScale = 6
objectNoiseScale = 3
noiseSeed = 1024
)
// NewAtlas creates a new empty atlas
func NewAtlas(chunkSize int) Atlas {
// NewChunkAtlas creates a new empty atlas
func NewChunkAtlas(chunkSize int) Atlas {
// Start up with one chunk
a := Atlas{
ChunkSize: chunkSize,
Chunks: make([]Chunk, 1),
LowerBound: vector.Vector{X: 0, Y: 0},
UpperBound: vector.Vector{X: chunkSize, Y: chunkSize},
terrainNoise: opensimplex.New(noiseSeed),
objectNoise: opensimplex.New(noiseSeed),
a := chunkBasedAtlas{
ChunkSize: chunkSize,
Chunks: make([]chunk, 1),
LowerBound: maths.Vector{X: 0, Y: 0},
UpperBound: maths.Vector{X: chunkSize, Y: chunkSize},
worldGen: NewNoiseWorldGen(noiseSeed),
}
// Initialise the first chunk
a.populate(0)
return a
return &a
}
// SetTile sets an individual tile's kind
func (a *Atlas) SetTile(v vector.Vector, tile Tile) {
func (a *chunkBasedAtlas) SetTile(v maths.Vector, tile roveapi.Tile) {
c := a.worldSpaceToChunkWithGrow(v)
local := a.worldSpaceToChunkLocal(v)
a.setTile(c, local, byte(tile))
}
// SetObject sets the object on a tile
func (a *Atlas) SetObject(v vector.Vector, obj objects.Object) {
func (a *chunkBasedAtlas) SetObject(v maths.Vector, obj Object) {
c := a.worldSpaceToChunkWithGrow(v)
local := a.worldSpaceToChunkLocal(v)
a.setObject(c, local, obj)
}
// QueryPosition will return information for a specific position
func (a *Atlas) QueryPosition(v vector.Vector) (byte, objects.Object) {
func (a *chunkBasedAtlas) QueryPosition(v maths.Vector) (roveapi.Tile, Object) {
c := a.worldSpaceToChunkWithGrow(v)
local := a.worldSpaceToChunkLocal(v)
a.populate(c)
chunk := a.Chunks[c]
i := a.chunkTileIndex(local)
return chunk.Tiles[i], chunk.Objects[i]
return roveapi.Tile(chunk.Tiles[i]), chunk.Objects[i]
}
// chunkTileID returns the tile index within a chunk
func (a *Atlas) chunkTileIndex(local vector.Vector) int {
func (a *chunkBasedAtlas) chunkTileIndex(local maths.Vector) int {
return local.X + local.Y*a.ChunkSize
}
// populate will fill a chunk with data
func (a *Atlas) populate(chunk int) {
func (a *chunkBasedAtlas) populate(chunk int) {
c := a.Chunks[chunk]
if c.Tiles != nil {
return
}
c.Tiles = make([]byte, a.ChunkSize*a.ChunkSize)
c.Objects = make(map[int]objects.Object)
c.Objects = make(map[int]Object)
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}
// Get the terrain noise value for this location
t := a.terrainNoise.Eval2(float64(origin.X+i)/terrainNoiseScale, float64(origin.Y+j)/terrainNoiseScale)
var tile Tile
switch {
case t > 0.5:
tile = TileGravel
case t > 0.05:
tile = TileSand
default:
tile = TileRock
}
c.Tiles[j*a.ChunkSize+i] = byte(tile)
// Set the tile
c.Tiles[j*a.ChunkSize+i] = byte(a.worldGen.GetTile(loc))
// Get the object noise value for this location
o := a.objectNoise.Eval2(float64(origin.X+i)/objectNoiseScale, float64(origin.Y+j)/objectNoiseScale)
var obj = objects.None
switch {
case o > 0.6:
obj = objects.LargeRock
case o > 0.5:
obj = objects.SmallRock
// Set the object
obj := a.worldGen.GetObject(loc)
if obj.Type != roveapi.Object_ObjectUnknown {
c.Objects[j*a.ChunkSize+i] = obj
}
if obj != objects.None {
c.Objects[j*a.ChunkSize+i] = objects.Object{Type: obj}
}
}
}
// Set up any objects
for i := 0; i < len(c.Tiles); i++ {
if rand.Intn(16) == 0 {
c.Objects[i] = objects.Object{Type: objects.LargeRock}
} else if rand.Intn(32) == 0 {
c.Objects[i] = objects.Object{Type: objects.SmallRock}
}
}
@ -165,7 +114,7 @@ func (a *Atlas) populate(chunk int) {
}
// setTile sets a tile in a specific chunk
func (a *Atlas) setTile(chunk int, local vector.Vector, tile byte) {
func (a *chunkBasedAtlas) setTile(chunk int, local maths.Vector, tile byte) {
a.populate(chunk)
c := a.Chunks[chunk]
c.Tiles[a.chunkTileIndex(local)] = tile
@ -173,12 +122,12 @@ func (a *Atlas) setTile(chunk int, local vector.Vector, tile byte) {
}
// setObject sets an object in a specific chunk
func (a *Atlas) setObject(chunk int, local vector.Vector, object objects.Object) {
func (a *chunkBasedAtlas) setObject(chunk int, local maths.Vector, object Object) {
a.populate(chunk)
c := a.Chunks[chunk]
i := a.chunkTileIndex(local)
if object.Type != objects.None {
if object.Type != roveapi.Object_ObjectUnknown {
c.Objects[i] = object
} else {
delete(c.Objects, i)
@ -187,12 +136,12 @@ func (a *Atlas) setObject(chunk int, local vector.Vector, object objects.Object)
}
// worldSpaceToChunkLocal gets a chunk local coordinate for a tile
func (a *Atlas) worldSpaceToChunkLocal(v vector.Vector) vector.Vector {
return vector.Vector{X: maths.Pmod(v.X, a.ChunkSize), Y: maths.Pmod(v.Y, a.ChunkSize)}
func (a *chunkBasedAtlas) worldSpaceToChunkLocal(v maths.Vector) maths.Vector {
return maths.Vector{X: maths.Pmod(v.X, a.ChunkSize), Y: maths.Pmod(v.Y, a.ChunkSize)}
}
// worldSpaceToChunkID gets the current chunk ID for a position in the world
func (a *Atlas) worldSpaceToChunkIndex(v vector.Vector) int {
func (a *chunkBasedAtlas) worldSpaceToChunkIndex(v maths.Vector) int {
// Shift the vector by our current min
v = v.Added(a.LowerBound.Negated())
@ -208,13 +157,13 @@ func (a *Atlas) worldSpaceToChunkIndex(v vector.Vector) int {
}
// chunkOriginInWorldSpace returns the origin of the chunk in world space
func (a *Atlas) chunkOriginInWorldSpace(chunk int) vector.Vector {
func (a *chunkBasedAtlas) chunkOriginInWorldSpace(chunk int) maths.Vector {
// Calculate the width
width := a.UpperBound.X - a.LowerBound.X
widthInChunks := width / a.ChunkSize
// Reverse the along the corridor and up the stairs
v := vector.Vector{
v := maths.Vector{
X: chunk % widthInChunks,
Y: chunk / widthInChunks,
}
@ -225,15 +174,15 @@ func (a *Atlas) chunkOriginInWorldSpace(chunk int) vector.Vector {
}
// getNewBounds gets new lower and upper bounds for the world space given a vector
func (a *Atlas) getNewBounds(v vector.Vector) (lower vector.Vector, upper vector.Vector) {
lower = vector.Min(v, a.LowerBound)
upper = vector.Max(v.Added(vector.Vector{X: 1, Y: 1}), a.UpperBound)
func (a *chunkBasedAtlas) getNewBounds(v maths.Vector) (lower maths.Vector, upper maths.Vector) {
lower = maths.Min2(v, a.LowerBound)
upper = maths.Max2(v.Added(maths.Vector{X: 1, Y: 1}), a.UpperBound)
lower = vector.Vector{
lower = maths.Vector{
X: maths.RoundDown(lower.X, a.ChunkSize),
Y: maths.RoundDown(lower.Y, a.ChunkSize),
}
upper = vector.Vector{
upper = maths.Vector{
X: maths.RoundUp(upper.X, a.ChunkSize),
Y: maths.RoundUp(upper.Y, a.ChunkSize),
}
@ -241,7 +190,7 @@ func (a *Atlas) getNewBounds(v vector.Vector) (lower vector.Vector, upper vector
}
// worldSpaceToTrunkWithGrow will expand the current atlas for a given world space position if needed
func (a *Atlas) worldSpaceToChunkWithGrow(v vector.Vector) int {
func (a *chunkBasedAtlas) worldSpaceToChunkWithGrow(v maths.Vector) int {
// If we're within bounds, just return the current chunk
if v.X >= a.LowerBound.X && v.Y >= a.LowerBound.Y && v.X < a.UpperBound.X && v.Y < a.UpperBound.Y {
return a.worldSpaceToChunkIndex(v)
@ -253,13 +202,12 @@ func (a *Atlas) worldSpaceToChunkWithGrow(v vector.Vector) int {
size = size.Divided(a.ChunkSize)
// Create the new empty atlas
newAtlas := Atlas{
ChunkSize: a.ChunkSize,
LowerBound: lower,
UpperBound: upper,
Chunks: make([]Chunk, size.X*size.Y),
terrainNoise: a.terrainNoise,
objectNoise: a.objectNoise,
newAtlas := chunkBasedAtlas{
ChunkSize: a.ChunkSize,
LowerBound: lower,
UpperBound: upper,
Chunks: make([]chunk, size.X*size.Y),
worldGen: a.worldGen,
}
// Log that we're resizing

305
pkg/rove/command_test.go Normal file
View file

@ -0,0 +1,305 @@
package rove
import (
"encoding/json"
"testing"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/stretchr/testify/assert"
)
func TestCommand_Invalid(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_none})
assert.Error(t, err)
}
func TestCommand_Toggle(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_toggle})
assert.NoError(t, err)
w.Tick()
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)
}
func TestCommand_Turn(t *testing.T) {
w := NewWorld(8)
a, err := w.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)
}
func TestCommand_Stash(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
info, err := w.GetRover(name)
assert.NoError(t, err)
assert.Empty(t, info.Inventory)
// Drop a pickup below us
w.Atlas.SetObject(info.Pos, Object{Type: roveapi.Object_RockSmall})
// Try and stash it
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_stash})
assert.NoError(t, err)
w.Tick()
// 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])
// Check it's no longer on the atlas
_, obj := w.Atlas.QueryPosition(info.Pos)
assert.Equal(t, Object{Type: roveapi.Object_ObjectUnknown}, obj)
}
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)
}
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)
}

44
pkg/rove/objects.go Normal file
View file

@ -0,0 +1,44 @@
package rove
import (
"github.com/mdiluz/rove/proto/roveapi"
)
// 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
}
// IsBlocking checks if an object is a blocking object
func (o *Object) IsBlocking() bool {
var blocking = [...]roveapi.Object{
roveapi.Object_RoverLive,
roveapi.Object_RockLarge,
}
for _, t := range blocking {
if o.Type == t {
return true
}
}
return false
}
// IsStashable checks if an object is stashable
func (o *Object) IsStashable() bool {
var stashable = [...]roveapi.Object{
roveapi.Object_RockSmall,
roveapi.Object_RoverParts,
}
for _, t := range stashable {
if o.Type == t {
return true
}
}
return false
}

File diff suppressed because it is too large Load diff

View file

@ -1,459 +0,0 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: rove/rove.proto
/*
Package rove is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package rove
import (
"context"
"io"
"net/http"
"github.com/golang/protobuf/descriptor"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/grpc-ecosystem/grpc-gateway/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/status"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = descriptor.ForMessage
func request_Rove_ServerStatus_0(ctx context.Context, marshaler runtime.Marshaler, client RoveClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ServerStatusRequest
var metadata runtime.ServerMetadata
msg, err := client.ServerStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Rove_ServerStatus_0(ctx context.Context, marshaler runtime.Marshaler, server RoveServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ServerStatusRequest
var metadata runtime.ServerMetadata
msg, err := server.ServerStatus(ctx, &protoReq)
return msg, metadata, err
}
func request_Rove_Register_0(ctx context.Context, marshaler runtime.Marshaler, client RoveClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RegisterRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Register(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Rove_Register_0(ctx context.Context, marshaler runtime.Marshaler, server RoveServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RegisterRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Register(ctx, &protoReq)
return msg, metadata, err
}
func request_Rove_Command_0(ctx context.Context, marshaler runtime.Marshaler, client RoveClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CommandRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Command(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Rove_Command_0(ctx context.Context, marshaler runtime.Marshaler, server RoveServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CommandRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Command(ctx, &protoReq)
return msg, metadata, err
}
func request_Rove_Radar_0(ctx context.Context, marshaler runtime.Marshaler, client RoveClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RadarRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Radar(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Rove_Radar_0(ctx context.Context, marshaler runtime.Marshaler, server RoveServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RadarRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Radar(ctx, &protoReq)
return msg, metadata, err
}
func request_Rove_Status_0(ctx context.Context, marshaler runtime.Marshaler, client RoveClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StatusRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Status(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Rove_Status_0(ctx context.Context, marshaler runtime.Marshaler, server RoveServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StatusRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Status(ctx, &protoReq)
return msg, metadata, err
}
// RegisterRoveHandlerServer registers the http handlers for service Rove to "mux".
// UnaryRPC :call RoveServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
func RegisterRoveHandlerServer(ctx context.Context, mux *runtime.ServeMux, server RoveServer) error {
mux.Handle("GET", pattern_Rove_ServerStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Rove_ServerStatus_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_ServerStatus_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Register_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Rove_Register_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Register_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Command_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Rove_Command_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Command_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Radar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Rove_Radar_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Radar_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Status_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Rove_Status_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Status_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterRoveHandlerFromEndpoint is same as RegisterRoveHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterRoveHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterRoveHandler(ctx, mux, conn)
}
// RegisterRoveHandler registers the http handlers for service Rove to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterRoveHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterRoveHandlerClient(ctx, mux, NewRoveClient(conn))
}
// RegisterRoveHandlerClient registers the http handlers for service Rove
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "RoveClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "RoveClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "RoveClient" to call the correct interceptors.
func RegisterRoveHandlerClient(ctx context.Context, mux *runtime.ServeMux, client RoveClient) error {
mux.Handle("GET", pattern_Rove_ServerStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Rove_ServerStatus_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_ServerStatus_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Register_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Rove_Register_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Register_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Command_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Rove_Command_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Command_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Radar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Rove_Radar_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Radar_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Status_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Rove_Status_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Status_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_Rove_ServerStatus_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"server-status"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_Rove_Register_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"register"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_Rove_Command_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"command"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_Rove_Radar_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"radar"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_Rove_Status_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"status"}, "", runtime.AssumeColonVerbOpt(true)))
)
var (
forward_Rove_ServerStatus_0 = runtime.ForwardResponseMessage
forward_Rove_Register_0 = runtime.ForwardResponseMessage
forward_Rove_Command_0 = runtime.ForwardResponseMessage
forward_Rove_Radar_0 = runtime.ForwardResponseMessage
forward_Rove_Status_0 = runtime.ForwardResponseMessage
)

View file

@ -1,435 +0,0 @@
{
"swagger": "2.0",
"info": {
"title": "Rove",
"description": "Rove is an asychronous nomadic game about exploring a planet as part of a loose community",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/command": {
"post": {
"summary": "Send commands to rover",
"description": "Sending commands to this endpoint will queue them to be executed during the following ticks, in the order sent",
"operationId": "Rove_Command",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/roveCommandResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/gatewayruntimeError"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/roveCommandRequest"
}
}
],
"tags": [
"Rove"
]
}
},
"/radar": {
"post": {
"summary": "Get radar information",
"description": "Gets the radar output for the given rover",
"operationId": "Rove_Radar",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/roveRadarResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/gatewayruntimeError"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/roveRadarRequest"
}
}
],
"tags": [
"Rove"
]
}
},
"/register": {
"post": {
"summary": "Register an account",
"description": "Tries to register an account with the given name",
"operationId": "Rove_Register",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/roveRegisterResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/gatewayruntimeError"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/roveRegisterRequest"
}
}
],
"tags": [
"Rove"
]
}
},
"/server-status": {
"get": {
"summary": "Server status",
"description": "Responds with various details about the current server status",
"operationId": "Rove_ServerStatus",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/roveServerStatusResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/gatewayruntimeError"
}
}
},
"tags": [
"Rove"
]
}
},
"/status": {
"post": {
"summary": "Get rover information",
"description": "Gets information for the account's rover",
"operationId": "Rove_Status",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/roveStatusResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/gatewayruntimeError"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/roveStatusRequest"
}
}
],
"tags": [
"Rove"
]
}
}
},
"definitions": {
"gatewayruntimeError": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string"
},
"value": {
"type": "string",
"format": "byte"
}
}
},
"roveAccount": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"secret": {
"type": "string"
}
}
},
"roveCommand": {
"type": "object",
"properties": {
"command": {
"type": "string",
"title": "The command to execute\n\"move\" - Move the rover in a direction, requires bearing\n\"stash\" - Stashes item at current location in rover inventory\n\"repair\" - Repairs the rover using an inventory object\n\"recharge\" - Waits a tick to add more charge to the rover"
},
"bearing": {
"type": "string",
"title": "The bearing, example: NE"
}
}
},
"roveCommandRequest": {
"type": "object",
"properties": {
"account": {
"$ref": "#/definitions/roveAccount",
"title": "The account to execute these commands"
},
"commands": {
"type": "array",
"items": {
"$ref": "#/definitions/roveCommand"
},
"title": "The set of desired commands"
}
}
},
"roveCommandResponse": {
"type": "object",
"title": "Empty placeholder"
},
"roveLog": {
"type": "object",
"properties": {
"time": {
"type": "string",
"title": "The unix timestamp of the log"
},
"text": {
"type": "string",
"title": "The text of the log"
}
}
},
"roveRadarRequest": {
"type": "object",
"properties": {
"account": {
"$ref": "#/definitions/roveAccount",
"title": "The account for this request"
}
}
},
"roveRadarResponse": {
"type": "object",
"properties": {
"range": {
"type": "integer",
"format": "int32",
"title": "The range in tiles from the rover of the radar data"
},
"tiles": {
"type": "string",
"format": "byte",
"title": "A 1D array representing range*2 + 1 squared set of tiles, origin bottom left and in row-\u003ecolumn order"
},
"objects": {
"type": "string",
"format": "byte",
"title": "A similar array to the tile array, but containing objects"
}
}
},
"roveRegisterRequest": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "The desired account name"
}
}
},
"roveRegisterResponse": {
"type": "object",
"properties": {
"account": {
"$ref": "#/definitions/roveAccount",
"title": "The registered account information"
}
},
"title": "Empty placeholder"
},
"roveServerStatusResponse": {
"type": "object",
"properties": {
"version": {
"type": "string",
"title": "The version of the server in v{major}.{minor}-{delta}-{sha} form"
},
"ready": {
"type": "boolean",
"format": "boolean",
"title": "Whether the server is ready to accept requests"
},
"tickRate": {
"type": "integer",
"format": "int32",
"title": "The tick rate of the server in minutes (how many minutes per tick)"
},
"currentTick": {
"type": "integer",
"format": "int32",
"title": "The current tick of the server"
},
"next_tick": {
"type": "string",
"title": "The time the next tick will occur"
}
}
},
"roveStatusRequest": {
"type": "object",
"properties": {
"account": {
"$ref": "#/definitions/roveAccount",
"title": "The account for this request"
}
}
},
"roveStatusResponse": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "The name of the rover"
},
"position": {
"$ref": "#/definitions/roveVector",
"title": "Position of the rover in world coordinates"
},
"range": {
"type": "integer",
"format": "int32",
"title": "The range of this rover's radar"
},
"inventory": {
"type": "string",
"format": "byte",
"title": "The items in the rover inventory"
},
"capacity": {
"type": "integer",
"format": "int32",
"title": "The capacity of the inventory"
},
"integrity": {
"type": "integer",
"format": "int32",
"title": "The current health of the rover"
},
"maximumIntegrity": {
"type": "integer",
"format": "int32",
"title": "The maximum health of the rover"
},
"charge": {
"type": "integer",
"format": "int32",
"title": "The energy stored in the rover"
},
"maximumCharge": {
"type": "integer",
"format": "int32",
"title": "The max energy the rover can store"
},
"incomingCommands": {
"type": "array",
"items": {
"$ref": "#/definitions/roveCommand"
},
"title": "The set of currently incoming commands for this tick"
},
"queuedCommands": {
"type": "array",
"items": {
"$ref": "#/definitions/roveCommand"
},
"title": "The set of currently queued commands"
},
"logs": {
"type": "array",
"items": {
"$ref": "#/definitions/roveLog"
},
"title": "The most recent logs"
}
}
},
"roveVector": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"format": "int32"
},
"y": {
"type": "integer",
"format": "int32"
}
}
}
}
}

137
pkg/rove/rover.go Normal file
View file

@ -0,0 +1,137 @@
package rove
import (
"bufio"
"fmt"
"log"
"math/rand"
"os"
"time"
"github.com/google/uuid"
"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
// Text contains the information in this log entry
Text string
}
// Rover describes a single rover in the world
type Rover struct {
// Unique name of this rover
Name string
// Pos represents where this rover is in the world
Pos maths.Vector
// Bearing is the current direction the rover is facing
Bearing roveapi.Bearing
// Range represents the distance the unit's radar can see
Range int
// Inventory represents any items the rover is carrying
Inventory []Object
// Capacity is the maximum number of inventory items
Capacity int
// Integrity represents current rover health
Integrity int
// MaximumIntegrity is the full integrity of the rover
MaximumIntegrity int
// Charge is the amount of energy the rover has
Charge int
// 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
// 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(),
}
}
// AddLogEntryf adds an entry to the rovers log
func (r *Rover) AddLogEntryf(format string, args ...interface{}) {
text := fmt.Sprintf(format, args...)
log.Printf("%s log entry: %s", r.Name, text)
r.Logs = append(r.Logs,
RoverLogEntry{
Time: time.Now(),
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()
}

840
pkg/rove/world.go Normal file
View file

@ -0,0 +1,840 @@
package rove
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"sync"
"github.com/mdiluz/rove/pkg/accounts"
"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
// Current number of ticks from the start
CurrentTicks int
// Rovers is a id->data map of all the rovers in the game
Rovers map[string]*Rover
// Atlas represends the world map of chunks and tiles
Atlas Atlas
// Wind is the current wind direction
Wind roveapi.Bearing
// Commands is the set of currently executing command streams per rover
CommandQueue map[string]CommandStream
// Accountant
Accountant accounts.Accountant
// Mutex to lock around all world operations
worldMutex sync.RWMutex
// Mutex to lock around command operations
cmdMutex sync.RWMutex
}
// 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,
}
}
// SpawnRover adds an rover to the game (without lock)
func (w *World) SpawnRover(account string) (string, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
// Initialise the rover
rover := DefaultRover()
// Assign the owner
rover.Owner = account
// Spawn in a random place near the origin
rover.Pos = maths.Vector{
X: 10 - rand.Intn(20),
Y: 10 - rand.Intn(20),
}
// Seach until we error (run out of world)
for {
_, obj := w.Atlas.QueryPosition(rover.Pos)
if !obj.IsBlocking() {
break
} else {
// Try and spawn to the east of the blockage
rover.Pos.Add(maths.Vector{X: 1, Y: 0})
}
}
// Add a log entry for robot creation
rover.AddLogEntryf("created at %+v", rover.Pos)
// 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
}
// GetRover gets a specific rover by name
func (w *World) GetRover(rover string) (Rover, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
i, ok := w.Rovers[rover]
if !ok {
return Rover{}, fmt.Errorf("Failed to find rover with name: %s", rover)
}
return *i, nil
}
// RoverRecharge charges up a rover
func (w *World) RoverRecharge(rover string) (int, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return 0, fmt.Errorf("Failed to find rover with name: %s", rover)
}
// We can only recharge during the day
if !w.Daytime() {
return i.Charge, nil
}
// Add one charge
if i.Charge < i.MaximumCharge {
i.Charge++
i.AddLogEntryf("recharged to %d", i.Charge)
}
return i.Charge, nil
}
// RoverBroadcast broadcasts a message to nearby rovers
func (w *World) RoverBroadcast(rover string, message []byte) (err error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("Failed to find rover with name: %s", rover)
}
// Use up a charge as needed, if available
if i.Charge == 0 {
return
}
i.Charge--
// Check all rovers
for r, rover := range w.Rovers {
if rover.Name == i.Name {
continue
}
// Check if this rover is within range
if i.Pos.Distance(rover.Pos) < float64(i.Range) {
rover.AddLogEntryf("recieved %s from %s", string(message), i.Name)
w.Rovers[r] = rover
}
}
i.AddLogEntryf("broadcasted %s", string(message))
return
}
// DestroyRover Removes an rover from the game
func (w *World) DestroyRover(rover string) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, 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
}
// RoverPosition returns the position of the rover
func (w *World) RoverPosition(rover string) (maths.Vector, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
i, ok := w.Rovers[rover]
if !ok {
return maths.Vector{}, fmt.Errorf("no rover matching id")
}
return i.Pos, nil
}
// SetRoverPosition sets the position of the rover
func (w *World) SetRoverPosition(rover string, pos maths.Vector) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("no rover matching id")
}
i.Pos = pos
return nil
}
// RoverInventory returns the inventory of a requested rover
func (w *World) RoverInventory(rover string) ([]Object, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
i, ok := w.Rovers[rover]
if !ok {
return nil, fmt.Errorf("no rover matching id")
}
return i.Inventory, nil
}
// WarpRover sets an rovers position
func (w *World) WarpRover(rover string, pos maths.Vector) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("no rover matching id")
}
// Nothing to do if these positions match
if i.Pos == pos {
return nil
}
// Check the tile is not blocked
_, obj := w.Atlas.QueryPosition(pos)
if obj.IsBlocking() {
return fmt.Errorf("can't warp rover to occupied tile, check before warping")
}
i.Pos = pos
return nil
}
// TryMoveRover attempts to move a rover in a specific direction
func (w *World) TryMoveRover(rover string, b roveapi.Bearing) (maths.Vector, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return maths.Vector{}, fmt.Errorf("no rover matching id")
}
// Try the new move position
newPos := i.Pos.Added(maths.BearingToVector(b))
// Get the tile and verify it's empty
_, obj := w.Atlas.QueryPosition(newPos)
if !obj.IsBlocking() {
i.AddLogEntryf("moved %s to %+v", b.String(), newPos)
// Perform the move
i.Pos = newPos
} 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)
}
return i.Pos, nil
}
// RoverStash will stash an item at the current rovers position
func (w *World) RoverStash(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 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--
_, obj := w.Atlas.QueryPosition(r.Pos)
if !obj.IsStashable() {
return roveapi.Object_ObjectUnknown, nil
}
r.AddLogEntryf("stashed %c", obj.Type)
r.Inventory = append(r.Inventory, obj)
w.Atlas.SetObject(r.Pos, 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()
defer w.worldMutex.RUnlock()
r, ok := w.Rovers[rover]
if !ok {
err = fmt.Errorf("no rover matching id")
return
}
// The radar should span in range direction on each axis, plus the row/column the rover is currently on
radarSpan := (r.Range * 2) + 1
roverPos := r.Pos
// Get the radar min and max values
radarMin := maths.Vector{
X: roverPos.X - r.Range,
Y: roverPos.Y - r.Range,
}
radarMax := maths.Vector{
X: roverPos.X + r.Range,
Y: roverPos.Y + r.Range,
}
// Gather up all tiles within the range
radar = make([]roveapi.Tile, radarSpan*radarSpan)
objs = make([]roveapi.Object, radarSpan*radarSpan)
for j := radarMin.Y; j <= radarMax.Y; j++ {
for i := radarMin.X; i <= radarMax.X; i++ {
q := maths.Vector{X: i, Y: j}
tile, obj := w.Atlas.QueryPosition(q)
// Get the position relative to the bottom left of the radar
relative := q.Added(radarMin.Negated())
index := relative.X + relative.Y*radarSpan
radar[index] = tile
objs[index] = obj.Type
}
}
// Add all rovers to the radar
for _, r := range w.Rovers {
// If the rover is in range
dist := r.Pos.Added(roverPos.Negated())
dist = dist.Abs()
if dist.X <= r.Range && dist.Y <= r.Range {
relative := r.Pos.Added(radarMin.Negated())
index := relative.X + relative.Y*radarSpan
objs[index] = roveapi.Object_RoverLive
}
}
return radar, objs, nil
}
// RoverCommands returns current commands for the given rover
func (w *World) RoverCommands(rover string) (queued CommandStream) {
if c, ok := w.CommandQueue[rover]; ok {
queued = c
}
return
}
// Enqueue will queue the commands given
func (w *World) Enqueue(rover string, commands ...*roveapi.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()))
}
for _, b := range c.GetData() {
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:
// Nothing to verify
default:
return fmt.Errorf("unknown command: %s", c.Command)
}
}
// Lock our commands edit
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
w.CommandQueue[rover] = commands
return nil
}
// Tick will execute any commands in the current command queue and tick the world
func (w *World) Tick() {
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
// Iterate through all the current commands
for rover, cmds := range w.CommandQueue {
if len(cmds) != 0 {
// Execute the command
if done, err := w.ExecuteCommand(cmds[0], 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 {
// Clean out the empty entry
delete(w.CommandQueue, rover)
}
}
// 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
}
}
}
// 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)
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)
}
// Decrement the repeat number
c.Repeat--
return c.Repeat < 0, err
}
// Daytime returns if it's currently daytime
// for simplicity this uses the 1st half of the day as daytime, the 2nd half as nighttime
func (w *World) Daytime() bool {
tickInDay := w.CurrentTicks % w.TicksPerDay
return tickInDay < w.TicksPerDay/2
}
// RLock read locks the world
func (w *World) RLock() {
w.worldMutex.RLock()
w.cmdMutex.RLock()
}
// RUnlock read unlocks the world
func (w *World) RUnlock() {
w.worldMutex.RUnlock()
w.cmdMutex.RUnlock()
}
// Lock locks the world
func (w *World) Lock() {
w.worldMutex.Lock()
w.cmdMutex.Lock()
}
// Unlock unlocks the world
func (w *World) Unlock() {
w.worldMutex.Unlock()
w.cmdMutex.Unlock()
}

359
pkg/rove/world_test.go Normal file
View file

@ -0,0 +1,359 @@
package rove
import (
"testing"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/stretchr/testify/assert"
)
func TestNewWorld(t *testing.T) {
// Very basic for now, nothing to verify
world := NewWorld(4)
if world == nil {
t.Error("Failed to create world")
}
}
func TestWorld_CreateRover(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover("")
assert.NoError(t, err)
b, err := world.SpawnRover("")
assert.NoError(t, err)
// Basic duplicate check
if a == b {
t.Errorf("Created identical rovers")
} else if len(world.Rovers) != 2 {
t.Errorf("Incorrect number of rovers created")
}
}
func TestWorld_GetRover(t *testing.T) {
world := NewWorld(4)
a, err := world.SpawnRover("")
assert.NoError(t, err)
rover, err := world.GetRover(a)
assert.NoError(t, err, "Failed to get rover attribs")
assert.NotZero(t, rover.Range, "Rover should not be spawned blind")
assert.Contains(t, rover.Logs[len(rover.Logs)-1].Text, "created", "Rover logs should contain the creation")
}
func TestWorld_DestroyRover(t *testing.T) {
world := NewWorld(1)
a, err := world.SpawnRover("")
assert.NoError(t, err)
b, err := world.SpawnRover("")
assert.NoError(t, err)
err = world.DestroyRover(a)
assert.NoError(t, err, "Error returned from rover destroy")
// Basic duplicate check
if len(world.Rovers) != 1 {
t.Error("Too many rovers left in world")
} else if _, ok := world.Rovers[b]; !ok {
t.Error("Remaining rover is incorrect")
}
}
func TestWorld_GetSetMovePosition(t *testing.T) {
world := NewWorld(4)
a, err := world.SpawnRover("")
assert.NoError(t, err)
pos := maths.Vector{
X: 0.0,
Y: 0.0,
}
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
newPos, err := world.RoverPosition(a)
assert.NoError(t, err, "Failed to set position for rover")
assert.Equal(t, pos, newPos, "Failed to correctly set position for rover")
b := roveapi.Bearing_North
newPos, err = world.TryMoveRover(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.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)
assert.NoError(t, err, "Failed to move rover")
assert.Equal(t, pos, newPos, "Failed to correctly not move position for rover into wall")
}
func TestWorld_RadarFromRover(t *testing.T) {
// Create world that should have visible walls on the radar
world := NewWorld(2)
a, err := world.SpawnRover("")
assert.NoError(t, err)
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
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
// Check the radar results are stable
radar1, objs1, err := world.RadarFromRover(a)
assert.NoError(t, err)
radar2, objs2, err := world.RadarFromRover(a)
assert.NoError(t, err)
assert.Equal(t, radar1, radar2)
assert.Equal(t, objs1, objs2)
}
func TestWorld_RoverDamage(t *testing.T) {
world := NewWorld(2)
acc, err := world.Accountant.RegisterAccount("tmp")
assert.NoError(t, err)
a, err := world.SpawnRover(acc.Name)
assert.NoError(t, err)
pos := maths.Vector{
X: 0.0,
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})
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")
newinfo, err := world.GetRover(a)
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()
// Rover should have been destroyed now
_, err = world.GetRover(a)
assert.Error(t, err)
_, 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("")
assert.NoError(t, err)
// Remove rover charge
rover := world.Rovers[a]
rover.Charge = 0
world.Rovers[a] = rover
// Try and recharge, should work
_, err = world.RoverRecharge(a)
assert.NoError(t, err)
assert.Equal(t, 1, world.Rovers[a].Charge)
// Loop for half the day
for i := 0; i < world.TicksPerDay/2; i++ {
assert.True(t, world.Daytime())
world.Tick()
}
// Remove rover charge again
rover = world.Rovers[a]
rover.Charge = 0
world.Rovers[a] = rover
// Try and recharge, should fail
_, err = world.RoverRecharge(a)
assert.NoError(t, err)
assert.Equal(t, 0, world.Rovers[a].Charge)
// Loop for half the day
for i := 0; i < world.TicksPerDay/2; i++ {
assert.False(t, world.Daytime())
world.Tick()
}
}
func TestWorld_Broadcast(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover("")
assert.NoError(t, err)
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}))
// Broadcast from a
assert.NoError(t, world.RoverBroadcast(a, []byte{'A', 'B', 'C'}))
// Check if b heard it
ra, err := world.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, ra.MaximumCharge-1, ra.Charge, "A should have used a charge to broadcast")
assert.Contains(t, ra.Logs[len(ra.Logs)-1].Text, "ABC", "Rover B should have heard the broadcast")
// Check if a logged it
rb, err := world.GetRover(b)
assert.NoError(t, err)
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})
assert.NoError(t, world.WarpRover(b, maths.Vector{X: ra.Range, Y: 0}))
// Broadcast from a again
assert.NoError(t, world.RoverBroadcast(a, []byte{'X', 'Y', 'Z'}))
// Check if b heard it
ra, err = world.GetRover(b)
assert.NoError(t, err)
assert.NotContains(t, ra.Logs[len(ra.Logs)-1].Text, "XYZ", "Rover B should not have heard the broadcast")
// Check if a logged it
rb, err = world.GetRover(a)
assert.NoError(t, err)
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})
assert.NoError(t, world.WarpRover(b, maths.Vector{X: ra.Range + 1, Y: 0}))
// Broadcast from a again
assert.NoError(t, world.RoverBroadcast(a, []byte{'H', 'J', 'K'}))
// Check if b heard it
ra, err = world.GetRover(b)
assert.NoError(t, err)
assert.NotContains(t, ra.Logs[len(ra.Logs)-1].Text, "HJK", "Rover B should have heard the broadcast")
// Check if a logged it
rb, err = world.GetRover(a)
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)
}

70
pkg/rove/worldgen.go Normal file
View file

@ -0,0 +1,70 @@
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
}

View file

@ -1,23 +0,0 @@
Google APIs
============
Project: Google APIs
URL: https://github.com/google/googleapis
Revision: 3544ab16c3342d790b00764251e348705991ea4b
License: Apache License 2.0
Imported Files
---------------
- google/api/annotations.proto
- google/api/http.proto
- google/api/httpbody.proto
Generated Files
----------------
They are generated from the .proto files by protoc-gen-go.
- google/api/annotations.pb.go
- google/api/http.pb.go

View file

@ -1,31 +0,0 @@
// Copyright (c) 2015, Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.api;
import "google/api/http.proto";
import "google/protobuf/descriptor.proto";
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "AnnotationsProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
extend google.protobuf.MethodOptions {
// See `HttpRule`.
HttpRule http = 72295728;
}

View file

@ -1,318 +0,0 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.api;
option cc_enable_arenas = true;
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "HttpProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
// Defines the HTTP configuration for an API service. It contains a list of
// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
// to one or more HTTP REST API methods.
message Http {
// A list of HTTP configuration rules that apply to individual API methods.
//
// **NOTE:** All service configuration rules follow "last one wins" order.
repeated HttpRule rules = 1;
// When set to true, URL path parmeters will be fully URI-decoded except in
// cases of single segment matches in reserved expansion, where "%2F" will be
// left encoded.
//
// The default behavior is to not decode RFC 6570 reserved characters in multi
// segment matches.
bool fully_decode_reserved_expansion = 2;
}
// `HttpRule` defines the mapping of an RPC method to one or more HTTP
// REST API methods. The mapping specifies how different portions of the RPC
// request message are mapped to URL path, URL query parameters, and
// HTTP request body. The mapping is typically specified as an
// `google.api.http` annotation on the RPC method,
// see "google/api/annotations.proto" for details.
//
// The mapping consists of a field specifying the path template and
// method kind. The path template can refer to fields in the request
// message, as in the example below which describes a REST GET
// operation on a resource collection of messages:
//
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}";
// }
// }
// message GetMessageRequest {
// message SubMessage {
// string subfield = 1;
// }
// string message_id = 1; // mapped to the URL
// SubMessage sub = 2; // `sub.subfield` is url-mapped
// }
// message Message {
// string text = 1; // content of the resource
// }
//
// The same http annotation can alternatively be expressed inside the
// `GRPC API Configuration` YAML file.
//
// http:
// rules:
// - selector: <proto_package_name>.Messaging.GetMessage
// get: /v1/messages/{message_id}/{sub.subfield}
//
// This definition enables an automatic, bidrectional mapping of HTTP
// JSON to RPC. Example:
//
// HTTP | RPC
// -----|-----
// `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))`
//
// In general, not only fields but also field paths can be referenced
// from a path pattern. Fields mapped to the path pattern cannot be
// repeated and must have a primitive (non-message) type.
//
// Any fields in the request message which are not bound by the path
// pattern automatically become (optional) HTTP query
// parameters. Assume the following definition of the request message:
//
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http).get = "/v1/messages/{message_id}";
// }
// }
// message GetMessageRequest {
// message SubMessage {
// string subfield = 1;
// }
// string message_id = 1; // mapped to the URL
// int64 revision = 2; // becomes a parameter
// SubMessage sub = 3; // `sub.subfield` becomes a parameter
// }
//
//
// This enables a HTTP JSON to RPC mapping as below:
//
// HTTP | RPC
// -----|-----
// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))`
//
// Note that fields which are mapped to HTTP parameters must have a
// primitive type or a repeated primitive type. Message types are not
// allowed. In the case of a repeated type, the parameter can be
// repeated in the URL, as in `...?param=A&param=B`.
//
// For HTTP method kinds which allow a request body, the `body` field
// specifies the mapping. Consider a REST update method on the
// message resource collection:
//
//
// service Messaging {
// rpc UpdateMessage(UpdateMessageRequest) returns (Message) {
// option (google.api.http) = {
// put: "/v1/messages/{message_id}"
// body: "message"
// };
// }
// }
// message UpdateMessageRequest {
// string message_id = 1; // mapped to the URL
// Message message = 2; // mapped to the body
// }
//
//
// The following HTTP JSON to RPC mapping is enabled, where the
// representation of the JSON in the request body is determined by
// protos JSON encoding:
//
// HTTP | RPC
// -----|-----
// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })`
//
// The special name `*` can be used in the body mapping to define that
// every field not bound by the path template should be mapped to the
// request body. This enables the following alternative definition of
// the update method:
//
// service Messaging {
// rpc UpdateMessage(Message) returns (Message) {
// option (google.api.http) = {
// put: "/v1/messages/{message_id}"
// body: "*"
// };
// }
// }
// message Message {
// string message_id = 1;
// string text = 2;
// }
//
//
// The following HTTP JSON to RPC mapping is enabled:
//
// HTTP | RPC
// -----|-----
// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")`
//
// Note that when using `*` in the body mapping, it is not possible to
// have HTTP parameters, as all fields not bound by the path end in
// the body. This makes this option more rarely used in practice of
// defining REST APIs. The common usage of `*` is in custom methods
// which don't use the URL at all for transferring data.
//
// It is possible to define multiple HTTP methods for one RPC by using
// the `additional_bindings` option. Example:
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http) = {
// get: "/v1/messages/{message_id}"
// additional_bindings {
// get: "/v1/users/{user_id}/messages/{message_id}"
// }
// };
// }
// }
// message GetMessageRequest {
// string message_id = 1;
// string user_id = 2;
// }
//
//
// This enables the following two alternative HTTP JSON to RPC
// mappings:
//
// HTTP | RPC
// -----|-----
// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")`
// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")`
//
// # Rules for HTTP mapping
//
// The rules for mapping HTTP path, query parameters, and body fields
// to the request message are as follows:
//
// 1. The `body` field specifies either `*` or a field path, or is
// omitted. If omitted, it indicates there is no HTTP request body.
// 2. Leaf fields (recursive expansion of nested messages in the
// request) can be classified into three types:
// (a) Matched in the URL template.
// (b) Covered by body (if body is `*`, everything except (a) fields;
// else everything under the body field)
// (c) All other fields.
// 3. URL query parameters found in the HTTP request are mapped to (c) fields.
// 4. Any body sent with an HTTP request can contain only (b) fields.
//
// The syntax of the path template is as follows:
//
// Template = "/" Segments [ Verb ] ;
// Segments = Segment { "/" Segment } ;
// Segment = "*" | "**" | LITERAL | Variable ;
// Variable = "{" FieldPath [ "=" Segments ] "}" ;
// FieldPath = IDENT { "." IDENT } ;
// Verb = ":" LITERAL ;
//
// The syntax `*` matches a single path segment. The syntax `**` matches zero
// or more path segments, which must be the last part of the path except the
// `Verb`. The syntax `LITERAL` matches literal text in the path.
//
// The syntax `Variable` matches part of the URL path as specified by its
// template. A variable template must not contain other variables. If a variable
// matches a single path segment, its template may be omitted, e.g. `{var}`
// is equivalent to `{var=*}`.
//
// If a variable contains exactly one path segment, such as `"{var}"` or
// `"{var=*}"`, when such a variable is expanded into a URL path, all characters
// except `[-_.~0-9a-zA-Z]` are percent-encoded. Such variables show up in the
// Discovery Document as `{var}`.
//
// If a variable contains one or more path segments, such as `"{var=foo/*}"`
// or `"{var=**}"`, when such a variable is expanded into a URL path, all
// characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. Such variables
// show up in the Discovery Document as `{+var}`.
//
// NOTE: While the single segment variable matches the semantics of
// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2
// Simple String Expansion, the multi segment variable **does not** match
// RFC 6570 Reserved Expansion. The reason is that the Reserved Expansion
// does not expand special characters like `?` and `#`, which would lead
// to invalid URLs.
//
// NOTE: the field paths in variables and in the `body` must not refer to
// repeated fields or map fields.
message HttpRule {
// Selects methods to which this rule applies.
//
// Refer to [selector][google.api.DocumentationRule.selector] for syntax details.
string selector = 1;
// Determines the URL pattern is matched by this rules. This pattern can be
// used with any of the {get|put|post|delete|patch} methods. A custom method
// can be defined using the 'custom' field.
oneof pattern {
// Used for listing and getting information about resources.
string get = 2;
// Used for updating a resource.
string put = 3;
// Used for creating a resource.
string post = 4;
// Used for deleting a resource.
string delete = 5;
// Used for updating a resource.
string patch = 6;
// The custom pattern is used for specifying an HTTP method that is not
// included in the `pattern` field, such as HEAD, or "*" to leave the
// HTTP method unspecified for this rule. The wild-card rule is useful
// for services that provide content to Web (HTML) clients.
CustomHttpPattern custom = 8;
}
// The name of the request field whose value is mapped to the HTTP body, or
// `*` for mapping all fields not captured by the path pattern to the HTTP
// body. NOTE: the referred field must not be a repeated field and must be
// present at the top-level of request message type.
string body = 7;
// Optional. The name of the response field whose value is mapped to the HTTP
// body of response. Other response fields are ignored. When
// not set, the response message will be used as HTTP body of response.
string response_body = 12;
// Additional HTTP bindings for the selector. Nested bindings must
// not contain an `additional_bindings` field themselves (that is,
// the nesting may only be one level deep).
repeated HttpRule additional_bindings = 11;
}
// A custom pattern is used for defining custom HTTP verb.
message CustomHttpPattern {
// The name of this custom HTTP verb.
string kind = 1;
// The path matched by this custom verb.
string path = 2;
}

View file

@ -1,78 +0,0 @@
// Copyright 2018 Google LLC.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
syntax = "proto3";
package google.api;
import "google/protobuf/any.proto";
option cc_enable_arenas = true;
option go_package = "google.golang.org/genproto/googleapis/api/httpbody;httpbody";
option java_multiple_files = true;
option java_outer_classname = "HttpBodyProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
// Message that represents an arbitrary HTTP body. It should only be used for
// payload formats that can't be represented as JSON, such as raw binary or
// an HTML page.
//
//
// This message can be used both in streaming and non-streaming API methods in
// the request as well as the response.
//
// It can be used as a top-level request field, which is convenient if one
// wants to extract parameters from either the URL or HTTP template into the
// request fields and also want access to the raw HTTP body.
//
// Example:
//
// message GetResourceRequest {
// // A unique request id.
// string request_id = 1;
//
// // The raw HTTP body is bound to this field.
// google.api.HttpBody http_body = 2;
// }
//
// service ResourceService {
// rpc GetResource(GetResourceRequest) returns (google.api.HttpBody);
// rpc UpdateResource(google.api.HttpBody) returns
// (google.protobuf.Empty);
// }
//
// Example with streaming methods:
//
// service CaldavService {
// rpc GetCalendar(stream google.api.HttpBody)
// returns (stream google.api.HttpBody);
// rpc UpdateCalendar(stream google.api.HttpBody)
// returns (stream google.api.HttpBody);
// }
//
// Use of this type only changes how the request and response bodies are
// handled, all other features will continue to work unchanged.
message HttpBody {
// The HTTP Content-Type header value specifying the content type of the body.
string content_type = 1;
// The HTTP request/response body as raw binary.
bytes data = 2;
// Application specific response metadata. Must be set in the first response
// for streaming APIs.
repeated google.protobuf.Any extensions = 3;
}

View file

@ -1,186 +0,0 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.rpc;
option go_package = "google.golang.org/genproto/googleapis/rpc/code;code";
option java_multiple_files = true;
option java_outer_classname = "CodeProto";
option java_package = "com.google.rpc";
option objc_class_prefix = "RPC";
// The canonical error codes for Google APIs.
//
//
// Sometimes multiple error codes may apply. Services should return
// the most specific error code that applies. For example, prefer
// `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply.
// Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`.
enum Code {
// Not an error; returned on success
//
// HTTP Mapping: 200 OK
OK = 0;
// The operation was cancelled, typically by the caller.
//
// HTTP Mapping: 499 Client Closed Request
CANCELLED = 1;
// Unknown error. For example, this error may be returned when
// a `Status` value received from another address space belongs to
// an error space that is not known in this address space. Also
// errors raised by APIs that do not return enough error information
// may be converted to this error.
//
// HTTP Mapping: 500 Internal Server Error
UNKNOWN = 2;
// The client specified an invalid argument. Note that this differs
// from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments
// that are problematic regardless of the state of the system
// (e.g., a malformed file name).
//
// HTTP Mapping: 400 Bad Request
INVALID_ARGUMENT = 3;
// The deadline expired before the operation could complete. For operations
// that change the state of the system, this error may be returned
// even if the operation has completed successfully. For example, a
// successful response from a server could have been delayed long
// enough for the deadline to expire.
//
// HTTP Mapping: 504 Gateway Timeout
DEADLINE_EXCEEDED = 4;
// Some requested entity (e.g., file or directory) was not found.
//
// Note to server developers: if a request is denied for an entire class
// of users, such as gradual feature rollout or undocumented whitelist,
// `NOT_FOUND` may be used. If a request is denied for some users within
// a class of users, such as user-based access control, `PERMISSION_DENIED`
// must be used.
//
// HTTP Mapping: 404 Not Found
NOT_FOUND = 5;
// The entity that a client attempted to create (e.g., file or directory)
// already exists.
//
// HTTP Mapping: 409 Conflict
ALREADY_EXISTS = 6;
// The caller does not have permission to execute the specified
// operation. `PERMISSION_DENIED` must not be used for rejections
// caused by exhausting some resource (use `RESOURCE_EXHAUSTED`
// instead for those errors). `PERMISSION_DENIED` must not be
// used if the caller can not be identified (use `UNAUTHENTICATED`
// instead for those errors). This error code does not imply the
// request is valid or the requested entity exists or satisfies
// other pre-conditions.
//
// HTTP Mapping: 403 Forbidden
PERMISSION_DENIED = 7;
// The request does not have valid authentication credentials for the
// operation.
//
// HTTP Mapping: 401 Unauthorized
UNAUTHENTICATED = 16;
// Some resource has been exhausted, perhaps a per-user quota, or
// perhaps the entire file system is out of space.
//
// HTTP Mapping: 429 Too Many Requests
RESOURCE_EXHAUSTED = 8;
// The operation was rejected because the system is not in a state
// required for the operation's execution. For example, the directory
// to be deleted is non-empty, an rmdir operation is applied to
// a non-directory, etc.
//
// Service implementors can use the following guidelines to decide
// between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`:
// (a) Use `UNAVAILABLE` if the client can retry just the failing call.
// (b) Use `ABORTED` if the client should retry at a higher level
// (e.g., when a client-specified test-and-set fails, indicating the
// client should restart a read-modify-write sequence).
// (c) Use `FAILED_PRECONDITION` if the client should not retry until
// the system state has been explicitly fixed. E.g., if an "rmdir"
// fails because the directory is non-empty, `FAILED_PRECONDITION`
// should be returned since the client should not retry unless
// the files are deleted from the directory.
//
// HTTP Mapping: 400 Bad Request
FAILED_PRECONDITION = 9;
// The operation was aborted, typically due to a concurrency issue such as
// a sequencer check failure or transaction abort.
//
// See the guidelines above for deciding between `FAILED_PRECONDITION`,
// `ABORTED`, and `UNAVAILABLE`.
//
// HTTP Mapping: 409 Conflict
ABORTED = 10;
// The operation was attempted past the valid range. E.g., seeking or
// reading past end-of-file.
//
// Unlike `INVALID_ARGUMENT`, this error indicates a problem that may
// be fixed if the system state changes. For example, a 32-bit file
// system will generate `INVALID_ARGUMENT` if asked to read at an
// offset that is not in the range [0,2^32-1], but it will generate
// `OUT_OF_RANGE` if asked to read from an offset past the current
// file size.
//
// There is a fair bit of overlap between `FAILED_PRECONDITION` and
// `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific
// error) when it applies so that callers who are iterating through
// a space can easily look for an `OUT_OF_RANGE` error to detect when
// they are done.
//
// HTTP Mapping: 400 Bad Request
OUT_OF_RANGE = 11;
// The operation is not implemented or is not supported/enabled in this
// service.
//
// HTTP Mapping: 501 Not Implemented
UNIMPLEMENTED = 12;
// Internal errors. This means that some invariants expected by the
// underlying system have been broken. This error code is reserved
// for serious errors.
//
// HTTP Mapping: 500 Internal Server Error
INTERNAL = 13;
// The service is currently unavailable. This is most likely a
// transient condition, which can be corrected by retrying with
// a backoff.
//
// See the guidelines above for deciding between `FAILED_PRECONDITION`,
// `ABORTED`, and `UNAVAILABLE`.
//
// HTTP Mapping: 503 Service Unavailable
UNAVAILABLE = 14;
// Unrecoverable data loss or corruption.
//
// HTTP Mapping: 500 Internal Server Error
DATA_LOSS = 15;
}

View file

@ -1,200 +0,0 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.rpc;
import "google/protobuf/duration.proto";
option go_package = "google.golang.org/genproto/googleapis/rpc/errdetails;errdetails";
option java_multiple_files = true;
option java_outer_classname = "ErrorDetailsProto";
option java_package = "com.google.rpc";
option objc_class_prefix = "RPC";
// Describes when the clients can retry a failed request. Clients could ignore
// the recommendation here or retry when this information is missing from error
// responses.
//
// It's always recommended that clients should use exponential backoff when
// retrying.
//
// Clients should wait until `retry_delay` amount of time has passed since
// receiving the error response before retrying. If retrying requests also
// fail, clients should use an exponential backoff scheme to gradually increase
// the delay between retries based on `retry_delay`, until either a maximum
// number of retires have been reached or a maximum retry delay cap has been
// reached.
message RetryInfo {
// Clients should wait at least this long between retrying the same request.
google.protobuf.Duration retry_delay = 1;
}
// Describes additional debugging info.
message DebugInfo {
// The stack trace entries indicating where the error occurred.
repeated string stack_entries = 1;
// Additional debugging information provided by the server.
string detail = 2;
}
// Describes how a quota check failed.
//
// For example if a daily limit was exceeded for the calling project,
// a service could respond with a QuotaFailure detail containing the project
// id and the description of the quota limit that was exceeded. If the
// calling project hasn't enabled the service in the developer console, then
// a service could respond with the project id and set `service_disabled`
// to true.
//
// Also see RetryDetail and Help types for other details about handling a
// quota failure.
message QuotaFailure {
// A message type used to describe a single quota violation. For example, a
// daily quota or a custom quota that was exceeded.
message Violation {
// The subject on which the quota check failed.
// For example, "clientip:<ip address of client>" or "project:<Google
// developer project id>".
string subject = 1;
// A description of how the quota check failed. Clients can use this
// description to find more about the quota configuration in the service's
// public documentation, or find the relevant quota limit to adjust through
// developer console.
//
// For example: "Service disabled" or "Daily Limit for read operations
// exceeded".
string description = 2;
}
// Describes all quota violations.
repeated Violation violations = 1;
}
// Describes what preconditions have failed.
//
// For example, if an RPC failed because it required the Terms of Service to be
// acknowledged, it could list the terms of service violation in the
// PreconditionFailure message.
message PreconditionFailure {
// A message type used to describe a single precondition failure.
message Violation {
// The type of PreconditionFailure. We recommend using a service-specific
// enum type to define the supported precondition violation types. For
// example, "TOS" for "Terms of Service violation".
string type = 1;
// The subject, relative to the type, that failed.
// For example, "google.com/cloud" relative to the "TOS" type would
// indicate which terms of service is being referenced.
string subject = 2;
// A description of how the precondition failed. Developers can use this
// description to understand how to fix the failure.
//
// For example: "Terms of service not accepted".
string description = 3;
}
// Describes all precondition violations.
repeated Violation violations = 1;
}
// Describes violations in a client request. This error type focuses on the
// syntactic aspects of the request.
message BadRequest {
// A message type used to describe a single bad request field.
message FieldViolation {
// A path leading to a field in the request body. The value will be a
// sequence of dot-separated identifiers that identify a protocol buffer
// field. E.g., "field_violations.field" would identify this field.
string field = 1;
// A description of why the request element is bad.
string description = 2;
}
// Describes all violations in a client request.
repeated FieldViolation field_violations = 1;
}
// Contains metadata about the request that clients can attach when filing a bug
// or providing other forms of feedback.
message RequestInfo {
// An opaque string that should only be interpreted by the service generating
// it. For example, it can be used to identify requests in the service's logs.
string request_id = 1;
// Any data that was used to serve this request. For example, an encrypted
// stack trace that can be sent back to the service provider for debugging.
string serving_data = 2;
}
// Describes the resource that is being accessed.
message ResourceInfo {
// A name for the type of resource being accessed, e.g. "sql table",
// "cloud storage bucket", "file", "Google calendar"; or the type URL
// of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic".
string resource_type = 1;
// The name of the resource being accessed. For example, a shared calendar
// name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current
// error is [google.rpc.Code.PERMISSION_DENIED][google.rpc.Code.PERMISSION_DENIED].
string resource_name = 2;
// The owner of the resource (optional).
// For example, "user:<owner email>" or "project:<Google developer project
// id>".
string owner = 3;
// Describes what error is encountered when accessing this resource.
// For example, updating a cloud project may require the `writer` permission
// on the developer console project.
string description = 4;
}
// Provides links to documentation or for performing an out of band action.
//
// For example, if a quota check failed with an error indicating the calling
// project hasn't enabled the accessed service, this can contain a URL pointing
// directly to the right place in the developer console to flip the bit.
message Help {
// Describes a URL link.
message Link {
// Describes what the link offers.
string description = 1;
// The URL of the link.
string url = 2;
}
// URL(s) pointing to additional information on handling the current error.
repeated Link links = 1;
}
// Provides a localized error message that is safe to return to the user
// which can be attached to an RPC error.
message LocalizedMessage {
// The locale used following the specification defined at
// http://www.rfc-editor.org/rfc/bcp/bcp47.txt.
// Examples are: "en-US", "fr-CH", "es-MX"
string locale = 1;
// The localized error message in the above locale.
string message = 2;
}

View file

@ -1,92 +0,0 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.rpc;
import "google/protobuf/any.proto";
option go_package = "google.golang.org/genproto/googleapis/rpc/status;status";
option java_multiple_files = true;
option java_outer_classname = "StatusProto";
option java_package = "com.google.rpc";
option objc_class_prefix = "RPC";
// The `Status` type defines a logical error model that is suitable for different
// programming environments, including REST APIs and RPC APIs. It is used by
// [gRPC](https://github.com/grpc). The error model is designed to be:
//
// - Simple to use and understand for most users
// - Flexible enough to meet unexpected needs
//
// # Overview
//
// The `Status` message contains three pieces of data: error code, error message,
// and error details. The error code should be an enum value of
// [google.rpc.Code][google.rpc.Code], but it may accept additional error codes if needed. The
// error message should be a developer-facing English message that helps
// developers *understand* and *resolve* the error. If a localized user-facing
// error message is needed, put the localized message in the error details or
// localize it in the client. The optional error details may contain arbitrary
// information about the error. There is a predefined set of error detail types
// in the package `google.rpc` that can be used for common error conditions.
//
// # Language mapping
//
// The `Status` message is the logical representation of the error model, but it
// is not necessarily the actual wire format. When the `Status` message is
// exposed in different client libraries and different wire protocols, it can be
// mapped differently. For example, it will likely be mapped to some exceptions
// in Java, but more likely mapped to some error codes in C.
//
// # Other uses
//
// The error model and the `Status` message can be used in a variety of
// environments, either with or without APIs, to provide a
// consistent developer experience across different environments.
//
// Example uses of this error model include:
//
// - Partial errors. If a service needs to return partial errors to the client,
// it may embed the `Status` in the normal response to indicate the partial
// errors.
//
// - Workflow errors. A typical workflow has multiple steps. Each step may
// have a `Status` message for error reporting.
//
// - Batch operations. If a client uses batch request and batch response, the
// `Status` message should be used directly inside batch response, one for
// each error sub-response.
//
// - Asynchronous operations. If an API call embeds asynchronous operation
// results in its response, the status of those operations should be
// represented directly using the `Status` message.
//
// - Logging. If some API errors are stored in logs, the message `Status` could
// be used directly after any stripping needed for security/privacy reasons.
message Status {
// The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
int32 code = 1;
// A developer-facing error message, which should be in English. Any
// user-facing error message should be localized and sent in the
// [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
string message = 2;
// A list of messages that carry the error details. There is a common set of
// message types for APIs to use.
repeated google.protobuf.Any details = 3;
}

View file

@ -1,197 +0,0 @@
syntax = "proto3";
// Rove
//
// Rove is an asychronous nomadic game about exploring a planet as part of a loose community
package rove;
option go_package = "github.com/mdiluz/rove/pkg/rove";
import "google/api/annotations.proto";
service Rove {
// Server status
//
// Responds with various details about the current server status
rpc ServerStatus(ServerStatusRequest) returns (ServerStatusResponse) {
option (google.api.http) = {
get: "/server-status"
};
}
// Register an account
//
// Tries to register an account with the given name
rpc Register(RegisterRequest) returns (RegisterResponse) {
option (google.api.http) = {
post: "/register"
body: "*"
};
}
// Send commands to rover
//
// Sending commands to this endpoint will queue them to be executed during the following ticks, in the order sent
rpc Command(CommandRequest) returns (CommandResponse) {
option (google.api.http) = {
post: "/command"
body: "*"
};
}
// Get radar information
//
// Gets the radar output for the given rover
rpc Radar(RadarRequest) returns (RadarResponse) {
option (google.api.http) = {
post: "/radar"
body: "*"
};
}
// Get rover information
//
// Gets information for the account's rover
rpc Status(StatusRequest) returns (StatusResponse) {
option (google.api.http) = {
post: "/status"
body: "*"
};
}
}
message Command {
// The command to execute
// "move" - Move the rover in a direction, requires bearing
// "stash" - Stashes item at current location in rover inventory
// "repair" - Repairs the rover using an inventory object
// "recharge" - Waits a tick to add more charge to the rover
string command = 1;
// The bearing, example: NE
string bearing = 2;
}
message CommandRequest {
// The account to execute these commands
Account account = 1;
// The set of desired commands
repeated Command commands = 2;
}
// Empty placeholder
message CommandResponse {}
message Error {
// An explanation for the HTTP error returned
string error = 1;
}
message RadarRequest {
// The account for this request
Account account = 1;
}
message RadarResponse {
// The range in tiles from the rover of the radar data
int32 range = 1;
// A 1D array representing range*2 + 1 squared set of tiles, origin bottom left and in row->column order
bytes tiles = 2;
// A similar array to the tile array, but containing objects
bytes objects = 3;
}
message RegisterRequest {
// The desired account name
string name = 1;
}
// Empty placeholder
message RegisterResponse{
// The registered account information
Account account = 1;
}
message StatusRequest {
// The account for this request
Account account = 1;
}
message Log {
// The unix timestamp of the log
string time = 1;
// The text of the log
string text = 2;
}
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
int32 range = 3;
// The items in the rover inventory
bytes inventory = 4;
// The capacity of the inventory
int32 capacity = 5;
// The current health of the rover
int32 integrity = 6;
// The maximum health of the rover
int32 maximumIntegrity = 7;
// The energy stored in the rover
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 = 11;
// The most recent logs
repeated Log logs = 12;
}
// Empty placeholder
message ServerStatusRequest {}
message ServerStatusResponse {
// The version of the server in v{major}.{minor}-{delta}-{sha} form
string version = 1;
// Whether the server is ready to accept requests
bool ready = 2;
// The tick rate of the server in minutes (how many minutes per tick)
int32 tickRate = 3;
// The current tick of the server
int32 currentTick = 4;
// The time the next tick will occur
string next_tick = 5;
}
message Vector {
int32 x = 1;
int32 y = 2;
}
message Account {
string name = 1;
string secret = 2;
}

2202
proto/roveapi/roveapi.pb.go Normal file

File diff suppressed because it is too large Load diff

322
proto/roveapi/roveapi.proto Normal file
View file

@ -0,0 +1,322 @@
syntax = "proto3";
// Rove
//
// Rove is an asychronous nomadic game about exploring a planet as part of a
// loose community
package roveapi;
option go_package = "github.com/mdiluz/rove/proto/roveapi";
// The Rove server hosts a single game session and world with multiple players
service Rove {
// Server status
// Responds with various details about the current server status
rpc ServerStatus(ServerStatusRequest) returns (ServerStatusResponse) {}
// Register an account
// Tries to register an account with the given name
rpc Register(RegisterRequest) returns (RegisterResponse) {}
// Send commands to rover
// Sending commands to this endpoint will queue them to be executed during the
// following ticks, in the order sent. Commands sent within the same tick will
// overwrite until the tick has finished and the commands are queued
rpc Command(CommandRequest) returns (CommandResponse) {}
// Get radar information
// Gets the radar output for the given rover
rpc Radar(RadarRequest) returns (RadarResponse) {}
// Get rover information
// Gets information for the account's rover
rpc Status(StatusRequest) returns (StatusResponse) {}
}
//
// ServerStatus
//
// ServerStatusRequest is an empty placeholder
message ServerStatusRequest {}
// ServerStatusResponse is a response with useful server information
message ServerStatusResponse {
// The version of the server in v{major}.{minor}-{delta}-{sha} form
string version = 1;
// Whether the server is ready to accept requests
bool ready = 2;
// The tick rate of the server in minutes (how many minutes per tick)
int32 tickRate = 3;
// The current tick of the server
int32 currentTick = 4;
// The time the next tick will occur
string next_tick = 5;
}
//
// Register
//
// RegisterRequest contains data to register an account
message RegisterRequest {
// The desired account name
string name = 1;
}
// Account describes a registered account
message Account {
// The account name
string name = 1;
// The account secret value, given when creating the account
string secret = 2;
}
// RegisterResponse is the response given to registering an account
message RegisterResponse {
// The registered account information
Account account = 1;
}
//
// Command
//
// 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;
// Stashes item at current location in rover inventory
stash = 4;
// 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;
}
// 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;
}
// Command is a single command for a rover
message Command {
// The command type
CommandType command = 1;
// The number of times to repeat the command after the first
int32 repeat = 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;
}
// CommandRequest describes a set of commands to be requested for the rover
message CommandRequest {
// The account to execute these commands
Account account = 1;
// The set of desired commands
repeated Command commands = 2;
}
// CommandResponse is an empty placeholder
message CommandResponse {}
//
// Radar
//
// Types of objects
enum Object {
// ObjectUnknown represents no object at all
ObjectUnknown = 0;
// RoverLive represents a live rover
RoverLive = 1;
// RoverDormant describes a dormant rover
RoverDormant = 2;
// RockSmall is a small stashable rock
RockSmall = 3;
// RockLarge is a large blocking rock
RockLarge = 4;
// RoverParts is one unit of rover parts, used for repairing and fixing the
// rover
RoverParts = 5;
}
enum Tile {
// TileUnknown is a keyword for nothing
TileUnknown = 0;
// Rock is solid rock ground
Rock = 1;
// Gravel is loose rocks
Gravel = 2;
// Sand is sand
Sand = 3;
}
// RadarRequest is the data needed to request the radar for a rover
message RadarRequest {
// The account for this request
Account account = 1;
}
// RadarResponse describes radar information
message RadarResponse {
// The range in tiles from the rover of the radar data
int32 range = 1;
// A 1D array representing range*2 + 1 squared set of tiles, origin bottom
// left and in row->column order
repeated Tile tiles = 2;
// A similar array to the tile array, but containing objects
repeated Object objects = 3;
}
//
// Status
//
// StatusRequest is information needed to request rover status
message StatusRequest {
// The account for this request
Account account = 1;
}
// Log is a single log item
message Log {
// The unix timestamp of the log
string time = 1;
// The text of the log
string text = 2;
}
// Vector describes a point or vector in 2D space
message Vector {
int32 x = 1;
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 {
// The name of the rover
string name = 1;
// 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;
// The items in the rover inventory
bytes inventory = 3;
// The current health of the rover
int32 integrity = 4;
// The energy stored in the rover
int32 charge = 5;
// 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;
// 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;
}

View file

@ -27,15 +27,7 @@ apps:
environment:
WORDS_FILE : "$SNAP/data/words_alpha.txt"
DATA_PATH : $SNAP_USER_DATA
rove-rest-server:
command: bin/rove-server-rest-proxy
plugs:
- network
- network-bind
environment:
DATA_PATH : $SNAP_USER_DATA
parts:
go-rove:
plugin: go