NEAR Tutorial, Part 3

Keeping score.

The Counter Example

There's already an example implementation keeping score on NEAR right here: Counter Example

Obviously there may be other ways to implement this, but since it's an example on near.org and I'm new to this myself, it makes sense to start there.

It's good to open the example up in gitpod and poke around a bit, you can hover over the code and definitions will pop up helping you understand. Like in the previous tutorial, you're going to want to open up the assembly folder and look at main.ts

First we have a function called incrementCounter, I'm going to change some details for clarity:


            export function addToScore(value: i32): void {
                const newScore = storage.getPrimitive("score", 0) + value;
                storage.set("score", newScore);
              }
        

In this function we're adding a number 'value' to the score, we do this by using storage class function called getPrimitive that gets a value stored under a key. Our key in this case is 'score' and the 0 is a default value if it can't find anything for that key. Then we add whatever 'value' was passed in to the existing score. Then we take newScore and use storage.set to store our newScore under the key "score."

If you look at incrementCounter in the counter example, there's also a logging step, but since that's fluff, I didn't include it here.

deIncrementCounter is exactly the same, except instead of adding the value you subtract.

Two more functions here, getScore and resetScore, so simple:


            export function getScore(): i32 {
                return storage.getPrimitive("score", 0);
              }
              
              export function resetScore(): void {
                storage.set("score", 0);
              }
        

See, exactly the same functions storage.getPrimitive and storage.set, same as you used in addToScore. At this point all you need is to deploy the contract and hook it up to your HTML5 game. This time why don't we actually deploy instead of dev deploy?

So let's add that code to our previous project myFirstApp in the index.ts file, we don't need to add any imports, because storage is already being imported.


            export function addToScore(value: i32): void {
                const newScore = storage.getPrimitive("score", 0) + value;
                storage.set("score", newScore);
              }
              export function subFromScore(value: i32): void {
                const newScore = storage.getPrimitive("score", 0) - value;
                storage.set("score", newScore);
              }
              export function getScore(): i32 {
                return storage.getPrimitive("score", 0);
              }
              export function resetScore(): void {
                storage.set("score", 0);
              }
        

Go ahead and delete the previous dev-random number account, if you want to keep the last example working, you can go back and add the contract name you deploy to so that it keeps working.

near delete devTODELETE YOURACCOUNT.testnet 

Then we make a special sub account to deploy to:

near create-account sub.yourAccount.testnet --masterAccount yourAccount.testnet

Pick whatever sub name you want, for example tutorial.yourAccount.testnet would be fine.

Make sure you save your changes and then build the contract:

yarn build:contract

You can find that yarn script and others in the package.json file under scripts.

Once it's built you can deploy! There's more than one way to do this, so let's discuss this. The first way is described in the readme, I modified step 3 to just deploy the contract and not the front end:


            Step 2: set contract name in code
            ---------------------------------
            
            Modify the line in `src/config.js` that sets the account name of the contract. 
            Set it to the account id you used above.
            
            const CONTRACT_NAME = process.env.CONTRACT_NAME || 'myFirstApp.YOUR-NAME.testnet'

            Step 3: deploy!
            ---------------

            One commands:
                yarn deploy:contract

        

Of course that requires you to change the CONTRACT_NAME in the config.js file, if you want to deploy with just the wasm file, that you now have after the build command:

near deploy --accountId sub.yourAccount.testnet --wasmFile out/main.wasm

In this case the wasmFile is in out/main.wasm, but depending on how things are set up it may be elsewhere.

Testing from command line

First we can call the view method getScore, it should tell us the score is 0. To make things easier export your sub account ID first, so that you can type less. If it doesn't work check if you made a typo in your name, common mistake.


            export ID= sub.yourAccount.testnet
            near view $ID getScore
        

Five commands here: let's add 10 to the score, subtract 5 from the score, and then call view again to make sure it's at 5. Then we'll reset the score and call view one last time to make sure it's back to 0.


            near call $ID addToScore '{"value":10}' --accountId $ID
            near call $ID subFromScore '{"value":5}' --accountId $ID
            near view $ID getScore
            near call $ID resetScore --accountId $ID
            near view $ID getScore
        

Refresher here, when you call a method that changes the state it's a call method and you need the accountId of the caller, but it's not necessary when it's just a view method.

If you're clever right now is when you realize that this method stores one score accross all users, so if I make a game out of this I'll just end up with everyone points getting squished together! Oh no! *Dramatic pause*

All it takes is a tiny change to make this work for individual scores... you need a different key for each user! Look at how the greetings were being saved in the code from before:

const accountId = Context.sender

So now let's apply that change to our functions:


            export function addToScore(value: i32): void {
                const key = Context.sender+"score";
                const newScore = storage.getPrimitive(key, 0) + value;
                storage.set(key, newScore);
              }
              export function subFromScore(value: i32): void {
                const key = Context.sender+"score";
                const newScore = storage.getPrimitive(key, 0) - value;
                storage.set(key, newScore);
              }
              export function getScore(accountId: string): i32 {
                return storage.getPrimitive(accountId+"score", 0);
              }
              export function resetScore(): void {
                const key = Context.sender+"score";
                storage.set(key, 0);
              }
        

Go ahead rebuild and redeploy it:


        yarn build:contract
        near deploy --accountId sub.yourAccount.testnet --wasmFile out/main.wasm
        

Test everything again:


            near call $ID addToScore '{"value":10}' --accountId $ID
            near call $ID subFromScore '{"value":5}' --accountId $ID
            near view $ID getScore '{"accountId": "yourAccount.testnet"}'
            near call $ID resetScore --accountId $ID
            near view $ID getScore '{"accountId": "yourAccount.testnet"}'
        

MiniGame

This time we'll do the login/logout right inside the canvas element without any buttons/event listeners.

Starter code

Here we start with very similar set up to tutorial 2, we just have a few notable differences:


          <!doctype html>
            <html>
                    <head><meta charset="utf-8" />
                        <!-- The near api -->
                        <script src="https://cdn.jsdelivr.net/npm/near-api-js/dist/near-api-js.min.js"></script>
                        </script>
                        <!-- Styles, optionally you can have a styles.css file for these. -->
                        <style>
                        /* These are my styles you can change to suit youself */
                          body{
                            text-align: center;
                          }
                          canvas{
                            /*If you don't like pink, go ahead and change it.*/
                            background: #ff7675; 
                            width:300px;
                            height:300px;
                          }
                        </style>
                  </head> 
            <body style="margin:0;overflow: hidden;">
            <canvas id=canvas>   
            </canvas>
            </body>
            </html>
            
            <!-- Our JavaScript code goes here, if you prefer you can put it in a scripts.js file -->
            <script>
                var CONTRACT_NAME='sub.yourAccount.testnet';

                async function connect() {
        // Connect to nearApi
        let near = await nearApi.connect({
        nodeUrl: "https://rpc.testnet.near.org",
        walletUrl: "https://wallet.testnet.near.org",
        helperUrl: "https://helper.testnet.near.org",
        explorerUrl: "https://explorer.testnet.near.org",
        networkId: 'testnet', // We are using testnet
        keyStore: new window.nearApi.keyStores.BrowserLocalStorageKeyStore(window.localStorage, 'Example App')
        });

        // Connect to user's wallet
        window.walletConnection = new nearApi.WalletConnection(near);
        let account;
        if (window.walletConnection.isSignedIn()) {
            // Logged in account, can write as user signed up through wallet
            account = walletConnection.account();
            // connect to a NEAR smart contract
            window.contract = new nearApi.Contract(account, CONTRACT_NAME, {
            viewMethods: ['getScore'],
            changeMethods: ['addToScore', 'subFromScore', 'resetScore']
            });
        } else {
            // Contract account, normally only gonna work in read only mode
            account = new nearApi.Account(near.connection, CONTRACT_NAME);
        }
    }

    nearApi.nearInitPromise = connect()
        .then(checkStatus)
        .catch(console.error);
</script>
        

Some async code

So let's makes some javascript async functions for each contract function, and a score variable.

    
          var score=0;
          // ###### Async Code #########
          // async function to call contract view function getScore() and update score
          async function viewScore(){
          // call the contract function
          let result = await window.contract.getScore({
              accountId: window.walletConnection.getAccountId() }
              );
              return result;
          }
      
          // async function to call contract change function addToScore()
          async function addToScore(n){
          // call the contract function
          let result = await window.contract.addToScore({
              value: n}
              );
              return;
          }
      
          // async function to call contract change function subFromScore()
          async function subFromScore(n){
          // call the contract function
          let result = await window.contract.subFromScore({
              value: n}
              );
              return;
          }
          
          // async function to call contract change function resetScore()
          async function resetScore(){
          // call the contract function
          let result = await window.contract.resetScore({});
              return result;
          }
          // ###### END Async Code #########

Test it

Let's make the function checkStatus that is being called after connect, and for now we'll fill it with some testing code. When you load this page, first you'll be required to login, then because of the async functions 0 will be printed first then 10, and if you refresh you'll get 0, then 20.


            function  checkStatus(){
              // Check to see if signed in:
              if (!window.walletConnection.getAccountId()) {
                  window.walletConnection.requestSignIn(CONTRACT_NAME, 'Example App');
              } else {
                  addToScore(10).then(viewScore).then(
                          function(value) {
                                  score = value;
                                  console.log(score);
                              });
                  console.log(score);
              }
          }
          

You could say we're keeping score of how many times you refresh the page, but that's not really a game, and it only uses one method.

Make a game!

Now you should do your own game here, but I've got a little snowman, so I'm going to make a click on the snowflakes game, you get 10 points when you click on 50 snowflakes and you lose 5 points if you miss 50 snowflakes. We want to avoid sending to many change calls, so we're not going to track every snowflake.

I'm not going to include all the code here but this is what it looks like: Snow Flakes and here's a link to the code on github

Things to remember

  1. Change calls shouldn't be overused.
  2. The NEAR part isn't that many lines of code.
  3. Don't put the contact call in a place where it will get called repeatedly.

    For example when I call addToScore(10) I do it inside an if condition that checks if my counter is >50, then resets the counters right away. If I did not reset the clicked counter every loop after it got to 50 it would keep calling every loop. Alternatively I could have used if(clicked%50 == 0).

    
              if(clicked>50){
                clicked = 0;
                missed = 0;
                if(window.walletConnection.getAccountId()){
                    addToScore(10).then(viewScore).then(
                    function(value) {
                            score = value;
                        });
                    }
              }
            
  4. Login and logout are as simple as an onlick event, draw a button or even just text
    
                //In onclick event
                // Click on sign-in/ sign-out
                if( x<c.w*.3 && y>c.h*.95){
                    if(window.walletConnection.getAccountId()){
                        window.walletConnection.signOut();
                    }else{
                        window.walletConnection.requestSignIn(CONTRACT_NAME, 'SnowFlake APP');
                    }
                }
              
              // Inside the animation loop
              if(window.walletConnection.getAccountId()){
                print('Sign Out!', c.w*.1, c.h*.99, 1, "#000");
              }else{
                print('Sign In!', c.w*.1, c.h*.99, 1, "#000");
              }
              
  5. Let me know what game you make, I'm on twitter: @vertfromageio

Next tutorial

Next we will make a leaderboard!

NEAR tutorial 4