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.
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.
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.
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
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
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.
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';
}
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
.
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
players
: An array of player objects (fromreadPlayers
) to consider for games in the tournamenteachRound
: A function to execute each roundoptions.callback
: A callback to execute when the tournament is finishedoptions.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 playeroptions.numRandomizations
: the number of times to re-randomize score datasets in testing statistical significance (default 1000). Ignored unlessoptions.rounds
is set to'UNTIL_SIGNIFICANT'
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 unlessoptions.rounds
is set to'UNTIL_SIGNIFICANT'
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
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:
- A new client is initialized for each player, so the same client might be running multiple times at once.
- The client is executed through the command line. All messages are sent through standard output and received through standard input.
- 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. - Each line is considered a message. If multiple lines are received at once, split them before processing.
- 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.
- 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.
- 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. Whateverrespond()
returns should be data in the client's response to the game. - 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.