#sidechain

By Gauthier Sebille

In our previous blogpost, we introduced the ground of a blockchain app with Typescript & Deku.

For the next 30 minutes you will create the state machine for your own cookie-clicker game!

Each player will have a cookieBaker record type, which stores every counters:

  • number of cookies
  • number of cursors
  • number of grandmas

There are three possible actions:

       - mint a cookie => simply add one cookie in the current amount of cookies
       - buy a cursor => if enough cookies to buy one, add one cursor to the current amount of cursors. Every second, one cursor will mint one cookie for the user
       - buy a grandma => if enough cookies to buy one, add one grandma to the current amount of grandmas. Every second, one grandma will mint three cookies for the user.

The passive cookie minting from Cursors and Grandmas will be implemented in the third and last part of this tutorial, in which we will create a frontend application interacting with Deku-P HTTP APIs to perform actions on our state machine.

Define our actions

We will use a reducer pattern to dispatch the several actions. In this tutorial, we will only implement three of them:

  • Increment amount of cookies
  • Increment amount of Cursors
  • Increment amount of Grandmas

Let’s create an enum holding all the possible actions:


export enum actions {
    cookie = "cookie",
    cursor = "cursor",
    grandma = "grandma"
}

Define our state

The state, which will be saved using the set("myState", nextValue); (operation explained in the following section), will look like:


export type cookieBaker = {
    cookies: number,
    cursors: number,
    grandmas: number
}

Create a basic saveState function

The saveState function is just a syntactic sugar to avoid duplicated code lines, because we will save the state after each action.

Moreover, in this function, we will add some console.log to ensure everything is going well:


const saveState = (source: transaction, cookieBaker: cookieBaker) => {
    const cookieBakerJSON = JSON.stringify(cookieBaker);
    set(source, cookieBakerJSON);
    console.log("Successfully saved cookieBaker: " + cookieBakerJSON);
}

Our new transition function

First step for our new transition function is to update the state regarding the provided action

If you remember from the previous tutorial, we will need to get:

       - source: the public address of the user
       - operation
: the provided payload
       - state
: the current state for this source

Everything is explained in the previous part, but here is how our transition function will look like:


const transition = (tx: transaction) => {
    const source = tx.source;
    const operation = tx.operation;
    const sourceValue = JSON.parse(get(source));
    let cookieBaker: cookieBaker;
    // if there is not value for this public address, we must create a new one
    if (sourceValue === undefined || sourceValue === null) {
        cookieBaker = createEmptyCookieBaker();
        // The previous line, simply return a cookieBaker with all counters set to 0
    }
    // if there is already one, use it
    else {
        cookieBaker = JSON.parse(sourceValue);
    }
    // We will perform other actions here

}

Adding a cookie

Everything is ready, we can perform the correct business action from the “‘cookie’” payload provided by the user, basically, this will just add 1 to the current amount of cookies:


export const addCookie = (cookieBaker: st.cookieBaker): st.cookieBaker => {
    cookieBaker.cookies = cookieBaker.cookies + 1;
    console.log("Successfully added cookie: " + cookieBaker.cookies);
    return cookieBaker;
}

Simply call this function when user provided it, by adding a simple switch on the operation:


const transition = (tx: transaction) => {
    const source = tx.source;
    const operation = tx.operation;
    const sourceValue = JSON.parse(get(source));
    let cookieBaker: cookieBaker;
    if (sourceValue === undefined || sourceValue === null) {
        cookieBaker = createEmptyCookieBaker();
    }
    else {
        cookieBaker = JSON.parse(sourceValue);
    }

    switch (operation) {
        case actions.cookie: {
            cookieBaker = addCookie(cookieBaker);
            console.log("Successfully minted cookie");
            saveState(source, cookieBaker);
            break;
        }
}

New actions!

Let’s buy a Cursor or a Grandma! Of course, he needs to have enough cookies to perform this action!

When a user buys a Cursor or a grandma, there are four actions to perform:

  • Add a new cursor
  • Decrease the amount of cookies (because we buy cursor with cookies)
  • Calculate the price of the next cursor (we used the cookie-clicker formula)
  • Update the passive “cookie per second” given by the cursor

Hence, we need to update our state to store these new attributes:


export type cookieBaker = {
    cookies: number,
    cursors: number,
    grandmas: number,

    cursorCost: number,
    grandmaCost: number,

    cursorCps: number,
    grandmaCps: number
}

Same as before, let’s create an addCursor function which will perform the correct action (adding one cursor, decreasing cookies, calculate new cost, update cookies per second of cursors):


export const addCursor = (cookieBaker: st.cookieBaker): st.cookieBaker => {
    if (cookieBaker.cookies >= cookieBaker.cursorCost) {
        console.log("Enough cookie to buy a Cursor");
        // adding cursor
        cookieBaker.cursors = cookieBaker.cursors + 1;
        // removing cursor cost
        cookieBaker.cookies = cookieBaker.cookies -  cookieBaker.cursorCost;
        // calculating next cursor price
        cookieBaker.cursorCost = calculateCost(actions.cursor, cookieBaker);
        // calculate new cps
        cookieBaker.cursorCps = cookieBaker.cursors * st.initialCursorCps;
        return cookieBaker;
    } else {
        console.log("Not enough cookie to buy a cursor, needed: " + cookieBaker.cursorCost + " actual amount: " + cookieBaker.cookies);
        return cookieBaker;
    }
}

I think you have guessed it, we need the addGrandma function now!

Here is its content, but I am sure you already figure out:


export const addGrandma = (cookieBaker: st.cookieBaker): st.cookieBaker => {
    if (cookieBaker.cookies >= cookieBaker.grandmaCost) {
        console.log("Enough cookie to buy a Grandma");
        // adding grandma
        cookieBaker.grandmas = cookieBaker.grandmas + 1;
        // removing grandma cost
        cookieBaker.cookies = cookieBaker.cookies - cookieBaker.grandmaCost;
        // calculating next grandma price
        cookieBaker.grandmaCost = calculateCost(actions.grandma, cookieBaker);
        // calculate new cps
        cookieBaker.grandmaCps = cookieBaker.grandmas * st.initialGrandmaCps;
        return cookieBaker;
    } else {
        console.log("Not enough cookie to buy a grandma, needed: " + cookieBaker.grandmaCost + " actual amount: " + cookieBaker.cookies);
        return cookieBaker;
    }
}

And finally, we need to add this case in our switch:


case actions.grandma: {
            //action successful, update state
            cookieBaker = addGrandma(cookieBaker);
            console.log("Successfully minted grandma");
            saveState(source, cookieBaker);
            break;
        }

🎉 Voilà!! 👏

We now have a more complex example than a basic Hello World!

You can play with it using the following commands (remember to have your deku-cluster up and ready!):


# Do not forget to build your new state, by running:
$ npm run build
# Then in deku main folder
$ ./tutorial.sh
# Then, submit the new operations!
$ deku-cli submit-transaction wallet.json '"cookie"'
$ deku-cli submit-transaction wallet.json '"cursor"'
$ deku-cli submit-transaction wallet.json '"grandma"'

The whole code of this state machine is available here, of course, we also added other buildings from the cookie-clicker game!

More complex action

From now on, we only use a flat string. But in the near future, we would like to support some more complex actions, like sending cookies to your friend, or eating some of your cookies.

Hence, we have to declare and use a more complex action:


{"type":"mint","operation":"cookie"}

It is not difficult at all, and you can access to the type field by doing:


const transition = (tx: transaction) => {
   const op: string = tx.operation;
   const operation = JSON.parse(op);
   const value = operation.type
}

Used by deku-cli, it becomes:


$ deku-cli submit-transaction wallet.json 
'{"type":"mint","operation":"cookie"}'

$ deku-cli submit-transaction wallet.json 
'{"type":"mint","operation":"cookie"}'

$ deku-cli submit-transaction wallet.json 
'{"type":"mint","operation":"cookie"}'

🎉 Voilà (bis)!! 👏

This is awesome, we managed to interact with the Deku blockchain and create our first “complex” state machine.

In the last tutorial, we will create a frontend application to interact with our state machine via the Deku HTTP APIs!

If you want to know more about Marigold, please follow us on social media (Twitter, Reddit, Linkedin)!

Scroll to top