diff --git a/commands.md b/commands.md new file mode 100644 index 0000000..9920627 --- /dev/null +++ b/commands.md @@ -0,0 +1,104 @@ +This is a list of all the commands that are available from the client to the +server along with the expected response. + + +# Message types + +## Client + +### Hello + +Sent during initiali connection phase. + +### Query + +Query information from the server about a specific topic + +### Command + +Request an action to be performed on the server + + +## Server + +### Hello + +Responses and further queries during the connection phase. Example is asking +what faction they want to be. + +### Update + +Response to a query or a command. Examples would be responding to a query of +how much money the player has. A response updating the target positon of a ship +and it's location. + +This is also used to provide updates about the game state. There are periodic +updates that define where everything should be. This is the type used there. + +### Event + +This is an event that the client did not create or ask for. For example, a +battle starting, a building finishing, a planet found. + +### Error + +A response sent when the requested action can not be completed. This is likely +to be a frequent response to things like not having enough resources. + + +# Full message definitions + +For all communication with the server the player you need the player id + +## Hello + +For the hello message neither key is optional for connecting. They are both +required. If a new player is created then it does not matter what the faction +key is (as long as it is a valid faction) however when connecting an existing +player the faction key *must* match the key stored on the server or a +connection error will happen. + +| Domain | Payload keys | Expected response | +|:-------|:-------------------|:----------------------------------------------------------------------| +| Player | connect: player_id | An update with payload success OR an update with a payload of faction | +| | faction: faction | An update to the player (when creating a player) that has the faction | +| | | | +| | | | + + +## Queries + +For queries the keys just need to exist. There are not any values for the +payload keys. You can also apply as many keys in a single request as you would +like. All parts of the message should be in snake_case. All of the values for +the keys may be whatever you wish, they are discarded by the server though. + +| Domain | Payload keys | Expected response | +|:-------|:-------------|--------------------------------------------------------------------| +| Game | time | The current game time. | +| | size | The current size of the game universe. | +| Player | resources | The resources the player has | +| | planets | The planets the player can see (All planets owned, and discovered) | +| | buildings | The buildings the player has | +| | ships | The ships the player has | +| | construction | The builds in progress the player has | +| | | | +| | | | + + +## Commands + +This lists the domain and the payload keys required as well as the expected +value. + +| Domain | Payload keys | Expected response | +|:-------|:----------------------------------------------------|:-----------------------------------------------| +| Game | resign | An update with payload success. | +| Player | build: type [tech, building, ship], name: item name | An update with payload success. | +| | move: type [ship], name: item\_id | An update with the new target of the item. | +| | equip: item\_id, target: item\_id | An update with the stats of the targeted item. | +| | | | +| | | | +| | | | +| | | | +| | | | diff --git a/lib/app.rb b/lib/app.rb index 68a4864..17d79ce 100644 --- a/lib/app.rb +++ b/lib/app.rb @@ -39,12 +39,7 @@ loop do begin encoded_command = JSON.parse(line.chomp) response = '' - if encoded_command['type'] == 'hello' - # Here we need to get the client information. - response = game.player_connect(client, encoded_command).to_json - else - response = game.parse_command(encoded_command).to_json - end + response = game.parse_command(client, encoded_command) puts "Received: #{line.chomp}" puts "Sending: #{response}" client.puts "#{response}" diff --git a/lib/game/command_parser.rb b/lib/game/command_parser.rb new file mode 100644 index 0000000..e749c8a --- /dev/null +++ b/lib/game/command_parser.rb @@ -0,0 +1,147 @@ +# Nathan Hinton 12 Jan 2026 +# +# Parse a command from the client. This somehow will end up connecting with the +# game. This is because it will need to be able to communicate with the objects +# in the game and have game level access. + + +class CommandParser + def initialize + end + + def parse(client, command) + response = {'request_id' => command['request_id']} + case command['type'] + when 'hello' + parse_hello(client, command) + when 'query' + parse_query(command) + when 'command' + parse_command(command) + else + raise CommandParserError, "Unable to parse unknown type of '#{commad['type']}'" + end + return response + end + + def parse_hello(client, command) + response = {'type' => command['type']} + id = command['player_id'] + case command['domain'] + when 'player' + hello_player(client, id, command['payload']) + else + raise CommandParserError, "Unable to parse the hello type, domain '#{command['domain']}'" + end + return response + end + + def parse_query(command) + response = {'type' => command['type']} + id = command['player_id'] + case command['domain'] + when 'game' + query_game(id, command['payload']) + when 'player' + query_player(id, command['payload']) + else + raise CommandParserError, "Unable to parse the query type, domain '#{command['domain']}'" + end + return response + end + + def parse_command(command) + response = {'type' => command['type']} + id = command['player_id'] + case command['domain'] + when 'game' + command_game(id, command['payload']) + when 'player' + command_player(id, command['payload']) + else + raise CommandParserError, "Unable to parse the command type, domain '#{command['domain']}'" + end + return response + end + + # Perform the actions for the type of hello and domain of player. + def hello_player(client, id, payload) + response = {} + payload.keys.each do |key, value| + case key + when 'connect' + game.connect_player(client, id, value) + when 'faction' + game.set_player_faction(id, value) + else + raise CommandParserError, "Unable to parse the type hello, domain player, key '#{key}'" + end + end + return response + end + + # Perform the actions for the type of query and the domain of game + def query_game(id, payload) + response = {} + payload.keys.each do |key, value| + case key + when 'time' + game.send_time(id, value) + when 'size' + game.send_size(id, value) + else + raise CommandParserError, "Unable to parse the type query, domain game, key '#{key}'" + end + end + return response + end + + # Perform the actions for the type of query and the domain of player + def query_player(id, payload) + response = {} + payload.keys.each do |key, value| + case key + when 'resources' + game.get_player_resources(id, value) + when 'planets' + game.get_player_planets(id, value) + when 'buildings' + game.get_player_buildings(id, value) + when 'ships' + game.get_player_ships(id, value) + when 'construction' + game.get_player_construction(id, value) + else + raise CommandParserError, "Unable to parse the type query, domain player, key '#{key}'" + end + end + return response + end + + # Perform the actions for the type command and the domain of game + def command_game(id, payload) + response = {} + payload.keys.each do |key, value| + case key + else + raise CommandParserError, "Unable to parse the type command, domain game, key '#{key}'" + end + end + return response + end + + # Perform the actions for the type command and the domain of player + def command_player(id, payload) + response = {} + payload.keys.each do |key, value| + case key + else + raise CommandParserError, "Unable to parse the type command, domain player, key '#{key}'" + end + end + return response + end +end + +class CommandParserError < StandardError +end diff --git a/lib/game/game.rb b/lib/game/game.rb index 22bcb56..ae8acfe 100644 --- a/lib/game/game.rb +++ b/lib/game/game.rb @@ -176,19 +176,17 @@ module Game # The entry point for all messages that arrive from a client. The # payload must be a hash decoded from JSON and contain a `type` field. # + # @param client [TCPSocket] The socket that the client used. # @param command [Hash] The parsed client command # # @return [String] JSON-encoded response # - def parse_command(command) + def parse_command(client, command) return error_response('Player not registered!') unless command['player_id'] case command['type'] - when 'dbg' - # Debugger - do nothing in production - { 'type' => 'ok', 'payload' => 'debugging' }.to_json when 'hello' - error_response('Hello command should already have been handled.') + parse_hello(client, command) when 'query' parse_query(command) when 'command' @@ -233,7 +231,18 @@ module Game # @return [String] JSON-encoded error response # def error_response(request_id, msg) - { 'type' => 'error', 'request_id', => id, 'payload' => msg }.to_json + { 'type' => 'error', 'request_id' => id, 'payload' => msg }.to_json + end + + ## + # Return an update response to the client + def update_response(request_id, domain, payload) + {'type' => 'update', 'domain' => domain, 'request_id' => request_id, 'payload' => payload}.to_json + end + + # Return an event response to the client + def event_response(request_id, domain, payload) + {'type' => 'event', 'domain' => domain, 'request_id' => request_id, 'payload' => payload}.to_json end ## @@ -367,16 +376,6 @@ module Game @players.find { |p| p.player_id == player_id } || false end - # -------------------------------------------------------------------- - # PRIVATE HELPERS - kept simple for the test-suite - # -------------------------------------------------------------------- - - private - - def error_response(msg) - { 'type' => 'error', 'payload' => msg }.to_json - end - def load_players(ids) ids.map { |id| Player.load_from_id(id) } end diff --git a/spec/app_spec.rb b/spec/app_spec.rb index 87d2fb6..1e65610 100644 --- a/spec/app_spec.rb +++ b/spec/app_spec.rb @@ -34,7 +34,7 @@ require 'socket' require 'json' HOSTNAME = 'localhost' -PORT = 2000 +PORT = 20001 RSpec.describe "app.rb" do let(:client) {TCPSocket.open(HOSTNAME, PORT)}