Skip to content

10 Project 3: Cookie hunt

thelionroars edited this page Jun 15, 2015 · 12 revisions

Our final project will take a sneak peek at the frontier of Internet of Things applications. This will feature a game where several ESP8266 devices all act as one server and each instance must be hunted for and discovered within a geographical area. The game will make use of a Javascript cookie, which will be shared among all of the ESP8266 devices, and used to track the level of completion of the participants.

Learning Outcomes

  • Run the ESP8266 as a Wi-Fi Access Point (AP) and basic web server
  • Learn how to upload files other than Lua scripts onto the device
  • Learn how to use Internet of Things Wi-Fi devices for applications sensitive to geographical area
  1. Wire up your ESP-01 or ESP-12 to your PC and start up ESPlorer. You may want to format your device and restart it, if it contains a lot of files already and/or is set to run programs automatically on booting.

  2. Let's have a look at the page we're serving (index.html), which contains html 5 and Javascript to maintain state through the use of the cookie:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <title>Cookie Hunt</title>
    <!-- author: Edward Francis Gilbert
         date: Monday 4th May 2015 
         May the Fourth be with you -->
    <script type="text/javascript">
        // Put the station_id here
        // station ids start at 0 and are consecutive
        // make sure you declare the right number of total stations in functions.js
        var station_id = 2;
        var cookie = document.cookie;
    </script>
</head>
<body>
    <noscript>
        <h1>Please turn on Javascript!</h1>
        <p>This game requires Javascript in order to work. Please turn on Javascript in your browser settings and make sure cookies are enabled.</p>
    </noscript>
    <script type="text/javascript">
        "use strict";function max_score(o){for(var e=0,t=0;o>t;t++)e+=Math.pow(2,t);return e}function get_tally(o,e){for(var t=0,i=0;e>i;i++)Math.pow(2,i)&o&&t++;return t}function split_cookie(){split_cookie={};var o,e;o=cookie.split("&");
for(var t=0;
t<o.length;t++)e=o[t].split("="),split_cookie[e[0] ]=e[1];return split_cookie}function get_rating(o,e){var t,i=o/e;return.25>i?t="Prefer ice cream":.5>i?t="Connoisseur":.75>i?t="Aficionado":1>i?t="Maniac":1==i&&(t="Monster!"),t}
function write_page(o,e){var t=new Date,i=Math.pow(2,o),n=split_cookie(),r=(n.name,parseInt(n.score));t.setTime(parseInt(t.getTime()+COOKIE_LIFETIME)),r?i&r?(document.write("<h1>Welcome back</h1>"),
document.write("<p>You've already found this cookie. Keep looking for the rest!</p>")):(r+=i,document.cookie="name=cookie_hunt&score="+r+"; expires="+t.toGMTString(),
document.write(r==max_score(e)?"<h1>Congratulations - you found all the cookies!</h1>":"<h1>You've found another cookie!</h1>")):(r=i,
document.cookie="name=cookie_hunt&score="+r+"; expires="+t.toGMTString(),document.write("<h1>You found your first cookie!</h1>"),document.write("<p>Finish the game by collecting all the cookies</p>")),
document.write("<p>You've found "+get_tally(r,e)+" out of "+e+" cookies. Your cookie rating is: "+get_rating(get_tally(r,e),e)+"</p>")}var total_stations=3,COOKIE_LIFETIME=864e5;window.onload=write_page(station_id,total_stations,cookie);
    </script>
</body>
</html>

It's important to note that the content of the page is written dynamically, so unless the Javascript runs, the body of the displayed page will be empty. Javascript fails silently, so if your browser encounters an error when trying to process it, the html in the page won't be written. As our game requires Javascript, we've also included a message to turn on Javascript between the 'noscript' tags. This will only be visible if the user has turned off Javascript in their browser settings.

  1. Now let's look at the Javascript. You may have noticed that there are 2 script sections inside the page. The first is easy to read:
   var station_id = 2;
   var cookie = document.cookie;

This sets the ID of the station in the Javascript code. IDs for each ESP8266 should start at 0 and increase incrementally (so the other device IDs will be 1, 2 and so on for as many devices you're using).

The second script is a hot mess, but that's because it's been minified using a free online service (just do a search for 'javascript minify' to find a number of free services to choose from). We've then split the lines so that they are 255 characters or less, to avoid any problems with buffering on the ESP8266. Here is the 'unminified' version:

"use strict";
var total_stations = 3; // Change to however many stations you desire.
var COOKIE_LIFETIME = 3600 * 24 * 1000; // = 24 hours in milliseconds

/* Function to calculate max score
 * The score is a bit register:
 * - game progress is represented by no. of on bits, NOT the size of 
 *  the score (eg. 127 is much better than 128)
 * - the score for each station found is represented by 2 to the 
 *  power of station_id
 * - Whether a station has been found or not can be determined by 
 *  <score for station> & <current score>
 *  (where & is the binary AND operation)
 */
function max_score(total) {
    var max_score = 0;
    for(var i = 0; i < total; i++)
        max_score += Math.pow(2, i);
    return max_score;
}

/* Get the number of stations found */
function get_tally(score, total_stations) {
    var cookies = 0;
    for(var i = 0; i < total_stations; i++) {
        if(Math.pow(2, i) & score)
            cookies++;
    }
    return cookies;
}

/* Parse the cookie into a 2D array
 * Assumes tuples separated by '&',key + value separated by '='
 */
function split_cookie() {
    split_cookie = {};
    var s1, s2;
    s1 = cookie.split("&");
    for(var i = 0; i < s1.length; i++) {
        s2 = s1[i].split("=");
        split_cookie[s2[0] ]=s2[1];
    }
    return split_cookie;
}

/* Returns a rating (string) of current progress */
function get_rating(tally, total_stations) {
    var rating;
    var progress = tally / total_stations;
    if(progress < 0.25)
        rating = "Prefer ice cream";
    else if(progress < 0.5)
        rating = "Connoisseur";
    else if(progress < 0.75)
        rating = "Aficionado";
    else if(progress < 1)
        rating = "Maniac";
    else if(progress == 1)
        rating = "Monster!";
    return rating;
}

function write_page(station_id, total_stations, cookie) {
    var expiration_date = new Date();
    var station_score = Math.pow(2, station_id);
    // Parse the cookie to get the score
    var cookie_data = split_cookie();
    var cookie_name = cookie_data['name'];
    var score = parseInt(cookie_data['score']);
    // Cookie will be set to expire 24 hours from last access
    expiration_date.setTime(parseInt(expiration_date.getTime() + COOKIE_LIFETIME));
    if(!score) {
        score = station_score;
        document.cookie = "name=cookie_hunt&score=" + score + "; expires=" + expiration_date.toGMTString();
        document.write("<h1>You found your first cookie!</h1>");
        document.write("<p>Finish the game by collecting all the cookies</p>");
    }
    else {
        
        // Check if this cookie has already been found
        if(station_score & score) {
            document.write("<h1>Welcome back</h1>");
            document.write("<p>You've already found this cookie. Keep looking for the rest!</p>");
        }
        // If not, add this station_score to the score
        else {
            score += station_score;
            document.cookie = "name=cookie_hunt&score=" + score + "; expires=" + expiration_date.toGMTString();
            // If all the cookies have been found, show the result
            if(score == max_score(total_stations)) {
                document.write("<h1>Congratulations - you found all the cookies!</h1>");
            }
            // Otherwise, show the new cookie found message
            else {
                document.write("<h1>You've found another cookie!</h1>");
            }
        }
    }
    document.write("<p>You've found " + get_tally(score, total_stations) + " out of " + total_stations + " cookies. Your cookie rating is: " + get_rating(get_tally(score,total_stations), total_stations) + "</p>");
}
window.onload = write_page(station_id, total_stations, cookie);

This script both checks and sets the cookie for the page. The cookie keeps the 'score' as a single number, and then calculates the real score (the tally of how many stations the player has connected to) by performing a bitwise 'AND' operation, to check if the unique 'station score' of each station has been added to the cookie. The station store is the station ID as a power of 2, so station 0 = 2^0 = 1, station 1 = 2^1 = 2, station 3 = 2^3 = 8, and so on. Once the score is identified, the page is written dynamically according to whether this station (the ESP8266 device) has been connected to before, or whether all of the stations have been found. The number of stations visited is displayed, as well as a rating which is determined by the percentage of the total stations that you've found.

  1. Let's load the page onto the device. From the bottom controls on the left pane of ESPlorer, click on the 'Upload' button, and use the file selection menu to choose index.html. ESPlorer will upload the page onto the device.

  2. Now for the Lua code to serve the page to connected clients. Upload the following code onto the device and save it as init.lua:

cfg={}
cfg.ssid="Cookies"
cfg.pwd="i<3cookies"
wifi.ap.config(cfg)
 
wifi.setmode(wifi.SOFTAP)
cfg = nil

srv = net.createServer(net.TCP, 3) 
srv:listen(80,function(conn) 
    conn:on("receive",function(conn,data)
        print(data)
        file.open("index.html", 'r')
        stop = false
        while(stop == false)
        do
            line = file.readline()
            if(line ~= nil) then
                conn:send(line)
            else
                stop = true
            end
        end
        file.close()
        conn.close()
    end) 
end)

If you've read through the earlier projects, most of the API calls used here will be familiar to you. We first set the SSID and password for our ESP8266 as an access point, then start the Wi-Fi in this mode. Then, the TCP server is created on port 80 with:

srv = net.createServer(net.TCP, 3)

The conn:on("receive") function will then be run whenever a connection is made. This function prints the received request to the console, then opens the index.html file for reading. The file is then read and sent to the client a line at a time, until the end of the file is reached. The file and TCP connection are then closed.

  1. To run the game, repeat this process for the other devices. Make sure to change the station ID in each 'index.html' page to a unique incremental value, starting from 0. If the stations are going to be within range of each other, you should also use unique SSIDs - for example, "Cookies", "Cookies2" and "Cookies3". You can also use more (or less) than 3 devices, but make sure you change this in functions.js before you minify it and add it to the page.

The game will now be ready to play! Add a battery pack to each device and you're good to go.