Building the Obyte Stack Game Bot


A step by step guide to building an Obyte Bot integrated with an Obyte AA. We will use the Obyte Sample Bot as a starting point. This tutorial has been written for Windows, but should be very similar and simpler on Linux.

Together the Bot and the AA will run a simple stack Game.

1. About the Game

The ObyStack Game is a simple multi-player stack game, where players bid for a chance to win the entire stack of bytes.

To start playing on testnet, go to your testnet wallet, and pair with the ObyStack Bot using the following code:

AziMKfNVh+TNOpnFJ8qKh1DpywCyQWU20ALmX5zA5rAm@obyte.org/bb-test#StackGame

The game is run on the Obyte network using the ObyStack Game Bot and the ObyStack Game Autonomous Agent.

Find out more about the ObyStack Game.

2. Setting up a development environment

Follow the Obyte Bot Dev Environment Setup Tutorial to set-up the Bot development environment.

3. Getting Started

Add your new project directory to the editor and open start.js file.

Moving import of the device libraries to the top

Move const device = require('ocore/device.js'); to the top of the start.js file.

Remove const device = require('ocore/device.js'); from other parts of the start.js file.

Modifying message sent to a user on Pairing

We are going to start by modifying the message displayed to a user on pairing.

Go to the section of the code where a user pairs his/her device with the bot and simply modify the message as following:

eventBus.on('paired', (from_address, pairing_secret) => {
  device.sendMessageToDevice(from_address, 'text', "Welcome to Obyte Stack Game Bot!");
});

4. Restructuring your code: adding chatting module

We are going to write a few hundred lines of code and I like to keep my programs to not much bigger than 100 lines, so we are going to restructure this project.

Start by separating the logic that governs the bot communication with the user.

Create a chat.js file in your project directory, containing the following:

const validationUtils = require('ocore/validation_utils');
const device = require('ocore/device.js');

function chatting(from_address, text) {
  if (!text.match(/^You said/))
    device.sendMessageToDevice(from_address, 'text', "You said to me: " + text);
}

exports.chatting = chatting;

You will notice that the above code exports the chatting function which takes the user's device address ( from_address) and user's message (text). Right now this function will play back to the user his/her message, but we will be expending it further later.

Now lets import this function into the main program. Open the start.js file and add the following in the top part of the program :

// modules
const chatting = require('./chat.js');

Next, call the chatting function from within the eventBus.on('text'....) event replacing the sample code as follows:

/** user sends message to the bot **/
eventBus.on('text', (from_address, text) => {
  text = text.trim();
  chatting.chatting(from_address, text);
});

Test this by talking to your bot.

5. Restructuring your code: adding newTransactions module

Next create anewTransactions.js file in your project directory containing the newTransactions function as follows:

const db = require('ocore/db');
const device = require('ocore/device.js');

function newTransactions(arrUnits) {
  console.log('New Transaction(s) received');
}

exports.newTransactions = newTransactions;

Again, we will expand the functionality of this function later.

Import this function into the main program. Open the start.js file and add the following in the top part of the program :

// modules
const newTransactions = require('./newTransactions.js');

Next, call the newTransactions function from within the eventBus.on('new_my_transactions'....) event, replacing sample code as follows:

// user pays to the bot
eventBus.on('new_my_transactions', (arrUnits) => {
  newTransactions.newTransactions(arrUnits);
});

Test above code by sending a payment to your bot's address. You should see the New Transaction(s) received message in the log.txt file in your /AppData/Local/ project directory.

6. Cleaning up: removing the unnecessary code

Some sample code in our bot will not be required for our project due to functional requirements and restructuring. Clean up thestart.js file by removing the following lines of code:

const db = require('ocore/db');
...
const validationUtils = require('ocore/validation_utils');
...
// payment is confirmed
eventBus.on('my_transactions_became_stable', (arrUnits) => {
	//stableTransactions.stableTransactions(arrUnits);
});

Your start.js code should look like the following:

/*jslint node: true */
'use strict';
// Obyte imports (libraries)
const constants = require('ocore/constants.js');
const conf = require('ocore/conf');
const eventBus = require('ocore/event_bus');
const headlessWallet = require('headless-obyte');
const device = require('ocore/device.js');
// Game imports (modules)
const chatting = require('./chat.js');
const newTransactions = require('./newTransactions.js');

// headless wallet is ready Event
eventBus.once('headless_wallet_ready', () => {
  headlessWallet.setupChatEventHandlers();

  // user pairs his device with the bot
  eventBus.on('paired', (from_address, pairing_secret) => {
    device.sendMessageToDevice(from_address, 'text', "Welcome to Obyte Stack Game Bot!");
  });

  // user sends message to the bot
  eventBus.on('text', (from_address, text) => {
    text = text.trim();
    chatting.chatting(from_address, text);
  });
});

// user pays to the AA
eventBus.on('new_my_transactions', (arrUnits) => {
  newTransactions.newTransactions(arrUnits);
});

process.on('unhandledRejection', up => { throw up; });

Repeat previous tests to make sure everything still works.

7. Accessing the Obyte database and creating custom tables

Accessing the database

By default Obyte uses the free and open source SQLight database

Install SQLight3 client for windows by running the following commands at the command prompt:

bash
sudo apt-get install sqlite3
exit

If you do not have bash installed previously, you will need to install it first.

SQLightStudio is an open source and free database management application.

In case of a local install of the Obyte light node, the database can be found in the project directory in the /AppData/Local folder.

Open byteball-light.sqlite database in the SQLightStudio.

Creating tables to store user and game data

The Obyte Sample Bot comes with a sample db.sql file, which we can use as a template for creation of tables to store our application data.

Open the db.sql file in your Text Editor and replace the comments with the following code:

-- query separator
DROP TABLE IF EXISTS xwf_stack_game_user;
-- query separator
DROP TABLE IF EXISTS xwf_stack_game;
-- query separator
CREATE TABLE IF NOT EXISTS xwf_stack_game_user (
  user_device CHAR(33) NOT NULL,
  user_wallet CHAR(32) NOT NULL,
  user_status CHAR(8) NOT NULL,
  PRIMARY KEY (user_device, user_wallet),
  FOREIGN KEY (user_device) REFERENCES correspondent_devices(device_address)
);
-- query separator
CREATE TABLE IF NOT EXISTS xwf_stack_game (
  game_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  game_amount INTEGER NOT NULL DEFAULT 0,
  game_author CHAR(32) NOT NULL,
  game_leader CHAR(32) NOT NULL,
  game_status CHAR(7) NOT NULL,
  game_start TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Create the db_import.js file as follows:

'use strict';
const fs = require('fs');
const db = require('ocore/db.js');

let db_sql = fs.readFileSync('db.sql', 'utf8');
db_sql.split('-- query separator').forEach(function(sql) {
  if (sql) {
    db.query(sql, [], (rows) => {
      console.log(sql);
    });
  }
});

Run the following command at the command prompt:

node db_import.js

This should create your xwf_stack_game_user and xwf_stack_game tables.

Test by checking the database using the SQLight3 client application.

8. Creating the Obyte Stack Game AA

Follow the Obyte Stack Game AA Tutorial to learn how to create an AA for this game.

Alternatively use my Testnet Obyte Stack Game AA, which can be found at the following address: RQI2WE6EVSK6CY3NRNLHGQXUNJGZGU35

9. Storing the Game Config parameters - conf_game.js

In your project directory create a conf_game.js file as follows:

// Config setting for the game
"use strict";
exports.aaAddress = "4UR2XOZMB52T6JGHDSANKV7E5OZTKLH6"; // my AA
exports.aaName = "Obyte Stack Game AA";
exports.minBidAmount = 100000;  // 100,000 is about £0.02 at the moment
exports.gameDuration = 60000;
exports.notificationFrequency = 10000;
exports.commissionRate = 1;  // represents 1%
exports.botWallet = '4H2FOFBP7ST6BLYWHZ3GUV5PHY626AM4';  // your bot's single wallet address
exports.botPairingCode = 'A/R1S1zX9R9KzN34IA5PCUbYbRB5WEDLEdVaNo/0s/Xu@obyte.org/bb-test#StackGame';

Make sure to replace my bot wallet address and pairing Code with your bot wallet address and pairing code. Also use your AA address, if you have one.

Your bot's wallet address and pairing code are shown on the console every time you start your bot.

10. Asking user to play the Game - toPlay.js

Next, lets create a toPlay.js file containing a few simple functions asking users to play the Game by sending a valid bid to the AA:

// Obyte imports (libraries)
const device = require('ocore/device.js');
const db = require('ocore/db');
// Game imports (modules)
const game = require('./conf_game.js');

function stackGame(device_address, wallet_address) {
  // check if there is an active game
  db.query(`SELECT game_status, game_leader FROM xwf_stack_game
    WHERE game_status='started' OR game_status='running'`, gameRows => {
      if (gameRows.length === 0) newGame(device_address); // no game found
      else if (gameRows.length === 1) { // game is running
        let gameRow = gameRows[0];
        if (gameRow.game_leader !== wallet_address) nextBid(device_address);
      } // game is running
      else console.log('Error: more than one current game found.');
  });
}

function nextBid(device_address) {
  device.sendMessageToDevice(device_address, 'text',
    '[balance](byteball:' + game.aaAddress + '?amount=' + game.minBidAmount + ')');
}

function newGame(device_address) {
  device.sendMessageToDevice(device_address, 'text', 'Send ' + game.minBidAmount +
    ' bytes to start a new Game.  Type STOP to stop playing at any time.');
  device.sendMessageToDevice(device_address, 'text',
    'If you are confirmed as the initiator of the new Game by the AA, you will ' +
    'automatically receive 50% of the winnings.');
  device.sendMessageToDevice(device_address, 'text', '[balance](byteball:' + game.aaAddress + '?amount=' + game.minBidAmount + ')');
}

exports.nextBid = nextBid;
exports.newGame = newGame;
exports.stackGame = stackGame;

11. Chatting to the user - chatUtils.js

Lets create a chatUtils.js file containing a number of utility functions that will be used when talking to users as follows:

// Obyte imports (libraries)
const db = require('ocore/db');
const device = require('ocore/device.js');
// Game imports (modules)
const toPlay = require('./toPlay.js');

function saveUserAddress(device_address, wallet_address) {
  db.query(`INSERT INTO xwf_stack_game_user (user_device, user_wallet, user_status)
    VALUES (?,?,?)`, [device_address, wallet_address, 'active']);
  device.sendMessageToDevice(device_address, 'text', 'Your address is saved');
  toPlay.stackGame(device_address, wallet_address);  // ask user to play
}

function notificationsSuspended(device_address) {
  device.sendMessageToDevice(device_address, 'text', 'Notifications suspended. ' +
    'Type START at anytime to start recieving game notifications and to play.');
}

function updateUserStatus(newStatus, user_wallet, user_device) {
  db.query(`UPDATE xwf_stack_game_user SET user_status=?
    WHERE user_wallet=?`, [newStatus, user_wallet]);
  if (newStatus === 'sleeping') notificationsSuspended(user_device);
  if (newStatus === 'active') toPlay.stackGame(user_device, user_wallet);
}

exports.saveUserAddress = saveUserAddress;
exports.notificationsSuspended = notificationsSuspended;
exports.updateUserStatus = updateUserStatus;

12. Chatting to the user - chat.js

Open the chat.js file in the Editor and replace its content with the following:

// Obyte imports (libraries)
const validationUtils = require('ocore/validation_utils');
const db = require('ocore/db');
const device = require('ocore/device.js');
// Game imports (modules)
const chatUtils = require('./chatUtils.js');
const toPlay = require('./toPlay.js');

function chatting(from_address, text) {
  db.query(`SELECT user_wallet, user_status FROM xwf_stack_game_user WHERE user_device=?`,
    [from_address], userRows => {

      // ** NO user address is found in the db ** //
      if (userRows.length === 0) {
        // ** User entered valid address ** //
        if (validationUtils.isValidAddress(text)) chatUtils.saveUserAddress(from_address, text);
        // ** No valid address was provided ** //
        else device.sendMessageToDevice(from_address, 'text', "Please send me your address");
      }

      // ** User address is found in the db ** //
      else {
        let row = userRows[0];
        // ** User entered valid wallet address **//
        if (validationUtils.isValidAddress(text)) {
          if (row.user_wallet === text)
            device.sendMessageToDevice(from_address, 'text',
              "Thank you. We alredy have this address for you.");
          else
            device.sendMessageToDevice(from_address, 'text',
              "Hm, we have different wallet address for you in our records. " +
              "The address we have is " + row.user_wallet);
        }

        // ** User is active ** //
        if (row.user_status !== 'sleeping') {
          // ** User asked to suspend notifications
          if (text.toUpperCase() === 'STOP')
            chatUtils.updateUserStatus('sleeping', row.user_wallet, from_address);
          // ** ask user to play ** //
          else toPlay.stackGame(from_address, row.user_wallet);
        }

        // ** User is sleeping but asked to play ** //
        else if (text.toUpperCase() === 'START')
          chatUtils.updateUserStatus('active', row.user_wallet, from_address);

        // ** User is sleeping but started talking to the bot ** //
        else chatUtils.notificationsSuspended(from_address);

      } // user address was found in the db
  });
}

exports.chatting = chatting;

Test this by chatting with your bot and saving your wallet's address. You can also send some test payments to the AA.

13. Validating bids and informing players - newBid.js

We are going to create a number of helper functions and modules to help our bot monitor, validate and react to user bids as they come in.

First, lets create a newBid.js fie. It will help to validate new bids and keep players informed. Add the following code:

// Obyte imports (libraries)
const device = require('ocore/device.js');
const db = require('ocore/db');
// Game imports (modules)
const game = require('./conf_game.js');
const toPlay = require('./toPlay.js');

// ** validate the bid and inform the users ** //
function validateAndNotify(unitAuthorWallet, unitAmount) {
  // ** Talk to user who sent the bid ** //
  // get user device address of the user who sent the bid
  db.query(`SELECT user_device FROM xwf_stack_game_user WHERE user_wallet=?`,
    [unitAuthorWallet], userRows => {
      if (userRows.length === 0) console.log('User device not found for user walelt ' + unitAuthorWallet);
      else userRows.forEach(userRow => {
        let userDevice = userRow.user_device
        if (unitAmount < game.minBidAmount) {  // user bid is too small
          device.sendMessageToDevice(userDevice, 'text',
            'Your bid of ' + unitAmount +' bytes is less than required amount. ' +
            'It will be rejected by the ' + game.aaName + ' Make a minimum payment of ' +
            game.minBidAmount + ' bytes, for your chance to win.');
          toPlay.nextBid(userDevice);
        }
        else { // bid is valid
          device.sendMessageToDevice(userDevice, 'text',
            'Your payment of ' + unitAmount + ' bytes has been received by the ' +
            game.aaName + ' and will be added to the Game Stack once confirmed.');
        }
      });
  });  // db query to get device of user who sent the bid

  // ** Tell all other active users about a valid bid ** //
  if (unitAmount >= game.minBidAmount) {  // valid bid
    db.query(`SELECT user_device FROM xwf_stack_game_user
      WHERE user_wallet!=? AND user_status!='sleeping'`,
      [unitAuthorWallet], otherUsers => {
        if (otherUsers.length > 0 ) {
          otherUsers.forEach(otherUser => { // for each row
            let otherUserDevice = otherUser.user_device;
            device.sendMessageToDevice(otherUserDevice, 'text',
              '************** NEW BID **************');
            device.sendMessageToDevice(otherUserDevice, 'text',
              'A bid of ' + unitAmount + ' bytes has just been received by the ' +
              game.aaName + ' from another player and will be added to the overall pot once confirmed.');
            toPlay.nextBid(otherUserDevice);
          }); // for each row
        }
    });  // get user devices
  } // tell all other users about a valid bid
}

exports.validateAndNotify = validateAndNotify;

14. Creating a new game and updating an existing game - newTxnsUtils.js

Next, lets create a couple of functions to save a new game and update an existing game in response to a valid bid.

In your project folder create a newTxnsUtils.js file as follows:

/ Obyte imports (libraries)
const device = require('ocore/device.js');
const db = require('ocore/db');
// Game imports (modules)
const game = require('./conf_game.js');

// ** save new game ** //
function saveNewGame(unitAuthorWallet, unitAmount) {
  db.query(`INSERT INTO xwf_stack_game
    (game_amount, game_author, game_leader, game_status)
    VALUES (?,?,?,?)`,
    [unitAmount, unitAuthorWallet, unitAuthorWallet, 'started']);
}

// ** update the game ** //
function updateGame(gameId, unitAuthorWallet, gameAmount) {
  db.query(`UPDATE xwf_stack_game
    SET game_status=?, game_amount=?, game_leader=?
    WHERE game_id=? AND game_status='started' OR game_status='running'`,
    ['running', gameAmount, unitAuthorWallet, gameId]);
}

exports.saveNewGame = saveNewGame;
exports.updateGame = updateGame;

15. Keeping players informed at every tick of the clock - timer.js

The first bid will initiate the game, the second bid will start the timer. Any subsequent bid will re-start the clock. When the timer runs-out the game will stop. Lets create some logic, that will keep users informed of the game's status. This code will run at every tick of the clock.

In your project folder create a timer.js file as follows:

// Obyte imports (libraries)
const device = require('ocore/device.js');
const db = require('ocore/db');
// Game imports (modules)
const game = require('./conf_game.js');
const toPlay = require('./toPlay.js');

// ** user messages from the timer function ** //
function gameStatusMessages(remainingTime, amount, leader, author) {
  // get device addresses of all users
  db.query(`SELECT user_device, user_wallet, user_status FROM xwf_stack_game_user`,
    [], userRows => {
    userRows.forEach(userRow => {
      // if time ran out, broadcast Game Finished message to all
      if (remainingTime === 0) {  // time ran out
        let potAmount = amount - amount * game.commissionRate / 100;

        // when Game is finished, broadcast Game Finished message to all active users
        if (userRow.user_status === 'active') {  // user wants to receive notifications
          device.sendMessageToDevice(userRow.user_device, 'text',
            '********** GAME IS FINISHED **********');
          device.sendMessageToDevice(userRow.user_device, 'text',
            'Estimated Pot size was ' + potAmount + ' bytes.');
          device.sendMessageToDevice(userRow.user_device, 'text',
            `Invite others to play by sharing this Bot's Pairing Code: ` +
            game.botPairingCode);
        } // user wants to receive notifications

        // tell the winner/initiator of the game, they won
        if (userRow.user_wallet === leader || userRow.user_wallet === author ) {
          device.sendMessageToDevice(userRow.user_device, 'text',
            '********** Congratulations! **********');
          if (leader === author)
            device.sendMessageToDevice(userRow.user_device, 'text',
              'As the initiator and the winner of the game, you have won an entire ' +
              'estimated Pot of ' + potAmount + ' bytes, subject to sucessful confirmations by the AA.');
          else if (userRow.user_wallet === author)
            device.sendMessageToDevice(userRow.user_device, 'text',
              'As the initiator of the game, you have won 50% of an estimated Pot of '
              + potAmount + ' bytes, subject to sucessful confirmations by the AA.');
          else
            device.sendMessageToDevice(userRow.user_device, 'text',
              'As the winner of the game, you have won 50% of an estimated Pot of '
              + potAmount + ' bytes, subject to sucessful confirmations by the AA.');
        } // tell the winner/initiator of the game, they won

        // ask all active users to play a new Game
        if (userRow.user_status === 'active') {
          device.sendMessageToDevice(userRow.user_device, 'text',
            '************* PLAY AGAIN *************');
          toPlay.newGame(userRow.user_device);
        }
        else
          device.sendMessageToDevice(userRow.user_device, 'text',
            'Type START at any time to play another game.');
      }  // time ran out

      else {  // game timer is running
        // broadcast pot size and time remaining to all active users
        if (userRow.user_status === 'active') {  // user wants to receive notifications
          let potAmount = amount - amount * game.commissionRate / 100;
          device.sendMessageToDevice(userRow.user_device, 'text',
            remainingTime/1000 + ' sec left. Pot size is ' + potAmount + ' bytes.');
          // if user is not in the lead, ask for payment
          if (userRow.user_wallet !== leader) toPlay.nextBid(userRow.user_device);
        } // user wants to receive notifications
      } // game timer is running

    });  // for each user
  }); // db query
}

exports.gameStatusMessages = gameStatusMessages;

16. Telling AA that the game has stopped - aaCommand.js

When the time runs out, we need to notify the AA.

In your project folder create a aaCommand.js file as follows:

// Obyte imports (libraries)
const network = require('ocore/network.js');
const composer = require('ocore/composer.js');
const objectHash = require('ocore/object_hash.js');
const device = require('ocore/device.js');
const headlessWallet = require('headless-obyte');
// Game imports (modules)
const game = require('./conf_game.js');

function stopTheGame() {
  let dataFeed = {};
  dataFeed.command = 'Stop';
  var opts = {
    paying_addresses: [game.botWallet],
    change_address: game.botWallet,
    messages: [
        {
            app: "data_feed",
            payload_location: "inline",
            payload_hash: objectHash.getBase64Hash(dataFeed),
            payload: dataFeed
        }
    ],
    to_address: game.aaAddress,
    amount: 10000
  };
  headlessWallet.sendMultiPayment(opts, (err, unit) => {
    if (err) {
      console.log('Error paying winnings for lottery id: ' + lotteryId);
      return;
    }
    else if (unit) console.log('STOP Command sent from the Bot to the AA, unit: ' + unit);
  });
}

exports.stopTheGame = stopTheGame;

17. Putting it all together - newTransactions.js

Finally, we can update the newTransactions.js file replacing its content with the following code:

// Obyte imports (libraries)
const db = require('ocore/db');
const device = require('ocore/device.js');
// Game imports (modules)
const game = require('./conf_game.js');
const newBid = require('./newBid.js');
const newTxnsUtils = require('./newTxnsUtils.js');
//const toPlay = require('./toPlay.js');
const timer = require('./timer.js');
const aaCommand = require('./aaCommand');
// Game variables
let remainingTime, gameTimer;
let amount, leader, author;

function newTransactions(arrUnits) {
  // for each new transaction unit
  for(let i=0; i {
        //if (outboundTxn.length === 1) // outbound Transaction

        if (outboundTxn.length === 0) {  // inbound Transaction
          // ** Get the bid details from the unit ** //
          let unitAmount = '';
          let unitUserWalletAddress = '';
          db.query("SELECT address, amount, asset FROM outputs WHERE unit=?", [unit], rows => {
            rows.forEach(row => {
              if (row.asset === null) {  // assets are in bytes
                if (row.address === game.aaAddress) unitAmount = row.amount;
                else unitUserWalletAddress = row.address;
              }
            });

            // ** Validate the bid and inform the users ** //
            newBid.validateAndNotify(unitUserWalletAddress, unitAmount);

            // update the db with valid bid
            if (unitAmount >= game.minBidAmount) {  // valid bid
              // check db for an active game
              db.query(`SELECT game_id, game_status, game_amount, game_author, game_leader
                FROM xwf_stack_game WHERE game_status='started' OR game_status='running'`,
                gameRows => {
                  if (gameRows.length > 1) console.log('Error: more than 1 current game');
                  else if (gameRows.length === 0) { // no active game, start the game
                    newTxnsUtils.saveNewGame(unitUserWalletAddress, unitAmount);
                  }
                  else {  // game is running, update game
                    let gameRow = gameRows[0];
                    let gameId = gameRow.game_id;
                    amount = gameRow.game_amount + unitAmount;;
                    leader = unitUserWalletAddress;
                    author = gameRow.game_author;
                    // update game
                    newTxnsUtils.updateGame(gameId, leader, amount);
                    // start Timer
                    clearInterval(gameTimer);
                    remainingTime = game.gameDuration;
                    gameTimer = setInterval(clock, game.notificationFrequency);
                  }  // game is running, update game
              });  // check if there is an active game
            } // update the db with valid bid
          });  // get the bid details from the unit
        } // inbound Transaction
    }); // db
  }  // for each new transaction unit
}

function clock() {
  // tick-tock
  remainingTime = remainingTime - game.notificationFrequency;
  // inform users of the game status, e.g. running or stopped. Pot size, Time, Winners
  timer.gameStatusMessages(remainingTime, amount, leader, author);
  // game is over
  if (remainingTime === 0) {
    clearInterval(gameTimer);
    // close current lottery to new bids
    db.query(`UPDATE xwf_stack_game SET game_status='closed'
      WHERE game_status='running'`);
    aaCommand.stopTheGame(); // TELL AA TO STOP
  }
}

exports.newTransactions = newTransactions;

Your game in ready for testing in a multi-player mode. Use a minimum of two devices to fully test your game.