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.
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"}'
This time we'll do the login/logout right inside the canvas element without any buttons/event listeners.
Here we start with very similar set up to tutorial 2, we just have a few notable differences:
viewMethods: ['getScore'], changeMethods: ['addToScore', 'subFromScore', 'resetScore']
<!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>
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 #########
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.
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
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;
});
}
}
//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");
}
Next we will make a leaderboard!
NEAR tutorial 4