Colin McGraw
Published © GPL3+

The Tickle Monster! - An Alexa Game

Fun (maybe terror) for the whole family! Enter a list of participants, then ask the Tickle Monster to name who gets tickled next!

IntermediateProtip4 hours6,006

Things used in this project

Hardware components

Amazon Echo
Amazon Alexa Amazon Echo
×1

Software apps and online services

AWS Lambda
Amazon Web Services AWS Lambda
Full source code provided in the "Code" section.
Alexa Skills Kit
Amazon Alexa Alexa Skills Kit

Story

Read more

Schematics

Files In Zip Format

Supporting files in zip format for those who prefer it to cutting/pasting from the Hackster.io project page's text.

Code

AWS Lambda Function for Tickle Monster

JavaScript
Amazon's Lambda makes setup of a simple skill pretty easy and I included the source code in a format that is easy to copy/paste into the online editor.

See here for detail on how to set up something similar:
https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/developing-an-alexa-skill-as-a-lambda-function
//The Tickle Monster speechlet. 
//This is a single file to make cutting/pasting/fiddling easier.
exports.handler = function (event, context) {
    //Security check. There's only one true Tickle Monster!
    var appId = "your app id";
    if (event.session.application.applicationId !== "amzn1.echo-sdk-ams.app." + appId) 
         context.fail("Invalid Application ID");
         
    parseEvent(event).then(
            function(parsedEvent){
                context.succeed(
                    //determine intent/see how to respond
                    getEventReponse(parsedEvent)
                );
            }, 
            //participants not found or invalid
            function(error) {
                context.succeed(
                    //send back a message saying the user needs to configure participants
                    buildSpeechletResponse(
                        {
                            message: "Please view the message from Tickle Monster in your Alexa app and choose <break time='1ms'/> 'Link Account' <break time='1ms'/> to set up participant names.",
                            shouldResetNames: true,
                            shouldEndSession: true
                        }
                    )
                );
            }
        ).catch(
            function(err){
                context.fail("Exception: " + err);
            }
        );
};

function getEventReponse(parsedEvent){
    //variables set according to conditions below and passed to a buildSpeechletResponse() function at the end.
    var responseInfo = {
        message: "",
        repromptText: "",
        intentName: parsedEvent.intentName,
        shouldResetNames: false,
        shouldEndSession: true
    };
    
    //if user only says "launch Tickle Monster", then give instructions on use.
    if (parsedEvent.isLaunchRequest) 
      launchMessage(responseInfo);
    else if(parsedEvent.intentName === "AMAZON.HelpIntent") 
      helpMessage(responseInfo);
    else if(parsedEvent.intentName === "AMAZON.StopIntent") 
      stopMessage(responseInfo);
    else if(parsedEvent.intentName === "WhosNextIntent")
      whosNext(parsedEvent, responseInfo);
    else if(parsedEvent.intentName === "ResetNamesIntent")
      confirmNameReset(responseInfo);
    //if user is responding to the question of whether or not to reset the names
    else if(parsedEvent.previousIntentName === "ResetNamesIntent"){
        if(parsedEvent.intentName === "AMAZON.YesIntent")
          yesResetNames(responseInfo);
        else if(parsedEvent.intentName === "AMAZON.NoIntent")
          noResetNames(responseInfo);
    }
    
    return buildSpeechletResponse(responseInfo);
}

function parseEvent(event){
    var promise = new Promise(function(resolve, reject) {
        //Technically, I hacked the OAuth protocol to store the possible names in the access token
        //to avoid having to worry about management of credentials or even a database.
        //Typically the access token is only a key used to then lookup user information (not a storage mechanism FOR user info),
        //I'm just being tricky.
        var unparsedParticipants = event.session.user.accessToken;
        
        if(unparsedParticipants && unparsedParticipants !== "")
        {
            var parsedParticipants = getParticipants(unparsedParticipants);
            
            var intent = event.request.intent ? event.request.intent.name : "";
            //track the previous intent so we can, for example, respond to a yes/no question
            var previousIntent = (event.session.attributes && event.session.attributes.previousIntentName);
            
            var isLaunchRequest = event.request.type === "LaunchRequest";
            
            //pass back the info we most care about
            resolve(
                {
                    event: event, 
                    participants: parsedParticipants,
                    intentName: intent,
                    previousIntentName: previousIntent,
                    isLaunchRequest: isLaunchRequest
                });
        }
        else {
            reject(Error("No participants found!"));
        }
    });
    
    return promise;
}

//See the response section of this document for more detail on what's going on here: 
//https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference
function buildSpeechletResponse(responseInfo) {
    var speechletResponse = { 
        version: "1.0",
        sessionAttributes: { previousIntentName: responseInfo.intentName },
        response: {
            //note the use of SSML (speech markup). this can alternately be plain text if you say so in the response.
            outputSpeech: {
                type: "SSML",
                ssml: "<speak>" + responseInfo.message + "</speak>"
            },
            shouldEndSession: responseInfo.shouldEndSession
        }
    };
    
    //if we want to include repromt text (necessary for certification in case of LaunchRequest)
    if(responseInfo.repromptText) 
        speechletResponse.response.reprompt =
            {
                outputSpeech:{
                    type: "SSML",
                    ssml: "<speak>" + responseInfo.repromptText + "</speak>"
                }
            }
    
    //if we want to reset the names, then send a "LinkAccount" card to the Alexa app so the user can do so.
    if(responseInfo.shouldResetNames) speechletResponse.response.card = { type: "LinkAccount" };
    
    return speechletResponse;
}

//keep the responses varied so they seem less robotic (and more fun!)
//uses rudamentary SSML for pauses. see here for details:
//https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speech-synthesis-markup-language-ssml-reference
var variedTickleResponses = [
    'Hmmm. <break time="1s"/> Maybe {TickleVictim}!',
    'I think you should tickle <break time="1s"/> {TickleVictim}!',
    '{TickleVictim}! Definitely {TickleVictim}!',
    'This time? <break time="1s"/> I think {TickleVictim}',
    'Maybe <break time="1s"/> {TickleVictim}!',
    'How about <break time="1s"/> {TickleVictim}!',
    'Let me think. <break time="1s"/> Hmmm. <break time="1s"/> {TickleVictim}!',
    'You want to know who\'s next? <break time="1s"/> {TickleVictim}!',
    'Better watch out {TickleVictim}, you\'re next!',
    '{TickleVictim}! Get {TickleVictim}!',
    'I think {TickleVictim} could use some tickles!'
    ];
    
    
//Message Configuration
//********"Alexa, Launch Tickle Monster"**************************
function launchMessage(responseInfo){
  responseInfo.message = "All right! Let's get tickling! Just ask <break time='1ms'/> \"who's' next\", or say \"reset the participant names\".";
  responseInfo.repromptText = "I didn't hear a response. You can ask <break time='1ms'/> \"who's next\", or say \"reset the participant names\".";
  responseInfo.shouldEndSession = false;
}

//********"Alexa, Ask Tickle Monster For Help"*********************
function helpMessage(responseInfo){
  responseInfo.message = "To choose the next person to be tickled, just ask <break time='1ms'/> \"who's next\". Otherwise say \"reset the participant names\" to enter who's playing.";
  responseInfo.repromptText = "I didn't hear a response. " + responseInfo.message;
  responseInfo.shouldEndSession = false;
}

//********"Stop"****************************************************
function stopMessage(responseInfo){
  responseInfo.message = "Okay. Tickle Monster signing off!";
}

//********"Alexa, Ask Tickle Monster Who's Next"********************
function whosNext(parsedEvent, responseInfo){
  responseInfo.message = getVariedTickleResponse(parsedEvent.participants);
}

//*********"Alexa, Ask Tickle Monster To Reset Participant Names"***
function confirmNameReset(responseInfo){
  responseInfo.message = "Are you sure you want Tickle Monster to reset the names?";
  responseInfo.repromptText = "Do you want to reset the participant names? Please answer \"yes\" or \"no\".";
  responseInfo.shouldEndSession = false;
}

//*********"Are You Sure You Want to Reset Participant Names?********
//*********"Yes"*****************************************************
function yesResetNames(responseInfo){
  //there will be a link in the Alexa app 
  //that lets the user revisit the name setup page.
  responseInfo.message = "Please view the message from Tickle Monster in your Alexa app and choose <break time='1ms'/> 'Link Account' <break time='1ms'/> to finish resetting the names.";
  responseInfo.shouldResetNames = true;
}

//*********"Are You Sure You Want to Reset Participant Names?********
//*********"No"******************************************************
function noResetNames(responseInfo){
  responseInfo.message = "Okay. Cancelled.";
}

//***
//*Helpers
//***
function getNextTickleVictim(participants){
    //add the possibility of "Everyone", just to spice things up a bit
    var chanceIn = 20;
    if(getRandomInt(0, chanceIn) == chanceIn) 
        return "Everyone";
    
    var participantsCount = participants.length;
    
    //get a random tickle victim to return.
    var randomIndex = getRandomInt(0, participantsCount - 1);
    return participants[randomIndex];
}

function getVariedTickleResponse(participants){
    var nextVictim = getNextTickleVictim(participants);
    
    var randomResponseIndex = getRandomInt(0, variedTickleResponses.length - 1)
    return variedTickleResponses[randomResponseIndex].replace(/{TickleVictim}/g, nextVictim);
}

function getParticipants(unparsedParticipants){
    //crude, reasonable validation for character limit.
    var charLimit = 1000;
    if(unparsedParticipants.length>charLimit)
        unparsedParticipants=unparsedParticipants.substring(0,charLimit);

    //crude validation using Regex. remove anything that's not a letter, comma, space or single quote
    unparsedParticipants = unparsedParticipants.replace(/[^a-z\s',]/gi, "");

    return unparsedParticipants.split(",");
}

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

The Account Linking Web Page (With Javascript)

HTML
Amazon supports "Account Linking" via the OAuth 2 protocol. Here, I hack the protocol to store participant choices which I use to associate Tickle Monster game participants with an Amazon Echo.
<!DOCTYPE html>
<html>
  <head>
      <title>Who Are The Tickled?</title>
      <meta name=viewport content="width=device-width, initial-scale=1">
      <link href='https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300' rel='stylesheet' type='text/css'>
      <link href='https://fonts.googleapis.com/css?family=Dancing+Script:700' rel='stylesheet' type='text/css'>
      <script src="https://code.jquery.com/jquery-2.2.3.min.js" integrity="sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo=" crossorigin="anonymous"></script>
      <!--tag manager jQuery plugin. see https://maxfavilli.com/jquery-tag-manager-->
      <script src="https://cdnjs.cloudflare.com/ajax/libs/tagmanager/3.0.2/tagmanager.min.js"></script>
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tagmanager/3.0.2/tagmanager.min.css">
      
      <!--I'm keeping everything in one file for simplicity/ease of tinkering-->
      <link rel="stylesheet" href="TickleMonster.css">
      <script>
        $(function(){
          $("#the-tickled").tagsManager();
          $("#add-more").click(pushTag);
          $("#here-we-go").click(function(){
                pushTag(); 
                var tags = $("#the-tickled").tagsManager("tags");
                if(tags.length == 0){
                  alert("The Tickle Monster needs to know who to tickle! Please enter some names.");
                  return false; //cancel click
                }

                //assigned by Amazon
                var vendorId = "<<Amazon-assigned vendor ID goes here>>";
                var returnUrl = "https://pitangui.amazon.com/spa/skill/account-linking-status.html?vendorId=" + vendorId;

                //get the query string values amazon passed to us
                var amazonValues = getQueryParameters();

                //hacking the OAuth protocol to store tickle victims'
                //names as the access token?!?
                //no need for databases or passwords?!? how sneaky and
                //mischievous! so like the Tickle Monster. 
                var theTickled = encodeURIComponent(tags.toString());
                
                //redirect the user back to Amazon 
                //so we can store their choices for use with the skill.
                //see description of implicit grant flow here: 
                //https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/linking-an-alexa-user-with-a-user-in-your-system
                location = returnUrl 
                            + "#state=" 
                            + amazonValues.state 
                            //the hack part
                            + "&access_token=" 
                            + theTickled 
                            + "&token_type=Bearer";
              }
          );

          function pushTag(){
            var text = $("#the-tickled").val();
            if(text != "") 
               $("#the-tickled").tagsManager("pushTag", text);
          }

          //credit: https://css-tricks.com/snippets/jquery/get-query-params-object/
          function getQueryParameters(str) {
            return (str || document.location.search).replace(/(^\?)/,'').split("&").map(function(n){return n = n.split("="),this[n[0]] = n[1],this}.bind({}))[0];
          }
        })
      </script>
  </head>
  <body>
      <div class="centered">
        <div><img src="/images/TickleMonster.png" alt="The Tickle Monster"/></div>
        <div id="tickle-monster-title">The Tickle Monster!</div> 
        <label for="the-tickled">
          Please add the names of those <br/>playing the tickle game!
        </label>
      </div>
        <div id="name-inputs">
          <input id="the-tickled" type="text" name="tags" placeholder="Enter A Name" class="tm-input-info big-space" />
          <br/>
          <input id="add-more" class="themed-button" type="button" value="Add 'Em!">
        </div>
        <div class="centered"><a href="#" id="here-we-go">I'm Done Adding Names. <br/>Now Let's Get Tickling!</a></div>
  </body>
</html>

TickleMonster.css (For Use With Account Linking Web Page)

CSS
Styling for account linking page (background, button, picker, etc.).
        body{
          background-color: #9DBCCA;
          font-size: 23px;
          font-family: 'Open Sans Condensed', sans-serif;
          color:black;
          padding: 20px;
          margin: 0 15px 0 15px;
        }

        .centered{
          text-align: center;
        }

        #tickle-monster-title{
          font-family: 'Dancing Script', cursive;
          font-size: 32px;
          color:pink;  
          text-shadow: 2px 2px 4px #000000;
        }
        
        #the-tickled{
          line-height: 1em;
          font-family: 'Open Sans Condensed', sans-serif;
          color:gray;
          font-size: large;  
        }

        #name-inputs{
          margin: 10px 0;
          border: 1px dashed #717C90;
          padding: 8px;
        }

        .themed-button{
          border-radius: 5px;
          background: #BECBD0;
          box-shadow: 0 2px #707C92;
        }

        .themed-button:active{
          box-shadow: none;
        }
        
        #here-we-go{
          margin-top: 8px;
          width: 100%;
          box-shadow: 0px 1px 0px 0px #fff6af;
          background:linear-gradient(to bottom, #ffec64 5%, #ffab23 100%);
          background-color:#ffec64;
          border-radius:6px;
          border:1px solid #ffaa22;
          display:inline-block;
          cursor:pointer;
          color:#333333;
          font-family:Arial;
          font-size:15px;
          font-weight:bold;
          padding:6px 0;
          text-decoration:none;
          text-shadow:0px 1px 0px #ffee66;
        }

        #add-more{
         font-family: 'Open Sans Condensed', sans-serif;
          font-size: medium;
          margin-right: 15px;  
        }

Credits

Colin McGraw

Colin McGraw

1 project • 5 followers
With 14 years in tech, I was a speaker at Microsoft events, competitive coder and I write awesome software and apps for a variety of industries.
Thanks to Jennifer Huebner.

Comments