Making a Decentralized Game on Hive - Part 2

code-944499_1280.jpg

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.

tic-tac-toe-col-row.jpg


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
https://hiveblocks.com/tx/44799e6a27c64e935f9072ecb576602330cb80b8

image.png

And here is the console.log() confirmation in our app:
image.png


Follow me so you don't miss the next part. The final code of this part is on GitLab.

GitLab Repository
Website

Part 1

Next part >>

H2
H3
H4
3 columns
2 columns
1 column
5 Comments