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
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
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
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
- 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 toAND
. You can also set that toOR
as we will only provide one possible value. In the value field, enter the valuesameColor
and press return to add that value to the list. This challenge will only track events hat have the same value initem_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
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
Make sure you have set a User Id in Application Settings, as every action now is linked to this user id!
- Click on the Unlock button - the challenge is now unlocked and will show an Activate button
- Click on the Activate button - the challenge is now active and listens for incoming events and shows the goal and the current counter.
- 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 - Change back the user id (should be
1234
) and click on Update to bring back the activate challenges for user1234
- 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.
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
The counter should now show 1/5
. If that is not the case, reload the Playground application. If the counter did not
change, 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).
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
- Click a couple of times on the Send Event button until you have reached the challenges goal. Clicking 10 times should be enough.
- Once you have reached the goal, the challenges state will change and will show Claim Button
- 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.
- 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.
- 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.
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: