Building an extension game

Aidan Breen
8 min readOct 6, 2016

--

My last post received over 5.3k views! Thank you for reading. I’m going to continue documenting my exploration of browser extension development. This time, I’ll be tackling a simple game that should highlight how to use synchronised storage, alarms, and browser notifications.

Everything in this post is up on github — but the extension has not yet been published, while I clean it up a bit. Let me know if you’d use it!

The story so far…

In my last post, I described how to set up and test a simple browser extension with a basic UI, some permissions, and a long-running background page. If you’re trying to figure out the basics, start there!

What are we building?

I couldn’t think of any productivity extensions to make, so I decided to keep it fun and go for a simple ‘cookie clicker’ style game. Suggestions for future extensions are welcome in the comments, or on twitter!

Premise

The user gains points for every unique website they have visited. Points are totted up every 10 minutes. The more websites the user visits, the more points they gain every 10 minutes.

Discussion

I’m going to restrict the term website to prevent people using extra random url parameters to cheat. So I’ll try to (naively) find a root domain for any given url and store that.

I’ll also have to think about verifying the domain actually exists. This might be tricky with javascript alone, and I don’t want to use a server for this project (yet).

The game is super simple, but has the potential for some fun future development, like popular domains giving the user far more points per turn, but only allowing a single user to gain points from any one domain at a time. This would be super fun, but it requires a centralised database to verify ‘ownership’. Maybe I’ll tackle this in a future post.

Challenges

Firstly I want the domains that a person has visited to be tracked across all of their devices. We can achieve this using synchronised storage.

Secondly, I want the player’s score to update only every 10 minutes. We can’t use setTimeout because out background page will be unloaded to save processing power, but we can use chrome.alarms for this.

Finally, I want to let the player know when they have earned points, and when they have added a new URL to their arsenal. Browser notifications should be perfect for this.

Building the game

I’m going to clean up my previous repo and use it as a boilerplate for this, and any other extensions I might want to build in the future. I’m going to include jQuery and a reference to a google font for convenience. The boilerplate is available on github here.

Getting the URL

So we want to give the user points for each unique website they go to. To begin, let’s make sure we have the tabs permission to our manifest file (manifest.json):

//manifest.json"permissions": [
"tabs","storage"
],

Looks good. Now let’s get the URL. We want to do this when the user goes to a new page, which we can check for using tabs onUpdated in the background.js file. The callback function gives us the tab that has been updated, and we can read the url directly from that object.

//background.jschrome.tabs.onUpdated.addListener(function(id, changeInfo, tab){
console.log("Updated tab: "+tab.url);
});

As I said before, I want to restrict websites to the domain. If I planned on using a server in this project, I’d do a DNS lookup, which isn’t available in client side JS due to the same-origin policy, but I don’t want that hassle (and expense). The easier, less robust way of doing this is with some string manipulation, and in proper “hack things together quickly” fashion, I’m going to take a function I found on stackoverflow to do it. Using the extractDomain() function taken from here, our listener now looks like this:

//background.jschrome.tabs.onUpdated.addListener(function(id, changeInfo, tab){
console.log("Updated tab: "+extractDomain(tab.url));
});

That seems to be working nicely, but we haven’t verified that the url is real. Again, a DNS lookup would come in handy here, but we can avoid that. My first thought was to use a JSONP request to try and bypass the same-origin policy and just check the response. After much experimentation, I decided that would be an unreliable approach, even for this simple game.

Further searching uncovered this stackoverflow answer. It makes use of Yahoo!’s YQL API to check the domain — which is kind of like making a DNS lookup, except we don’t have to write the server-side code. Happy days!

Except this didn’t work perfectly either. The problem now is in using a synchronous AJAX request on the main thread. The solution is to chop up the function from stackoverflow, and do everything asynchronously. :(

Start by adding jQuery to our background scripts so we can use $.ajax() in background.js

"background": {
"scripts": ["jquery-3.1.1.min.js","background.js"],
"persistent": false
}

Then use the url validation function:

//background.js//testing the domain exists...
$.ajax({
url: "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20html%20where%20url%3D%22" + encodedURL + "%22&format=json",
type: "get",
dataType: "json",
success: function(data) {
if(data.query.results != null){
...

Finally, the onUpdated listener fires a bunch of times for each domain. We only need it once, so let’s try and reduce the number of requests we make by using the changeInfo object we got from the listener.

//background.js
//..listener starts up here
//try to minimize the amount of times we do the check
if(changeInfo.status === "complete"){
//..url validation goes here.
}

Synchronised storage

Chrome offers a nice API to store data across any browser a user is logged in to.

(This is sadly not available on Opera, but should work on firefox.)

Using storage.sync is as easy as using the localstorage API we used in the previous post. I should note however, that synchronised storage has storage limitations and read/write limitations, so I might end up reverting to localstorage if this becomes an issue. For now, let’s pretend it wont and we’ll start by checking if the user has visited the website before.

//background.js
//try to find the url in storage
chrome.storage.sync.get(url, function(result){
if(result[url]===undefined){
//domain not found in storage, so store it.
var storeURL = {};
storeURL[url] = 1;
chrome.storage.sync.set(storeURL, function(){
console.log("Stored: "+url);
});
}else{
//domain has been found in storage, so do nothing.
}
});

Timing and alarms

Now we have some domains stored, let’s give the user some points!

We want to update the score every 10 minutes. The obvious choice for those familiar with javascript would be setTimeout, which allows us to run a function with a specified delay before it actually executes. We can’t use this in our extension because the background page is unloaded when it’s not needed to save processing power.

The answer is alarms:

Use the chrome.alarms API to schedule code to run periodically or at a specified time in the future.

Perfect. Let’s add the alarms permission to our manifest file:

//manifest.json"permissions": [
"tabs","storage", "alarms"
],

We don’t need to worry about duplicate alarms, since the documentation states, regarding the creation of alarms:

If there is another alarm with the same name … it will be cancelled and replaced by this alarm.

When we distribute our extension, chrome will only allow alarms to be scheduled down to the minute. And even then, chrome does not guarantee that our alarm will fire at exactly the right time. This is all to save processing power. Thankfully, however, we can specify a shorter time delay when we’re testing the extension so we’re not waiting for minutes each time we want to test our alarm. More on alarms here.

//background.jschrome.alarms.create("Update-score", {
periodInMinutes:0.1
});

Right after this code, we’re going to create an alarm listener that will fire when our alarm goes off:

//background.jschrome.alarms.onAlarm.addListener(function(alarm){
if(alarm.name==="update-score"){
console.log("alarm fired.");
}
});

We don’t really need to check the alarm name, because it’s our only alarm, but let’s do it anyway incase we end up using more later.

Now we need to update the score. We’ll replace the console.log(“alarm fired.”) with the following; first finding all of the domains we have found, and then updating the score value.

//background.js    //calculate the score the user has earned
//sync.get(null) should give us all of the keys stored.
chrome.storage.sync.get(null, function(result){
//Find how much we should increment the score by
var scoreIncr = Object.keys(result).length;
//get the current score
chrome.storage.sync.get('score', function(result){
//handle the first ever score update
if (result.score===undefined) result.score = 0;
//get new value of score
var updatedScore = result.score+scoreIncr;
//store new value
chrome.storage.sync.set({score:updatedScore}, function(){
console.log("Score updated!");
});
});
});

Now we need to display the score for the player. Add a span to popup.html to display the score.

//popup.htmlScore:<span class="score"></span>

Then update the value in the span to the current score, inside our $(document).ready function.

//popup.js  //display the score
chrome.storage.sync.get('score', function(result){
if (result.score===undefined) result.score = 0;
$(".score").text(result.score);
});

We also want to update the contents of the score span when the value changes, so let’s set up a storage listener to do that.

//popup.js//update the displayed score
chrome.storage.onChanged.addListener(function(changes){
//make sure it was the score that has been changed.
if(changes['score']!==undefined){
//get the score
chrome.storage.sync.get('score', function(result){
if (result.score===undefined) result.score = 0;
$(".score").text(result.score);
});
}
});

Notifications

The player can open the extension popup to check their score, but that’s not very useful. Rather than relying on the player to remember to check, let’s show them a notification whenever the score is updated.

Chrome provides the notifications API for this. Let’s start by adding the permission to our manifest file.

//manifest.json"permissions": [
"tabs","storage", "alarms", "notifications"
],

Now, we will use the notifications.create method to show a new notification. We want to show the notification when we update the score, so let’s replace the console.log(“Score updated!”); message with the following:

//background.js//show notification
chrome.notifications.create("update-score", {
type:"image",
iconUrl:"icon.png",
title:"New Score!",
message:"You've earned "+scoreIncr+" more points! Your new score is "+updatedScore,
imageUrl:"thumbs.jpg"
});

The notification options documentation describes what we should include. I’ve been lazy here and used the normal 19x19 icon. I should really replace that with a larger icon that fits better. The image is just for fun.

Congratulations, you’ve earned worthless internet points.

I’m going to set the alarm time to 10 minutes so the notification isn’t so annoying. I’m also going to create a different notification to tell the user when they have discovered a new website. This goes in our tabs.onUpdated listener:

//background.js//show notification
chrome.notifications.create("new-url found", {
type:"image",
iconUrl:"icon.png",
title:"New website discovered!",
message:"You've discovered a new website and increased your scoring capacity!",
imageUrl:"internet.jpg"
});

Finishing up

That’s about all the features I wanted to implement.

We’ve covered a lot here, but the game could still use work. The graphics and images need some time to get right. The popup UI displays the score and nothing more. Maybe we should also let the user decide when they get notifications.

Beyond that, we could also use a centralised server to track high scores, and the other features I described earlier.

Your feedback

I’m not sure how well this will be received. The last post did really well, but that might have been dumb luck. Anyway, I really enjoy making these posts and learning about new stuff. If you’d like to see more posts like this, please let me know in the comments or on twitter.

Thanks for reading.

--

--