Making a Decentralized Game on Hive - Part 5

in Programming/Dev26 days ago

game-pixabay.jpg

Games are fun and most people like to play different kinds of games. I'm not sure about the game we are building. Whether it be fun or not, the purpose of this project is the tutorial. To have a step-by-step guide that developers can use as a reference in building apps on Hive.

Check out previous posts:


Building the transactions

We will build the back-end for the front-end that we built in the previous post. All goes into js/app.js.

const random = (length = 20) => {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
  let str = ''
  for (let i = 0; i < length; i++) {
    str += chars.charAt(Math.floor(Math.random() * chars.length))
  }
  return str
}

A simple function to generate a random string. We will use the random string for the game_id.


const createGame = async () => {
  const button = document.getElementById('create-game-btn')
  button.setAttribute('disabled', 'true')
  const errorOutput = document.getElementById('create-game-error')
  const successOutput = document.getElementById('create-game-success')
  errorOutput.innerHTML = ''
  successOutput.innerHTML = ''
  try {
    const game = {
      app: 'tictactoe/0.0.1',
      action: 'create_game',
      id: random(20),
      starting_player: document.getElementById('starting-player').value
    }
    const operations = [
      [
        'custom_json',
        {
          required_auths: [],
          required_posting_auths: [userData.username],
          id: 'tictactoe',
          json: JSON.stringify(game)
        }
      ]
    ]
    const tx = new hiveTx.Transaction()
    await tx.create(operations)
    const privateKey = hiveTx.PrivateKey.from(userData.key)
    tx.sign(privateKey)
    const result = await tx.broadcast()
    if (result && result.result && result.result.block_num) {
      successOutput.innerHTML =
        'Success! <a href="link to game">Click to see</a>'
    } else {
      errorOutput.innerHTML =
        'Error! Check console for details. Press Ctrl+Shift+J'
      console.error(result)
    }
  } catch (e) {
    errorOutput.innerHTML =
      'Error! Check console for details. Press Ctrl+Shift+J'
    console.error(e)
  }
  button.removeAttribute('disabled')
}

We create the transaction by using the hive-tx library then sign and broadcast it. We put the game link in the success message and show it to the user.

Now users can create the game and see the list of games. We create the game.html page for users to play the game.


Game page

We can add the game board, moves history, and the game stats like the winner, player1, and player2. I think we can make this page accessible by the game_id, like /game.html?id=game_id_here. Let's create the easier parts first.

game.html: We use the head from index.html and the same navbar code.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="A decentralized game on hive blockchain" />
  <title>Tic-Tac-Toe on Hive blockchain</title>
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous" />
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0"
    crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/hive-tx/dist/hive-tx.min.js"></script>
  <link rel="stylesheet" href="css/style.css" />
</head>

<body>
  <nav class="navbar navbar-expand navbar-dark bg-dark">
    <div class="container-fluid">
      <a class="navbar-brand" href="#">Tic-Tac-Toe</a>
      <ul class="navbar-nav">
        <li class="nav-item">
          <a class="nav-link" href="#" data-bs-toggle="modal" data-bs-target="#login-modal" id="login-button">
            Login
          </a>
        <li class="nav-item dropdown" id="logout-menu" style="display: none;">
          <a class="nav-link dropdown-toggle" href="#" id="username-button" role="button" data-bs-toggle="dropdown"
            aria-expanded="false"></a>
          <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="username-button">
            <li><a class="dropdown-item" href="#" onclick="logout()">Logout</a></li>
          </ul>
        </li>
        </li>
      </ul>
    </div>
  </nav>
  (html comment removed:  Login Modal )
  <div class="modal fade" id="login-modal" tabindex="-1" aria-labelledby="login-modal-title" aria-hidden="true">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="login-modal-title">Login</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          <form onsubmit="login(); return false">
            <div class="mb-3">
              <label for="username" class="form-label">Username:</label>
              <div class="input-group mb-3">
                <span class="input-group-text">@</span>
                <input type="text" class="form-control" placeholder="username" aria-label="username" id="username"
                  required>
              </div>
              <div class="form-text">Your Hive username. Lowercase.</div>
            </div>
            <div class="mb-3">
              <label for="posting-key" class="form-label">Posting key:</label>
              <input type="password" class="form-control" id="posting-key" placeholder="Private posting key" required>
              <div class="form-text">Your key will never leave your browser.</div>
            </div>
            <p id="login-error"></p>
            <button type="submit" class="btn btn-primary" id="login-form-btn">Login</button>
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
          </form>
        </div>
      </div>
    </div>
  </div>
  (html comment removed:  Modal end )

  <script src="js/app.js"></script>
</body>

</html>

Since we use app.js here too, we have to modify 2 lines in app.js:

// Run the script only in homepage
if (!window.location.pathname.match(/game.html$/)) {
  loadTheGames()
  setInterval(() => loadTheGames(), 5000)
}

In the above code, we can define which scripts to run on the homepage and which run on the game page.

We need an API for retrieving the game details by game_id. Let's set that up in the back-end.
api/game.js:

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

router.get('/game/:id', async (req, res) => {
  try {
    const id = req.params.id
    if (!id || id.length !== 20 || !id.match(/^[a-zA-Z0-9]+$/)) {
      return res.json({
        id: 0,
        error: 'Wrong id.'
      })
    }
    const game = await mysql.query(
      'SELECT `game_id`, `player1`, `player2`, `starting_player`, `status`, `winner` FROM `games`' +
        'WHERE `game_id`=?',
      [id]
    )
    if (!game || !Array.isArray(game) || game.length < 1) {
      return res.json({
        id: 1,
        game: []
      })
    }
    return res.json({
      id: 1,
      game
    })
  } catch (e) {
    return res.json({
      id: 0,
      error: 'Unexpected error.'
    })
  }
})

module.exports = router

The above code is similar to the other APIs we set up. Nothing new here. Now we can show the game details on the game.html page.

const getGameDetails = async (id) => {
  const data = await APICall('/game/' + id)
  if (data && data.id === 0) {
    document.getElementById('details-error').innerHTML = data.error
  } else if (data && data.id === 1) {
    const game = data.game[0]
    document.getElementById('game-details').innerHTML = `<tr>
    <td>${game.player1}</td>
    <td>${game.player2}</td>
    <td>${game.starting_player}</td>
    <td>${game.status}</td>
    <td>${game.winner}</td>
    </tr>`
    if (game.player1 === userData.username) {
      document.getElementById('req-message-1').style.display = 'block'
      document.getElementById('req-message-2').style.display = 'none'
    }
  }
}

And running the above function:

const queryString = window.location.search
const urlParams = new URLSearchParams(queryString)

// Run the script only in homepage
if (!window.location.pathname.match(/game.html$/)) {
  loadTheGames()
  setInterval(() => loadTheGames(), 5000)
} else {
  // Run the script only in game page
  if (urlParams.has('id')) {
    getGameDetails(urlParams.get('id'))
  }
}

We get the id from the page URL and use it in the API call. Then display the data in an HTML table.

<table class="table">
  <thead>
    <tr>
      <th>Player1</th>
      <th>Player2</th>
      <th>Starting player</th>
      <th>Status</th>
      <th>Winner</th>
    </tr>
  </thead>
  <tbody id="game-details">
  </tbody>
</table>

We can get the join requests from API and show them beside the game so player1 can accept one of the coming requests and start the game.
A simple HTML table:

<table class="table">
  <thead>
    <tr>
      <th>Player</th>
      <th>Status</th>
      <th>Action</th>
    </tr>
  </thead>
  <tbody id="request-list"></tbody>
</table>

And the API call:

const getRequests = async (id, creator = false) => {
  const data = await APICall('/requests/' + id)
  if (data && data.id === 0) {
    document.getElementById('requests-error').innerHTML = data.error
  } else if (data && data.id === 1) {
    let temp = ''
    for (let i = 0; i < data.requests.length; i++) {
      const request = data.requests[i]
      temp += `<tr>
        <td>${request.player}</td>
        <td>${request.status}</td>`
      if (creator) {
        // Add an Accept button if the visitor is player1 (creator)
        temp += `<td>
          <button class="btn btn-primary" onclick="acceptRequest(${id}, ${request.player})">
            Accept
          </button>
        </td>`
      } else {
        temp += '<td>---</td>'
      }
      temp += '</tr>'
    }
    if (data.requests.length < 1) {
      temp = 'None'
    }
    document.getElementById('request-list').innerHTML = temp
  }
}

We can call the getRequests function inside the getGameDetails function because we can know when the user (visitor) is the creator of the game aka player1. Then show them an Accept button based on that so the player1 can accept the request.

const getGameDetails = async (id) => {
  ... // skipped unchanged lines
    if (game.player1 === userData.username) {
      document.getElementById('req-message-1').style.display = 'block'
      document.getElementById('req-message-2').style.display = 'none'
      getRequests(id, true)
    } else {
      getRequests(id, false)
    }
  }
}

Also, let's make both functions run with an interval to auto-update the data.

// Run the script only in homepage
if (!window.location.pathname.match(/game.html$/)) {
  loadTheGames()
  setInterval(() => loadTheGames(), 5000)
} else {
  // Run the script only in game page
  if (urlParams.has('id')) {
    getGameDetails(urlParams.get('id'))
    setInterval(() => getGameDetails(urlParams.get('id')), 5000)
  }
}

We added the accept button so let's add its function and transaction too.

const acceptRequest = async (id, player) => {
  const success = document.getElementById('requests-success')
  const error = document.getElementById('requests-error')
  if (!userData.username) {
    return
  }
  try {
    const accept = {
      app: 'tictactoe/0.0.1',
      action: 'accept_request',
      id,
      player
    }
    const operations = [
      [
        'custom_json',
        {
          required_auths: [],
          required_posting_auths: [userData.username],
          id: 'tictactoe',
          json: JSON.stringify(accept)
        }
      ]
    ]
    const tx = new hiveTx.Transaction()
    await tx.create(operations)
    const privateKey = hiveTx.PrivateKey.from(userData.key)
    tx.sign(privateKey)
    const result = await tx.broadcast()
    if (result && result.result && result.result.block_num) {
      success.innerHTML = 'Success! Game started.'
    } else {
      error.innerHTML = 'Error! Check console for details. Press Ctrl+Shift+J'
      console.error(result)
    }
  } catch (e) {
    error.innerHTML = 'Error! Check console for details. Press Ctrl+Shift+J'
    console.error(e)
  }
}

Now let's add a button for other users to join the game.

<button id="join-btn" class="btn btn-primary" onclick="joinGame()">Join the game</button>

And build the transaction for it:

const joinGame = async (gameId) => {
  const success = document.getElementById('join-success')
  const error = document.getElementById('join-error')
  if (!urlParams.has('id')) {
    return
  }
  const id = urlParams.get('id')
  try {
    const joinReq = {
      app: 'tictactoe/0.0.1',
      action: 'request_join',
      id
    }
    const operations = [
      [
        'custom_json',
        {
          required_auths: [],
          required_posting_auths: [userData.username],
          id: 'tictactoe',
          json: JSON.stringify(joinReq)
        }
      ]
    ]
    const tx = new hiveTx.Transaction()
    await tx.create(operations)
    const privateKey = hiveTx.PrivateKey.from(userData.key)
    tx.sign(privateKey)
    const result = await tx.broadcast()
    if (result && result.result && result.result.block_num) {
      success.innerHTML = 'Success! Your request submitted.'
    } else {
      error.innerHTML = 'Error! Check console for details. Press Ctrl+Shift+J'
      console.error(result)
    }
  } catch (e) {
    error.innerHTML = 'Error! Check console for details. Press Ctrl+Shift+J'
    console.error(e)
  }
}

Next part

We finally finished most of the functions needed for starting the game. I think the only remaining challenge is the gameplay. We can use Canvas for the front-end graphical gameplay. I already built a project with Canvas but there is nothing easy about coding. It's still a challenge.

Let me list the remaining tasks:

  • Gameplay front-end
  • Gameplay back-end
  • resync method for the database
  • Front-end polishing

We are getting to the end of this project and I think we can finish it in the next post and finally play this boring game at least once.

Making tutorials and coding at the same time is really hard and tiring. I might continue writing this kind of series for different projects if there is enough interest but the target audience is too small. So let me know in the comments what you think.

I wouldn't mind building an interesting app or game which can be some kind of tutorial too.

TBH after writing the above line I wanted to remove it. Let's see what happens.

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

Enjoy the cat.

cat-pixabay.jpg

Images source: pixabay.com


GitLab
Part 1
Part 2
Part 3
Part 4


Vote for my witness:

Sort:  

Amazing sharing dear @mahdiyari
Thank you for your efforts, I'm sure it will inspire and influence those who carry coding, learning and gaming flame in their hearts. Of course count me in 💜

Namaskar to Hive and beyond 🤍

You do great jobs, thanks for sharing. I think 🤔 if you code a 📱 mobile game will be welcome to Hive Blockchain.
My be a Game Core!

Hiii Mahdiyari do you remember me?? i was alejandra23 in steemit... The owner of pitchperfect contest :OOO HIIIIIIIIIIIIII ♥♥♥♥

Hi!
How are you?
Did you lose the other account? @alejandra23

Yes :( i lost it :(( i wanna do again pitch perfect please if you wanna make a team again please please let me now ♥♥♥♥

I delegated you some HP so you can post and comment more. Sure, I can offer my upvote and some HIVE.

You are so kind, I always counted on you, let me organize the contest again, so that it is beneficial for the hive community and encourage more people