Sign in

Tetris (JavaScript)

Tetris is a simple variant of the famous game Tetris, created in JavaScript using the React framework. It’s open source and we took it to add SCILL challenges to it.

Create an app

Create a game in the Admin Panel:

Adding a game in the Admin Panel

Adding a game in the Admin Panel

After that, select the new app from the app selection in the top bar and navigate to “API Keys” to add an API key for the new game:

Adding an api key for our new game

Adding an api key for our new game

Defining events and challenges

Now, that we have set up the app in SCILL, we need to figure our which events we have in the game and what challenges we can create based on those events. We recommend getting started by defining obvious events and challenges for your game to get started. Once you have done that, you’ll figure our other interesting challenges which might require some other events. It’s a bit a back and forth process, but a lot of fun and as it’s an iterative process you can always start with something simple to get more complicated down the road.

SCILL events are preconfigured for you. You can find the complete list of event types in our Event Types list in this documentation.

By digging through the list, we found these events that we can pretty easily define and implement for this game:

This event has a couple of properties that we can set. One of those properties is item_id. We can use that to distinguish events with the same event name. In total, we’ll use three different values for item_id.

line
Is used for a “standard” Tetris row
sameColor
Describes a line where all pieces in the line have the same color
block
A block of four lines, cleaned at once

Every event has a couple of parameters that can be attached to the event. Some of them are required, others are optional. The documentation describes all those parameters in detail.

Based on those events, some challenges are obvious:

  • Complete 10 lines (listens on the item_id line)
  • 4 Lines at once (listens on the item_id block)
  • 5 lines with same color (listens on the item_id sameColor)

Of course, these are simple challenges, but especially the “5 lines with same color” challenge is completely changing the game. How many times have you played Tetris, and did you ever try to create lines with the same color? By just adding this simple challenge, the game is completely different and it’s fun to see different people figuring out different strategies to solve that problem.

Setting up Challenges

In the Admin Panel, make sure the app is selected from the list and click on “Challenges” in the menu. First, we need to create category. Challenges are organized into categories.

Adding Basic Challenges category

Adding Basic Challenges category

You can enable and disable categories. This way, you can create many challenges in advance and unlock them anytime. You could create “Christmas” challenges and unlock them at christmas or you could organize your challenges in seasons and switching them every month. If you add great rewards to some challenges, users will be forced to manage challenges in time which also drives users retention.

Let’s create our first, simple challenge. For this, choose the Challenges tab in the Personal Challenges section of the Admin Panel and click on Add Challenge.

Adding the 5 Lines with same color challenge

Adding the 5 Lines with same color challenge

Name
This is the description/name of the challenge. This is the action plan the user must follow to achieve this challenge. Keep it short and precise.
Category
This is the category the challenge is attached to. In this case, choose the “Basic Challenge” category created before.
Challenge-Type
Choose the event that this challenge will track. The challenge will wait for events coming in of this type and process them. You can further specify which events are processed in the Meta Data section below. We’ll come to that later. In this case we choose the destroy-item event type.
Repeatable
Challenges have a duration and need to be unlocked/activated. This setting defines what happens if the Challenge has been lost (timed out) or won (achieved within time constraint). If it’s set to be repeatable, it will be reset and available for another round. If it’s not repeatable this challenge will not be available for the user again. In this case we set it to be repeatable.
Difficulty Level
You can set an integer here. You can use this value to sort challenges on your end for example. We set it to 1 in this case.
Live Date
We leave this field blank, which means, we don’t set any date. You can create challenges and assign a date when they should “go live”. I.e. create a Halloween challenge and set to October 31th and this challenge will automatically pop up.
Challenge Icon
You can set any string here. In a web game, we just set it to the name of an image provided in the games asset folder. We set to h.png which is an image of a tile the user wins if he achieves this challenge.
Challenge Icon HD
Same as Challenge Icon, but allows you to set a high res image, for example for some teasers or for Desktop version of your game, while you use the Challenge Icon for the mobile version with a much denser UI. We leave that out in this example.
Duration
Set the time limit for the challenge in minutes. We set it to 60 minutes in this case.
Goal
This is the goal as an integer value of the challenge. As we want to have 10 kills, we set it to 5.
Challenge Goal Condition
This can be set to 0 or 1. If 0, than the challenge is achieved if you increment the counter greater or equal than the goal. I.e. we set the goal to 10, and the challenge requires to kill at least 10 enemies, so we set this to 0. We could also create a challenge: “Don’t get hit more than 5 times”, then we would need to set this to 1, because in this case the challenge will only be achieved if we stay below 5.
Reward, Price and XP
These are integer values that you can set and use as you like. We don’t make use of them right now, so just leave them blank.
Meta-Data
Here, we choose the property item_id and set the condition to AND. You can also set that to OR as we will only provide one possible value. In the value field, enter the value sameColor and press return to add that value to the list. This challenge will only track events hat have the same value in item_id. You could provide multiple values to make challenges respond to both (AND) or any of those (OR).

Save the challenge. Congratulations, we just created our first challenge. Time to play around with the challenge in SCILL Playground Application.

SCILL Playground Application

In our Playground application you can play around with challenges and events and see them from your users perspective. It’s a good place to simulate if challenges and events are working as you intended before adding User Interface or code to your application.

Setting up the Playground

On the left side, you need to enter your AppId and API key for the application you just created. And you need to enter a user id. In SCILL we link all events and challenges to these user ids. SCILL does not store any personal data of users nor does SCILL have any user accounts. It’s up to you to set user ids. For testing purposes use 1234. Leave the environment setting to Production, as you only have a production account.

You should see something like this in your browser:

SCILL Playground in Action

SCILL Playground in Action

Setting up a Webhook

First thing we need to do to play around with the Playground application is setting up a Webhook. A Webhook can be set in the Admin Panel for your application and is a URL that you can define that will be called from the SCILL backend whenever a challenge changes. The Playground Application also offers a simple Websocket server, routing incoming Webhocks to listening clients. You can use that Websocket server for development purposes, but you are required to implement/run that yourself if going in production, as our Playground Websocket Server is set up for high traffic or usage and can be changed anytime.

Click on Challenges in the Playground application. Here the Webhook URL is printed for you. You just need to Copy this URL and add set it as the Webhook URL in Admin Panel:

Get back into the Personal Challenges section of Admin Panel, click on Set Webhook (above the challenge list) and click on Add Webhook. This will bring up a dialog asking you for the URL (just copy & paste from the Playground). After that, your Admin Panel should look like this:

Playground Webhook set in the Admin Panel

Playground Webhook set in the Admin Panel

Sending events in Playground

Finally, we can play around in Playground. Click on the Challenges tab in the Playground application. You’ll see the challenges you have created in Admin Panel. You should see a Kill 10 enemies entry there and an unlock button.

Playground Webhook set in the Admin Panel

Playground Webhook set in the Admin Panel

Make sure you have set a User Id in Application Settings, as every action now is linked to this user id!

  1. Click on the Unlock button - the challenge is now unlocked and will show an Activate button
  2. Click on the Activate button - the challenge is now active and listens for incoming events and shows the goal and the current counter.
  3. As a test, change the User Id to 12345 and click on Update. As you can see, the challenges are now reflecting that users state. As this user did not do anything yet, challenges are locked and need to be unlocked
  4. Change back the user id (should be 1234) and click on Update to bring back the activate challenges for user 1234
  5. Now, reload your browser window. The Playground will end up in the same state. That is, because app info is persisted in your browsers localStorage and user data is persistent in the SCILL cloud.

Ok, time for sending some events and checking out if our challenge works.

On the right side, you can see the Event editor. Here, you can define an event and send it to the SCILL backend. From the list, choose destroy-item event and set the item_type to sameColor. Set the session_id to 1234 also.

Tip

The session id is a way to group events. All events within the same session id will increment the counter of listening challenges (i.e. activated challenges that have set the same event name). Whenever a new session id comes in, the counter will start from scratch.

We want the “Complete 10 lines” challenge to start from 0 whenever a new game starts. Therefore, in Tetris, whenever a new game starts, we update the session id which will reset the challenge counter. If we want to have a persistent counter we can just set the session id to the same value as the user id - which will never change.

Sending a kill-enemy event

Sending a kill-enemy event

If you have setup the Webhook correctly, you should now immediately see progress of the challenge in real time. The counter should now show 1/5. If that is not the case, reload the Playground application. If now shows 1/5 the Websocket connection (pushing changes to the playground application in real time) does not seem to work. Make sure you have set up the correct Webhook URL shown in the Playground application. If the counter did not change, try to send the event again. If that did not help, check out the challenge in Admin Panel and make sure it’s event name is set to destroy-enemy and that the challenge is still active (i.e. shows a progress bar).

If nothing works, please contact support.

After sending an event, the Playground application shows some more details about the event. First, it displays the JSON payload used to send an event. You can also see Code generated for you in various programming languages that you can just copy and paste into your code base to send that event.

Second, it shows the response from the server.

Code generated for you to send the event

Code generated for you to send the event

  1. Click a couple of times on the Send Event button until you have reached the challenges goal. Clicking 10 times should be enough.
  2. Once you have reached the goal, the challenges state will change and will show Claim Button
  3. Click on the Claim Button. Now, up to you, but often you now give the user some sort of an reward, being it an award or some experience points. Use the various fields provided in the challenge editor in Admin Panel to set a value that you can use in your application to distinguish rewards.
  4. Once a challenge is claimed, the Webhook is triggered and allows you to either activate the reward in the backend or send it to the client via Websockets to activate the reward in the client itself.
  5. As the challenge is repeatable it will come now back in Unlock state. If the challenge is not repeatable, it will now be gone (for this user id).

Congratulations: You have gone through a full SCILL challenges lifecyle! That was easy, right!

Adding events in JavaScript

By just clicking around a bit, you were able to set up complex events, created real time challenges and played around with the lifecycle of a challenge.

Now, we want to add events to the app. Best way to do that is setting up events in the Playground and examine the code generated for us by the Playground application. This way, we don’t miss anything. Of course, you can also just check out our events documentation.

Setup SCILL SDK in JavaScript

First, we need to setup the SCILL SDK. As this is a React based application, we can simple add the SCILL JavaScript package from NPM:

npm install @scillgame/scill-js --save

As we typically don’t want to expose user IDs and your API key in the client, it’s best to generate access tokens that are required to use SCILLs REST-APIs or the SDK in the backend. But to keep things simple, we generate the access token in the app, which exposes the API-key! We don’t want that in production.

In the file scillinfo.js we store the SCILL configuratoin and use it when setting up the SCILL SDK:

// SCILL
import * as SCILL from '@scillgame/scill-js'
import scillinfo from './scillinfo'

// Create EventsApi instance. Please note: The api key should not be exposed in clients, this is just for demonstration usage
// You should always create a backend to create an access token for a user id. You can also instantiate the EventsApi
// instance with the access token, which will not expose your API-key.
export const eventsApi = SCILL.getEventsApi(scillinfo.apiKey, scillinfo.environment)

With the EventsApi we can send events. Use the Playground application to generate source code that you can easily copy & paste into your own code base.

Tip

As we don’t have any user ids in this game, we just created a simple React store, that loads a user id value from localStorage of the browser. If that does not exist, a user id is auto generated with the current timestamp and is stored in the localStorage. This way we generate a persistent user id that will persist the users challenge state (for this browser).

Sending Destroy-Items Events

This game is a rect game. It uses the Redux Store to handle all game logic and game state. With this, it’s pretty easy to add events to the game, as every change in the state is well-defined and easy to find in the code. Whenever the game is reset (i.e. the board is cleaned after game over) we’ll update the session id and reset the counter by sending this event:

// Update Session Id
updateSessionId()

// Send a "reset" event to reset the line counter every game
// Send SCILL Event for destroying lines
const eventPayload = {
  event_name: 'destroy-item',
  event_type: 'group', // group sets the counter to amount instead of incrementing it by that
  session_id: getSessionId(),
  user_id: getUser().userId,
  meta_data: {
    amount: 0,
    item_id: 'line'
  }
}
eventsApi.sendEvent(eventPayload).then(() => {
  console.log("Reset destroy-item event sent", eventPayload)
})

Please not that item_id in meta_data. This field can be set to anything we want. In this case, we set it to the line value, as we only want to reset the counter in the challenge listening on line events.

In main.js function clearCompletedLines we added this block:

// Send SCILL Event for destroying lines
const eventPayload = {
  event_name: 'destroy-item',
  event_type: 'single',
  session_id: getSessionId(),
  user_id: getUser().userId,
  meta_data: {
    amount: numberOfClearedLines,
    item_id: 'line'
  }
}
eventsApi.sendEvent(eventPayload).then(() => {
  console.log("Destroy-item event sent", eventPayload);
})

// If number of lines is 5+ destroyed at once we send a monsterline event
if (numberOfClearedLines >= 4) {
  const eventPayload = {
    event_name: 'destroy-item',
    event_type: 'single',
    session_id: getSessionId(),
    user_id: getUser().userId,
    meta_data: {
      amount: 1,
      item_id: 'block'
    }
  }
  eventsApi.sendEvent(eventPayload).then(() => {
    console.log("Monsterline event sent", eventPayload);
  })
}

We send destroy-item events and set the amount to the number of lines completed. If we completed four lines at once we defined that to be a block and send an additional event for that block.

Sending Event for Special Lines

The challenge “5 lines with same color” is really interesting. It takes the games mechanics, but completely changes the game. If you want to achieve this challenge, you have to change your strategy and it’s really not easy to achieve. It brings a whole new dimension to the game. And it’s really easy to implement. We already have created the challenge in Admin Panel for that.

Whenever a line is removed from the board, this code examines the line if every item it is of same color. If that is the case, that event is sent with the sameColor attachment.

// We work through all lines that are destroyed and find rows where every item has the same color.
let numberOfRowsWithSameColor = 0;
fullRowIndeces.forEach(fullRowIndex => {
    const fullRow = newBoard[fullRowIndex];
    if (allEqual(fullRow)) {
      numberOfRowsWithSameColor++;
    }
});
if (numberOfRowsWithSameColor) {
  const eventPayload = {
    event_name: 'destroy-item',
    event_type: 'single',
    session_id: getUser().userId, //Use same session as user to make this persistent
    user_id: getUser().userId,
    meta_data: {
      amount: numberOfRowsWithSameColor,
      item_id: 'sameColor'
    }
  }
  eventsApi.sendEvent(eventPayload).then(() => {
    console.log("Same color destroy-item event sent", eventPayload);
  });
}

That’s it! We gave users a nice challenge to bite on and spend more time with the game. With a couple of clicks in the Admin Panel and a couple of lines of code!

Adding SCILL UI

We’ll create a very simple UI for this game by adding a new React component. It handles:

  • Loading personal challenges
  • Unlocks and Activates all challenges as we don’t want users to do that
  • Listens on real time updates of challenges by connecting to the Playground Websocket

You’ll find the React component here: https://github.com/scillgame/mimstris/blob/scill-integration/src/containers/Challenges.jsx

Loading challenges

In the components componentDidMount function, we do just that:

  componentDidMount () {
    // Create an access token - this should also be done in the backend to not expose the API key
    const authApi = SCILL.getAuthApi(scillinfo.apiKey, scillinfo.environment);
    authApi.generateAccessToken({
      user_id: this.props.user.userId
    }).then(accessToken => {
      this.props.updateAccessToken(accessToken.token);
      const challengesApi = SCILL.getChallengesApi(accessToken.token, scillinfo.environment);
      challengesApi.getPersonalChallenges(scillinfo.appId).then(categories => {
        this.props.updateChallenges(categories);
        this.claimRewards(categories);

        // Unlock and activate every challenge
        categories.forEach(categories => {
          categories.challenges.forEach(challenge => {
            if (challenge.type === 'unlock') {
              console.log("Unlocking challenge", challenge);
              challengesApi.unlockPersonalChallenge(scillinfo.appId, challenge.challenge_id).then(response => {
                console.log("Challenge unlocked, now activating", challenge);
                challengesApi.activatePersonalChallenge(scillinfo.appId, challenge.challenge_id).then(response => {
                  console.log("Challenge activated", challenge);
                }).catch(error => {
                  console.log("Failed to activate challenge", error, challenge);
                });
              }).catch(error => {
                console.warn("Failed to unlock challenge", error, challenge);
              });
            } else if (challenge.type === 'unlocked') {
              console.log("Challenge is unlocked, activating", challenge);
              challengesApi.activatePersonalChallenge(scillinfo.appId, challenge.challenge_id).then(response => {
                console.log("Challenge activated", challenge);
              }).catch(error => {
                console.log("Failed to activate challenge", error, challenge);
              });
            }
          })
        })
      });

      this.setupWebsocket();
    });
  }

As we want to unlock and activate each challenge, we walk through them and make sure that each challenge is tracking progress. In November 2020 we’ll roll out a new feature that will allow you to set challenges to be always active in the Admin Panel, then you don’t need to do that this way in the code.

Claim Rewards

In claimRewards we find challenges that have been achieved:

// A string with pieces that are unlocked later - we use the challenge_reward integer value (can be set in Admin Panel)
// as the char position in the string to unlock the item. This way, we can create challenges and set rewards without
// touching the code.
export const RewardPieces = '#YH'

...

  claimReward(challenge) {
    let activePieces = this.props.activePieces
    if (challenge.type === 'unclaimed') {
      // The challenge is achieved, find the piece to unlock and add to the active pieces list
      const reward_index = challenge.challenge_reward
      if (reward_index >= RewardPieces.length) {
        console.error("Reward index is out of bounds the RewardPieces", reward_index, RewardPieces)
      } else {
        // In Admin Panel we can set an integer value for challenge_reward. We use that integer to find the char
        // in the list of unlockables and add it to the list.
        const piece = RewardPieces.charAt(reward_index)
        if (piece) {
          if (!activePieces.includes(piece)) {
            activePieces += piece
          }
        }
      }
    }
    if (activePieces != this.props.activePieces) {
      console.log("SETTING ACTIVE PIECES", activePieces);
      this.props.setActivePieces(activePieces)
    }
  }

In Admin Panel you can set an integer value for challenge_reward. In this example we used that integer to identify a char in RewardPieces which is a simple string holding the items to unlock. The Tetris game uses chars to identify pieces. The game in its original state already allowed to set available pieces via a string value. We just use that to add pieces to the list, whenever a challenge is achieved.

Final thoughts

You can download/check out the full working example in our Github Repository.

If you compare the master branch to the scill-integration branch you can quickly identify which code changes we made to the original code base to add SCILL challenges to this game:

The Power of SCILL

The full power of SCILL comes now: We have added events to our game, that are loaded from the SCILL cloud whenever the game starts (or are changed). We can now create more challenges by just adding them in the Admin Panel, and they will show up in the game.

This brings great user retention, because they fell that you care about them and they have something to do all the time they update their app. And you don’t need to create new assets, build a new version, deploying it to your users. They just get it without any app updates.

Because of this, we suggest to send as many events as possible, as this gives you more freedom later in adding challenges.

Also make use of the optional properties and add as many infos as you have. This allows you to create new challenges by specifying filters in the Mata Data section of the Admin Panel.

References

Here are a couple of references in our documentation for things that we just scratched here: