/crane

A toolkit for building games for AIs.

Primary LanguageJavaScriptMIT LicenseMIT

Overview

Crane is a toolkit for writing games for AI players. It provides an API to communicate between a game written in JavaScript and players written in other languages, as well as utilities for running tournaments.

Version -1. All APIs unstable.

Quick start

Player

Put players in the /players directory. Each player must define the method respond, which takes a string command and returns a string response:

function respond(command) {
  return (command === 'move') ? 'rock' : 'err';
}

Crane currently supports players written in Python 3, JavaScript, and Java. For other languages, you'll need to write a client.

Game

Install with npm:

npm install --save @jacksondc/crane

Require:

var crane = require('@jacksondc/crane');

Call readPlayers to get a list of all players:

var players = crane.readPlayers();

Communicate with players using send:

players[0].send('move', function(move) {
    console.log('first player picks %s', move);
});

For a full example, see the rock paper scissors game.

Game API

readPlayers([players])

Returns an array of Player objects corresponding to files in the /players directory. Players not written in supported languages will be ignored. To receive players only for specific objects, pass in their filenames as an array.

crane.readPlayers(['python-player.py', 'js-player.js']); //only pick these two players

setTimoutLength(length)

Sets the amount of time, in milliseconds, that Crane will wait for player responses before throwing an error. Defaults to 1000.

crane.setTimeoutLength(500); //wait half a second for responses

Player.send(message)

Accepts a string message, which will be passed into the player's respond method as its only argument. Returns a string message returned by the respond method.

var response = player.send('move');

Messages are passed through stdin/stdout and aren't sanitized at all. Special characters or newlines will likely mess everything up. Whitespace is preserved as long as it doesn't start or end the message.

Player API

A player only needs to implement one method, respond, which accepts a single string argument, a message from the game, and returns a string to the game. Full examples of players written in all supported languages can be found in the rock paper scissors game.

function respond(message) {
  return 'response';
}

Language Notes

Crane currently has clients for Python 3, JavaScript, and Java (it will recognize files with the extensions .py, .js and .class.) It assumes python is executable as python3, node as node, and Java as java.

Automating Gameplay

playTournament(players, eachMatch, [options])

NOTE: playTournament currently only plays two-player games.

Plays some number of matches between each player in the tournament and tallies up the results, which are passed to the callback and (optionally) printed in a table. Matches between the same combinations of players will be played back-to-back.

Arguments

  1. players: An array of player objects (from readPlayers) to consider for games in the tournament
  2. eachRound: A function to execute each round
  3. options.callback: A callback to execute when the tournament is finished
  4. options.rounds: the number of times to play each combination of opponents against each other (default 100). Can also be set to 'UNTIL_SIGNIFICANT', which plays until the first-place player has a statistically-significant lead over the second-place player
  5. options.numRandomizations: the number of times to re-randomize score datasets in testing statistical significance (default 1000). Ignored unless options.rounds is set to 'UNTIL_SIGNIFICANT'
  6. options.pValueThreshold: the probability of a random outcome of the a gap between the two leading players greater than or equal to the actual gap below which no more rounds will be played (default 0.05). Ignored unless options.rounds is set to 'UNTIL_SIGNIFICANT'
  7. options.printTable: set to false to suppress table printing (default true)

The eachMatch function accepts two arguments, a match object and a callback. The match object consists of a players array at match.players with all the participating players and a match number at match.index (an integer starting at one). The callback accepts two arguments: an error and a score array, where each score is for the player at the corresponding index of in the players array.

options.callback will be passed two arguments: an error and an array of results. Results will be sorted with best performers (lowest ranks) at the beginning. Each entry in results has a player object at result.player, a cumulative score (across all matches played) at result.score, and an average score at result.avgScore.

An example call to playTournament and possible output:

crane.playTournament(crane.readPlayers(), function(match, cb) {
  // ...
  cb(null, [1, -1]); //win for player 0
}, {
  callback: function(err, results) {
    console.log('%s wins with %d points', results[0].player.getName(), results[0].score);
  },
  numRounds: 5
});
┌──────────┬────────────────────┬────────────┬────────────┐
│ Rank     │ Player             │ Score      │ Avg Score  │
├──────────┼────────────────────┼────────────┼────────────┤
│ 1        │ python-player      │          5 │       1.00 │
├──────────┼────────────────────┼────────────┼────────────┤
│ 2        │ js-player          │          1 │       0.20 │
├──────────┼────────────────────┼────────────┼────────────┤
│ 3        │ JavaPlayer         │         -6 │      -1.20 │
└──────────┴────────────────────┴────────────┴────────────┘

python-player wins with 5 points

Writing a Client

If you want to use a language that doesn't already have a client, you'll have to write your own. Here's the general framework:

  1. A new client is initialized for each player, so the same client might be running multiple times at once.
  2. The client is executed through the command line. All messages are sent through standard output and received through standard input.
  3. The client will receive one argument, the filename (without file extension) of its player. The client must then import this file and find its respond method.
  4. Each line is considered a message. If multiple lines are received at once, split them before processing.
  5. The first word (up to a space) of each message from the server is a unique message ID. The second word is a command. Anything after that is data.
  6. The first word of every response from the client is the same message ID. The second word is a status code: 0 for okay, 1 for error. Anything after that is data, which will be sent back to the game.
  7. There is (currently) only one valid command, player, which may be followed by data. If there is data, it should be passed as an argument to the player's respond() method. Before sending data to the player, remove whitespace (especially newlines) from either side. Whatever respond() returns should be data in the client's response to the game.
  8. Clients can also send messages that are not responses to server messages, by replacing the first token (normally the id) with 'err' or 'log' (depending on the type of message). This is useful for debugging or for errors finding the player file before the first message arrives.

Look at the existing clients for more implementation specifics.