Previously in part 1, we made a simple front-end and a login option with posting key.
In this part, we work on the back-end of the game. We use Nodejs for running our back-end codes. I assume you know how to create or run a Nodejs app. It's not complicated and I will cover most of it here.
API server
api/server.js
is the starting point of the API server. Let's initialize it with expressjs and some libraries for API usage.
const express = require('express')
const bodyParser = require('body-parser')
const hpp = require('hpp')
const helmet = require('helmet')
const app = express()
// more info: www.npmjs.com/package/hpp
app.use(hpp())
app.use(helmet())
// support json encoded bodies and encoded bodies
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(function (req, res, next) {
res.header(
'Access-Control-Allow-Origin',
'http://localhost https://tic-tac-toe.mahdiyari.info/'
)
res.header('Access-Control-Allow-Credentials', true)
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, access_key'
)
next()
})
const port = process.env.PORT || 2021
const host = process.env.HOST || '127.0.0.1'
app.listen(port, host, () => {
console.log(`Application started on ${host}:${port}`)
})
EDIT: added this part later!
I made a mistake in the above code and apparently, multiple values are not allowed in the Access-Control-Allow-Origin
header. So I modified the code as below:
app.use(function (req, res, next) {
const allowedOrigins = [
'http://localhost',
'https://tic-tac-toe.mahdiyari.info/'
]
const origin = req.headers.origin
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin)
}
res.header('Access-Control-Allow-Credentials', true)
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, access_key'
)
next()
})
Edit end.
Don't forget to install npm packages.
npm install express
npm install body-parser
npm install hpp
npm install helmet
hpp
and helmet
are for increased security and body-parser
for parsing request bodies for json encoded bodies and encoded bodies.
I also added http://localhost
and https://tic-tac-toe.mahdiyari.info/
to the Access-Control-Allow-Origin
header. You can add another URL to receive API calls from or just put *
. It basically limits the usage of the API server to the listed URLs.
Our API server will listen to 127.0.0.1:2021
by default. It does nothing at the moment. Let's continue with the main application.
Main application
We will run 2 Nodejs apps. One is the API server and the other is the main application where streaming blocks and processing data is happening. The reason for splitting applications is to run the API server in cluster mode. With cluster mode, we can run one API server for each CPU core. It will help with load balancing and keep API running as fast as possible while serving many requests. The cluster mode is useful only on API servers and especially Expressjs apps.
We will need a helper to stream the blocks.
helpers/streamBlock.js
:
const hiveTx = require('hive-tx')
const INTERVAL_TIME = 1000
const streamBlockNumber = async (cb) => {
try {
let lastBlock = 0
setInterval(async () => {
const gdgp = await hiveTx.call(
'condenser_api.get_dynamic_global_properties'
)
if (
gdgp &&
gdgp.result &&
gdgp.result.head_block_number &&
!isNaN(gdgp.result.head_block_number)
) {
if (gdgp.result.head_block_number > lastBlock) {
lastBlock = gdgp.result.head_block_number
cb(lastBlock)
}
}
}, INTERVAL_TIME)
} catch (e) {
throw new Error(e)
}
}
const streamBlockOperations = async (cb) => {
try {
streamBlockNumber(async (blockNumber) => {
const result = await hiveTx.call('condenser_api.get_block', [
blockNumber
])
if (result.result) {
const operations = result.result.transactions.map((transaction) => {
return transaction.operations
})
if (operations.length > 0) {
for (const operation of operations) {
cb(operation)
}
}
}
})
} catch (e) {
throw new Error(e)
}
}
module.exports = {
streamBlockNumber,
streamBlockOperations
}
install hive-tx: npm install hive-tx
We created 2 functions here. The first one streamBlockNumber
makes a call to get dynamic_global_properties
every INTERVAL_TIME
which I set to 1000ms (1 second) then checks for newly produced block number. If the block number is increased, then it calls the callback function with the new block number. It's a helpful function for getting newly generated block numbers.
We use the first function inside the second function streamBlockOperations
to get newly generated blocks and get operations inside that block by using the condenser_api.get_block
method.
streamBlockOperations
will call the callback function with newly added operations to the blockchain (except virtual operations).
index.js
:
const stream = require('./helpers/streamBlock')
try {
stream.streamBlockOperations((ops) => {
if (ops) {
const op = ops[0]
if (op && op[0] === 'custom_json' && op[1].id === 'tictactoe') {
processData(op[1].json)
}
}
})
} catch (e) {
throw new Error(e)
}
This will stream newly added operations to the blockchain and send the JSON from custom_json
operations with the id of tictactoe
to the processData
function.
We should define game mechanics and their corresponding custom_json.
Create a game
{
app: 'tictactoe/0.0.1'
action: 'create_game',
id: 'Random generated id',
starting_player: 'first or second'
}
Create a game and wait for the other player to join.
Request join a game
{
app: 'tictactoe/0.0.1',
action: 'request_join',
id: 'Game id'
}
Request to join an open game which is created by someone else.
Accept join request
{
app: 'tictactoe/0.0.1',
action: 'accept_request',
id: 'Game id',
player: 'username'
}
Accept the pending join request from another player to your created game.
Play
{
app: 'tictactoe/0.0.1',
action: 'play',
id: 'Game id',
col: '1 to 3',
row: '1 to 3'
}
Play or place an X/O on the board. col
is the column and row
is for the row of the placed X/O on the board.
Code implamantaion of the above in index.js
:
const processData = (jsonData) => {
try {
if (!jsonData) {
return
}
const data = JSON.parse(jsonData)
if (!data || !data.action || !data.app) {
return
}
if (data.action === 'create_game') {
createGame(data)
} else if (data.action === 'request_join') {
requestJoin(data)
} else if (data.action === 'accept_request') {
acceptRequest(data)
} else if (data.action === 'play') {
play(data)
}
} catch (e) {
// error might be on JSON.parse and wrong json format
return null
}
}
Let's create a function for each game mechanic.
createGame:
const createGame = (data) => {
if (!data || !data.id || !data.starting_player) {
return
}
// validating
if (
data.id.length !== 20 ||
(data.starting_player !== 'first' && data.starting_player !== 'second')
) {
return
}
// Add game to database
console.log('Create a game with id ' + data.id)
}
requestJoin:
const requestJoin = (data) => {
if (!data || !data.id || !data.id.length !== 20) {
return
}
// Check game id in database
// Join game
console.log('A player joined game id ' + data.id)
}
acceptRequest:
const acceptRequest = (data) => {
if (!data || !data.id || !data.player || !data.id.length !== 20) {
return
}
// Validate game in database
// Accept the join request
console.log('Accepted join request game id ' + data.id)
}
play:
const play = (data) => {
if (
!data ||
!data.id ||
!data.col ||
!data.row ||
!data.id.length !== 20 ||
data.col < 1 ||
data.col > 3 ||
data.row < 1 ||
data.row > 3
) {
return
}
// Validate game in database
// Validate the player round
// Play game
console.log('Played game id ' + data.id)
}
The above functions are not doing anything at the moment. We will complete those functions after setting up the database in the next part.
We can test our code by broadcasting custom_json operations. Let's see if the streaming method and processing data works.
We can run the app by node index.js
And here is the console.log() confirmation in our app:
Follow me so you don't miss the next part. The final code of this part is on GitLab.
Awesome, it’s great to see a series like this on Hive.
I have cross-posted this into the Game Development community. Hopefully, some others working on making games will take a look at this.
How about a nice game of chess?
This is very interesting and relevant to my interests
Hello again, trying to follow this but kinda stuck right now. I have opened 2 terminals, one I ran node api/server.js and the other node index.js. Not sure how to do this "We can test our code by broadcasting custom_json operations" Does this mean I need to write on the blockchain?
Also in streamBlocks, see below code snippet. It's like processing the latest block number, so is it possible when you broadcast the custom_operation, there's a chance of missing it if the block number it was included in is not the latest block ex. the block number increases too fast?
if (gdgp.result.head_block_number > lastBlock) {
lastBlock = gdgp.result.head_block_number
cb(lastBlock)
}
Sorry for the questions, really new to blockchain programming and JS.Lastly, my hive-tx config is node: 'https://api.hive.blog', should i change to a testnet?
TIA
Read up the docs on devportal and try out different API calls. I might write another startup guide targeted for newer devs. I think this was more for the devs familiar with the Hive blockchain overall and how it works.
The code reads off of the chain so yes.
The block number won't change fast. There is a minimum of 3s between each block produced and we pull the last block less than that. But, another extra check to make sure the increase is not more than 1 would be better.
A testnet would be ideal but using mainnet for small things wouldn't matter. Mainly because it doesn't involve financial transactions.