-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial: Creating a TicTacToe Bot
As an introduction to Merknera this tutorial will guide you through creating a bot to play Tic-Tac-Toe in JavaScript using node.js. The aim of this tutorial is to give you an introduction into creating a bot, registering and having it play its first games - this should give you a good understanding of how JSON-RPC works in the context of Merknera and how to test your bot using ngrok and postman.
A simple an non-effective implementation will be implemented and it will be left as an exercise of the reader to improve upon this.
You will need to have the following software download and installed before continuing with this tutorial:
- Create a new directory and navigate to it at the command line.
- Run
npm init
and following the on-screen instructions. - Run
npm install jayson --save
to install and add the dependency to our package.json file created for us in step 2.
When orchestrating a game Merknera acts as a client and makes calls to the bots participating in the game and the bots act as a server. We will start by implementing the 4 server methods required by Tic-Tac-Toe to play a game: Status.Ping
, TicTacToe.NextMove
, TicTacToe.Complete
& TicTacToe.Error
.
Status.Ping
is the easiest so we will start with that.
Status.Ping is called whenever the Merknera server starts to find out which bots are online and it also calls Status.Ping prior to any move being played, this enables Merknera to ensure the bot is still online and to wake up any bots that might be hosted on a free service such as Heroku that goes to sleep after periods of inactivity. Status.Ping
is the same for all game types so its code can be reused.
Full documentation can be found here
We need to be able to accept a method of Status.Ping
with no parameters and return with a result of "ping": "OK"
.
- Create a file called `index.js
- Add the following contents.
// Import the jayson module into a variable names "jayson"
var jayson = require('jayson');
// Create a jayson server.
var server = jayson.server({
// Register the Status.Ping method
"Status.Ping": function(callback) {
// Write a log message so we can see that this RPC method was called.
console.log("Status.Ping");
// Invoke the callback that will respond to the client.
callback(
null,
{ ping: "OK" } // Return an object with ping: "OK" that will be used in the response.
);
},
}, {
// don't collect params in a single argument
// More information can be found here: https://github.com/tedeh/jayson#named-parameters
collect: false
});
// Start the server and listen on port 3000.
server.http().listen(3000);
This code registers the Status.Ping method and returns ping: "OK"
when called.
We can now test this method using Postman.
- Launch your server by typing
node index.js
at the command line. - Launch Postman an create a new request.
- Click on the "GET" drop down and change it to "POST".
- In the request URL enter
http://localhost:3000
. - Click on the "Headers" tab and add a header with a "key" of
Content-Type
and a "value" ofapplication/json
. - Click on the "Body" tab and select the "raw" radio button option.
- Paste the following into the body in Postman.
{
"jsonrpc": "2.0",
"method": "Status.Ping",
"id": 1
}
- Click "Send" and you should get a result like the following. You can also check the command line and you should see a message written of
Status.Ping
.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ping": "OK"
}
}
If you followed the above steps and got the above result then everything worked and you can move on to the next RPC call.
This method is responsible for taking in the current game state and returning your bots next move. Full documentation can be found here.
- Add the following code to jayson.server after "Status.Ping"
var server = jayson.server({
...
"TicTacToe.NextMove": function(gameid, mark, gamestate, callback) {
// Write a log to the console so that we can see that this RPC method was
// called and what the parameters.
console.log("TicTacToe.NextMove: gameid: " + gameid + ", mark: " + mark + ", gamestate: " + gamestate);
// Invoke the callback that will respond to the client.
callback(
null,
{ position: 0 } // For now hard code position one to play in.
);
}
...
}
When adding this, don't forget to add a comma after the closing }
of "Status.Ping".
This code registers the TicTacToe.NextMove method. This method accepts the current game ID, the mark (either "X" or "O") that you are playing and the current game state as an array of each position on the board. See the GameBoard documentation for more information.
We can now test this method using Postman - this is mostly the same as when testing Status.Ping
.
- Launch your server by typing
node index.js
at the command line - kill it first if it is already running. - Launch Postman an create a new request.
- Click on the "GET" drop down and change it to "POST".
- In the request URL enter
http://localhost:3000
. - Click on the "Headers" tab and add a header with a "key" of
Content-Type
and a "value" ofapplication/json
. - Click on the "Body" tab and select the "raw" radio button option.
- Paste the following into the body in Postman.
{
"jsonrpc": "2.0",
"method": "TicTacToe.NextMove",
"params": {
"gameid": 78,
"mark": "X",
"gamestate": ["O", "X", "O", "", "O", "", "", "X", ""]
},
"id": 1
}
- Click "Send" and you should get a result like the following. You can also check the command line and you should see a message written of
TicTacToe.NextMove: gameid: 78, mark: X, gamestate: O,X,O,,O,,,X,
.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"position": 0
}
}
Currently the bot is hard-coded to always respond in position 0
. This will cause an error with Merknera if player has already played this move.
This tutorial will now enhance this logic to a point where it does not error but it will achieve this with a crude method and you should aim to enhance this once you have completed the tutorial. The method implemented in this tutorial will simply find the first available space and play that space.
- Change your
TicTacToe.NextMove
function to the following:
"TicTacToe.NextMove": function(gameid, mark, gamestate, callback) {
// Write a log to the console so that we can see that this RPC method was
// called and what the parameters.
console.log("TicTacToe.NextMove: gameid: " + gameid + ", mark: " + mark + ", gamestate: " + gamestate);
// Create a variable to store the position the bot will play.
var position;
// Loop through each position in the game state.
for (var i = 0; i < gamestate.length; i++) {
// If the position in the game state is an empty string then it has not
// yet been played so take this as out position.
if (gamestate[i] === "") {
// Set the position to the empty space we've found and break out of the loop.
position = i;
break;
}
}
// Invoke the callback that will respond to the client.
callback(
null,
{ position: position } // Return the position we just calculated.
);
}
Run the same test as we did before in Postman for TicTacToe.NextMove
. This time, however, the position should be the first available space which is 3. Ensure to restart your server with node index.js
if it is still running to load your latest modifications. The response should be as follows.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"position": 3
}
}
This method is called when a player has won the game or a draw has been found. You will be passed the game ID of the completed game, whether or not you were the winner, the final gamestate and the mark you were played (either "X" or "O"). This is really just a notification so the response does not matter too much. We will just log a message to the command line when a game is completed. Full documentation can be found here.
- Add the following code to jayson.server after "TicTacToe.NextMove".
"TicTacToe.Complete": function(gameid, mark, winner, gamestate, callback) {
// Write a log to the console so that we can see that this RPC method was
// called and what the parameters.
console.log("TicTacToe.Complete: gameid: " + gameid + ", mark: " + mark + ", winner: " + winner + ", gamestate: " + gamestate);
// Invoke the callback that will respond to the client.
callback(
null,
{ status: "OK" } // Return status: "OK" to indicate we received the message.
);
}
When adding this, don't forget to add a comma after the closing }
of "TicTacToe.NextMove".
You can now test this in the same way as Status.Ping
and TicTacToe.NextMove
with a request body as below. Ensure to restart your server with node index.js
if it is still running to load your latest modifications.
{
"jsonrpc": "2.0",
"method": "TicTacToe.Complete",
"params": {
"gameid": 21,
"winner": true,
"mark": "X",
"gamestate": ["O", "X", "O", "", "O", "X", "O", "X", ""]
},
"id": 1
}
And expect a response of the following and a message at the command line of TicTacToe.Complete: gameid: 21, mark: X, winner: true, gamestate: O,X,O,,O,X,O,X,
.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"status": "OK"
}
}
When your bot causes an error (for example, playing a move that has already been played) Merknera will mark you bot as error and games will cease to be played until you bot is re-registered (we'll get that that soon). Merknera will also call TicTacToe.Error with an error message so that you can diagnose any issues. For this bot we will just log the error to the command line and it will work very similar to TicTacToe.Complete
.
Full documentation can be found here.
- Add the following code to jayson.server after "TicTacToe.Complete".
"TicTacToe.Error": function(gameid, message, errorcode, gamestate, callback) {
// Write a log to the console so that we can see that this RPC method was
// called and what the parameters.
console.log("TicTacToe.Error: gameid: " + gameid + ", errorcode: " + errorcode + ", message: " + message);
// Invoke the callback that will respond to the client.
callback(
null,
{ status: "OK" } // Return status: "OK" to indicate we received the message.
);
}
When adding this, don't forget to add a comma after the closing }
of "TicTacToe.Complete".
You can now test this in the same way as TicTacToe.Complete
with a request body as below. Ensure to restart your server with node index.js
if it is still running to load your latest modifications.
{
"jsonrpc": "2.0",
"method": "TicTacToe.Error",
"params": {
"gameid": 21,
"message": "You played an invalid move, 'X' was already played here.",
"errorcode": 1548
},
"id": 1
}
And expect a response of the following and a message at the command line of TicTacToe.Error: gameid: 21, errorcode: 1548, message: You played an invalid move, 'X' was already played here.
.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"status": "OK"
}
}
That's it! The implementation of the server is now complete no we want to register our bot with Merknera and play some games.
Your bot acts as a client and Merknera as the server when registering your bot to play games. You do this by making a JSON-RPC call to the RegistrationService.Register
method on Merknera.
Once we have done this we cant then go on to registering with Merknera and playing our first games.
Bot registration should be called every time your bot starts up, this lets Merknera know that you're online and also allows Merknera to update any information relating to your bot such as its URL and its version. More information and full ARPC documentation for the Registration Service can be found here.
The jayson
library we are using to handle JSON-RPC also make client requests simple.
- Add the following code after the
var jayson = require('jayson');
line at the top of the file and beforevar server = jayson.server({
// Create the HTTPS client object to Merknera.
var client = jayson.client.https({
hostname: 'api.merknera.com',
path: '/rpc',
method: 'POST'
});
// Configure the registrations parameters.
var registrationParams = {
"token": "TODO", // API token as generated from www.merknera.com
"botname": "TODO", // The name you want to give to your bot. This must be unique.
"botversion": "0.0.1",
"game": "TICTACTOE",
"rpcendpoint": "TODO", // The URL that your bot is running on. We'll use ngrok for the tutorial.
"programminglanguage": "JavaScript",
"description": "My first TicTacToe bot created from the tutorial."
};
// Make the client request to the registration service RPC endpoint and log
// the response message.
client.request('RegistrationService.Register', registrationParams, function(err, response) {
if(err) throw err;
console.log(response.result.message);
});
- Now go to a new command line window and type
ngrok http 3000
. This will run ngrok and create a secure tunnel into your PC on port 3000. ngrok will display various URLs are the command line. Use the http forwarding one - it will look like this:http://e25620cb.ngrok.io
- In index.js edit the registrationParams object to complete the TODOs. Fill out the
rpcendpoint
with the forarding URL from ngrok, give your bot a name and get a token from merknera by following the Obtaning a Token guide. - Run your bot with
node index.js
you bot should register and begin playing games!
You have now successfully created your first bot! However, there are still some improvement you can make that will be left as an exercise to the reader.
- You can modify the logic used to play the game in the
TicTacToe.NextMove
RPC handler to make your bot more competitive. - Currently this bot only works if your PC is online and ngrok is running. You might want to look at hosting your bot on a free service such as Heroku.