Writing A Client
Warning: writing a client is an advanced subject for people who want to use programming languages outside the official clients, it is highly recommended for beginners to use the official clients
About This Guide
This guide will go through the steps of building a botskrieg client. The examples are written in Python3 and can be more or less translated into any language
Your programming language will need to support the following things
- Creating an SSL TCP socket
- Reading and writing UTF-8
- Decoding and encoding JSON
Connecting to Botskrieg
Botskrieg currently supports TCP connections (with Websocket Connections on the way)
Setting up the connection
Use your language's socket libraries to connect to botskrieg, use SSL to connect to port 4242 on "botskrieg.com"
Python3 Example Code:
import socket import ssl connection = socket.create_connection(("botskrieg.com", 4242)) context = ssl.create_default_context() connection = context.wrap_socket(connection, server_hostname="botskrieg.com")
A Note on BattleBox Development Servers
Its useful to developers of the server to be able to point clients towards their development servers. As a matter of convention, it would be nice to allow your client to accept a parameter specifying the URI of the server to connect to. Since most dev servers are run without SSL, its nice if your client can handle unencrypted TCP connections
As a matter of convention, please use the scheme "battleboxs://" to specify a SSL TCP connection, and "battlebox://" to specify a plaintext TCP connection
Some Examples:
- battlebox://localhost:4000 - A plain text TCP connection to localhost on port 4000
- battleboxs://localhost:4000 - An SSL TCP connection to localhost on port 4000
- battlebox://not-a-real-domain.com:4242 - A plain text TCP connection to not-a-real-domain.com on port 4242
- battleboxs://not-a-real-domain.com:4242 - An SSL TCP connection to not-a-real-domain.com on port 4242
Most users will not run development servers, so default to battleboxs://botskrieg.com:4242
Reading and Writing Messages
The structure of messages is shown below. Messages coming from the server should be in this format. Messages going to the server should be encoded in the same way
The first two bytes of the message represent the length in bytes of the JSON encoded data. The message length is Network Order (Big Endian) Unsigned 16 bit integer
Message length is strictly limited to 2 ** 16 (65536) bytes. Your client should enforce this limit, as attempting to send a message larger than 65536 bytes will render the connection unusable
A special ping message can be sent to the server to test your encoding
Python3 Example Code:
This uses the "connection" object from the previous example
import json import struct def send_message(connection, msg): msg_bytes = str.encode(json.dumps(msg)) header = struct.pack("!H", len(msg_bytes)) # b'\x00\x06' connection.sendall(header + msg_bytes) def receive_message(connection): msg_size_bytes = connection.recv(2) (msg_size,) = struct.unpack("!H", msg_size_bytes) message_json = connection.recv(msg_size) message = json.loads(message_json) return message send_message(connection, "PING") receive_message(connection) # PONG
Authenticating
After creating an account on botskrieg create an API key here (New API Key)
To authenticate, send the bot name and token in a JSON object of the following form
{ "bot": "some-bot-name", "token": "{YOUR KEY HERE}" }
Bot names can be any string providing the name meets the following rules
- May only contain alphanumeric characters or hyphens. ~r/^[a-zA-Z0-9-]+$/
- Cannot begin or end with a hyphen.
- Cannot have two hypens in a row
- Maximum is 39 characters.
- Cannot be in the reserved words (or contain bad words)
Python3 Example Code:
This builds on the functions from the previous examples
send_message(connection, {"bot": "some-bot-name", "token": "{your token here}"}) receive_message(connection)
On successful authentication you will receive
{ "bot_server_id": "b90fa044-af5c-4ec2-a3a2-35a273451f09", "connection_id": "eb0642f4-02b6-4e90-9bad-69c071757d4c", "status": "idle", "watch": { "bot": "{some url}", "user": "{some url" } }
Watch Links can be displayed to the user who can watch their bot/user play in browser
Some Authentication Errors You May Encounter (not limited to)
{"error":{"token":["Invalid API Key"]}}
{"error":{"user":["User is banned"]}}
{"error":{"user":["User connection limit exceeded"]}}
{"error":{"bot":{"name":["Can only contain alphanumeric characters or hyphens"]}}}
{"error":{"bot":{"name":["should be at most 39 character(s)"]}}}
{"error":{"bot":{"name":["Cannot end with a hyphen"]}}}
{"error":{"bot":{"name":["Cannot start with a hyphen"]}}}
{"error":{"bot":{"name":["Cannot contain two hyphens in a row"]}}}
Starting a Practice Match
Start a practice match in the following manner
{"action":"practice","arena":"some-arena","opponent":{}}
The opponent attribute can be composed of name and difficulty selectors, a random ai that meets the selectors will be choosen
"exact-name" # Match "exact-name" ["exact-name-1","exact-name-2"] # Match "exact-name-1" or "exact-name-2" {"name":"exact-name"} # Match "exact-name" {"name":["exact-name-1","exact-name-2"]} # Match "exact-name-1" or "exact-name-2" {"difficulty":4} # Match difficulty of exactly 4 {"difficulty":{"min":1}} # Match any difficulty greater than or equal to 1 {"difficulty":{"max":10}} # Match any difficulty greater than or equal to 1
Python3 Example Code:
send_message(connection, {"action": "practice", "arena": "robot-game-default", "opponent": "kansas"}) receive_message(connection)
On success, you'll recieve the following message
{ "bot_server_id": "028d7605-ceb9-4090-8c8d-6d947128b010", "connection_id": "1bfdaf91-8254-461a-9569-7f24bc8e9a04", "status": "match_making", "user_id": "8ca8bfc0-9715-4fa2-8e9c-ae4f63df7372" }
Some Errors You May Encounter (Not Limited to)
{"error":{"arena":["Arena \"some-arena\" does not exist"]}} {"error":{"opponent":["No opponent matching ({\"difficulty\":{\"min\":2},\"name\":\"not-real-opponent\"})"]}}
Start Match Making
Start match making in an arena in the following manner
{"action":"start_match_making","arena":"some-arena"}
Python3 Example Code:
send_message(connection, {"action": "start_match_making", "arena": "robot-game-default"}) receive_message(connection)
On success, you'll recieve the following message
{ "bot_server_id": "85c7defb-2d95-4951-bc4f-4f75a4f71136", "connection_id": "c2bcfb62-a57c-40e9-afce-834adabb4870", "status": "match_making", "watch": { "bot": "{some url}", "user": "{some url" } }
Some Errors You May Encounter (Not Limited to)
{"error":{"arena":["Arena \"some-arena\" does not exist"]}}
Accepting/Rejecting a Game (The Game Request)
While in the "match_making" state (either from match making or from a practice match) you will receive a message asking to accept a game
You'll get a message the looks like the following
{ "game_info": { "game_id": "{some uuid}", "player": 1, "settings": { "{game_specific_keys}": "{game_specific_vals}" } }, "game_type": "robot_game", "request_type": "game_request" }
Accepting
To accept the game send the following message
{ "action": "accept_game", "game_id": "{the game_id from the request}" }
Python3 Example Code:
# After getting into the match making state, accept the game as follows game_request = receive_message(connection) send_message(connection, {"action": "accept_game", "game_id": game_request["game_info"]["game_id"]}
The game request settings key will also contain information specific to the game you're playing, see the specifics for each supported game below
Important, check the "game_type" key to make sure that the arena game type is compatible with the type of code being run
Rejecting
If the game type key is not the one that is expected, you should "reject" the game and warn the user
{ "action": "reject_game", "game_id": "{the game_id from the request}" }
Python3 Example Code:
# If you need to reject a game, do the following game_request = receive_message(connection) send_message(connection, {"action": "reject_game", "game_id": game_request["game_info"]["game_id"]}
Note: if you don't respond to a game request within a certain amount of time (currently 2 seconds but subject to change), you automatically reject the game. As a courtesy if your client isn't able to play a game, promptly reject the game
Playing a Game (The Commands Request)
For each time during the game in which your bot will need to take action, you will receive a "commands request"
{ "commands_request": { "game_id": "{The game_id from the game request}", "game_state": { "{game_specific_key_1}": "{game_specific_val_1}", "{game_specific_key_2}": "{game_specific_val_2}" }, "maximum_time": 1000, "minimum_time": 250, "player": 2, "request_id": "{request id}" }, "request_type": "commands_request" }
Explanation of the Commands Request
- request_type: Key to signify that this a request for commands
- commands_request: Top level which holds the commands request info
- game_id: The UUID that corresponds to the game, this will match the game request
- game_state: A object containing game type specific keys and values, see the wire protocol for each individual game for more information
- minimum_time: the amount of milliseconds before the response will be acknowledged by the server. You may send your response before this deadline (highly recommended) the server will hold the response until this amount of time has elapsed. If the minimum time is 1000 (ms), then each turn will take at minimum 1 second. The reason minimum times exist is to make sure other games happening concurrently get a fair time slice.
- maximum_time: The amount of time in milliseconds before your response to this request will not be accepted. If your bot misses the deadline, the specific game will decide how to handle it. It is always in your best interest to return something before the timeout
- player: The integer corresponding to the player you are in this game
- request_id: An opaque JSON term that represents this request for commands. The request_id is currently implemented as a UUID, but may change in the future to any valid JSON term (including an object). Use this value to respond to this commands request
Responding to a Commands Request
In order to respond to a commands request, send a message in the following form
{ "action": "send_commands", "commands": "{game specific type}", "request_id": "{request_id_from_commands_request}" }
Python3 Example Code:
commands_request = receive_message(connection) commands = create_commands(commands_request) # some user provided implementation of game logic send_message(connection, { "action": "send_commands", "request_id": commands_request["commands_request"]["request_id"], "commands": commands })
When the Commands Deadline is Missed
In certain cases you may not respond to the server in time. The server will respond in the following way
{ "error": "invalid_commands_submission", "request_id": "{your request id}" }
This error could mean either
- You have an error in your logic around pulling the request_id out of the commands request (unlikely and fixable)
- You have not responded in time to the server for whatever reason (Your code took too long, the internet was flaky, ...etc)
The second case if very likely in the normal course of game play. It is HIGHLY recommend to handle this case
When the Game is Cancelled
In certain circumstances a game may be cancelled, most likely this is the result of one of the other players rejecting the game, but can also occur in some circumstances when the game code on the server throws an exception.
In the case a game is cancelled you will receive the following message
{ "game_id": "{game id}", "info": "game_cancelled" }
Following this message all things related to this game are over
The client TCP connection can be reused, and you can issue a "start_match_making" or "practice" command to play again
When the Game is Over
The game will eventually end, either through the natural end of the game, or an opponent disconnecting. The end of the game can potentially happen at any time
When the game ends you will receive a message that looks like this
{ "game_id": "{game_id}", "info": "game_over", "result": { "1": 10, "2": 4, "{some player id}": "{some score}" }, "watch": "{link to watch game}" }
Once this message is received, no further actions on the current game
Player IDs will Always be integers, scores themselves may be integers or may be any JSON term. See the specific game's wire protocol to know how scoring works
The TCP can (should) can be reused by issuing a "start_match_making" or "practice" command
Other Topics
Assorted Errors
Invalid Message Sent
If your client sends an unexpected message to the server, the server will respond with the following error
{ "error": "invalid_msg_sent" }
This error is especially tricky because a message can be valid in some contexts, but valid in others (i.e. while submitting a valid commands request, your opponent disconnects ending the game causing the commands request to become invalid). Long term the goal is to provide more actionable information within the error, but I'm still figuring out how to do that
Invalid Json
If you send the server JSON it isn't able to parse it will respond with the following message
{ "error": "invalid_json" }
This is likely caused by one of the following reasons
- Your message encoding is incorrect, double check the above section on sending messages
- You attempted to send a message greater than 2 ** 16 (65536) bytes
Bot Instance Failure
Though technically it should never happen, you may receive the following error
{ "error": "bot_instance_failure" }
Bot instance failures happen when the server code throws an exception, and represent a logic error in the server code
Bot instance failures are unrecoverable, the server will likely close the TCP connection, and if it does not, you should close the connection as it is unusable
Bot intance failures show up in the server logs, and will be debugged by the maintainers