mahdiyari cross-posted this post in HiveDevs 3 years ago


Making a Decentralized Game on Hive - Part 4

in Programming & Dev3 years ago (edited)

coding-pixabay.jpg

In the previous part, we made a back-end that is streaming blocks and detects 3 methods in custom_json. Create a game, request to join a game, and accept the request. We also set up a MySQL server with 3 tables for the methods.

You can find the links to the other parts at the end of the post.

API

Let's start by implementing the API for retrieving the games list. Our API is public so it doesn't require user authentication. The API will just return the data synced from the blockchain.
api/games.js:

const mysql = require('../helpers/mysql')
const express = require('express')
const router = express.Router()

// Return all games on route /games
router.get('/games', async (req, res) => {
  try {
    // Get games data from Database
    const games = await mysql.query(
      'SELECT `game_id`, `player1`, `player2`, `starting_player`, `status`, `winner` FROM `games`'
    )
    // Check for expected result
    if (!games || !Array.isArray(games) || games.length < 1) {
      // WE return id: 1 for success and id: 0 for errors
      return res.json({
        id: 1,
        games: []
      })
    }
    // We return `games` as we receive from MySQL but it's not a good practice
    // specially when you have critical data in the database
    // You can return data one by one like `games: [{game_id: games.game_id, ...}]`
    return res.json({
      id: 1,
      games
    })
  } catch (e) {
    // Return error for unexpected errors like connection drops
    res.json({
      id: 0,
      error: 'Unexpected error.'
    })
  }
})

module.exports = router

The comments are in the code itself for better understanding.
Note: We use try{} catch{} wherever we can. It is always good to handle errors.

We can test our API at this point to detect possible errors in the code.
Include the following code in api/server.js just above the port and host constants.

const games = require('./games')
app.use(games)

Run node api/server.js. We can see the console.log: Application started on 127.0.0.1:2021
Let's open 127.0.0.1:2021/games in the browser.

image.png

The API works as expected.

But it's not done yet. This API returns ALL the games without a specific order. We should implement pagination and define an order for our list.

Updated code api/games.js:

const mysql = require('../helpers/mysql')
const express = require('express')
const router = express.Router()

router.get('/games/:page', async (req, res) => {
  try {
    if (isNaN(req.params.page)) {
      res.json({
        id: 0,
        error: 'Expected number.'
      })
    }
    const page = Math.floor(req.params.page)
    if (page < 1) {
      res.json({
        id: 0,
        error: 'Expected > 0'
      })
    }
    const offset = (page - 1) * 10
    const games = await mysql.query(
      'SELECT `game_id`, `player1`, `player2`, `starting_player`, `status`, `winner` FROM `games`' +
        ' ORDER BY `id` DESC LIMIT 10 OFFSET ?',
      [offset]
    )
    if (!games || !Array.isArray(games) || games.length < 1) {
      return res.json({
        id: 1,
        games: []
      })
    }
    return res.json({
      id: 1,
      games
    })
  } catch (e) {
    res.json({
      id: 0,
      error: 'Unexpected error.'
    })
  }
})

module.exports = router

We used id to order our list and get the newly created games first. Each page returns up to 10 games. We can try 127.0.0.1:2021/games/1 for testing.


Let's set another API for requests. The code is almost similar but we return only requests for the specific game_id.
api/requests.js:

const mysql = require('../helpers/mysql')
const express = require('express')
const router = express.Router()

router.get('/requests/:id', async (req, res) => {
  try {
    if (!req.params.id) {
      res.json({
        id: 0,
        error: 'Expected game_id.'
      })
    }
    // We are passing user input into the database
    // You should be careful in such cases
    // We use ? for parameters which escapes the characters
    const requests = await mysql.query(
      'SELECT `player`, `status` FROM `requests` WHERE `game_id`= ?',
      [req.params.id]
    )
    if (!requests || !Array.isArray(requests) || requests.length < 1) {
      return res.json({
        id: 1,
        requests: []
      })
    }
    return res.json({
      id: 1,
      requests
    })
  } catch (e) {
    res.json({
      id: 0,
      error: 'Unexpected error.'
    })
  }
})

module.exports = router

Note: :id in the above router represents a variable named id. So for example http://127.0.0.1:2021/requests/mygameidhere in this request, the id variable is mygameidhere which is accessible by req.params.id.


A similar code for the moves table. There wasn't a better name in my mind for this table.
api/moves.js:

const mysql = require('../helpers/mysql')
const express = require('express')
const router = express.Router()

router.get('/moves/:id', async (req, res) => {
  try {
    if (!req.params.id) {
      res.json({
        id: 0,
        error: 'Expected game_id.'
      })
    }
    const moves = await mysql.query(
      'SELECT `player`, `col`, `row` FROM `moves` WHERE `game_id`= ?',
      [req.params.id]
    )
    if (!moves || !Array.isArray(moves) || moves.length < 1) {
      return res.json({
        id: 1,
        moves: []
      })
    }
    return res.json({
      id: 1,
      moves
    })
  } catch (e) {
    res.json({
      id: 0,
      error: 'Unexpected error.'
    })
  }
})

module.exports = router

Now our 3 APIs are ready to be implemented on the front-end.


Here is the updated api/server.js after including the APIs:

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()
})

// APIs
const games = require('./games')
const requests = require('./requests')
const moves = require('./moves')

app.use(games)
app.use(requests)
app.use(moves)

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}`)
})

Front-end

I think using pure HTML is a mistake and I would prefer something like Angular for the web applications but that comes with its own learning process which can make this tutorial complex. So my recommendation is to learn something like Angular or Vue and live a happy life. Anyway, coding time.

I'm not going to drop index.html here. It doesn't need much explanation and it's long. You can see it on the GitLab repository. I will just add some references here used in app.js.

The table for listing the games and buttons for pagination.
index.html:

<div class="card-body">
  <h5 class="card-title" style="float: left;">Games list</h5>
  <span style="float: right;">Auto updating every 5s</span>
  <table class="table table-striped">
    <thead>
      <tr>
        <th>#</th>
        <th>Game ID</th>
        <th>Player 1</th>
        <th>Player 2</th>
        <th>Starting Player</th>
        <th>Status</th>
        <th>Winner</th>
        <th>Action</th>
      </tr>
    </thead>
    <tbody id="games-table-body">
    </tbody>
  </table>
  <nav aria-label="Page navigation example">
    <ul class="pagination justify-content-center">
      <li class="page-item disabled" id="prev-btn">
        <a class="page-link" onclick="prevGamesPage()">&laquo;</a>
      </li>
      <li class="page-item disabled">
        <a class="page-link" id="page-number" tabindex="-1"> 1 </a>
      </li>
      <li class="page-item" id="next-btn">
        <a class="page-link" onclick="nextGamesPage()">&raquo;</a>
      </li>
    </ul>
  </nav>
</div>

We have to fill the table above. So let's implement some basic functions.
js/app.js:

const baseAPI = 'http://127.0.0.1:2021'
const APICall = async (api) => {
  return (await fetch(baseAPI + api)).json()
}

For ease of use, we define a function for GET calls using fetch and a variable for our API address.


const getGames = async (page = 1) => {
  const games = await APICall('/games/' + page)
  return games.games
}

This function basically gets the games from the API per page.


const fillGamesTable = (data) => {
  const tbody = document.getElementById('games-table-body')
  let temp = ''
  for (let i = 0; i < data.length; i++) {
    temp += `<tr>
    <td>${(gamesPage - 1) * 10 + i + 1}</td>
    <td>${data[i].game_id}</td>
    <td>${data[i].player1}</td>
    <td>${data[i].player2}</td>
    <td>${data[i].starting_player}</td>
    <td>${data[i].status}</td>
    <td>${data[i].winner}</td>
    <td></td>
    </tr>`
  }
  if (data.length < 1) {
    temp = 'No games.'
  }
  tbody.innerHTML = temp
}

fillGamesTable takes the result from getGames function and fills the HTML table with data using a for loop.


let gamesPage = 1
const loadTheGames = async () => {
  const games = await getGames(gamesPage)
  fillGamesTable(games)
  if (games.length < 10) {
    document.getElementById('next-btn').className = 'page-item disabled'
  } else {
    document.getElementById('next-btn').className = 'page-item'
  }
  if (gamesPage === 1) {
    document.getElementById('prev-btn').className = 'page-item disabled'
  } else {
    document.getElementById('prev-btn').className = 'page-item'
  }
  document.getElementById('page-number').innerHTML = ` ${gamesPage} `
}
loadTheGames()
setInterval(() => loadTheGames(), 5000)

With this function, we call the two previously defined functions to do their job and update the pagination buttons and the page number every time we update the table data. Also, every 5 seconds, it gets new data from API and updates the table with new data so users don't have to reload the page for new data.


const nextGamesPage = () => {
  gamesPage++
  loadTheGames()
}

const prevGamesPage = () => {
  gamesPage--
  loadTheGames()
}

And two functions for changing pages. Simple as that.


The final result with dummy data looks like this on the browser:

Capture.PNG


That's it for this part. I'm really happy with the results we are getting. I didn't plan anything beforehand and I'm coding as I'm writing the posts.

Today we made 3 API calls and created the basic front-end which for now only shows the list of games that are submitted to the Hive blockchain. In the next part, we will implement the methods for creating and joining the games on the client side. We also probably need to create a game page where actual gaming happens.

Thanks for reading. Make sure to follow me and share the post. Upvote if you like and leave a comment.


GitLab
Part 1
Part 2
Part 3

Next part >>


Vote for my witness: