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.
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.
Follow the Obyte Bot Dev Environment Setup Tutorial to set-up the Bot development environment.
Add your new project directory to the editor and open start.js
file.
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.
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!"); });
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.
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.
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.
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.
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.
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
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';
Your bot's wallet address and pairing code are shown on the console every time you start your bot.
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;
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;
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.
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;
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;
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;
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;
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.