/battlebot-server

A server for battlebots! Mostly experiments in RxJS.

Primary LanguageJavaScript

Build Statuscodecov

Battlebot server

This is a server for AI battles! The idea is that client bots connect to the server via WebSockets and play each other in turn-based abstract strategy games, with the server managing and validating the games and recording the results.

It's currently being hosted on Heroku: https://blunderdome-server.herokuapp.com/

Server code is located here: https://github.com/dprgarner/battlebot-server

A boilerplate Python Client is located here: https://github.com/dprgarner/battlebot-client-py

Overview

Before playing games, a new bot must first be registered with the server, which involves making a POST request to an API, specifying the name of the bot and (optionally) its owner, and saving the login credentials returned by the request. A registered bot is associated to a single type of game.

> curl -X POST http://blunderdome-server.herokuapp.com/bots/numberwang -H "Content-Type: application/json" -d '{ "bot_id": "MyAwesomeBot", "owner": "David" }'
{
  "game": "numberwang",
  "bot_id": "MyAwesomeBot",
  "pass_hash": "8ad86f2934f346abf60ee7c192c96fbc838a54273c4c092de7ae97153b84d934"
}

To play a game, the bot should connect to the server via a (secure) WebSocket and authenticate itself in its first message to the server. The server will initially send a JSON message containing a 64-character single key salt. The client should respond with JSON identifying the ID of the bot bot_id, the name of the game game, and the login_hash. The login hash is generated by taking the bot's pass_hash, appending the salt string, and encoding the string with sha256 in hexdigest. If the authentication fails, the server will (attempt to) send { "authentication": "failed" } and then close the connection. If the authentication succeeds, the server will respond with a confirmation message.

// From the server:
{ "salt": "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b" }

// To the server:
{
  "game": "numberwang",
  "bot_id": "MyAwesomeBot",
  "login_hash": "53c1219f5758fb5db92165142e4105810a596a3734e8e013b3e0b5ebc440312c"
}

// From the server:
{
  "authentication": "OK",
  "game": "numberwang",
  "bot_id": "MyAwesomeBot"
}

Once a client has successfully authenticated itself, the server will then either immediately match up the bot with an available connected bot, or wait until another bot connects, or disconnect after a few minutes.

Once a game starts, the server will send an update to both bots containing the initial state of the game in the key state, including the starting game board and the next player to move. The structure of this object is specific to the particular game, but it will always include the list of the playing bots players, the ID of the next bot to move nextPlayer, and a boolean complete stating whether the game is still in progress.

When it is a bot's turn to move, the bot should send a turn to the server over the WebSocket. The format of the turn will be specific to the game, but it should always be valid JSON, and should not need to reference the game name or the bot name (as this is already known the server).

The server will then reply with an object containing the keys state and turn. The turn will be the most recently attempted turn, but will also include the extra data of the player that made the turn player, the time the turn was made, and the boolean valid stating whether the turn was valid or not. If the move is invalid, then this update is sent only to the bot which attempted the invalid move, along with the (unchanged) state of the game. If the move is valid, then the turn and new state of the game will be sent to both bots.

Once the game ends, the server will attempt to send a final update to both bots containing the final state of the board, and then close both connections. The final state of the board will contain the boolean key complete set to true, the key victor set to the ID of the winning bot, or null if the game ends in a draw, and the key reason stating how the game was decided. A game can be completed normally, but can also end if a bot is disqualified by disconnecting early, making three invalid turns during the course of the game, or taking longer than three seconds to take a turn. If an error is thrown by the server during the running of the game, then the game will (hopefully) be recorded as a draw.

Codebase

The WebSocket server is implemented in JavaScript with RxJS 5, an implementation of the Reactive Extensions design pattern/framework. The HTTP site is written with Express.

The server saves registered bots and completed games to a MongoDB database.

To start the server, start up a MongoDB server, and run npm start with the port and MongoDB URI in the env variables:

MONGODB_URI=mongodb://localhost:27017/battlebots PORT=3000 npm start

Creating Games

A game can be added simply by dropping a new file into ./games. This will create the API registration endpoint /bots/newgame, and will start saving registered bots and finished games to the database.

A game module must export a function validate (State, Turn) => Bool which, for given state and turn objects, must return a boolean of whether the move should be accepted as valid or not. This should also check that the player attempting to make the turn is the nextPlayer.

The game module must also export a function reducer (State, Turn) => State, which creates the new state of the game from the existing state of the game. This new state must contain players, nextPlayer, and complete. The reducer should also record the reason the game ended.

Tests

As it stands, the Noughts and Crosses game has tests, but the remaining code is untested. The remaining code should hopefully have some tests soon.

APIs

There's a GraphQL API. The interactive tool GraphiQL is located here.

Games

Numberwang

This isn't a real game, it's just something I was using to check that the server/client communication protocol was working properly.

Noughts and Crosses

The initial state of a game is of this form:

{
  "state": {
    "players": [
      "IdiotBot2",
      "IdiotBot"
    ],
    "complete": false,
    "board": [
      ["", "", ""],
      ["", "", ""],
      ["", "", ""]
    ],
    "nextPlayer": "IdiotBot2",
    "marks": {
      "X": "IdiotBot2",
      "O": "IdiotBot"
    }
  }
}

A turn dispatched from the client to the server should specify the mark to place and the space to place it in. A space is specified as a two-entry array, with each entry an integer from 0 to 2, specifying the row and column to place the mark in respectively. The mark should be "O" or "X", depending on whether the client is playing "X"es or "O"s.

{ "mark": "X", "space": [2, 2] }

The following is an example of a server response, at the end of the game, including the last valid turn:

{
  "turn": {
    "player": "IdiotBot2",
    "valid": true,
    "space": [1, 2],
    "time": 1498036964996,
    "mark": "X"
  },
  "state": {
    "complete": true,
    "players": [
      "IdiotBot2",
      "IdiotBot"
    ],
    "reason": "complete",
    "board": [
      ["O", "O", "X"],
      ["X", "O", "X"],
      ["O", "X", "X"]
    ],
    "marks": {
      "X": "IdiotBot2",
      "O": "IdiotBot"
    },
    "nextPlayer": "IdiotBot",
    "victor": "IdiotBot2"
  }
}

Development

Download Docker and Docker Compose. Build and start the servers with:

docker-compose up

TODO

  • Log when a bot disconnects
  • Fix the coverage reporter
  • Keep writing them tests