Gavin Wood
Published © CC BY-NC

Making Playful Experiences with Alexa and Twine

This Alexa skill uses Twine to create a playful experience out of the children's rhyme: Pat-a-cake.

IntermediateWork in progress20 hours1,776
Making Playful Experiences with Alexa and Twine

Things used in this project

Hardware components

Amazon Echo
Amazon Alexa Amazon Echo
×1

Software apps and online services

Twine

Story

Read more

Schematics

Editing the story in Twine

I created this story for my Alexa Skill using Twine (https://twinery.org/).

Code

Alexa Node JS Skill

JavaScript
'use strict';

const STORY_URL = "https://gist.githubusercontent.com/BaaWolf/251450b708fb6debd3677a8a052feea7/raw/c2e2f253add16c6ee4a93e7a2f10579c87c9cd5a/patacake";
const END_STRING = "<break time='500ms'/>This story session has come to an end. Goodbye";

// Route the incoming request based on type (LaunchRequest, IntentRequest,
// etc.) The JSON body of the request is provided in the event parameter.
exports.handler = function (event, context) {
    try {
        console.log("event.session.application.applicationId=" + event.session.application.applicationId);


        //     if (event.session.application.applicationId !== "amzn1.echo-sdk-ams.app.05aecccb3-1461-48fb-a008-822ddrt6b516") {
        //         context.fail("Invalid Application ID");
        //      }

        if (event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }

        //
        if (event.request.type === "LaunchRequest") {
            onLaunch(event.request,
                event.session,
                function callback(sessionAttributes, speechletResponse) {
                    context.succeed(buildResponse(sessionAttributes, speechletResponse));
                });
        }
        if (event.request.type === "IntentRequest") {
            onIntent(event.request,
                event.session,
                function callback(sessionAttributes, speechletResponse) {
                    context.succeed(buildResponse(sessionAttributes, speechletResponse));
                });
        }
        if (event.request.type === "SessionEndedRequest") {
            onSessionEnded(event.request, event.session);
            context.succeed();
        }
    } catch (e) {
        context.fail("Exception: " + e);
    }
};

/**
 * Automatically called when the session starts - regardless of intent name
 */
function onSessionStarted(sessionStartedRequest, session) {
    console.log("onSessionStarted requestId=" + sessionStartedRequest.requestId
        + ", sessionId=" + session.sessionId);

    // add any session init logic here
}

/**
 * Is this a valid Json string
 */
function isJsonString(str) {
    try {
        JSON.parse(str);
    } catch (e) {
        return false;
    }
    return true;
}

/**
 * Remove the brackets
 */
 
function removeBrackets( content )
{
    // Remove brackets
    return content.replace("[[", "").replace("]]", "");
}

/**
 * Format the content inside []
 */
 
function formatContent( content )
{
    // Remove brackets
    return content.replace(/\[.*?\]/g, "");
}

/**
 * Decode the story from the JSON
 */
 
function decodeStory( json)
{
    return JSON.parse(json);    
}

/**
 * Called when the session first starts
 */
 
function doReset( session, jsonString )
{
    // Parse the story
    var parsedStory = decodeStory(jsonString);
    
    var storyLength = parsedStory.length;
     
    session.attributes = { passageIndex: 0,
                           jsonStory: jsonString,
                           passageCount: parsedStory.length,
                           storyLength: storyLength
                         };
}

/**
 * Increment the passage index
 */
 
function setPassageIndex(session, pid)
{
    session.attributes.passageIndex = pid;
}

/**
 * Called when the session first starts
 */
 
function respondWithPassage(session, callback) {
    
     // Parse the story
     var parsedStory = decodeStory(session.attributes.jsonStory);
     
     // Get the story index
     var passageIndex = session.attributes.passageIndex;

     // Read the title card (but we won't use it - the user could add this to the passage with a delay if they want to hear it)
     var passageTitle = parsedStory[passageIndex].name;

     // Read the content
     var passageContent = formatContent( parsedStory[passageIndex].content );
     
     // Concatenate the speech content
     var speechContent = passageContent;
     
     var endSession = "false";
     
     var childrenCount = parsedStory[passageIndex].childrenNames.length;
    
     if( childrenCount === 0 )
     {
        endSession = "true";   
     }

     console.log("responding with passage" + speechContent + " count= " + childrenCount );

     callback(session.attributes, buildSpeechletResponseWithoutCard(speechContent, speechContent, endSession));
}

function findHelp(session, callback)
{
     var endSession = "false";
     var text = "say open pat a cake. then say help";
            
     if( session.attributes )
     {
         // Parse the story
         var parsedStory = decodeStory( session.attributes.jsonStory );
         
         // Cache the storylength
         var storyLength = session.attributes.storyLength;
    
          // Get the story index
         var passageIndex = session.attributes.passageIndex;
    
         var found = false;
         var foundPassageIndex = 0;
         
         console.log("looking for help" );

         var childrenCount = parsedStory[passageIndex].childrenNames.length;
    
         // now find this passage 
         for (passageIndex=0; passageIndex<storyLength; passageIndex++ )
         {
             var title = parsedStory[passageIndex].name;
         
             console.log("comparing against passage " + title );
         
             if( title.toLowerCase() === "help" )
             {
                 foundPassageIndex = passageIndex;
                 found = true;
             }
         }
        
         setPassageIndex( session, foundPassageIndex );
    
         if( found )
         {
            respondWithPassage( session, callback );
         }
         else
         {
            endSession = "false";
            callback(session.attributes, buildSpeechletResponseWithoutCard( "no help has been made for this story", "no help has been made for this story", endSession));
         }
     }
     else
     {
         endSession = "false";
         text = "say open pat a cake. then say help";
         callback(session.attributes, buildSpeechletResponseWithoutCard( text, text, endSession));
     }
}
        
function findGivenAnswer(session, callback, givenAnswer)
{
     // Parse the story
     var parsedStory = decodeStory( session.attributes.jsonStory );
     
     // Cache the storylength
     var storyLength = session.attributes.storyLength;

      // Get the story index
     var passageIndex = session.attributes.passageIndex;

     var found = false;
     var foundPassageIndex = 0;
     
     console.log("looking for child name: " + givenAnswer );

     var childrenCount = parsedStory[passageIndex].childrenNames.length;
 
     for( var child=0; child<childrenCount; child++ )
     {
         var childName = removeBrackets( parsedStory[passageIndex].childrenNames[child] );

         console.log("comparing against passage: " + childName );

         if( childName.toLowerCase() === givenAnswer.toLowerCase() )
         {
             console.log("found " + childName );
        
             found = true;
             
             // now find this passage 
             for (passageIndex=0; passageIndex<storyLength; passageIndex++ )
             {
                 var title = parsedStory[passageIndex].name;
        
                 console.log("comparing against passage " + title );
        
                 if( title.toLowerCase() === childName.toLowerCase() )
                 {
                     foundPassageIndex = passageIndex;
                 }
             }
         }
     }
     
     if( found )
     {
        setPassageIndex( session, foundPassageIndex );
        
        var text = "Found the passage with index " + foundPassageIndex;
        
        //callback(session.attributes, buildSpeechletResponseWithoutCard(text, "", "false"));
        respondWithPassage( session, callback );
     }
     else
     {
        respondWithPassage( session, callback );
     }
}

/**
 * Called when the user invokes the skill without specifying what they want.
 */
function onLaunch(launchRequest, session, callback) {
    console.log("onLaunch requestId=" + launchRequest.requestId
        + ", sessionId=" + session.sessionId);

    // Get JSON from the website
    var getJson = function (eventCallback) {

        var https = require('https');
    
        https.get(STORY_URL, function(res) {
            var body = '';
        
            //res.setEncoding('utf8');
            res.on('data', function (chunk) {
                body += chunk;
            });
    
            res.on('end', function () {
                eventCallback(body);
            });
        }).on('error', function (e) {
            console.log("Got error: ", e);
        });
    };

    getJson(function (data) {

        console.log("data "+data+" length "+data.length);

        var stripBOM = "";
        for( var i=0; i<data.length; i++ )
        {
            var charcode = data.charCodeAt(i);

            if( charcode !== 0xFEFF )
            {
                stripBOM = stripBOM.concat( String.fromCharCode( charcode ) );
            }
        }
        
        console.log("stripped BOM "+stripBOM+" length "+stripBOM.length);

        var message = "";
        
        if( isJsonString(stripBOM) )
        {
            message = "parsed";
        }
        else
        {
            message = "not parsed";
        }
        console.log("parsed: " + message );

        // Reset the story	
        doReset( session, stripBOM );

        // Respond with the first passage
        respondWithPassage( session, callback );
    });
}

/**
 * Called when the user specifies an intent for this skill.
 */
function onIntent(intentRequest, session, callback) {
    console.log("onIntent requestId=" + intentRequest.requestId
        + ", sessionId=" + session.sessionId);
    
    var text, endSession;
    
    var intent = intentRequest.intent,
        intentName = intentRequest.intent.name;

    if (intent.name === "AMAZON.StopIntent") {
        text = END_STRING;
        endSession = "true";
        callback(session.attributes, buildSpeechletResponseWithoutCard(text, "", endSession));
    }
    else if (intent.name === "AMAZON.RepeatIntent") {
        respondWithPassage(session, callback);
    }
    else if (intent.name === "AMAZON.HelpIntent") {
        findHelp( session, callback );
    }
    else
    {
        // Get what was said by the user
        var givenAnswer = intent.slots.Answer.value;

        // find the given answer
        findGivenAnswer( session, callback, givenAnswer );
    }

    //{
    //    throw "Invalid intent";
    //}
}

/**
 * Called when the user ends the session.
 * Is not called when the skill returns shouldEndSession=true.
 */
function onSessionEnded(sessionEndedRequest, session) {
    console.log("onSessionEnded requestId=" + sessionEndedRequest.requestId
        + ", sessionId=" + session.sessionId);

    // Add any cleanup logic here
    //session.attributes.myCount = 0;
}

// ------- Helper functions to build responses -------

function buildSpeechletResponseWithoutCard(text, repromptText, shouldEndSession) {
    
    var outputSSML = "<speak>" + text + "</speak>";
       
    return {
        outputSpeech: {
            type: "SSML",
            ssml: outputSSML
        },
        reprompt: {
            outputSpeech: {
              type: "SSML",
              ssml: outputSSML
            }
        },
        shouldEndSession: shouldEndSession
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: "1.0",
        sessionAttributes: sessionAttributes,
        response: speechletResponse
    };
}

Alexa Intent Schema

Plain text
Created through the develop.amazon.com website and Interaction Model webpages.
{
  "intents": [
    {
      "intent": "PassageIntent",
      "slots": [
          {
            "name": "Answer",
            "type": "RHYME_WORDS"
          }
        ]
    },
    {
      "intent": "AMAZON.RepeatIntent"
    },
    {
      "intent": "AMAZON.HelpIntent"
    },    
    {
      "intent": "AMAZON.StopIntent"
    },
    {
      "intent": "AMAZON.CancelIntent"
    }
  ]
}

Alexa Utterances

Plain text
PassageIntent {Answer}
AMAZON.RepeatIntent Repeat
AMAZON.StopIntent Stop
AMAZON.StopIntent Close
AMAZON.StopIntent Finish
AMAZON.StopIntent Exit
AMAZON.HelpIntent Help
AMAZON.HelpIntent Help me

RHYME_WORDS

Plain text
pat a cake
bakers
man
bake
a
cake
as
fast
you
can
roll
it
pat it
and
mark
with
bee
put
in
the
oven
for
baby
me
start

Credits

Gavin Wood

Gavin Wood

1 project • 1 follower
Researcher at NorSC Lab, Northumbria University. Read more about my research and find my publications here: https://about.me/gwood

Comments