Making a Decentralized Game on Hive - Tic Tac Toe - Part 1

in HiveDevs3 years ago (edited)

tic-tac-toe.png

We will develop a decentralized game by the end of this series. The goal is to make learning development on Hive blockchain easier by using a simple game as the training project.

Target audience

Of course, it's not for everybody. I will try to keep it simple as possible for people with less experience with development. But some degree of understanding code is necessary to learn something. I will explain anything related to the Hive blockchain that we use.

You can find the links for the final result of this part at the end of this post.

Why "tic-tac-toe"?

It's a simple multiplayer game and it covers most of the topics needed for a bigger decentralized game or application. Most people are already familiar with this game and it's easier to code and its rules are simple.

Development

We will use Javascript for the game because it's simple enough so most people understand easily. The front-end will be pure HTML. Also, I think MySQL is a good fit as the database. The game needs a database to keep track of games and players. MySQL docker can be set up in a few minutes.

The decentralized game will work without depending on one central software. The game will talk only to the blockchain and there is no central database. It doesn't need a private entity to hold players' data. Anyone can run an interface for the game.

(We have a database but it's not a central private database and it can be synced through blockchain. Anyone should be able to run an instance of the game and the game will get the same exact database by reading data from the blockchain. It's like hivemind, the database that holds and serves most of the data on Hive.)

Tools I'm using

  • Visual Studio Code
  • Nodejs
  • MySQL docker setup
  • Chrome browser

I didn't plan anything beforehand so I don't know how many posts it will take. I will list the things that come to my mind right now.

Planning

  • Front-end
    • Make a login method on the client-side
    • Display available games list
    • Create/Request to join a game
    • Design the game visuals and controls
  • Back-end
    • Stream the blockchain and listen for custom_json operations
    • Define custom_json operations
    • Game mechanics
    • API to communicate with front-end
    • Replay/resync method to update the database on newly deployed game clients

The above list may or may not change. Anyway, let's start with the front-end and add a login method.


Part 1: Front-end - Login method

I think the HTML part doesn't need any explanation. Page title, description, bootstrap navbar, and a login link. Added css/style.css and js/app.js too. When the user clicks on the "login" link, a modal with the login form will appear. It will then fire login() function on submit.

index.html:

<!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>
  <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>
  <script src="js/app.js"></script>
</body>

</html>

Styles

css/style.css:

.navbar-nav {
  margin-right: 30px;
}
.navbar-brand {
  margin-left: 30px;
}
#login-error {
  color: #e31337;
  display: none;
}

Javascript

js/app.js
An object for user data.

const userData = {
  authorized: false,
  username: '',
  key: ''
}

Let's define the login function. It will verify the posting key and username then keep the data in localStorage. We use the hive-tx library for converting keys.

const login = async () => {
  const loginModal = bootstrap.Modal.getInstance(
    document.getElementById('login-modal')
  )
  const loginButtonForm = document.getElementById('login-form-btn')
  loginButtonForm.setAttribute('disabled', 'true')
  const loginError = document.getElementById('login-error')
  loginError.style.display = 'none'
  const username = document.getElementById('username').value
  const key = document.getElementById('posting-key').value
  const validate = await validatePostingKey(username, key)
  if (validate.result === 0) {
    loginError.innerHTML = validate.error
    loginError.style.display = 'block'
    loginButtonForm.removeAttribute('disabled')
    return
  }
  userData.authorized = true
  userData.username = username
  userData.key = key
  window.localStorage.setItem('userData', JSON.stringify(userData))
  loginButtonForm.removeAttribute('disabled')
  updateState()
  loginModal.hide()
}

As you can see there are 2 more functions used inside the login function. The first one is validatePostingKey(). It's a post for itself.
Let me explain it. First, we make a call to the Hive RPC API node by using hive-tx library and get the account information including the public posting key. Then we convert the user's private posting key to the public key and compare the two values. If the two values are equal then the user-provided private key is correct and we can authorize the user. There are also other ways to achieve the same result like signing a message with the private key and validating that signature with the public key.

const validatePostingKey = async (username, privateKey) => {
  const accounts = await hiveTx.call('condenser_api.get_accounts', [[username]])
  if (
    !accounts ||
    !accounts.result ||
    !Array.isArray(accounts.result) ||
    accounts.result.length < 1
  ) {
    return { result: 0, error: 'Network error or wrong username' }
  }
  try {
    const account = accounts.result[0]
    const publicWif = account.posting.key_auths[0][0] || ''
    const generatedPublicKey = hiveTx.PrivateKey.from(privateKey)
      .createPublic()
      .toString()

    if (generatedPublicKey !== publicWif) {
      return { result: 0, error: 'Wrong key' }
    }
    return { result: 1 }
  } catch (e) {
    return { result: 0, error: 'Wrong key or network error' }
  }
}

updateState() is used to update the HTML interface after user login and logout.

const updateState = () => {
  const loginButton = document.getElementById('login-button')
  const logoutMenu = document.getElementById('logout-menu')
  const usernameButton = document.getElementById('username-button')
  if (userData.authorized && userData.username && userData.key) {
    loginButton.style.display = 'none'
    logoutMenu.style.display = 'block'
    usernameButton.innerHTML = '@' + userData.username
  } else {
    loginButton.style.display = 'block'
    logoutMenu.style.display = 'none'
  }
}

And it's time for the logout function.

const logout = () => {
  userData.authorized = false
  userData.username = ''
  userData.key = ''
  window.localStorage.removeItem('userData')
  updateState()
}

We need to check localStorage on the page reload and log in the user if the key is in the localStorage.

const checkState = () => {
  const localData = window.localStorage.getItem('userData')
  let data
  if (!localData) {
    return
  }
  try {
    data = JSON.parse(localData)
  } catch (e) {
    data = userData
  }
  if (data.authorized && data.username && data.key) {
    userData.authorized = true
    userData.username = data.username
    userData.key = data.key
    updateState()
  }
}
checkState()

Now we have a working login and logout system. It keeps user data in localStorage which stays on the browser only.
What we have done so far is just the front-end. Our game needs a back-end server to provide the game data. We will stream blocks on the back-end and process game data then serve it through API. Our front-end will broadcast transactions which will update the back-end. In other words, the back-end is only serving the data it receives through the blockchain. We could stream the blocks on the client-side (browser) but it's not efficient and it is just unnecessary bandwidth waste.


You can see the running app on https://tic-tac-toe.mahdiyari.info/
The final code is on GitLab https://gitlab.com/mahdiyari/decentralized-game-on-hive

In the next part, we will set up our back-end server and database. Now that I think about it, MySQL might be an overkill for this project. Anyway, let's stick to it for now.

I want to get feedback from the community before continuing further. Help me with your comments and let me know what you think about this project. How can I improve it? Should I explain everything from the basics? I greatly appreciate your feedback.

Thanks for reading.

Next part >>

Sort:  

Hi,
Trying to figure out this part of the code, what is the meaning of "[[username]]"? Won't username suffice?

const accounts = await hiveTx.call('condenser_api.get_accounts', [[username]])

Sorry not a JS dev :)

 2 years ago  

That's not related to JS. The Hive API node wants a call in that format. Check APIs on devportal
Parameters are sent in an array. Here, we have to put another array inside that array because API accepts a list of accounts not a single account.

The above code is equivalent to this curl:

curl -s --data '{"jsonrpc":"2.0", "method":"condenser_api.get_accounts", "params":[["username"]], "id":1}' https://api.hive.blog

Thanks!

Does this allow players to used Hive-Keychain to login?

If not could you focus on how that works and how the data is passed after the login.

 3 years ago  

It's simple addition and can be added later.

ok.
Looking forward to the next few posts.

Looking forward to it

@mahdiyari : I did tried to get initiated with such a project(although unequipped with coding). After my thesis submission I had few spare time last year. But unfortunately I got busy with doing science again and I stopped that project in between. Thank you for posting this on twitter and it made me to come back to HIVE blockchain. This is a great initiative in fact. Please keep writing about it from basics. I am following it. <3

 3 years ago  

Thanks for following me through this. I will do my best.

thank you :)

Very interesting post @mahdiyari My son who is a gamer is very interested

Shared in discord