Compare commits

...

378 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
2671398593
Merge pull request #15 from mdiluz/rover-logs-and-communication
Add rover logs
2020-07-09 19:42:48 +01:00
30ca488890 Use string for the timestamp, proto uses this under the hood anyway
https://github.com/grpc-ecosystem/grpc-gateway/issues/438
2020-07-09 19:38:23 +01:00
b748846c55 Use a unix timestamp rather than a timestamppb 2020-07-09 19:29:04 +01:00
55c85d2a22 Add logs to the rover status output 2020-07-09 19:01:09 +01:00
b2f169d99f Remove Warped log, unneeded 2020-07-09 18:31:51 +01:00
8866f28bf5 Add test coverage checks for logging additions 2020-07-09 18:26:24 +01:00
0dc3cab9c0 Store log entries for actions in the rover 2020-07-09 18:19:49 +01:00
84be8bff05
Merge pull request #14 from mdiluz/improved-world-gen
Noise based world gen
2020-07-09 00:11:46 +01:00
9682cfa7ea Spawn objects using OpenSimplex noise as well 2020-07-09 00:04:46 +01:00
4b715bdff3 Move to OpenSimplex noise
Apart from other benefits, this produces much nicer direction agnostic noise
2020-07-08 23:58:11 +01:00
7b4541716a Add gravel tiles 2020-07-08 23:45:52 +01:00
ed9ecef80a Add perlin based generation for the terrain tiles 2020-07-08 23:38:08 +01:00
10959ef726 Refactor populate to be an Atlas function
This simplifies usage greatly
2020-07-08 19:40:15 +01:00
2ff4bcded7
Merge pull request #12 from mdiluz/fix-unstable-radar
Fix unstable radar
2020-07-07 23:05:47 +01:00
0386617c51 Add error checks in TestWorld_RadarFromRover 2020-07-07 23:01:28 +01:00
089f5e5337 Fix chunk empty chunk population in QueryPosition 2020-07-07 22:57:55 +01:00
3e1e3a5456 Amend to TestWorld_RadarFromRover to show the issue 2020-07-07 22:49:34 +01:00
d9e97ea468 Add some additional logging to requests and world resizes 2020-07-07 22:47:34 +01:00
47df02ec7e
Merge pull request #10 from mdiluz/basic-account-security
WIP: Add basic account security
2020-07-07 22:27:06 +01:00
92222127a6 Add basic account security
This adds a secret token associated with each account

	The token must then be sent with follow-up requests to ensure they get accepted

	This is _very_ basic security, and without TLS is completely vulnerable to MITM attacks, as well as brute force guessing (though it'd take a while to guess the a correct UUID)
2020-07-07 22:20:23 +01:00
df30a0d689
Merge pull request #1 from mdiluz/day-night-cycle
Add a day-night cycle for solar charging
2020-07-07 21:38:37 +01:00
5980de5ba7 Fix lint check 2020-07-07 21:33:32 +01:00
254957cde5 Add a test to check daytime and rover recharge 2020-07-07 21:30:51 +01:00
3ba7652c74 Add current tick information to the server-status 2020-07-07 18:40:38 +01:00
1412579c6c Only charge during the day 2020-07-07 18:37:59 +01:00
526e9c69eb Ensure world tick properties are properly named in json 2020-07-07 18:37:45 +01:00
20385c5ae7 Add tick tracking to the world 2020-07-07 18:36:20 +01:00
5928dfdb20 Rename the tick variable 2020-07-07 18:24:16 +01:00
c9af3772da
Merge pull request #5 from mdiluz/clean-out-status
Remove status doc, now tracked on GitHub
2020-07-07 18:07:37 +01:00
3faf709f10 Remove status doc, now tracked on GitHub 2020-07-07 17:58:41 +01:00
3493d51d36 Set the server tick rate to 5 2020-07-07 17:45:36 +01:00
b6d47833f6 Merge branch 'master' of github.com:mdiluz/rove 2020-07-07 13:13:25 +01:00
ad13ed8ee2
Fix git diff check 2020-07-07 13:13:17 +01:00
5b4b9c30eb Update rove.pb.go 2020-07-07 13:13:02 +01:00
a1b79a8df5
Display diff change 2020-07-07 13:08:33 +01:00
c66e61921f Add generated files check to github actions 2020-07-07 12:59:10 +01:00
fc4fa3decf Remove go dependencies fetch to test need for it 2020-07-06 18:10:02 +01:00
75910efbe5 Apply all golangci-lint fixes 2020-07-06 18:04:10 +01:00
945b3299ac Add golangci-lint from https://github.com/actions-contrib/golangci-lint 2020-07-06 17:53:35 +01:00
ed6de9eac4 Remove swagger install from tests 2020-07-06 17:49:01 +01:00
718252731b Give up on getting protoc and lint to work in the action 2020-07-06 17:46:39 +01:00
2fbe2dc1a8 Re-order code checkout 2020-07-06 17:18:40 +01:00
408fffb0c6 Fix to use the new golangci-lint action 2020-07-06 17:16:09 +01:00
62741caf5e Add check for golang lint 2020-07-06 17:12:32 +01:00
7563896a59 Merge branch 'master' of github.com:mdiluz/rove 2020-07-06 17:12:27 +01:00
771c95da14 Update makefile with more echos 2020-07-06 17:10:14 +01:00
2c6aba6897 Remove Gopkg usage in github action 2020-07-06 17:10:04 +01:00
9eb2e08667 Add check to github action that we've run make gen 2020-07-06 17:09:49 +01:00
35d7400a83 Also build the rove command line client into the docker 2020-07-05 13:37:56 +01:00
a112c3ed47 Override incoming commands rather than appending 2020-07-05 13:16:19 +01:00
233a6b3281 Add incoming and queued commands to status output 2020-07-05 13:16:08 +01:00
ea4b7de4ac Rename "commands" to "command" 2020-07-05 12:55:01 +01:00
1554b7c21b Update README.md
Fix the link to the proto
Add the logo
2020-07-04 23:38:59 +01:00
31c0753341 Fix InnerMain test 2020-07-04 23:15:12 +01:00
28639b4cac Fix up a comment and the help text 2020-07-04 23:11:22 +01:00
894359142b Rename "rover" to "status" 2020-07-04 23:11:12 +01:00
f8e594cb39 Rename "status" command to "server-status" 2020-07-04 23:05:08 +01:00
98eea89484 Adjust goals to focus on core rover gameplay 2020-07-04 22:59:27 +01:00
31308db6b5 Tick off energy in upcoming features and simplify current features 2020-07-04 22:57:19 +01:00
87af905bc8 Rename charge command to recharge 2020-07-04 22:56:58 +01:00
7272749614 Enable command line client to accept new commands 2020-07-04 22:42:37 +01:00
e875f82b13 Add command "charge" to charge up the rover's energy store 2020-07-04 22:42:20 +01:00
15c337c067 Make moving and stashing cost rover charge 2020-07-04 22:35:25 +01:00
8b83672dcc Fix Atlas gen with simplification
Only track lower and upper bounds in world space, and speak in terms of world space and chunks
2020-07-04 22:34:28 +01:00
dbe944bb4e Add charge and apply it to rover actions 2020-07-04 12:30:40 +01:00
143fba505e Add Charge and Max Charge attributes to the rover 2020-07-04 12:26:42 +01:00
b066277ddf Add MaximumIntegrity to the rover 2020-07-04 12:26:42 +01:00
2eaed1447d Add rover inventory capacity and test 2020-07-04 12:26:42 +01:00
e6ff453ff1 Add note for intelligent world-gen 2020-07-04 12:26:42 +01:00
9cd5324465 Fix small and large rock spawning 2020-07-03 17:13:52 +01:00
1a1ef9a376 go mod tidy and update 2020-07-03 17:06:28 +01:00
c4b0762ebe Fix up the tile print now that the radar returns objects 2020-07-03 17:05:31 +01:00
062f9cfec8 Split Atlas chunks into tiles and objects 2020-07-03 17:00:04 +01:00
74dcae6542
Fix badges 2020-07-01 13:46:39 +01:00
e6bfc7a8fc Set up Github docker auth 2020-07-01 13:39:47 +01:00
821c83549b
Rename the docker action 2020-07-01 13:20:11 +01:00
c0726a2345
Rename the tests file 2020-07-01 13:19:50 +01:00
13482c1893
Add a docker build action
Created from workflow template
2020-07-01 13:19:01 +01:00
b5707ab71c Fix all go vet issues 2020-07-01 00:01:20 +01:00
204c786103 Rename rove-reverse-proxy to rove-server-rest-proxy 2020-06-30 23:37:38 +01:00
abcebcebb6 Simplify - remove rove-accountant
This was a fun little gRPC experiment but it's simply not needed
2020-06-30 23:34:49 +01:00
984ff56664 Add flatpak file, unused but functional 2020-06-30 22:43:53 +01:00
c07a9b2659 Merge branch 'master' of github.com:mdiluz/rove 2020-06-30 17:41:16 +01:00
77bde53a52 Rename the main design doc 2020-06-30 17:41:08 +01:00
Marc Di Luzio
1f2669b643 Set theme jekyll-theme-merlot 2020-06-30 17:40:23 +01:00
e3169cdbdd Update the docs and status pages 2020-06-30 16:50:16 +01:00
e09cea328b Refactor into singular account in the config 2020-06-28 15:52:46 +01:00
b9198c546c Update the status
HTTPS makes sense for the docs, but is not essential, the real goal is to have token security
2020-06-28 12:30:32 +01:00
0d3aac49b1 Don't expose the rove-accountant 2020-06-28 12:26:51 +01:00
06cf44f129 Increase the chunk size to 1kb per chunk 2020-06-28 11:02:56 +01:00
e5ee0eaece Rename a couple of Atlas variables
Sometimes names can be too long
2020-06-28 11:01:01 +01:00
9bb91920c9 Make Atlas grow in X and Y dimensions independently
Fixes exponential growth
2020-06-28 00:18:39 +01:00
b116cdf291 Convert Atlas to infinite lazy growth
The atlas will now expand as needed for any query, but only initialise the chunk tile memory when requested

	While this may still be a pre-mature optimisation, it does simplify some code and ensures that our memory footprint stays small, for the most part
2020-06-27 14:48:21 +01:00
2556c0d049 Call rand.Seed to end current determinism 2020-06-27 02:08:52 +01:00
6ba6584ae1 Default to a much faster tick rate for now 2020-06-27 02:03:12 +01:00
5b5f80be7d Clean up logging a little 2020-06-27 02:02:18 +01:00
5bbb2ff37f Fix help text for commands 2020-06-27 01:41:19 +01:00
693b8a54f1 Add repair command to repair using inventory item 2020-06-27 01:39:10 +01:00
7957454ec1 Add rover integrity
Rovers are damaged by bumping into solid objects
2020-06-27 01:18:18 +01:00
12dc9e478d Remove usage of os arg in help, it's confusing for snaps 2020-06-27 00:56:28 +01:00
adf3def488 Small Status update 2020-06-27 00:51:51 +01:00
1ed1c60de0 Simplify - remove RoverAttributes and rover UUIDs 2020-06-27 00:32:27 +01:00
f9c30f541c Rename USER_DATA to ROVE_USER_DATA 2020-06-27 00:02:07 +01:00
4a343f36a8 Remove ROVE_HOST
No need for two ways to set this
2020-06-26 23:58:58 +01:00
b2f6c1a0b1 Update status doc 2020-06-26 23:45:31 +01:00
e6a25a5310 Add the rover inventory to the "rover" response 2020-06-26 23:44:52 +01:00
e1bff92a56 Remove Item type in favor of just byte 2020-06-26 23:41:36 +01:00
d08a15e201 De-scope - Remove unused rover capacity 2020-06-26 23:39:07 +01:00
71c2c09270 Write test to check rover has item in inventory 2020-06-26 23:37:10 +01:00
f0d40cc46c Change help print to standard format 2020-06-26 23:31:57 +01:00
6c09ee3826 Refactor main to accept commands and arguments 2020-06-26 23:31:06 +01:00
d624a3ca21 Add verification for "stash" command 2020-06-26 23:30:42 +01:00
2f6465987d More de-scope - remove duration on move command
This isn't even needed, as commands can just be queued up
2020-06-26 22:26:27 +01:00
383e834cef Add RoverStash test 2020-06-26 20:14:00 +01:00
00bdad6b40 Fix stashing
Now checks if object is stashable and clears the tile
2020-06-26 19:47:01 +01:00
2846ed796e Refactor tiles to objects to be re-used 2020-06-26 19:45:24 +01:00
8b1eca0aee Implement basic stash command 2020-06-26 18:59:12 +01:00
db3c2c2c2e De-scope, remove rover speed 2020-06-26 18:48:07 +01:00
a84709564c Minor refactor to move name to top of attributes class 2020-06-26 18:24:03 +01:00
7ee340e976 Move Rover position into main class 2020-06-26 18:22:37 +01:00
8019ea4e25 Add an inventory to the rover 2020-06-26 18:13:23 +01:00
15b8f0a427 Update the PoC doc to a status doc 2020-06-26 18:06:41 +01:00
93b99b7989 Fix tests after rover change 2020-06-25 23:51:31 +01:00
f0f5a6b2e0 Remove installing wamerican 2020-06-25 23:51:12 +01:00
5c12fabb63 Refactor so Rover's aren't in the atlas 2020-06-25 23:27:07 +01:00
ccd9e13c51 Add an icon 2020-06-25 22:57:57 +01:00
8aeb23e40b Set up data paths within the snaps 2020-06-25 22:25:42 +01:00
58f9d8baf2 Reformat to use a words file rather than babble 2020-06-25 22:02:11 +01:00
5f732bd6c5 Add public domain words file from https://github.com/dwyl/english-words at 728408da58fc6010ad2e5503442927d87e21065c 2020-06-23 19:45:10 +01:00
98349b8935 Ensure we label with proper tags and version the build 2020-06-23 18:48:28 +01:00
996970fd81 Move version and config out to early bails 2020-06-23 18:47:46 +01:00
073f8846aa Set the built snap to stable when it's tagged or devel when not 2020-06-23 18:25:58 +01:00
ddc2631bb0 Customise the snap version define directly 2020-06-23 18:21:22 +01:00
4232c547ce Fix quotes in yaml 2020-06-23 18:09:11 +01:00
ef718199f0 Add the license and architectures to the snapcraft file 2020-06-23 18:04:47 +01:00
c268555e2e Change "-version" to a command 2020-06-23 18:03:36 +01:00
19b3685e8c Add the server, accountant, and rest proxy to the snap 2020-06-23 00:06:10 +01:00
dd76e61e44 Make the ports have default values 2020-06-23 00:05:26 +01:00
9fb0a79480 Remove comment about unregistered name 2020-06-22 22:36:39 +01:00
cc29f13ce5
Update README.md 2020-06-22 18:14:06 +01:00
d4b686f510
Update README.md 2020-06-22 16:16:16 +01:00
edce245c6f
Create LICENSE 2020-06-22 15:59:07 +01:00
Marc Di Luzio
06e5c0bf2f Add starter snapcraft configuration
Contains a few leftover TODOs, the build version one is the most problematic due to the go plugin having no current support for adding ldflags
2020-06-22 11:21:44 +01:00
Marc Di Luzio
3f1b8a4c2a Create the filepath for the config file 2020-06-22 11:14:08 +01:00
Marc Di Luzio
b33e366500 Fix up param names for conventions 2020-06-22 11:12:55 +01:00
bea08d54f1 Add description to rove documentation 2020-06-13 13:38:12 +01:00
187a0a6165 Finish HTTP tests and adjust APIs to allow them to pass 2020-06-13 13:18:22 +01:00
ba52458fd6 Start to implement proper validation of HTTP interface 2020-06-13 12:35:37 +01:00
42ee69b1a2 Prepend license to wait-for-it 2020-06-13 12:16:05 +01:00
389fb7e9db Add wait-for-it.sh from https://github.com/vishnubob/wait-for-it at c096cfa 2020-06-13 12:14:54 +01:00
a4a04a15fb Remove empty proto i/o structs in favor of placeholders 2020-06-13 11:57:27 +01:00
fcbc29c80b Fix rove gRPC path given to tests 2020-06-13 11:49:24 +01:00
dc9eb8cf2e Fix filtering 's from babble names for rovers 2020-06-13 11:42:28 +01:00
914eef05c0 Use standard PORT for host port 2020-06-13 11:41:24 +01:00
3f879f9501 Add back a dummy HTTP test file 2020-06-13 11:18:26 +01:00
98249948a1 Fix up host ports and env variables 2020-06-13 11:17:52 +01:00
7c830f58be Add missing log import 2020-06-13 10:59:47 +01:00
9d91fb836f Re-instate the stagger doc server 2020-06-13 10:59:25 +01:00
55cd4fe4a5 Fix rove-reverse-proxy as well 2020-06-13 10:44:40 +01:00
84163ce9e1 Fix logging for rove-server 2020-06-13 10:44:03 +01:00
856771dac7 Rename the RoveServer proto to Rove 2020-06-13 10:43:35 +01:00
ccb34d4452 Make env variables required 2020-06-13 10:42:59 +01:00
51030ac162 Fix swagger gen path 2020-06-13 10:42:40 +01:00
8c6230ca20 Implement a reverse proxy using grpc-gateway 2020-06-13 00:23:21 +01:00
7ababb79f6 Migrate to gRPC rather than REST with swagger
Will also be adding in a RESTful endpoint to the server as well so it can consume both types
2020-06-12 22:51:18 +01:00
b815284199 Fix the rover attributes listing 2020-06-12 19:50:52 +01:00
50c3795578 Add comments and fix up the API doc 2020-06-12 19:34:14 +01:00
04a1b8ea1e Add a description for the command 2020-06-12 19:17:39 +01:00
c1d7952034 Fix error 400 descriptions 2020-06-12 19:12:24 +01:00
86a1200113 Put url back 2020-06-12 19:05:50 +01:00
6cfc9444f3 Simplify the APIs to return http status codes 2020-06-12 19:05:26 +01:00
663cd77c94 Fix the hostname 2020-06-12 18:00:06 +01:00
47921e6c41 Move game server to port 8080 and host docs on 80 2020-06-12 17:54:47 +01:00
b121b4463e Download and install swagger 2020-06-12 17:27:29 +01:00
30f8d666c3 Move the swagger yml to root 2020-06-12 17:22:55 +01:00
6342d9dc4d Add TODO notes about hack with rovers in the atlas 2020-06-11 20:47:45 +01:00
faaa556ad0 Move the Atlas code into it's own package 2020-06-11 20:42:59 +01:00
8cd7b06c0c Privatise Atlas functions that should only be internal 2020-06-11 20:34:30 +01:00
de3c2b9134 Fix printing the tile grid 2020-06-11 20:26:06 +01:00
790e1464e5 Remove time mechanics from the PoC stretch 2020-06-11 20:25:55 +01:00
bf88f9984b Add a "config" arg to the cmdline client 2020-06-11 20:25:36 +01:00
1a6bd8fed9 Add unit tests to the test target 2020-06-11 20:25:03 +01:00
2f5863b17a Use log instead of fmt for logging
Also fix up a few errors to lower case
2020-06-11 19:04:53 +01:00
1cafd4f2ce Fix makefile and coverage output 2020-06-11 18:55:53 +01:00
223c50228e Add comments to the accounts proto file 2020-06-11 18:38:34 +01:00
537d309235 Add creation time to new accounts 2020-06-11 18:38:18 +01:00
7d1a2d7efd Fix crash with fresh data 2020-06-11 18:27:19 +01:00
14424c16ca Refactor testing into docker file
This means a decent scale refactor but ends with our testing being much simpler

	Key changes:
		* single Dockerfile for all services
		* tests moved into docker up so don't need to be run locally
		* configurations moved to environment
2020-06-11 18:16:11 +01:00
99da6c5d67 Move accountant to it's own deployment using gRCP 2020-06-11 13:24:42 +01:00
8f25f55658 Refactor accounts to store a data map rather than just a rover ID 2020-06-10 22:48:45 +01:00
7749854eb7 Remove account IDs in favor of just account names
These were a "security" feature but pre-emptive and just add complications when we can implement secrets later
2020-06-10 18:57:43 +01:00
b3b369f608 Remove /spawn POST endpoint
This was increasing complexity for no added benefit

	/register now performs the spawn in 4 lines of code
2020-06-10 18:48:56 +01:00
6fb7ee598d Move server package out into rove-server 2020-06-10 18:20:05 +01:00
62d6213c1a Change GET and SET to CamelCase 2020-06-10 17:27:55 +01:00
14c4e61660 Fix up gocritic issues 2020-06-10 12:34:04 +01:00
2ee68e74ac Enqueue the incoming commands at the next tick
This sync commands for all users and in the future will let you view which moves and commands are currently being executed
2020-06-09 20:44:25 +01:00
217e579cec Fix InnerMain test for rove commands 2020-06-09 18:35:36 +01:00
6a868d3e41 Fix up TODOs and comments that have now been done 2020-06-09 18:33:30 +01:00
a784b06c2a Fix error messages that still talk about direction 2020-06-09 18:33:05 +01:00
339163e06d Fix command line arg order 2020-06-09 18:32:44 +01:00
4558e8a9b0 Rename Direction -> Bearing 2020-06-09 18:09:51 +01:00
51fe918090 Split out maths functions into maths, vector and bearing 2020-06-09 18:08:07 +01:00
aae668fb57 Fix instabilities caused by random rocks 2020-06-09 00:16:49 +01:00
520f78b5c3 Stop spawning rovers outside the chunks or warping into other rovers 2020-06-09 00:07:25 +01:00
ae2cb6598a Fix WarpRover when warping onto itself 2020-06-08 23:41:33 +01:00
de94b39a50 Fix TestCommand_Move instability by increasing the size of the atlas 2020-06-08 23:39:13 +01:00
066df58705 Fix the names including apostrophes 2020-06-08 23:37:03 +01:00
43588c0e4b Fix world spawning and radar
Also expand test coverage a little to ensure it's correct
2020-06-08 23:32:52 +01:00
65 changed files with 376818 additions and 2725 deletions

View file

@ -1,43 +0,0 @@
name: Build and Test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Install dict
run: sudo apt-get update && sudo apt-get install wamerican
- name: Check out repo
uses: actions/checkout@v2
- name: Get go dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
- name: Build and Test
run: make test
- name: Upload test coverage result
uses: actions/upload-artifact@v1
with:
name: Coverage
path: /tmp/coverage.html

25
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Docker
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Docker Login
uses: azure/docker-login@v1
with:
login-server: docker.pkg.github.com
username: $GITHUB_ACTOR
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build the Docker image
run: |
VERSION=$(git describe --always --long --dirty --tags)
docker build . --tag docker.pkg.github.com/mdiluz/rove/rove:$VERSION --tag docker.pkg.github.com/mdiluz/rove/rove:latest
docker push docker.pkg.github.com/mdiluz/rove/rove:$VERSION
docker push docker.pkg.github.com/mdiluz/rove/rove:latest

44
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run golangci-lint
uses: actions-contrib/golangci-lint@v1
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Check generated files
run: |
PROTOC_ZIP=protoc-3.6.1-linux-x86_64.zip
curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/$PROTOC_ZIP
sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc
sudo unzip -o $PROTOC_ZIP -d /usr/local 'include/*'
sudo chmod -R o+r /usr/local/include/google/
rm -f $PROTOC_ZIP
make gen
git update-index --refresh || (git diff; exit 1)
- name: Build and Test
run: make test
- name: Upload test coverage result
uses: actions/upload-artifact@v1
with:
name: Coverage
path: /tmp/coverage.html

28
.vscode/launch.json vendored
View file

@ -1,28 +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": {},
"args": [],
},
{
"name": "Launch Rove",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/rove/main.go",
"cwd": "${workspaceFolder}",
"env": {},
"args": ["radar"],
}
]
}

View file

@ -5,9 +5,8 @@ WORKDIR /app
COPY . .
RUN go mod download
# For /usr/share/dict/words
RUN apt-get update && apt-get install -y wamerican
# 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
CMD [ "./rove-server" ]

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Marc Di Luzio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,24 +1,28 @@
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
go mod download
go install -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=${VERSION}'" ./...
test:
gen:
@echo Installing go dependencies
go install github.com/golang/protobuf/protoc-gen-go
go mod download
go build ./...
@echo Generating rove server gRPC
protoc --proto_path proto --go_out=plugins=grpc,paths=source_relative:proto/ proto/roveapi/roveapi.proto
# Run the server and shut it down again to ensure our docker-compose works
ROVE_ARGS="--quit 1" docker-compose up --build --exit-code-from=rove-server --abort-on-container-exit
test:
@echo Run unit and integration tests
docker-compose -f docker-compose-test.yml up --build --exit-code-from=rove-tests --abort-on-container-exit rove-tests
docker-compose -f docker-compose-test.yml down
go tool cover -html=/tmp/coverage-data/c.out -o /tmp/coverage.html
@echo Done, coverage data can be found in /tmp/coverage.html
# Run tests with coverage
go test -v ./... -cover -coverprofile=/tmp/c.out -count 1
# Convert the coverage data to html
go tool cover -html=/tmp/c.out -o /tmp/coverage.html
.PHONY: install test
.PHONY: build install test gen

View file

@ -1,13 +1,8 @@
Rove
====
![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 [api.go](https://github.com/mdiluz/rove/blob/master/pkg/rove/api.go) for the current server-client API.
Build Status
------------
![Build and Test](https://github.com/mdiluz/rove/workflows/Build%20and%20Test/badge.svg)
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,148 +0,0 @@
package main
import (
"fmt"
"os"
"testing"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/rove"
"github.com/mdiluz/rove/pkg/server"
"github.com/stretchr/testify/assert"
)
// To be set by the main function
var serv rove.Server
func TestMain(m *testing.M) {
s := server.NewServer()
if err := s.Initialise(); err != nil {
fmt.Println(err)
os.Exit(1)
}
serv = rove.Server(s.Addr())
go s.Run()
fmt.Printf("Test server hosted on %s", serv)
code := m.Run()
if err := s.StopAndClose(); err != nil {
fmt.Println(err)
os.Exit(1)
}
os.Exit(code)
}
func TestServer_Status(t *testing.T) {
status, err := serv.Status()
assert.NoError(t, err)
assert.True(t, status.Ready)
assert.NotZero(t, len(status.Version))
}
func TestServer_Register(t *testing.T) {
d1 := rove.RegisterData{
Name: uuid.New().String(),
}
r1, err := serv.Register(d1)
assert.NoError(t, err)
assert.True(t, r1.Success)
assert.NotZero(t, len(r1.Id))
d2 := rove.RegisterData{
Name: uuid.New().String(),
}
r2, err := serv.Register(d2)
assert.NoError(t, err)
assert.True(t, r2.Success)
assert.NotZero(t, len(r2.Id))
r3, err := serv.Register(d1)
assert.NoError(t, err)
assert.False(t, r3.Success)
}
func TestServer_Spawn(t *testing.T) {
d1 := rove.RegisterData{
Name: uuid.New().String(),
}
r1, err := serv.Register(d1)
assert.NoError(t, err)
assert.True(t, r1.Success)
assert.NotZero(t, len(r1.Id))
s := rove.SpawnData{}
r2, err := serv.Spawn(r1.Id, s)
assert.NoError(t, err)
assert.True(t, r2.Success)
}
func TestServer_Command(t *testing.T) {
d1 := rove.RegisterData{
Name: uuid.New().String(),
}
r1, err := serv.Register(d1)
assert.NoError(t, err)
assert.True(t, r1.Success)
assert.NotZero(t, len(r1.Id))
s := rove.SpawnData{}
r2, err := serv.Spawn(r1.Id, s)
assert.NoError(t, err)
assert.True(t, r2.Success)
c := rove.CommandData{
Commands: []game.Command{
{
Command: game.CommandMove,
Bearing: "N",
Duration: 1,
},
},
}
r3, err := serv.Command(r1.Id, c)
assert.NoError(t, err)
assert.True(t, r3.Success)
}
func TestServer_Radar(t *testing.T) {
d1 := rove.RegisterData{
Name: uuid.New().String(),
}
r1, err := serv.Register(d1)
assert.NoError(t, err)
assert.True(t, r1.Success)
assert.NotZero(t, len(r1.Id))
s := rove.SpawnData{}
r2, err := serv.Spawn(r1.Id, s)
assert.NoError(t, err)
assert.True(t, r2.Success)
r3, err := serv.Radar(r1.Id)
assert.NoError(t, err)
assert.True(t, r3.Success)
}
func TestServer_Rover(t *testing.T) {
d1 := rove.RegisterData{
Name: uuid.New().String(),
}
r1, err := serv.Register(d1)
assert.NoError(t, err)
assert.True(t, r1.Success)
assert.NotZero(t, len(r1.Id))
s := rove.SpawnData{}
r2, err := serv.Spawn(r1.Id, s)
assert.NoError(t, err)
assert.True(t, r2.Success)
r3, err := serv.Rover(r1.Id)
assert.NoError(t, err)
assert.True(t, r3.Success)
}

View file

@ -0,0 +1,169 @@
package internal
import (
"context"
"fmt"
"log"
"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, *roveapi.ServerStatusRequest) (*roveapi.ServerStatusResponse, error) {
response := &roveapi.ServerStatusResponse{
Ready: true,
Version: version.Version,
TickRate: int32(s.minutesPerTick),
CurrentTick: int32(s.world.CurrentTicks),
}
// If there's a schedule, respond with it
if len(s.schedule.Entries()) > 0 {
response.NextTick = s.schedule.Entries()[0].Next.Format("15:04:05")
}
return response, nil
}
// Register registers a new account for a gRPC request
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.world.Accountant.RegisterAccount(req.Name); err != nil {
return nil, err
} else if _, err := s.SpawnRoverForAccount(req.Name); err != nil {
return nil, fmt.Errorf("failed to spawn rover for account: %s", err)
} else if err := s.SaveWorld(); err != nil {
return nil, fmt.Errorf("internal server error when saving world: %s", err)
} else {
return &roveapi.RegisterResponse{
Account: &roveapi.Account{
Name: acc.Name,
Secret: acc.Data["secret"],
},
}, nil
}
}
// Status returns rover information for a gRPC request
func (s *Server) Status(ctx context.Context, req *roveapi.StatusRequest) (response *roveapi.StatusResponse, err error) {
log.Printf("Handling status request: %s\n", req.Account.Name)
if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
return nil, err
} else if !valid {
return nil, fmt.Errorf("Secret incorrect for account %s", req.Account.Name)
} else if resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover"); err != nil {
return nil, err
} else if rover, err := s.world.GetRover(resp); err != nil {
return nil, fmt.Errorf("error getting rover: %s", err)
} else {
var inv []byte
for _, i := range rover.Inventory {
inv = append(inv, byte(i.Type))
}
queued := s.world.RoverCommands(resp)
var logs []*roveapi.Log
for _, log := range rover.Logs {
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 = &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,
},
}
}
return response, nil
}
// Radar returns the radar information for a rover
func (s *Server) Radar(ctx context.Context, req *roveapi.RadarRequest) (*roveapi.RadarResponse, error) {
log.Printf("Handling radar request: %s\n", req.Account.Name)
if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
return nil, err
} else if !valid {
return nil, fmt.Errorf("Secret incorrect for account %s", req.Account.Name)
}
response := &roveapi.RadarResponse{}
resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover")
if err != nil {
return nil, err
} else if rover, err := s.world.GetRover(resp); err != nil {
return nil, fmt.Errorf("error getting rover attributes: %s", err)
} else if radar, objs, err := s.world.RadarFromRover(resp); err != nil {
return nil, fmt.Errorf("error getting radar from rover: %s", err)
} else {
response.Objects = objs
response.Tiles = radar
response.Range = int32(rover.Range)
}
return response, nil
}
// Command issues commands to the world based on a gRPC request
func (s *Server) Command(ctx context.Context, req *roveapi.CommandRequest) (*roveapi.CommandResponse, error) {
log.Printf("Handling command request: %s and %+v\n", req.Account.Name, req.Commands)
if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
return nil, err
} else if !valid {
return nil, fmt.Errorf("Secret incorrect for account %s", req.Account.Name)
}
resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover")
if err != nil {
return nil, err
}
if err := s.world.Enqueue(resp, req.Commands...); err != nil {
return nil, err
}
return &roveapi.CommandResponse{}, nil
}

View file

@ -0,0 +1,234 @@
package internal
import (
"fmt"
"log"
"net"
"os"
"path"
"sync"
"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
// EphemeralData will let the server neither load or save out any of it's data
EphemeralData
)
// Server contains the relevant data to run a game server
type Server struct {
// Internal state
world *rove.World
// gRPC server
netListener net.Listener
grpcServ *grpc.Server
// Config settings
address string
persistence int
minutesPerTick int
// sync point for sub-threads
sync sync.WaitGroup
// cron schedule for world ticks
schedule *cron.Cron
}
// ServerOption defines a server creation option
type ServerOption func(s *Server)
// OptionAddress sets the server address for hosting
func OptionAddress(address string) ServerOption {
return func(s *Server) {
s.address = address
}
}
// OptionPersistentData sets the server data to be persistent
func OptionPersistentData() ServerOption {
return func(s *Server) {
s.persistence = PersistentData
}
}
// OptionTick defines the number of minutes per tick
// 0 means no automatic server tick
func OptionTick(minutes int) ServerOption {
return func(s *Server) {
s.minutesPerTick = minutes
}
}
// NewServer sets up a new server
func NewServer(opts ...ServerOption) *Server {
// Set up the default server
s := &Server{
address: "",
persistence: EphemeralData,
schedule: cron.New(),
world: rove.NewWorld(32),
}
// Apply all options
for _, o := range opts {
o(s)
}
return s
}
// Initialise sets up internal state ready to serve
func (s *Server) Initialise(fillWorld bool) (err error) {
// Add to our sync
s.sync.Add(1)
// Load the world file
if err := s.LoadWorld(); err != nil {
return err
}
// Set up the RPC server and register
s.netListener, err = net.Listen("tcp", s.address)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Load TLS
var opts []grpc.ServerOption
if len(os.Getenv("NO_TLS")) == 0 {
pem := path.Join("/etc/letsencrypt/live/", cert, "fullchain.pem")
key := path.Join("/etc/letsencrypt/live/", cert, "privkey.pem")
creds, err := credentials.NewServerTLSFromFile(pem, key)
if err != nil {
log.Fatalf("failed to setup TLS: %v", err)
}
opts = append(opts, grpc.Creds(creds))
}
s.grpcServ = grpc.NewServer(opts...)
roveapi.RegisterRoveServer(s.grpcServ, s)
reflection.Register(s.grpcServ)
return nil
}
// Addr will return the server address set after the listen
func (s *Server) Addr() string {
return s.address
}
// Run executes the server
func (s *Server) Run() {
defer s.sync.Done()
// Set up the schedule if requested
if s.minutesPerTick != 0 {
if err := s.schedule.AddFunc(fmt.Sprintf("0 */%d * * *", s.minutesPerTick), func() {
// Ensure we don't quit during this function
s.sync.Add(1)
defer s.sync.Done()
log.Println("Executing server tick")
// Tick the world
s.world.Tick()
// Save out the new world state
if err := s.SaveWorld(); err != nil {
log.Fatalf("Failed to save the world: %s", err)
}
}); err != nil {
log.Fatal(err)
}
s.schedule.Start()
log.Printf("First server tick scheduled for %s\n", s.schedule.Entries()[0].Next.Format("15:04:05"))
}
// Serve the RPC server
log.Printf("Serving gRPC on %s\n", s.address)
if err := s.grpcServ.Serve(s.netListener); err != nil && err != grpc.ErrServerStopped {
log.Fatalf("failed to serve gRPC: %s", err)
}
}
// Stop will stop the current server
func (s *Server) Stop() error {
// Stop the cron
s.schedule.Stop()
// Stop the gRPC
s.grpcServ.Stop()
return nil
}
// Close waits until the server is finished and closes up shop
func (s *Server) Close() error {
// Wait until the world has shut down
s.sync.Wait()
// Save and return
return s.SaveWorld()
}
// StopAndClose waits until the server is finished and closes up shop
func (s *Server) StopAndClose() error {
// Stop the server
if err := s.Stop(); err != nil {
return err
}
// Close and return
return s.Close()
}
// SaveWorld will save out the world file
func (s *Server) SaveWorld() error {
if s.persistence == PersistentData {
s.world.RLock()
defer s.world.RUnlock()
if err := persistence.SaveAll("world", s.world); err != nil {
return fmt.Errorf("failed to save out persistent data: %s", err)
}
}
return nil
}
// LoadWorld will load all persistent data
func (s *Server) LoadWorld() error {
if s.persistence == PersistentData {
s.world.Lock()
defer s.world.Unlock()
if err := persistence.LoadAll("world", &s.world); err != nil {
return err
}
}
return nil
}
// SpawnRoverForAccount spawns the rover rover for an account
func (s *Server) SpawnRoverForAccount(account string) (string, error) {
inst, err := s.world.SpawnRover(account)
if err != nil {
return "", err
}
return inst, nil
}

View file

@ -1,6 +1,7 @@
package server
package internal
import (
"os"
"testing"
)
@ -30,10 +31,11 @@ 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")
} else if err := server.Initialise(); err != nil {
} else if err := server.Initialise(true); err != nil {
t.Error(err)
}
@ -45,10 +47,11 @@ 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")
} else if err := server.Initialise(); err != nil {
} else if err := server.Initialise(true); err != nil {
t.Error(err)
}

View file

@ -3,44 +3,79 @@ package main
import (
"flag"
"fmt"
"log"
"math/rand"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/mdiluz/rove/cmd/rove-server/internal"
"github.com/mdiluz/rove/pkg/persistence"
"github.com/mdiluz/rove/pkg/server"
"github.com/mdiluz/rove/pkg/version"
)
var ver = flag.Bool("version", false, "Display version number")
var quit = flag.Int("quit", 0, "Quit after n seconds, useful for testing")
var address = flag.String("address", "", "The address to host on, automatically selected if empty")
var data = flag.String("data", "", "Directory to store persistant data, no storage if empty")
var tick = flag.Int("tick", 5, "Number of minutes per server tick (0 for no tick)")
// Path for persistent storage
var data = os.Getenv("DATA_PATH")
// The tick rate of the server in seconds
var tick = os.Getenv("TICK_RATE")
// InnerMain is our main function so tests can run it
func InnerMain() {
// Ensure we've seeded rand
rand.Seed(time.Now().UTC().UnixNano())
flag.Parse()
// Print the version if requested
if *ver {
fmt.Println(version.Version)
log.Println(version.Version)
return
}
fmt.Printf("Initialising version %s...\n", version.Version)
// Address to host the server on
var iport int
var port = os.Getenv("PORT")
if len(port) == 0 {
iport = 9090
} else {
var err error
iport, err = strconv.Atoi(port)
if err != nil {
log.Fatal("$PORT not valid int")
}
}
log.Printf("Initialising version %s...\n", version.Version)
// Set the persistence path
persistence.SetPath(*data)
if len(data) == 0 {
log.Fatal("DATA_PATH not set")
} else if err := persistence.SetPath(data); err != nil {
log.Fatal(err)
}
// Convert the tick rate
tickRate := 1
if len(tick) > 0 {
var err error
tickRate, err = strconv.Atoi(tick)
if err != nil {
log.Fatalf("TICK_RATE not set to valid int: %s", err)
}
}
// Create the server data
s := server.NewServer(
server.OptionAddress(*address),
server.OptionPersistentData(),
server.OptionTick(*tick))
s := internal.NewServer(
internal.OptionAddress(fmt.Sprintf(":%d", iport)),
internal.OptionPersistentData(),
internal.OptionTick(tickRate))
// Initialise the server
if err := s.Initialise(); err != nil {
if err := s.Initialise(true); err != nil {
panic(err)
}
@ -49,22 +84,13 @@ func InnerMain() {
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
fmt.Println("Quit requested, exiting...")
log.Println("Quit requested, exiting...")
if err := s.Stop(); err != nil {
panic(err)
}
}()
// Quit after a time if requested
if *quit != 0 {
go func() {
time.Sleep(time.Duration(*quit) * time.Second)
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
}()
}
// Run the server
fmt.Printf("Serving HTTP on %s\n", s.Addr())
s.Run()
// Close the server
@ -74,6 +100,5 @@ func InnerMain() {
}
func main() {
flag.Parse()
InnerMain()
}

View file

@ -3,16 +3,12 @@ package main
import (
"flag"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_InnerMain_Version(t *testing.T) {
flag.Set("version", "1")
assert.NoError(t, flag.Set("version", "1"))
InnerMain()
flag.Set("version", "0")
}
func Test_InnerMain_Quit(t *testing.T) {
flag.Set("quit", "1")
InnerMain()
flag.Set("quit", "0")
assert.NoError(t, flag.Set("version", "0"))
}

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,238 +1,423 @@
package main
import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"math"
"os"
"path"
"path/filepath"
"strconv"
"time"
"github.com/mdiluz/rove/pkg/game"
"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 USAGE = ""
var home = os.Getenv("HOME")
var defaultDataPath = path.Join(home, ".local/share/")
// Command usage
// TODO: Allow COMMAND to be used first
func Usage() {
fmt.Printf("Usage: %s [OPTIONS]... COMMAND\n", os.Args[0])
fmt.Println("\nCommands:")
fmt.Println("\tstatus \tprints the server status")
fmt.Println("\tregister\tregisters an account and stores it (use with -name)")
fmt.Println("\tspawn \tspawns a rover for the current account")
fmt.Println("\tmove \tissues move command to rover")
fmt.Println("\tradar \tgathers radar data for the current rover")
fmt.Println("\trover \tgets data for current rover")
fmt.Println("\nOptions:")
flag.PrintDefaults()
func printUsage() {
fmt.Fprintln(os.Stderr, "Usage: rove ARG [OPT...]")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "Arguments:")
fmt.Fprintln(os.Stderr, "\tversion outputs version")
fmt.Fprintln(os.Stderr, "\thelp outputs this usage text")
fmt.Fprintln(os.Stderr, "\tconfig [HOST] outputs the local config, optionally sets host")
fmt.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)
}
var home = os.Getenv("HOME")
var filepath = path.Join(home, ".local/share/rove.json")
const gRPCport = 9090
// General usage
var ver = flag.Bool("version", false, "Display version number")
var host = flag.String("host", "", "path to game host server")
var data = flag.String("data", filepath, "data file for storage")
// For register command
var name = flag.String("name", "", "used with status command for the account name")
// For the duration command
var duration = flag.Int("duration", 1, "used for the move command duration")
var bearing = flag.String("bearing", "", "used for the move command bearing (compass direction)")
// Account stores data for an account
type Account struct {
Name string
Secret string
}
// Config is used to store internal data
type Config struct {
Host string `json:"host,omitempty"`
Accounts map[string]string `json:"accounts,omitempty"`
Host string
Account Account
}
// verifyId will verify an account ID
func verifyId(id string) error {
if len(id) == 0 {
// ConfigPath returns the configuration path
func ConfigPath() string {
// Allow overriding the data path
var datapath = defaultDataPath
var override = os.Getenv("ROVE_USER_DATA")
if len(override) > 0 {
datapath = override
}
datapath = path.Join(datapath, "roveapi.json")
return datapath
}
// LoadConfig loads the config from a chosen path
func LoadConfig() (config Config, err error) {
datapath := ConfigPath()
// Create the path if needed
path := filepath.Dir(datapath)
if _, err := os.Stat(path); os.IsNotExist(err) {
if err := os.MkdirAll(path, os.ModePerm); err != nil {
return Config{}, fmt.Errorf("Failed to create data path %s: %s", path, err)
}
} else {
// Read the file
_, err = os.Stat(datapath)
if !os.IsNotExist(err) {
if b, err := ioutil.ReadFile(datapath); err != nil {
return Config{}, fmt.Errorf("failed to read file %s error: %s", datapath, err)
} else if len(b) == 0 {
return Config{}, fmt.Errorf("file %s was empty, assumin fresh data", datapath)
} else if err := json.Unmarshal(b, &config); err != nil {
return Config{}, fmt.Errorf("failed to unmarshal file %s error: %s", datapath, err)
}
}
}
return
}
// SaveConfig saves the config out
func SaveConfig(config Config) error {
// Save out the persistent file
datapath := ConfigPath()
if b, err := json.MarshalIndent(config, "", "\t"); err != nil {
return fmt.Errorf("failed to marshal data error: %s", err)
} else if err := ioutil.WriteFile(datapath, b, os.ModePerm); err != nil {
return fmt.Errorf("failed to save file %s error: %s", datapath, err)
}
return nil
}
// checkAccount will verify an account ID
func checkAccount(a Account) error {
if len(a.Name) == 0 {
return fmt.Errorf("no account ID set, must register first")
} else if len(a.Secret) == 0 {
return fmt.Errorf("empty account secret, must register first")
}
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) error {
func InnerMain(command string, args ...string) error {
// Early simple bails
switch command {
case "help":
printUsage()
return nil
case "version":
fmt.Println(version.Version)
return nil
}
// Load in the persistent file
var config = Config{
Accounts: make(map[string]string),
config, err := LoadConfig()
if err != nil {
return err
}
_, err := os.Stat(*data)
if !os.IsNotExist(err) {
if b, err := ioutil.ReadFile(*data); err != nil {
return fmt.Errorf("failed to read file %s error: %s", *data, err)
} else if len(b) == 0 {
return fmt.Errorf("file %s was empty, assumin fresh data", *data)
} else if err := json.Unmarshal(b, &config); err != nil {
return fmt.Errorf("failed to unmarshal file %s error: %s", *data, err)
// Run config command before server needed
if command == "config" {
if len(args) > 0 {
config.Host = args[0]
}
}
// If there's a host set on the command line, override the one in the config
if len(*host) != 0 {
config.Host = *host
fmt.Printf("host: %s\taccount: %s\n", config.Host, config.Account)
return SaveConfig(config)
}
// If there's still no host, bail
if len(config.Host) == 0 {
return fmt.Errorf("no host set, please set one with -host")
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
var server = rove.Server(config.Host)
// Grab the account
var account = config.Accounts[config.Host]
// Print the config info
fmt.Printf("host: %s\taccount: %s\n", config.Host, account)
clientConn, err := grpc.Dial(fmt.Sprintf("%s:%d", config.Host, gRPCport), opts...)
if err != nil {
return err
}
var client = roveapi.NewRoveClient(clientConn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Handle all the commands
switch command {
case "status":
if response, err := server.Status(); err != nil {
case "server-status":
response, err := client.ServerStatus(ctx, &roveapi.ServerStatusRequest{})
switch {
case err != nil:
return err
} else {
default:
fmt.Printf("Ready: %t\n", response.Ready)
fmt.Printf("Version: %s\n", response.Version)
fmt.Printf("Tick: %d\n", response.Tick)
fmt.Printf("Tick Rate: %d\n", response.TickRate)
fmt.Printf("Current Tick: %d\n", response.CurrentTick)
fmt.Printf("Next Tick: %s\n", response.NextTick)
}
case "register":
d := rove.RegisterData{
Name: *name,
}
if response, err := server.Register(d); err != nil {
return err
} else if !response.Success {
return fmt.Errorf("Server returned failure: %s", response.Error)
} else {
fmt.Printf("Registered account with id: %s\n", response.Id)
config.Accounts[config.Host] = response.Id
}
case "spawn":
d := rove.SpawnData{}
if err := verifyId(account); err != nil {
return err
} else if response, err := server.Spawn(account, d); err != nil {
return err
} else if !response.Success {
return fmt.Errorf("Server returned failure: %s", response.Error)
} else {
fmt.Printf("Spawned rover with attributes %+v\n", response.Attributes)
if len(args) == 0 || len(args[0]) == 0 {
return fmt.Errorf("must pass name to 'register'")
}
case "move":
d := rove.CommandData{
Commands: []game.Command{
{
Command: game.CommandMove,
Duration: *duration,
Bearing: *bearing,
},
resp, err := client.Register(ctx, &roveapi.RegisterRequest{
Name: args[0],
})
switch {
case err != nil:
return err
default:
fmt.Printf("Registered account with id: %s\n", resp.Account.Name)
config.Account.Name = resp.Account.Name
config.Account.Secret = resp.Account.Secret
}
case "command":
if err := checkAccount(config.Account); err != nil {
return err
} else if len(args) == 0 {
return fmt.Errorf("must pass commands to 'commands'")
}
// Iterate through each command
var commands []*roveapi.Command
for i := 0; i < len(args); i++ {
var cmd *roveapi.Command
switch args[i] {
case "turn":
i++
if len(args) == i {
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,
}
default:
// By default just use the command literally
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, &roveapi.CommandRequest{
Account: &roveapi.Account{
Name: config.Account.Name,
Secret: config.Account.Secret,
},
}
Commands: commands,
})
if err := verifyId(account); err != nil {
return err
} else if response, err := server.Command(account, d); err != nil {
switch {
case err != nil:
return err
} else if !response.Success {
return fmt.Errorf("Server returned failure: %s", response.Error)
} else {
default:
fmt.Printf("Request succeeded\n")
}
case "radar":
if err := verifyId(account); err != nil {
if err := checkAccount(config.Account); err != nil {
return err
} else if response, err := server.Radar(account); err != nil {
}
response, err := client.Radar(ctx, &roveapi.RadarRequest{
Account: &roveapi.Account{
Name: config.Account.Name,
Secret: config.Account.Secret,
},
})
switch {
case err != nil:
return err
} else if !response.Success {
return fmt.Errorf("Server returned failure: %s", response.Error)
default:
} else {
// Print the radar
// Print out the radar
num := int(math.Sqrt(float64(len(response.Tiles))))
for i := 0; i < num; i++ {
for j := num - 1; j >= 0; j-- {
fmt.Printf("%d", response.Tiles[i+num*j])
for j := num - 1; j >= 0; j-- {
for i := 0; i < num; i++ {
t := response.Tiles[i+num*j]
o := response.Objects[i+num*j]
if o != roveapi.Object_ObjectUnknown {
fmt.Printf("%c", internal.ObjectGlyph(o))
} else {
fmt.Printf("%c", internal.TileGlyph(t))
}
}
fmt.Print("\n")
}
}
case "rover":
if err := verifyId(account); err != nil {
case "status":
if err := checkAccount(config.Account); err != nil {
return err
} else if response, err := server.Rover(account); err != nil {
}
response, err := client.Status(ctx, &roveapi.StatusRequest{
Account: &roveapi.Account{
Name: config.Account.Name,
Secret: config.Account.Secret,
},
})
switch {
case err != nil:
return err
} else if !response.Success {
return fmt.Errorf("Server returned failure: %s", response.Error)
} else {
fmt.Printf("attributes: %+v\n", response.Attributes)
default:
fmt.Printf("rover info: %+v\n", response)
}
default:
return fmt.Errorf("Unknown command: %s", command)
// Print the usage
fmt.Fprintf(os.Stderr, "Error: unknown command %s\n", command)
printUsage()
os.Exit(1)
}
// Save out the persistent file
if b, err := json.MarshalIndent(config, "", "\t"); err != nil {
return fmt.Errorf("failed to marshal data error: %s", err)
} else {
if err := ioutil.WriteFile(*data, b, os.ModePerm); err != nil {
return fmt.Errorf("failed to save file %s error: %s", *data, err)
}
}
return nil
return SaveConfig(config)
}
// Simple main
func main() {
flag.Usage = Usage
flag.Parse()
// Print the version if requested
if *ver {
fmt.Println(version.Version)
return
}
// Verify we have a single command line arg
args := flag.Args()
if len(args) != 1 {
Usage()
// 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)
}
// Run the inner main
if err := InnerMain(args[0]); err != nil {
if err := InnerMain(os.Args[1], os.Args[2:]...); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}

View file

@ -1,83 +1,67 @@
// +build integration
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"testing"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/server"
"github.com/stretchr/testify/assert"
)
var address string
func TestMain(m *testing.M) {
s := server.NewServer()
if err := s.Initialise(); err != nil {
fmt.Println(err)
os.Exit(1)
}
address = s.Addr()
go s.Run()
fmt.Printf("Test server hosted on %s", address)
code := m.Run()
if err := s.StopAndClose(); err != nil {
fmt.Println(err)
os.Exit(1)
}
os.Exit(code)
}
func Test_InnerMain(t *testing.T) {
// Set up the flags to act locally and use a temporary file
flag.Set("data", path.Join(os.TempDir(), uuid.New().String()))
os.Setenv("NO_TLS", "1")
// First attempt should error
assert.Error(t, InnerMain("status"))
// Use temporary local user data
tmp, err := ioutil.TempDir(os.TempDir(), "rove-")
assert.NoError(t, err)
os.Setenv("ROVE_USER_DATA", tmp)
// Now set the host
flag.Set("host", address)
// Used for configuring this test
var address = os.Getenv("ROVE_GRPC")
if len(address) == 0 {
log.Fatal("Must set $ROVE_GRPC")
}
// No error now as we have a host
assert.NoError(t, InnerMain("status"))
// First attempt should error without a host
assert.Error(t, InnerMain("server-status"))
// Set the host in the config
assert.NoError(t, InnerMain("config", address))
assert.NoError(t, InnerMain("server-status"))
// Register should fail without a name
assert.Error(t, InnerMain("register"))
// These methods should fail without an account
assert.Error(t, InnerMain("spawn"))
assert.Error(t, InnerMain("move"))
assert.Error(t, InnerMain("radar"))
assert.Error(t, InnerMain("rover"))
assert.Error(t, InnerMain("status"))
// Now set the name
flag.Set("name", uuid.New().String())
// Perform the register
assert.NoError(t, InnerMain("register"))
// We've not spawned a rover yet so these should fail
assert.Error(t, InnerMain("command"))
assert.Error(t, InnerMain("radar"))
assert.Error(t, InnerMain("rover"))
// Spawn a rover
assert.NoError(t, InnerMain("spawn"))
assert.NoError(t, InnerMain("register", uuid.New().String()))
// These should now work
assert.NoError(t, InnerMain("radar"))
assert.NoError(t, InnerMain("rover"))
assert.NoError(t, InnerMain("status"))
// Move should work with arguments
flag.Set("bearing", "N")
flag.Set("duration", "1")
assert.NoError(t, InnerMain("move"))
// Commands should fail with no commands
assert.Error(t, InnerMain("command"))
// Give it commands
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", "unknown"))
assert.Error(t, InnerMain("command", "broadcast"))
assert.Error(t, InnerMain("command", "upgrade"))
assert.Error(t, InnerMain("command", "1"))
}

3
data/icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -0,0 +1,36 @@
{
"app-id": "io.github.mdiluz.Rove",
"runtime": "org.freedesktop.Platform",
"runtime-version": "19.08",
"sdk": "org.freedesktop.Sdk",
"sdk-extensions" : [
"org.freedesktop.Sdk.Extension.golang"
],
"finish-args" : [
"--share=network"
],
"command": "rove",
"modules": [
{
"name": "rove",
"buildsystem": "simple",
"build-options": {
"env": {
"GOBIN": "/app/bin/"
},
"build-args": [
"--share=network"
]
},
"build-commands" : [
". /usr/lib/sdk/golang/enable.sh; make install"
],
"sources": [
{
"type": "dir",
"path": ".."
}
]
}
]
}

370104
data/words_alpha.txt Normal file

File diff suppressed because it is too large Load diff

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

@ -7,10 +7,19 @@ services:
rove-server:
build:
context: .
dockerfile: cmd/rove-server/Dockerfile
image: rove-server:latest
dockerfile: Dockerfile
image: rove:latest
ports:
- "80:80"
command: ./rove-server --address ":80" --data=/mnt/rove-server ${ROVE_ARGS}
- "9090:9090"
environment:
- PORT=9090
- DATA_PATH=/mnt/rove-server
- WORDS_FILE=data/words_alpha.txt
- TICK_RATE=3
- CERT_NAME=${CERT_NAME}
volumes:
- persistent-data:/mnt/rove-server:rw
- /etc/letsencrypt/:/etc/letsencrypt/
command: [ "./rove-server"]

61
docs/README.md Normal file
View file

@ -0,0 +1,61 @@
Rove
=====
Rove is an asynchronous nomadic game about exploring a planet as part of a loose community.
-------------------------------------------
## Core gameplay
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.
### Key Components
* 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
-------------------------------------------
## Installing
On Ubuntu:
```
$ snap install rove
```
Elsewhere (with [go](https://golang.org/doc/install) installed)
```
go get github.com/mdiluz/rove
cd $GOPATH/src/github.com/mdiluz/rove/
make install
```
-------------------------------------------
### 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.

1
docs/_config.yml Normal file
View file

@ -0,0 +1 @@
theme: jekyll-theme-merlot

View file

@ -1,86 +0,0 @@
Rove
=====
An asynchronous nomadic game about exploring a planet as part of a loose
community.
-------------------------------------------
## The Basics
### Core
Control a rover on the surface of the planet using a remote control interface.
Commands are sent and happen asynchronously, and the rover feeds back information about position and surroundings, as well as photos.
### Goal
To reach the pole.
### General
Movement is slow and sometimes dangerous. Hazards damage the rover.
Resources can be collected to fix and upgrade the rover.
Rovers recharge power during the day.
Enough collected resources allow you to create and fire a new rover a significant distance in any direction, losing control of the current one and leaving it dormant.
Finding a dormant rover gives you a choice - scrap it to gain minor resources, or fire it a distance just like a new rover, taking control of it.
“Dying” triggers a self destruct and fires a new basic rover in a random direction towards the equator
## Multiplayer
The planet itself and things that happen on it are persistent. Players can view each other, and use very rudimentary signals.
Dormant rovers store full history of travel, owners, and keep damage, improvements and resources.
Players have no other forms of direct communication.
Players can view progress of all rovers attached to their name.
Limit too many players in one location with a simple interference mechanic - only a certain density can exist at once to operate properly, additional players cant move within range.
-------------------------------------------
### Implementation
Two functional parts
A server that receives the commands, sends out data, and handles interactions between players.
An app, or apps, that interface with the server to let you control and view rover information
-------------------------------------------
### To Solve
#### What kinds of progression/upgrades exist?
Needs a very simple set of rover internals defined, each of which can be upgraded.
#### How does the game encourage lateral movement?
Could simply be the terrain is constructed in very lateral ways, blocking progress frequently
#### How does the game encourage cooperation?
How exactly would a time delay mechanic enhance the experience?
Currently its just to make the multiplayer easier to use, and to make interactions a little more complicated. The game could limit the number of bytes (commands) you can send over time.
#### How would the gameplay prevent griefing?
-------------------------------------------
### 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.
Build up a certain level of knowledge and ownership of a place, but then destroy it or give it up. Or build up a character and then leave it behind.
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.

View file

@ -1,30 +0,0 @@
Proof Of Concept
================
For a proof of concept we'll implement a very small subset of the final features
### Core Features (done)
* Create an account
* Control an rover in a 2D map
* Query for what the rover can see
* Persistent accounts and world
* Multiple users
* Populate map with locations/objects
* Commands happen in "real time"
### Stretch goals
* Rover inventory
* Rover internals
* Basic time mechanics
* Basic survival mechanics
### Key missing features
* No token security (very hackable)
* No time dilation effects
* No global coordinates
* No render of rover camera view
* No rover replacement mechanic
* No advanced app/interface

14
go.mod
View file

@ -3,9 +3,19 @@ module github.com/mdiluz/rove
go 1.14
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.4.2
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.4
github.com/grpc-ecosystem/grpc-gateway v1.14.6
github.com/kr/pretty v0.1.0 // indirect
github.com/ojrac/opensimplex-go v1.0.1
github.com/robfig/cron v1.2.0
github.com/stretchr/testify v1.6.0
github.com/tjarratt/babble v0.0.0-20191209142150-eecdf8c2339d
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/grpc v1.30.0
google.golang.org/protobuf v1.25.0
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
)

133
go.sum
View file

@ -1,18 +1,143 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/grpc-ecosystem/grpc-gateway v1.14.6 h1:8ERzHx8aj1Sc47mu9n/AksaKCSWrMchFtkdrS4BIj5o=
github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/ojrac/opensimplex-go v1.0.1 h1:XslvpLP6XqQSATUtsOnGBYtFPw7FQ6h6y0ihjVeOLHo=
github.com/ojrac/opensimplex-go v1.0.1/go.mod h1:MoSgj04tZpH8U0RefZabnHV2AbLgv/2mo3hLJtWqSEs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tjarratt/babble v0.0.0-20191209142150-eecdf8c2339d h1:b7oHBI6TgTdCDuqTijsVldzlh+6cfQpdYLz1EKtCAoY=
github.com/tjarratt/babble v0.0.0-20191209142150-eecdf8c2339d/go.mod h1:O5hBrCGqzfb+8WyY8ico2AyQau7XQwAfEQeEQ5/5V9E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884 h1:fiNLklpBwWK1mth30Hlwk+fcdBmIALlgF5iy77O37Ig=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -1,85 +1,28 @@
package accounts
import (
"fmt"
// 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
const kAccountsFileName = "rove-accounts.json"
// 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
// Id represents a unique ID per account and is set one registered
Id uuid.UUID `json:"id"`
// Rover represents the rover that this account owns
Rover uuid.UUID `json:"rover"`
}
// Represents the accountant data to store
type accountantData struct {
}
// Accountant manages a set of accounts
type Accountant struct {
Accounts map[uuid.UUID]Account `json:"accounts"`
}
// NewAccountant creates a new accountant
func NewAccountant() *Accountant {
return &Accountant{
Accounts: make(map[uuid.UUID]Account),
}
}
// RegisterAccount adds an account to the set of internal accounts
func (a *Accountant) RegisterAccount(name string) (acc Account, err error) {
// Set the account ID to a new UUID
acc.Id = uuid.New()
acc.Name = name
// 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")
} else if a.Id == acc.Id {
return Account{}, fmt.Errorf("Account ID already registered")
}
}
// Simply add the account to the map
a.Accounts[acc.Id] = acc
return
}
// AssignRover assigns rover ownership of an rover to an account
func (a *Accountant) AssignRover(account uuid.UUID, rover uuid.UUID) error {
// Find the account matching the ID
if this, ok := a.Accounts[account]; ok {
this.Rover = rover
a.Accounts[account] = this
} else {
return fmt.Errorf("no account found for id: %s", account)
}
return nil
}
// GetRover gets the rover rover for the account
func (a *Accountant) GetRover(account uuid.UUID) (uuid.UUID, error) {
// Find the account matching the ID
if this, ok := a.Accounts[account]; !ok {
return uuid.UUID{}, fmt.Errorf("no account found for id: %s", account)
} else if this.Rover == uuid.Nil {
return uuid.UUID{}, fmt.Errorf("no rover spawned for account %s", account)
} else {
return this.Rover, nil
}
// Data represents internal account data
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
@ -36,11 +36,6 @@ func TestAccountant_RegisterAccount(t *testing.T) {
t.Errorf("Missmatched account name after register, expected: %s, actual: %s", nameb, acca.Name)
}
// Verify our accounts have differing IDs
if acca.Id == accb.Id {
t.Error("Duplicate account IDs fo separate accounts")
}
// Verify another request gets rejected
_, err = accountant.RegisterAccount(namea)
if err == nil {
@ -48,11 +43,8 @@ func TestAccountant_RegisterAccount(t *testing.T) {
}
}
func TestAccountant_AssignGetRover(t *testing.T) {
accountant := NewAccountant()
if len(accountant.Accounts) != 0 {
t.Error("New accountant created with non-zero account number")
}
func TestAccountant_AssignGetData(t *testing.T) {
accountant := NewSimpleAccountant()
name := uuid.New().String()
a, err := accountant.RegisterAccount(name)
@ -60,16 +52,12 @@ func TestAccountant_AssignGetRover(t *testing.T) {
t.Error(err)
}
inst := uuid.New()
err = accountant.AssignRover(a.Id, inst)
err = accountant.AssignData(a.Name, "key", "value")
if err != nil {
t.Error("Failed to set rover for created account")
} else if accountant.Accounts[a.Id].Rover != inst {
t.Error("Rover for assigned account is incorrect")
} else if id, err := accountant.GetRover(a.Id); err != nil {
t.Error("Failed to get rover for account")
} else if id != inst {
t.Error("Fetched rover is incorrect for account")
t.Error("Failed to set data for created account")
} else if id, err := accountant.GetValue(a.Name, "key"); err != nil {
t.Error("Failed to get data for account")
} else if id != "value" {
t.Error("Fetched data is incorrect for account")
}
}

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,186 +0,0 @@
package game
import (
"fmt"
"log"
"math/rand"
)
// Chunk represents a fixed square grid of tiles
type Chunk struct {
// Tiles represents the tiles within the chunk
Tiles []Tile `json:"tiles"`
}
// Atlas represents a grid of Chunks
type Atlas 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"`
// size is the current width/height of the given atlas
Size int `json:"size"`
// ChunkSize is the dimensions of each chunk
ChunkSize int `json:"chunksize"`
}
// NewAtlas creates a new empty atlas
func NewAtlas(size int, chunkSize int) Atlas {
if size%2 != 0 {
log.Fatal("atlas size must always be even")
}
a := Atlas{
Size: size,
Chunks: make([]Chunk, size*size),
ChunkSize: chunkSize,
}
// Initialise all the chunks
for i := range a.Chunks {
a.Chunks[i] = Chunk{
Tiles: make([]Tile, chunkSize*chunkSize),
}
}
return a
}
// SpawnWorld spawns the current world
func (a *Atlas) SpawnWorld() error {
extent := a.ChunkSize * (a.Size / 2)
// Pepper the current world with rocks
for i := -extent; i < extent; i++ {
for j := -extent; j < extent; j++ {
if rand.Intn(16) == 0 {
if err := a.SetTile(Vector{i, j}, TileRock); err != nil {
return err
}
}
}
}
// Surround the atlas in walls
for i := -extent; i < extent; i++ {
if err := a.SetTile(Vector{i, extent - 1}, TileWall); err != nil {
return err
} else if a.SetTile(Vector{extent - 1, i}, TileWall); err != nil {
return err
} else if a.SetTile(Vector{-extent, i}, TileWall); err != nil {
return err
} else if a.SetTile(Vector{i, extent - 1}, TileWall); err != nil {
return err
}
}
return nil
}
// SetTile sets an individual tile's kind
func (a *Atlas) SetTile(v Vector, tile Tile) error {
chunk := a.ToChunk(v)
if chunk >= len(a.Chunks) {
return fmt.Errorf("location outside of allocated atlas")
}
local := a.ToChunkLocal(v)
tileId := local.X + local.Y*a.ChunkSize
if tileId >= len(a.Chunks[chunk].Tiles) {
return fmt.Errorf("location outside of allocated chunk")
}
a.Chunks[chunk].Tiles[tileId] = tile
return nil
}
// GetTile will return an individual tile
func (a *Atlas) GetTile(v Vector) (Tile, error) {
chunk := a.ToChunk(v)
if chunk >= len(a.Chunks) {
return 0, fmt.Errorf("location outside of allocated atlas")
}
local := a.ToChunkLocal(v)
tileId := local.X + local.Y*a.ChunkSize
if tileId >= len(a.Chunks[chunk].Tiles) {
return 0, fmt.Errorf("location outside of allocated chunk")
}
return a.Chunks[chunk].Tiles[tileId], nil
}
// ToChunkLocal gets a chunk local coordinate for a tile
func (a *Atlas) ToChunkLocal(v Vector) Vector {
return Vector{Pmod(v.X, a.ChunkSize), Pmod(v.Y, a.ChunkSize)}
}
// GetChunkLocal gets a chunk local coordinate for a tile
func (a *Atlas) ToWorld(local Vector, chunk int) Vector {
return a.ChunkOrigin(chunk).Added(local)
}
// GetChunkID gets the chunk ID for a position in the world
func (a *Atlas) ToChunk(v Vector) int {
local := a.ToChunkLocal(v)
// Get the chunk origin itself
origin := v.Added(local.Negated())
// Divided it by the number of chunks
origin = origin.Divided(a.ChunkSize)
// Shift it by our size (our origin is in the middle)
origin = origin.Added(Vector{a.Size / 2, a.Size / 2})
// Get the ID based on the final values
return (a.Size * origin.Y) + origin.X
}
// ChunkOrigin gets the chunk origin for a given chunk index
func (a *Atlas) ChunkOrigin(chunk int) Vector {
v := Vector{
X: Pmod(chunk, a.Size) - (a.Size / 2),
Y: (chunk / a.Size) - (a.Size / 2),
}
return v.Multiplied(a.ChunkSize)
}
// GetWorldExtent gets the min and max valid coordinates of world
func (a *Atlas) GetWorldExtents() (min Vector, max Vector) {
min = Vector{
-(a.Size / 2) * a.ChunkSize,
-(a.Size / 2) * a.ChunkSize,
}
max = Vector{
-min.X - 1,
-min.Y - 1,
}
return
}
// Grow will return a grown copy of the current atlas
func (a *Atlas) Grow(size int) error {
if size%2 != 0 {
return fmt.Errorf("atlas size must always be even")
}
delta := size - a.Size
if delta < 0 {
return fmt.Errorf("Cannot shrink an atlas")
} else if delta == 0 {
return nil
}
// Create a new atlas
newAtlas := NewAtlas(size, a.ChunkSize)
// Copy old chunks into new chunks
for index, chunk := range a.Chunks {
// Calculate the new chunk location and copy over the data
newAtlas.Chunks[newAtlas.ToChunk(a.ChunkOrigin(index))] = chunk
}
// Copy the new atlas data into this one
*a = newAtlas
// Return the new atlas
return nil
}

View file

@ -1,152 +0,0 @@
package game
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAtlas_NewAtlas(t *testing.T) {
a := NewAtlas(2, 1)
assert.NotNil(t, a)
// Tiles should look like: 2 | 3
// -----
// 0 | 1
assert.Equal(t, 4, len(a.Chunks))
a = NewAtlas(4, 1)
assert.NotNil(t, a)
// Tiles should look like: 2 | 3
// -----
// 0 | 1
assert.Equal(t, 16, len(a.Chunks))
}
func TestAtlas_ToChunk(t *testing.T) {
a := NewAtlas(2, 1)
assert.NotNil(t, a)
// Tiles should look like: 2 | 3
// -----
// 0 | 1
tile := a.ToChunk(Vector{0, 0})
assert.Equal(t, 3, tile)
tile = a.ToChunk(Vector{0, -1})
assert.Equal(t, 1, tile)
tile = a.ToChunk(Vector{-1, -1})
assert.Equal(t, 0, tile)
tile = a.ToChunk(Vector{-1, 0})
assert.Equal(t, 2, tile)
a = NewAtlas(2, 2)
assert.NotNil(t, a)
// Tiles should look like:
// 2 | 3
// -----
// 0 | 1
tile = a.ToChunk(Vector{1, 1})
assert.Equal(t, 3, tile)
tile = a.ToChunk(Vector{1, -2})
assert.Equal(t, 1, tile)
tile = a.ToChunk(Vector{-2, -2})
assert.Equal(t, 0, tile)
tile = a.ToChunk(Vector{-2, 1})
assert.Equal(t, 2, tile)
a = NewAtlas(4, 2)
assert.NotNil(t, a)
// Tiles should look like:
// 12| 13|| 14| 15
// ----------------
// 8 | 9 || 10| 11
// ================
// 4 | 5 || 6 | 7
// ----------------
// 0 | 1 || 2 | 3
tile = a.ToChunk(Vector{1, 3})
assert.Equal(t, 14, tile)
tile = a.ToChunk(Vector{1, -3})
assert.Equal(t, 2, tile)
tile = a.ToChunk(Vector{-1, -1})
assert.Equal(t, 5, tile)
tile = a.ToChunk(Vector{-2, 2})
assert.Equal(t, 13, tile)
}
func TestAtlas_GetSetTile(t *testing.T) {
a := NewAtlas(4, 10)
assert.NotNil(t, a)
// Set the origin tile to 1 and test it
assert.NoError(t, a.SetTile(Vector{0, 0}, 1))
tile, err := a.GetTile(Vector{0, 0})
assert.NoError(t, err)
assert.Equal(t, Tile(1), tile)
// Set another tile to 1 and test it
assert.NoError(t, a.SetTile(Vector{5, -2}, 2))
tile, err = a.GetTile(Vector{5, -2})
assert.NoError(t, err)
assert.Equal(t, Tile(2), tile)
}
func TestAtlas_Grown(t *testing.T) {
// Start with a small example
a := NewAtlas(2, 2)
assert.NotNil(t, a)
assert.Equal(t, 4, len(a.Chunks))
// Set a few tiles to values
assert.NoError(t, a.SetTile(Vector{0, 0}, 1))
assert.NoError(t, a.SetTile(Vector{-1, -1}, 2))
assert.NoError(t, a.SetTile(Vector{1, -2}, 3))
// Grow once to just double it
err := a.Grow(4)
assert.NoError(t, err)
assert.Equal(t, 16, len(a.Chunks))
tile, err := a.GetTile(Vector{0, 0})
assert.NoError(t, err)
assert.Equal(t, Tile(1), tile)
tile, err = a.GetTile(Vector{-1, -1})
assert.NoError(t, err)
assert.Equal(t, Tile(2), tile)
tile, err = a.GetTile(Vector{1, -2})
assert.NoError(t, err)
assert.Equal(t, Tile(3), tile)
// Grow it again even bigger
err = a.Grow(10)
assert.NoError(t, err)
assert.Equal(t, 100, len(a.Chunks))
tile, err = a.GetTile(Vector{0, 0})
assert.NoError(t, err)
assert.Equal(t, Tile(1), tile)
tile, err = a.GetTile(Vector{-1, -1})
assert.NoError(t, err)
assert.Equal(t, Tile(2), tile)
tile, err = a.GetTile(Vector{1, -2})
assert.NoError(t, err)
assert.Equal(t, Tile(3), tile)
}
func TestAtlas_SpawnWorld(t *testing.T) {
// Start with a small example
a := NewAtlas(2, 2)
assert.NotNil(t, a)
assert.Equal(t, 4, len(a.Chunks))
assert.NoError(t, a.SpawnWorld())
tile, err := a.GetTile(Vector{1, 1})
assert.NoError(t, err)
assert.Equal(t, TileWall, tile)
tile, err = a.GetTile(Vector{-2, -2})
assert.NoError(t, err)
assert.Equal(t, TileWall, tile)
}

View file

@ -1,17 +0,0 @@
package game
const (
CommandMove = "move"
)
// Command represends a single command to execute
type Command struct {
Command string `json:"command"`
// Used in the move command
Bearing string `json:"bearing,omitempty"`
Duration int `json:"duration,omitempty"`
}
// CommandStream is a list of commands to execute in order
type CommandStream []Command

View file

@ -1,37 +0,0 @@
package game
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCommand_Move(t *testing.T) {
world := NewWorld()
a, err := world.SpawnRover()
assert.NoError(t, err)
pos := Vector{
X: 1.0,
Y: 2.0,
}
attribs, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to get rover attribs")
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
bearing := North
duration := 1
// Try the move command
moveCommand := Command{Command: CommandMove, Bearing: bearing.String(), Duration: duration}
assert.NoError(t, world.Enqueue(a, moveCommand), "Failed to execute move command")
// Tick the world
world.ExecuteCommandQueues()
newatributes, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to set position for rover")
pos.Add(Vector{0.0, duration * attribs.Speed}) // We should have moved duration*speed north
assert.Equal(t, pos, newatributes.Pos, "Failed to correctly set position for rover")
}

View file

@ -1,158 +0,0 @@
package game
import (
"fmt"
"math"
"strings"
)
// TODO: Pull this out into math package and get more test coverage
// Abs gets the absolute value of an int
func Abs(x int) int {
if x < 0 {
return -x
}
return x
}
// pmod is a mositive modulo
// golang's % is a "remainder" function si misbehaves for negative modulus inputs
func Pmod(x, d int) int {
x = x % d
if x >= 0 {
return x
} else if d < 0 {
return x - d
} else {
return x + d
}
}
// Max returns the highest int
func Max(x int, y int) int {
if x < y {
return y
}
return x
}
// Min returns the lowest int
func Min(x int, y int) int {
if x > y {
return y
}
return x
}
// Vector desribes a 3D vector
type Vector struct {
X int `json:"x"`
Y int `json:"y"`
}
// Add adds one vector to another
func (v *Vector) Add(v2 Vector) {
v.X += v2.X
v.Y += v2.Y
}
// Added calculates a new vector
func (v Vector) Added(v2 Vector) Vector {
v.Add(v2)
return v
}
// Negated returns a negated vector
func (v Vector) Negated() Vector {
return Vector{-v.X, -v.Y}
}
// Length returns the length of the vector
func (v Vector) Length() float64 {
return math.Sqrt(float64(v.X*v.X + v.Y*v.Y))
}
// Distance returns the distance between two vectors
func (v Vector) Distance(v2 Vector) float64 {
// Negate the two vectors and calciate the length
return v.Added(v2.Negated()).Length()
}
// Multiplied returns the vector multiplied by an int
func (v Vector) Multiplied(val int) Vector {
return Vector{v.X * val, v.Y * val}
}
// Divided returns the vector divided by an int
func (v Vector) Divided(val int) Vector {
return Vector{v.X / val, v.Y / val}
}
// Direction describes a compass direction
type Direction int
const (
North Direction = iota
NorthEast
East
SouthEast
South
SouthWest
West
NorthWest
)
// DirectionString simply describes the strings associated with a direction
type DirectionString struct {
Long string
Short string
}
// DirectionStrings is the set of strings for each direction
var DirectionStrings = []DirectionString{
{"North", "N"},
{"NorthEast", "NE"},
{"East", "E"},
{"SouthEast", "SE"},
{"South", "S"},
{"SouthWest", "SW"},
{"West", "W"},
{"NorthWest", "NW"},
}
// String converts a Direction to a String
func (d Direction) String() string {
return DirectionStrings[d].Long
}
// ShortString converts a Direction to a short string version
func (d Direction) ShortString() string {
return DirectionStrings[d].Short
}
// DirectionFromString gets the Direction from a string
func DirectionFromString(s string) (Direction, error) {
for i, d := range DirectionStrings {
if strings.ToLower(d.Long) == strings.ToLower(s) || strings.ToLower(d.Short) == strings.ToLower(s) {
return Direction(i), nil
}
}
return -1, fmt.Errorf("Unknown direction: %s", s)
}
var DirectionVectors = []Vector{
{0, 1}, // N
{1, 1}, // NE
{1, 0}, // E
{1, -1}, // SE
{0, -1}, // S
{-1, 1}, // SW
{-1, 0}, // W
{-1, 1}, // NW
}
// Vector converts a Direction to a Vector
func (d Direction) Vector() Vector {
return DirectionVectors[d]
}

View file

@ -1,27 +0,0 @@
package game
import "github.com/google/uuid"
// RoverAttributes contains attributes of a rover
type RoverAttributes struct {
// Speed represents the Speed that the rover will move per second
Speed int `json:"speed"`
// Range represents the distance the unit's radar can see
Range int `json:"range"`
// Name of this rover
Name string `json:"name"`
// Pos represents where this rover is in the world
Pos Vector `json:"pos"`
}
// Rover describes a single rover in the world
type Rover struct {
// Id is a unique ID for this rover
Id uuid.UUID `json:"id"`
// Attributes represents the physical attributes of the rover
Attributes RoverAttributes `json:"attributes"`
}

View file

@ -1,13 +0,0 @@
package game
// Tile represents the type of a tile on the map
type Tile byte
const (
TileEmpty = Tile(0)
TileRover = Tile(1)
// TODO: Is there even a difference between these two?
TileWall = Tile(2)
TileRock = Tile(3)
)

View file

@ -1,344 +0,0 @@
package game
import (
"fmt"
"math/rand"
"sync"
"github.com/google/uuid"
"github.com/tjarratt/babble"
)
// 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[uuid.UUID]Rover `json:"rovers"`
// Atlas represends the world map of chunks and tiles
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[uuid.UUID]CommandStream `json:"commands"`
// Mutex to lock around command operations
cmdMutex sync.RWMutex
}
// NewWorld creates a new world object
func NewWorld() *World {
return &World{
Rovers: make(map[uuid.UUID]Rover),
CommandQueue: make(map[uuid.UUID]CommandStream),
Atlas: NewAtlas(4, 8), // TODO: Choose an appropriate world size
}
}
// SpawnWorld spawns a border at the edge of the world atlas
func (w *World) SpawnWorld() error {
return w.Atlas.SpawnWorld()
}
// SpawnRover adds an rover to the game
func (w *World) SpawnRover() (uuid.UUID, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
// Initialise the rover
rover := Rover{
Id: uuid.New(),
Attributes: RoverAttributes{
Speed: 1.0,
Range: 5.0,
// Set the name randomly
// TODO: Fix the stupid "'s"
Name: babble.NewBabbler().Babble(),
},
}
// Spawn in a random place near the origin
rover.Attributes.Pos = Vector{
w.Atlas.ChunkSize - (rand.Int() % (w.Atlas.ChunkSize * 2)),
w.Atlas.ChunkSize - (rand.Int() % (w.Atlas.ChunkSize * 2)),
}
// Seach until we error (run out of world)
for {
if tile, err := w.Atlas.GetTile(rover.Attributes.Pos); err != nil {
return uuid.Nil, err
} else {
if tile == TileEmpty {
break
} else {
// Try and spawn to the east of the blockage
rover.Attributes.Pos.Add(Vector{1, 0})
}
}
}
// Set the world tile to a rover
if err := w.Atlas.SetTile(rover.Attributes.Pos, TileRover); err != nil {
return uuid.Nil, err
}
// Append the rover to the list
w.Rovers[rover.Id] = rover
return rover.Id, nil
}
// Removes an rover from the game
func (w *World) DestroyRover(id uuid.UUID) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[id]; ok {
// Clear the tile
if err := w.Atlas.SetTile(i.Attributes.Pos, TileEmpty); err != nil {
return fmt.Errorf("coudln't clear old rover tile: %s", err)
}
delete(w.Rovers, id)
} else {
return fmt.Errorf("no rover matching id")
}
return nil
}
// RoverAttributes returns the attributes of a requested rover
func (w *World) RoverAttributes(id uuid.UUID) (RoverAttributes, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
if i, ok := w.Rovers[id]; ok {
return i.Attributes, nil
} else {
return RoverAttributes{}, fmt.Errorf("no rover matching id")
}
}
// WarpRover sets an rovers position
func (w *World) WarpRover(id uuid.UUID, pos Vector) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[id]; ok {
// Update the world tile
// TODO: Make this (and other things) transactional
// TODO: Check this worldtile is free
if err := w.Atlas.SetTile(pos, TileRover); err != nil {
return fmt.Errorf("coudln't set rover tile: %s", err)
} else if err := w.Atlas.SetTile(i.Attributes.Pos, TileEmpty); err != nil {
return fmt.Errorf("coudln't clear old rover tile: %s", err)
}
i.Attributes.Pos = pos
w.Rovers[id] = i
return nil
} else {
return fmt.Errorf("no rover matching id")
}
}
// SetPosition sets an rovers position
func (w *World) MoveRover(id uuid.UUID, bearing Direction) (RoverAttributes, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[id]; ok {
// Calculate the distance
distance := i.Attributes.Speed
// Calculate the full movement based on the bearing
move := bearing.Vector().Multiplied(distance)
// Try the new move position
newPos := i.Attributes.Pos.Added(move)
// Get the tile and verify it's empty
if tile, err := w.Atlas.GetTile(newPos); err != nil {
return i.Attributes, fmt.Errorf("couldn't get tile for new position: %s", err)
} else if tile == TileEmpty {
// Set the world tiles
// TODO: Make this (and other things) transactional
if err := w.Atlas.SetTile(newPos, TileRover); err != nil {
return i.Attributes, fmt.Errorf("coudln't set rover tile: %s", err)
} else if err := w.Atlas.SetTile(i.Attributes.Pos, TileEmpty); err != nil {
return i.Attributes, fmt.Errorf("coudln't clear old rover tile: %s", err)
}
// Perform the move
i.Attributes.Pos = newPos
w.Rovers[id] = i
}
return i.Attributes, nil
} else {
return RoverAttributes{}, fmt.Errorf("no rover matching id")
}
}
// RadarFromRover can be used to query what a rover can currently see
func (w *World) RadarFromRover(id uuid.UUID) ([]Tile, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
if r, ok := w.Rovers[id]; ok {
// The radar should span in range direction on each axis, plus the row/column the rover is currently on
radarSpan := (r.Attributes.Range * 2) + 1
roverPos := r.Attributes.Pos
// Get the radar min and max values
radarMin := Vector{
X: roverPos.X - r.Attributes.Range,
Y: roverPos.Y - r.Attributes.Range,
}
radarMax := Vector{
X: roverPos.X + r.Attributes.Range,
Y: roverPos.Y + r.Attributes.Range,
}
// Make sure we only query within the actual world
worldMin, worldMax := w.Atlas.GetWorldExtents()
scanMin := Vector{
X: Max(radarMin.X, worldMin.X),
Y: Max(radarMin.Y, worldMin.Y),
}
scanMax := Vector{
X: Min(radarMax.X, worldMax.X),
Y: Min(radarMax.Y, worldMax.Y),
}
// Gather up all tiles within the range
var radar = make([]Tile, radarSpan*radarSpan)
for i := scanMin.X; i < scanMax.X; i++ {
for j := scanMin.Y; j < scanMax.Y; j++ {
q := Vector{i, j}
if tile, err := w.Atlas.GetTile(q); err != nil {
return nil, fmt.Errorf("failed to query tile: %s", err)
} else {
// Get the position relative to the bottom left of the radar
relative := q.Added(radarMin.Negated())
radar[relative.X+relative.Y*radarSpan] = tile
}
}
}
return radar, nil
} else {
return nil, fmt.Errorf("no rover matching id")
}
}
// Enqueue will queue the commands given
func (w *World) Enqueue(rover uuid.UUID, commands ...Command) error {
// First validate the commands
for _, c := range commands {
switch c.Command {
case "move":
if _, err := DirectionFromString(c.Bearing); err != nil {
return fmt.Errorf("unknown direction: %s", c.Bearing)
}
default:
return fmt.Errorf("unknown command: %s", c.Command)
}
}
// Lock our commands edit
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
// Append the commands to the current set
cmds := w.CommandQueue[rover]
w.CommandQueue[rover] = append(cmds, commands...)
return nil
}
// Execute will execute any commands in the current command queue
func (w *World) ExecuteCommandQueues() {
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
// Iterate through all commands
for rover, cmds := range w.CommandQueue {
if len(cmds) != 0 {
// Extract the first command in the queue
c := cmds[0]
// Execute the command and clear up if requested
if done, err := w.ExecuteCommand(&c, rover); err != nil {
w.CommandQueue[rover] = cmds[1:]
fmt.Println(err)
} else if done {
w.CommandQueue[rover] = cmds[1:]
} else {
w.CommandQueue[rover][0] = c
}
// If there was an error
} else {
// Clean out the empty entry
delete(w.CommandQueue, rover)
}
}
}
// ExecuteCommand will execute a single command
func (w *World) ExecuteCommand(c *Command, rover uuid.UUID) (finished bool, err error) {
fmt.Printf("Executing command: %+v\n", *c)
switch c.Command {
case "move":
if dir, err := DirectionFromString(c.Bearing); err != nil {
return true, fmt.Errorf("unknown direction in command %+v, skipping: %s\n", c, err)
} else if _, err := w.MoveRover(rover, dir); err != nil {
return true, fmt.Errorf("error moving rover in command %+v, skipping: %s\n", c, err)
} else {
// If we've successfully moved, reduce the duration by 1
c.Duration -= 1
// If we've used up the full duration, remove it, otherwise update
if c.Duration == 0 {
finished = true
}
}
default:
return true, fmt.Errorf("unknown command: %s", c.Command)
}
return
}
// 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,119 +0,0 @@
package game
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewWorld(t *testing.T) {
// Very basic for now, nothing to verify
world := NewWorld()
if world == nil {
t.Error("Failed to create world")
}
}
func TestWorld_CreateRover(t *testing.T) {
world := NewWorld()
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_RoverAttributes(t *testing.T) {
world := NewWorld()
a, err := world.SpawnRover()
assert.NoError(t, err)
attribs, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to get rover attribs")
assert.NotZero(t, attribs.Range, "Rover should not be spawned blind")
assert.NotZero(t, attribs.Speed, "Rover should not be spawned unable to move")
}
func TestWorld_DestroyRover(t *testing.T) {
world := NewWorld()
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()
a, err := world.SpawnRover()
assert.NoError(t, err)
attribs, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to get rover attribs")
pos := Vector{
X: 0.0,
Y: 0.0,
}
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
newAttribs, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to set position for rover")
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly set position for rover")
bearing := North
duration := 1
newAttribs, err = world.MoveRover(a, bearing)
assert.NoError(t, err, "Failed to set position for rover")
pos.Add(Vector{0, attribs.Speed * duration}) // We should have move one unit of the speed north
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly move position for rover")
// Place a tile in front of the rover
assert.NoError(t, world.Atlas.SetTile(Vector{0, 2}, TileWall))
newAttribs, err = world.MoveRover(a, bearing)
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly not move position for rover into wall")
}
func TestWorld_RadarFromRover(t *testing.T) {
world := NewWorld()
a, err := world.SpawnRover()
assert.NoError(t, err)
b, err := world.SpawnRover()
assert.NoError(t, err)
// Get a's attributes
attrib, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to get rover attribs")
// Warp the rovers so a can see b
bpos := Vector{-attrib.Range, -attrib.Range}
assert.NoError(t, world.WarpRover(a, Vector{0, 0}), "Failed to warp rover")
assert.NoError(t, world.WarpRover(b, bpos), "Failed to warp rover")
radar, err := world.RadarFromRover(a)
assert.NoError(t, err, "Failed to get radar from rover")
fullRange := attrib.Range + attrib.Range + 1
assert.Equal(t, fullRange*fullRange, len(radar), "Radar returned wrong number of rovers")
// bottom left should be a rover (we put one there with bpos)
assert.Equal(t, radar[0], TileRover, "Rover not found on radar in expected position")
// Centre should be rover
assert.Equal(t, radar[fullRange*fullRange/2], TileRover, "Rover not found on radar in expected position")
}

57
pkg/maths/maths.go Normal file
View file

@ -0,0 +1,57 @@
package maths
// Abs gets the absolute value of an int
func Abs(x int) int {
if x < 0 {
return -x
}
return x
}
// Pmod is a mositive modulo
// golang's % is a "remainder" function si misbehaves for negative modulus inputs
func Pmod(x, d int) int {
if x == 0 || d == 0 {
return 0
}
x = x % d
if x >= 0 {
return x
} else if d < 0 {
return x - d
} else {
return x + d
}
}
// Max returns the highest int
func Max(x, y int) int {
if x < y {
return y
}
return x
}
// Min returns the lowest int
func Min(x, y int) int {
if x > y {
return y
}
return x
}
// RoundUp rounds a value up to the nearest multiple
func RoundUp(toRound int, multiple int) int {
remainder := Pmod(toRound, multiple)
if remainder == 0 {
return toRound
}
return (multiple - remainder) + toRound
}
// RoundDown rounds a value down to the nearest multiple
func RoundDown(toRound int, multiple int) int {
remainder := Pmod(toRound, multiple)
return toRound - remainder
}

47
pkg/maths/maths_test.go Normal file
View file

@ -0,0 +1,47 @@
package maths
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAbs(t *testing.T) {
assert.Equal(t, 0, Abs(0))
assert.Equal(t, 1, Abs(1))
assert.Equal(t, 1, Abs(-1))
}
func TestPmod(t *testing.T) {
assert.Equal(t, 0, Pmod(0, 0))
assert.Equal(t, 2, Pmod(6, 4))
assert.Equal(t, 2, Pmod(-6, 4))
assert.Equal(t, 4, Pmod(-6, 10))
}
func TestMax(t *testing.T) {
assert.Equal(t, 500, Max(100, 500))
assert.Equal(t, 1, Max(-4, 1))
assert.Equal(t, -2, Max(-4, -2))
}
func TestMin(t *testing.T) {
assert.Equal(t, 100, Min(100, 500))
assert.Equal(t, -4, Min(-4, 1))
assert.Equal(t, -4, Min(-4, -2))
}
func TestRoundUp(t *testing.T) {
assert.Equal(t, 10, RoundUp(10, 5))
assert.Equal(t, 12, RoundUp(10, 4))
assert.Equal(t, -8, RoundUp(-8, 4))
assert.Equal(t, -4, RoundUp(-7, 4))
}
func TestRoundDown(t *testing.T) {
assert.Equal(t, 10, RoundDown(10, 5))
assert.Equal(t, 8, RoundDown(10, 4))
assert.Equal(t, -8, RoundDown(-8, 4))
assert.Equal(t, -8, RoundDown(-7, 4))
}

119
pkg/maths/vector.go Normal file
View file

@ -0,0 +1,119 @@
package maths
import (
"math"
"github.com/mdiluz/rove/proto/roveapi"
)
// Vector desribes a 3D vector
type Vector struct {
X int
Y int
}
// Add adds one vector to another
func (v *Vector) Add(v2 Vector) {
v.X += v2.X
v.Y += v2.Y
}
// Added calculates a new vector
func (v Vector) Added(v2 Vector) Vector {
v.Add(v2)
return v
}
// Negated returns a negated vector
func (v Vector) Negated() Vector {
return Vector{-v.X, -v.Y}
}
// Length returns the length of the vector
func (v Vector) Length() float64 {
return math.Sqrt(float64(v.X*v.X + v.Y*v.Y))
}
// Distance returns the distance between two vectors
func (v Vector) Distance(v2 Vector) float64 {
// Negate the two vectors and calciate the length
return v.Added(v2.Negated()).Length()
}
// Multiplied returns the vector multiplied by an int
func (v Vector) Multiplied(val int) Vector {
return Vector{v.X * val, v.Y * val}
}
// Divided returns the vector divided by an int
func (v Vector) Divided(val int) Vector {
return Vector{v.X / val, v.Y / val}
}
// DividedFloor returns the vector divided but floors the value regardless
func (v Vector) DividedFloor(val int) Vector {
x := float64(v.X) / float64(val)
if x < 0 {
x = math.Floor(x)
} else {
x = math.Floor(x)
}
y := float64(v.Y) / float64(val)
if y < 0 {
y = math.Floor(y)
} else {
y = math.Floor(y)
}
return Vector{X: int(x), Y: int(y)}
}
// Abs returns an absolute version of the vector
func (v Vector) Abs() Vector {
return Vector{Abs(v.X), Abs(v.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)}
}
// 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 game
package maths
import (
"math"
@ -180,15 +180,15 @@ func TestVector_Multiplied(t *testing.T) {
}{
{
name: "Basic multiply 1",
vec: North.Vector(),
vec: Vector{0, 1},
arg: 2,
want: Vector{0, 2},
},
{
name: "Basic multiply 2",
vec: NorthWest.Vector(),
vec: Vector{-1, 1},
arg: -1,
want: SouthEast.Vector(),
want: Vector{1, -1},
},
}
for _, tt := range tests {
@ -203,27 +203,3 @@ func TestVector_Multiplied(t *testing.T) {
})
}
}
func TestDirection(t *testing.T) {
dir := North
assert.Equal(t, "North", dir.String())
assert.Equal(t, "N", dir.ShortString())
assert.Equal(t, Vector{0, 1}, dir.Vector())
dir, err := DirectionFromString("N")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = DirectionFromString("n")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = DirectionFromString("north")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = DirectionFromString("NorthWest")
assert.NoError(t, err)
assert.Equal(t, NorthWest, dir)
}

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path"
)
@ -12,13 +13,13 @@ import (
var dataPath = os.TempDir()
// SetPath sets the persistent path for the data storage
func SetPath(path string) error {
if info, err := os.Stat(path); err != nil {
func SetPath(p string) error {
if info, err := os.Stat(p); err != nil {
return err
} else if !info.IsDir() {
return fmt.Errorf("path for persistence is not directory")
}
dataPath = path
dataPath = p
return nil
}
@ -29,40 +30,41 @@ func jsonPath(name string) string {
// Save will serialise the interface into a json file
func Save(name string, data interface{}) error {
path := jsonPath(name)
if b, err := json.MarshalIndent(data, "", " "); err != nil {
p := jsonPath(name)
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
} else {
if err := ioutil.WriteFile(jsonPath(name), b, os.ModePerm); err != nil {
return err
}
}
fmt.Printf("Saved %s\n", path)
if err := ioutil.WriteFile(p, b, os.ModePerm); err != nil {
return err
}
log.Printf("Saved %s\n", p)
return nil
}
// Load will load the interface from the json file
func Load(name string, data interface{}) error {
path := jsonPath(name)
p := jsonPath(name)
// Don't load anything if the file doesn't exist
_, err := os.Stat(path)
_, err := os.Stat(p)
if os.IsNotExist(err) {
fmt.Printf("File %s didn't exist, loading with fresh data\n", path)
log.Printf("File %s didn't exist, loading with fresh data\n", p)
return nil
}
// Read and unmarshal the json
if b, err := ioutil.ReadFile(path); err != nil {
if b, err := ioutil.ReadFile(p); err != nil {
return err
} else if len(b) == 0 {
fmt.Printf("File %s was empty, loading with fresh data\n", path)
log.Printf("File %s was empty, loading with fresh data\n", p)
return nil
} else if err := json.Unmarshal(b, data); err != nil {
return fmt.Errorf("failed to load file %s error: %s", path, err)
return fmt.Errorf("failed to load file %s error: %s", p, err)
}
fmt.Printf("Loaded %s\n", path)
log.Printf("Loaded %s\n", p)
return nil
}
@ -76,7 +78,7 @@ func doAll(f saveLoadFunc, args ...interface{}) error {
var ok bool
name, ok = a.(string)
if !ok {
return fmt.Errorf("Incorrect args")
return fmt.Errorf("incorrect args")
}
} else {
if err := f(name, a); err != nil {

View file

@ -1,130 +0,0 @@
package rove
import (
"path"
"github.com/mdiluz/rove/pkg/game"
)
// ==============================
// API: /status method: GET
// Status queries the status of the server
func (s Server) Status() (r StatusResponse, err error) {
s.GET("status", &r)
return
}
// StatusResponse is a struct that contains information on the status of the server
type StatusResponse struct {
Ready bool `json:"ready"`
Version string `json:"version"`
Tick int `json:"tick"`
NextTick string `json:"nexttick,omitempty"`
// TODO: return more useful info
}
// ==============================
// API: /register method: POST
// Register registers a user by name
// Responds with a unique ID for that user to be used in future requests
func (s Server) Register(d RegisterData) (r RegisterResponse, err error) {
err = s.POST("register", d, &r)
return
}
// RegisterData describes the data to send when registering
type RegisterData struct {
Name string `json:"name"`
}
// RegisterResponse describes the response to a register request
type RegisterResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
Id string `json:"id"`
}
// ==============================
// API: /{account}/spawn method: POST
// Spawn spawns the rover for an account
// Responds with the position of said rover
func (s Server) Spawn(account string, d SpawnData) (r SpawnResponse, err error) {
err = s.POST(path.Join(account, "spawn"), d, &r)
return
}
// SpawnData is the data to be sent for the spawn command
type SpawnData struct {
// Empty for now, reserved for data
}
// SpawnResponse is the data to respond with on a spawn command
type SpawnResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
// The attributes of the spawned entity
Attributes game.RoverAttributes `json:"attributes"`
}
// ==============================
// API: /{account}/command method: POST
// Command issues a set of commands from the user
func (s Server) Command(account string, d CommandData) (r CommandResponse, err error) {
err = s.POST(path.Join(account, "command"), d, &r)
return
}
// CommandData is a set of commands to execute in order
type CommandData struct {
Commands []game.Command `json:"commands"`
}
// CommandResponse is the response to be sent back
type CommandResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// ================
// API: /{account}/radar method: GET
// Radar queries the current radar for the user
func (s Server) Radar(account string) (r RadarResponse, err error) {
err = s.GET(path.Join(account, "radar"), &r)
return
}
// RadarResponse describes the response to a /radar call
type RadarResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
// The set of positions for nearby non-empty tiles
Range int `json:"range"`
Tiles []game.Tile `json:"tiles"`
}
// ================
// API: /{account}/rover method: GET
// Rover queries the current state of the rover
func (s Server) Rover(account string) (r RoverResponse, err error) {
err = s.GET(path.Join(account, "rover"), &r)
return
}
// RoverResponse includes information about the rover in question
type RoverResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
// The current position of this rover
Attributes game.RoverAttributes `json:"attributes"`
}

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})
}

231
pkg/rove/chunkAtlas.go Normal file
View file

@ -0,0 +1,231 @@
package rove
import (
"log"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
)
// chunk represents a fixed square grid of tiles
type chunk struct {
// Tiles represents the tiles within the chunk
Tiles []byte
// Objects represents the objects within the chunk
// only one possible object per tile for now
Objects map[int]Object
}
// 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
// LowerBound is the origin of the bottom left corner of the current chunks in world space (current chunks cover >= this value)
LowerBound maths.Vector
// UpperBound is the top left corner of the current chunks (curent chunks cover < this value)
UpperBound maths.Vector
// ChunkSize is the x/y dimensions of each square chunk
ChunkSize int
// worldGen is the internal world generator
worldGen WorldGen
}
const (
noiseSeed = 1024
)
// NewChunkAtlas creates a new empty atlas
func NewChunkAtlas(chunkSize int) Atlas {
// Start up with one chunk
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
}
// SetTile sets an individual tile's kind
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 *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 *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 roveapi.Tile(chunk.Tiles[i]), chunk.Objects[i]
}
// chunkTileID returns the tile index within a chunk
func (a *chunkBasedAtlas) chunkTileIndex(local maths.Vector) int {
return local.X + local.Y*a.ChunkSize
}
// populate will fill a chunk with data
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]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}
// Set the tile
c.Tiles[j*a.ChunkSize+i] = byte(a.worldGen.GetTile(loc))
// Set the object
obj := a.worldGen.GetObject(loc)
if obj.Type != roveapi.Object_ObjectUnknown {
c.Objects[j*a.ChunkSize+i] = obj
}
}
}
a.Chunks[chunk] = c
}
// setTile sets a tile in a specific chunk
func (a *chunkBasedAtlas) setTile(chunk int, local maths.Vector, tile byte) {
a.populate(chunk)
c := a.Chunks[chunk]
c.Tiles[a.chunkTileIndex(local)] = tile
a.Chunks[chunk] = c
}
// setObject sets an object in a specific chunk
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 != roveapi.Object_ObjectUnknown {
c.Objects[i] = object
} else {
delete(c.Objects, i)
}
a.Chunks[chunk] = c
}
// worldSpaceToChunkLocal gets a chunk local coordinate for a tile
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 *chunkBasedAtlas) worldSpaceToChunkIndex(v maths.Vector) int {
// Shift the vector by our current min
v = v.Added(a.LowerBound.Negated())
// Divide by the current size and floor, to get chunk-scaled vector from the lower bound
v = v.DividedFloor(a.ChunkSize)
// Calculate the width
width := a.UpperBound.X - a.LowerBound.X
widthInChunks := width / a.ChunkSize
// Along the corridor and up the stairs
return (v.Y * widthInChunks) + v.X
}
// chunkOriginInWorldSpace returns the origin of the chunk in world space
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 := maths.Vector{
X: chunk % widthInChunks,
Y: chunk / widthInChunks,
}
// Multiply up to world scale
v = v.Multiplied(a.ChunkSize)
// Shift by the lower bound
return v.Added(a.LowerBound)
}
// getNewBounds gets new lower and upper bounds for the world space given a vector
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 = maths.Vector{
X: maths.RoundDown(lower.X, a.ChunkSize),
Y: maths.RoundDown(lower.Y, a.ChunkSize),
}
upper = maths.Vector{
X: maths.RoundUp(upper.X, a.ChunkSize),
Y: maths.RoundUp(upper.Y, a.ChunkSize),
}
return
}
// worldSpaceToTrunkWithGrow will expand the current atlas for a given world space position if needed
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)
}
// Calculate the new bounds
lower, upper := a.getNewBounds(v)
size := upper.Added(lower.Negated())
size = size.Divided(a.ChunkSize)
// Create the new empty atlas
newAtlas := chunkBasedAtlas{
ChunkSize: a.ChunkSize,
LowerBound: lower,
UpperBound: upper,
Chunks: make([]chunk, size.X*size.Y),
worldGen: a.worldGen,
}
// Log that we're resizing
log.Printf("Re-allocating world, old: %+v,%+v new: %+v,%+v\n", a.LowerBound, a.UpperBound, newAtlas.LowerBound, newAtlas.UpperBound)
// Copy all old chunks into the new atlas
for chunk, chunkData := range a.Chunks {
// Calculate the chunk ID in the new atlas
origin := a.chunkOriginInWorldSpace(chunk)
newChunk := newAtlas.worldSpaceToChunkIndex(origin)
// Copy over the old chunk to the new atlas
newAtlas.Chunks[newChunk] = chunkData
}
// Overwrite the old atlas with this one
*a = newAtlas
return a.worldSpaceToChunkIndex(v)
}

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)
}

View file

@ -1,64 +0,0 @@
package rove
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
)
// Server is a simple wrapper to a server path
type Server string
// GET performs a GET request
func (s Server) GET(path string, out interface{}) error {
url := url.URL{
Scheme: "http",
Host: string(s),
Path: path,
}
if resp, err := http.Get(url.String()); err != nil {
return err
} else if resp.StatusCode != http.StatusOK {
return fmt.Errorf("http.Get returned status %d: %s", resp.StatusCode, resp.Status)
} else {
return json.NewDecoder(resp.Body).Decode(out)
}
}
// POST performs a POST request
func (s Server) POST(path string, in interface{}, out interface{}) error {
url := url.URL{
Scheme: "http",
Host: 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("POST", url.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 {
return fmt.Errorf("http returned status %d", resp.StatusCode)
} else {
return json.NewDecoder(resp.Body).Decode(out)
}
}

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
}

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,235 +0,0 @@
package server
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/rove"
"github.com/mdiluz/rove/pkg/version"
)
// Handler describes a function that handles any incoming request and can respond
type Handler func(*Server, map[string]string, io.ReadCloser, io.Writer) (interface{}, error)
// Route defines the information for a single path->function route
type Route struct {
path string
method string
handler Handler
}
// Routes is an array of all the Routes
var Routes = []Route{
{
path: "/status",
method: http.MethodGet,
handler: HandleStatus,
},
{
path: "/register",
method: http.MethodPost,
handler: HandleRegister,
},
{
path: "/{account}/spawn",
method: http.MethodPost,
handler: HandleSpawn,
},
{
path: "/{account}/command",
method: http.MethodPost,
handler: HandleCommand,
},
{
path: "/{account}/radar",
method: http.MethodGet,
handler: HandleRadar,
},
{
path: "/{account}/rover",
method: http.MethodGet,
handler: HandleRover,
},
}
// HandleStatus handles the /status request
func HandleStatus(s *Server, vars map[string]string, b io.ReadCloser, w io.Writer) (interface{}, error) {
// Simply return the current server status
response := rove.StatusResponse{
Ready: true,
Version: version.Version,
Tick: s.tick,
}
// If there's a schedule, respond with it
if len(s.schedule.Entries()) > 0 {
response.NextTick = s.schedule.Entries()[0].Next.Format("15:04:05")
}
return response, nil
}
// HandleRegister handles /register endpoint
func HandleRegister(s *Server, vars map[string]string, b io.ReadCloser, w io.Writer) (interface{}, error) {
var response = rove.RegisterResponse{
Success: false,
}
// Decode the registration info, verify it and register the account
var data rove.RegisterData
err := json.NewDecoder(b).Decode(&data)
if err != nil {
fmt.Printf("Failed to decode json: %s\n", err)
response.Error = err.Error()
} else if len(data.Name) == 0 {
response.Error = "Cannot register empty name"
} else if acc, err := s.accountant.RegisterAccount(data.Name); err != nil {
response.Error = err.Error()
} else if err := s.SaveAll(); err != nil {
response.Error = fmt.Sprintf("Internal server error when saving accounts: %s", err)
} else {
// Save out the new accounts
response.Id = acc.Id.String()
response.Success = true
}
fmt.Printf("register response:%+v\n", response)
return response, nil
}
// HandleSpawn will spawn the player entity for the associated account
func HandleSpawn(s *Server, vars map[string]string, b io.ReadCloser, w io.Writer) (interface{}, error) {
var response = rove.SpawnResponse{
Success: false,
}
id := vars["account"]
// Decode the spawn info, verify it and spawn the rover for this account
var data rove.SpawnData
if err := json.NewDecoder(b).Decode(&data); err != nil {
fmt.Printf("Failed to decode json: %s\n", err)
response.Error = err.Error()
} else if len(id) == 0 {
response.Error = "No account ID provided"
} else if id, err := uuid.Parse(id); err != nil {
response.Error = "Provided account ID was invalid"
} else if attribs, _, err := s.SpawnRoverForAccount(id); err != nil {
response.Error = err.Error()
} else if err := s.SaveWorld(); err != nil {
response.Error = fmt.Sprintf("Internal server error when saving world: %s", err)
} else {
response.Success = true
response.Attributes = attribs
}
fmt.Printf("spawn response \taccount:%s\tresponse:%+v\n", id, response)
return response, nil
}
// HandleSpawn will spawn the player entity for the associated account
func HandleCommand(s *Server, vars map[string]string, b io.ReadCloser, w io.Writer) (interface{}, error) {
var response = rove.CommandResponse{
Success: false,
}
id := vars["account"]
// Decode the commands, verify them and the account, and execute the commands
var data rove.CommandData
if err := json.NewDecoder(b).Decode(&data); err != nil {
fmt.Printf("Failed to decode json: %s\n", err)
response.Error = err.Error()
} else if len(id) == 0 {
response.Error = "No account ID provided"
} else if id, err := uuid.Parse(id); err != nil {
response.Error = fmt.Sprintf("Provided account ID was invalid: %s", err)
} else if inst, err := s.accountant.GetRover(id); err != nil {
response.Error = fmt.Sprintf("Provided account has no rover: %s", err)
} else if err := s.world.Enqueue(inst, data.Commands...); err != nil {
response.Error = fmt.Sprintf("Failed to execute commands: %s", err)
} else {
response.Success = true
}
fmt.Printf("command response \taccount:%s\tresponse:%+v\n", id, response)
return response, nil
}
// HandleRadar handles the radar request
func HandleRadar(s *Server, vars map[string]string, b io.ReadCloser, w io.Writer) (interface{}, error) {
var response = rove.RadarResponse{
Success: false,
}
id := vars["account"]
if len(id) == 0 {
response.Error = "No account ID provided"
} else if id, err := uuid.Parse(id); err != nil {
response.Error = fmt.Sprintf("Provided account ID was invalid: %s", err)
} else if inst, err := s.accountant.GetRover(id); err != nil {
response.Error = fmt.Sprintf("Provided account has no rover: %s", err)
} else if attrib, err := s.world.RoverAttributes(inst); err != nil {
response.Error = fmt.Sprintf("Error getting rover attributes: %s", err)
} else if radar, err := s.world.RadarFromRover(inst); err != nil {
response.Error = fmt.Sprintf("Error getting radar from rover: %s", err)
} else {
response.Tiles = radar
response.Range = attrib.Range
response.Success = true
}
fmt.Printf("radar response \taccount:%s\tresponse:%+v\n", id, response)
return response, nil
}
// HandleRover handles the rover request
func HandleRover(s *Server, vars map[string]string, b io.ReadCloser, w io.Writer) (interface{}, error) {
var response = rove.RoverResponse{
Success: false,
}
id := vars["account"]
if len(id) == 0 {
response.Error = "No account ID provided"
} else if id, err := uuid.Parse(id); err != nil {
response.Error = fmt.Sprintf("Provided account ID was invalid: %s", err)
} else if inst, err := s.accountant.GetRover(id); err != nil {
response.Error = fmt.Sprintf("Provided account has no rover: %s", err)
} else if attribs, err := s.world.RoverAttributes(inst); err != nil {
response.Error = fmt.Sprintf("Error getting radar from rover: %s", err)
} else {
response.Attributes = attribs
response.Success = true
}
fmt.Printf("rover response \taccount:%s\tresponse:%+v\n", id, response)
return response, nil
}

View file

@ -1,193 +0,0 @@
package server
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"path"
"testing"
"github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/rove"
"github.com/stretchr/testify/assert"
)
func TestHandleStatus(t *testing.T) {
request, _ := http.NewRequest(http.MethodGet, "/status", nil)
response := httptest.NewRecorder()
s := NewServer()
s.Initialise()
s.router.ServeHTTP(response, request)
assert.Equal(t, http.StatusOK, response.Code)
var status rove.StatusResponse
json.NewDecoder(response.Body).Decode(&status)
if status.Ready != true {
t.Errorf("got false for /status")
}
if len(status.Version) == 0 {
t.Errorf("got empty version info")
}
}
func TestHandleRegister(t *testing.T) {
data := rove.RegisterData{Name: "one"}
b, err := json.Marshal(data)
if err != nil {
t.Error(err)
}
request, _ := http.NewRequest(http.MethodPost, "/register", bytes.NewReader(b))
response := httptest.NewRecorder()
s := NewServer()
s.Initialise()
s.router.ServeHTTP(response, request)
assert.Equal(t, http.StatusOK, response.Code)
var status rove.RegisterResponse
json.NewDecoder(response.Body).Decode(&status)
if status.Success != true {
t.Errorf("got false for /register")
}
}
func TestHandleSpawn(t *testing.T) {
s := NewServer()
s.Initialise()
a, err := s.accountant.RegisterAccount("test")
assert.NoError(t, err, "Error registering account")
data := rove.SpawnData{}
b, err := json.Marshal(data)
assert.NoError(t, err, "Error marshalling data")
request, _ := http.NewRequest(http.MethodPost, path.Join("/", a.Id.String(), "/spawn"), bytes.NewReader(b))
response := httptest.NewRecorder()
s.router.ServeHTTP(response, request)
assert.Equal(t, http.StatusOK, response.Code)
var status rove.SpawnResponse
json.NewDecoder(response.Body).Decode(&status)
assert.Equal(t, http.StatusOK, response.Code)
if status.Success != true {
t.Errorf("got false for /spawn: %s", status.Error)
}
}
func TestHandleCommand(t *testing.T) {
s := NewServer()
s.Initialise()
a, err := s.accountant.RegisterAccount("test")
assert.NoError(t, err, "Error registering account")
// Spawn the rover rover for the account
_, inst, err := s.SpawnRoverForAccount(a.Id)
attribs, err := s.world.RoverAttributes(inst)
assert.NoError(t, err, "Couldn't get rover position")
data := rove.CommandData{
Commands: []game.Command{
{
Command: game.CommandMove,
Bearing: "N",
Duration: 1,
},
},
}
b, err := json.Marshal(data)
assert.NoError(t, err, "Error marshalling data")
request, _ := http.NewRequest(http.MethodPost, path.Join("/", a.Id.String(), "/command"), bytes.NewReader(b))
response := httptest.NewRecorder()
s.router.ServeHTTP(response, request)
assert.Equal(t, http.StatusOK, response.Code)
var status rove.CommandResponse
json.NewDecoder(response.Body).Decode(&status)
if status.Success != true {
t.Errorf("got false for /command: %s", status.Error)
}
attrib, err := s.world.RoverAttributes(inst)
assert.NoError(t, err, "Couldn't get rover attribs")
// Tick the command queues to progress the move command
s.world.ExecuteCommandQueues()
attribs2, err := s.world.RoverAttributes(inst)
assert.NoError(t, err, "Couldn't get rover position")
attribs.Pos.Add(game.Vector{X: 0.0, Y: attrib.Speed * 1}) // Should have moved north by the speed and duration
assert.Equal(t, attribs.Pos, attribs2.Pos, "Rover should have moved by bearing")
}
func TestHandleRadar(t *testing.T) {
s := NewServer()
s.Initialise()
a, err := s.accountant.RegisterAccount("test")
assert.NoError(t, err, "Error registering account")
// Spawn the rover rover for the account
_, id, err := s.SpawnRoverForAccount(a.Id)
assert.NoError(t, err)
// Warp this rover to 0
assert.NoError(t, s.world.WarpRover(id, game.Vector{}))
// Set a tile to wall below this rover
wallPos := game.Vector{X: 0, Y: -1}
assert.NoError(t, s.world.Atlas.SetTile(wallPos, game.TileWall))
request, _ := http.NewRequest(http.MethodGet, path.Join("/", a.Id.String(), "/radar"), nil)
response := httptest.NewRecorder()
s.router.ServeHTTP(response, request)
assert.Equal(t, http.StatusOK, response.Code)
var status rove.RadarResponse
json.NewDecoder(response.Body).Decode(&status)
if status.Success != true {
t.Errorf("got false for /radar: %s", status.Error)
}
assert.Equal(t, game.TileRover, status.Tiles[len(status.Tiles)/2])
}
func TestHandleRover(t *testing.T) {
s := NewServer()
s.Initialise()
a, err := s.accountant.RegisterAccount("test")
assert.NoError(t, err, "Error registering account")
// Spawn one rover for the account
attribs, _, err := s.SpawnRoverForAccount(a.Id)
assert.NoError(t, err)
request, _ := http.NewRequest(http.MethodGet, path.Join("/", a.Id.String(), "/rover"), nil)
response := httptest.NewRecorder()
s.router.ServeHTTP(response, request)
assert.Equal(t, http.StatusOK, response.Code)
var status rove.RoverResponse
json.NewDecoder(response.Body).Decode(&status)
if status.Success != true {
t.Errorf("got false for /rover: %s", status.Error)
} else if attribs != status.Attributes {
t.Errorf("Missmatched attributes: %+v, !=%+v", attribs, status.Attributes)
}
}

View file

@ -1,301 +0,0 @@
package server
import (
"context"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/mdiluz/rove/pkg/accounts"
"github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/persistence"
"github.com/robfig/cron"
)
const (
// PersistentData will allow the server to load and save it's state
PersistentData = iota
// EphemeralData will let the server neither load or save out any of it's data
EphemeralData
)
// Server contains the relevant data to run a game server
type Server struct {
// Internal state
accountant *accounts.Accountant
world *game.World
// HTTP server
listener net.Listener
server *http.Server
router *mux.Router
// Config settings
address string
persistence int
tick int
// sync point for sub-threads
sync sync.WaitGroup
// cron schedule for world ticks
schedule *cron.Cron
}
// ServerOption defines a server creation option
type ServerOption func(s *Server)
// OptionAddress sets the server address for hosting
func OptionAddress(address string) ServerOption {
return func(s *Server) {
s.address = address
}
}
// OptionPersistentData sets the server data to be persistent
func OptionPersistentData() ServerOption {
return func(s *Server) {
s.persistence = PersistentData
}
}
// OptionTick defines the number of minutes per tick
// 0 means no automatic server tick
func OptionTick(minutes int) ServerOption {
return func(s *Server) {
s.tick = minutes
}
}
// NewServer sets up a new server
func NewServer(opts ...ServerOption) *Server {
router := mux.NewRouter().StrictSlash(true)
// Set up the default server
s := &Server{
address: "",
persistence: EphemeralData,
router: router,
schedule: cron.New(),
}
// Apply all options
for _, o := range opts {
o(s)
}
// Set up the server object
s.server = &http.Server{Addr: s.address, Handler: s.router}
// Create the accountant
s.accountant = accounts.NewAccountant()
s.world = game.NewWorld()
return s
}
// Initialise sets up internal state ready to serve
func (s *Server) Initialise() (err error) {
// Add to our sync
s.sync.Add(1)
// Spawn a border on the default world
if err := s.world.SpawnWorld(); err != nil {
return err
}
// Load the accounts if requested
if err := s.LoadAll(); err != nil {
return err
}
// Set up the handlers
for _, route := range Routes {
s.router.HandleFunc(route.path, s.wrapHandler(route.method, route.handler))
}
// Start the listen
if s.listener, err = net.Listen("tcp", s.server.Addr); err != nil {
return err
}
s.address = s.listener.Addr().String()
return nil
}
// Addr will return the server address set after the listen
func (s *Server) Addr() string {
return s.address
}
// Run executes the server
func (s *Server) Run() {
defer s.sync.Done()
// Set up the schedule if requested
if s.tick != 0 {
if err := s.schedule.AddFunc(fmt.Sprintf("0 */%d * * *", s.tick), func() {
// Ensure we don't quit during this function
s.sync.Add(1)
defer s.sync.Done()
fmt.Println("Executing server tick")
// Run the command queues
s.world.ExecuteCommandQueues()
// Save out the new world state
s.SaveWorld()
}); err != nil {
log.Fatal(err)
}
s.schedule.Start()
fmt.Printf("First server tick scheduled for %s\n", s.schedule.Entries()[0].Next.Format("15:04:05"))
}
// Serve the http requests
if err := s.server.Serve(s.listener); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
// Stop will stop the current server
func (s *Server) Stop() error {
// Stop the cron
s.schedule.Stop()
// Try and shut down the http server
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.server.Shutdown(ctx); err != nil {
return err
}
return nil
}
// Close waits until the server is finished and closes up shop
func (s *Server) Close() error {
// Wait until the server has shut down
s.sync.Wait()
// Save and return
return s.SaveAll()
}
// Close waits until the server is finished and closes up shop
func (s *Server) StopAndClose() error {
// Stop the server
if err := s.Stop(); err != nil {
return err
}
// Close and return
return s.Close()
}
// SaveWorld will save out the world file
func (s *Server) SaveWorld() error {
if s.persistence == PersistentData {
s.world.RLock()
defer s.world.RUnlock()
if err := persistence.SaveAll("world", s.world); err != nil {
return fmt.Errorf("failed to save out persistent data: %s", err)
}
}
return nil
}
// SaveAccounts will save out the accounts file
func (s *Server) SaveAccounts() error {
if s.persistence == PersistentData {
if err := persistence.SaveAll("accounts", s.accountant); err != nil {
return fmt.Errorf("failed to save out persistent data: %s", err)
}
}
return nil
}
// SaveAll will save out all server files
func (s *Server) SaveAll() error {
// Save the accounts if requested
if s.persistence == PersistentData {
s.world.RLock()
defer s.world.RUnlock()
if err := persistence.SaveAll("accounts", s.accountant, "world", s.world); err != nil {
return err
}
}
return nil
}
// LoadAll will load all persistent data
func (s *Server) LoadAll() error {
if s.persistence == PersistentData {
s.world.Lock()
defer s.world.Unlock()
if err := persistence.LoadAll("accounts", &s.accountant, "world", &s.world); err != nil {
return err
}
}
return nil
}
// wrapHandler wraps a request handler in http checks
func (s *Server) wrapHandler(method string, handler Handler) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// Log the request
fmt.Printf("%s\t%s\n", r.Method, r.RequestURI)
vars := mux.Vars(r)
// Verify the method, call the handler, and encode the return
if r.Method != method {
w.WriteHeader(http.StatusMethodNotAllowed)
} else if val, err := handler(s, vars, r.Body, w); err != nil {
fmt.Printf("Failed to handle http request: %s", err)
w.WriteHeader(http.StatusInternalServerError)
} else if err := json.NewEncoder(w).Encode(val); err != nil {
fmt.Printf("Failed to encode reply to json: %s", err)
w.WriteHeader(http.StatusInternalServerError)
} else {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
}
}
}
// SpawnRoverForAccount spawns the rover rover for an account
func (s *Server) SpawnRoverForAccount(accountid uuid.UUID) (game.RoverAttributes, uuid.UUID, error) {
if inst, err := s.world.SpawnRover(); err != nil {
return game.RoverAttributes{}, uuid.UUID{}, err
} else if attribs, err := s.world.RoverAttributes(inst); err != nil {
return game.RoverAttributes{}, uuid.UUID{}, fmt.Errorf("No attributes found for created rover: %s", err)
} else {
if err := s.accountant.AssignRover(accountid, inst); err != nil {
// Try and clear up the rover
if err := s.world.DestroyRover(inst); err != nil {
fmt.Printf("Failed to destroy rover after failed rover assign: %s", err)
}
return game.RoverAttributes{}, uuid.UUID{}, err
} else {
return attribs, inst, nil
}
}
}

View file

@ -1,3 +1,4 @@
package version
// Version represents a version to be overrided with -ldflags
var Version = "undefined"

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;
}

202
script/wait-for-it.sh Executable file
View file

@ -0,0 +1,202 @@
#!/usr/bin/env bash
# The MIT License (MIT)
# Copyright (c) 2016 Giles Hall
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Use this script to test if a given TCP host/port are available
WAITFORIT_cmdname=${0##*/}
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
else
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
fi
WAITFORIT_start_ts=$(date +%s)
while :
do
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
WAITFORIT_result=$?
else
(echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
WAITFORIT_result=$?
fi
if [[ $WAITFORIT_result -eq 0 ]]; then
WAITFORIT_end_ts=$(date +%s)
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
break
fi
sleep 1
done
return $WAITFORIT_result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
else
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
fi
WAITFORIT_PID=$!
trap "kill -INT -$WAITFORIT_PID" INT
wait $WAITFORIT_PID
WAITFORIT_RESULT=$?
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
fi
return $WAITFORIT_RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
WAITFORIT_hostport=(${1//:/ })
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
shift 1
;;
--child)
WAITFORIT_CHILD=1
shift 1
;;
-q | --quiet)
WAITFORIT_QUIET=1
shift 1
;;
-s | --strict)
WAITFORIT_STRICT=1
shift 1
;;
-h)
WAITFORIT_HOST="$2"
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
WAITFORIT_HOST="${1#*=}"
shift 1
;;
-p)
WAITFORIT_PORT="$2"
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
WAITFORIT_PORT="${1#*=}"
shift 1
;;
-t)
WAITFORIT_TIMEOUT="$2"
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
WAITFORIT_TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
WAITFORIT_CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
# Check to see if timeout is from busybox?
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
WAITFORIT_BUSYTIMEFLAG=""
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
WAITFORIT_ISBUSY=1
# Check if busybox timeout uses -t flag
# (recent Alpine versions don't support -t anymore)
if timeout &>/dev/stdout | grep -q -e '-t '; then
WAITFORIT_BUSYTIMEFLAG="-t"
fi
else
WAITFORIT_ISBUSY=0
fi
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
wait_for
WAITFORIT_RESULT=$?
exit $WAITFORIT_RESULT
else
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
wait_for_wrapper
WAITFORIT_RESULT=$?
else
wait_for
WAITFORIT_RESULT=$?
fi
fi
if [[ $WAITFORIT_CLI != "" ]]; then
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
exit $WAITFORIT_RESULT
fi
exec "${WAITFORIT_CLI[@]}"
else
exit $WAITFORIT_RESULT
fi

50
snap/snapcraft.yaml Normal file
View file

@ -0,0 +1,50 @@
name: rove
title: Rove
base: core20
license: MIT
architectures: [ amd64 ]
icon: data/icon.svg
summary: An asynchronous nomadic game about exploring as part of a loose community.
# TODO: Add more to the description
description: |
An asynchronous nomadic game about exploring as part of a loose community.
confinement: strict
adopt-info: go-rove
apps:
rove:
command: bin/rove
plugs:
- network
environment:
ROVE_USER_DATA: $SNAP_USER_DATA
rove-server:
command: bin/rove-server
plugs:
- network
- network-bind
environment:
WORDS_FILE : "$SNAP/data/words_alpha.txt"
DATA_PATH : $SNAP_USER_DATA
parts:
go-rove:
plugin: go
source-type: local
source: .
build-packages:
- gcc-multilib
override-pull: |
snapcraftctl pull
version=$(git describe --always --long --dirty --tags)
sed -i "s/undefined/$version/" pkg/version/version.go
snapcraftctl set-version $version
git describe --exact-match --tags HEAD 2> /dev/null && snapcraftctl set-grade "stable" || snapcraftctl set-grade "devel"
copy-data:
plugin: dump
source-type: local
source: data
organize:
'*.txt' : data/

9
tools/tools.go Normal file
View file

@ -0,0 +1,9 @@
// +build tools
package tools
import (
_ "github.com/golang/protobuf/protoc-gen-go"
_ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway"
_ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger"
)