An Alexa Skill in Javascript – Part 5

Finally, the last post in the series.  Here, we will show Alexa how to Roll Some Dice with a modifier so that we can get our skill working properly.

We’re going to do this in two steps:

  1. Create a function that will take the diceCount, plusMinus and modifier parameters and return the results as speech text.
  2. We’ll modify the RollSomeDiceIntentHandler to pull those parameters from the Request and call the function we created in the first step.

Create a Function that can Roll Some Dice

To contain our function, we’ll create a new JavaScript file in the lambda\custom folder of our project.  Right-click on the lambda\custom folder and select New File from the menu.  Enter rollSomeDiceFunction.js as the file name.

In the new file, let’s stub out our function.  We need to export the function to be able to access it from the other file, index.js.  It will accept three parameters.  Like so:

exports.rollSomeDice = function rollSomeDice(diceCountParm, 
plusMinusParm, modifierParm) {

}

Next, we need to verify that the diceCount Slot parameter (diceCountParm) has been provided (i.e. is not null):

if (diceCountParm == null) {
    speechOutput = "Sorry, I did not hear that.  Please say something like roll 2 dice.";
    return (speechOutput);
}

Then, we need to convert the string parameters into numbers.  Slot values are always provided as strings.  We need to verify that the diceCount and modifier contain numeric values and parse the strings to actual numbers.

var diceCount = (isNaN(diceCountParm)) ? null :
    parseInt(diceCountParm);
const diceSize = 6;
var modifier = (isNaN(modifierParm)) ? 0 :
    parseInt(modifierParm);

Now, we need to check again that diceCount is not null:

if (diceCount == null) {
    speechOutput = "Sorry, I did not hear that.  Please say something like roll 2 dice.";
    return (speechOutput);
}

Next, we’re going to set up an array of possible dice rolls so we can keep track of what we roll:

var buckets = [0, 0, 0, 0, 0, 0, 0];
var bucketSlots = ['zero', 'one', 'two', 'three', 'four', 'five', 'six'];

And then we can finally roll the dice:

for (var i = 0; i < diceCount; i++) {
    let roll = 0;
    roll = Math.floor(Math.random() * diceSize) + 1;

And if the user supplied a plusMinus Slot value (in plusMinusParm) and a modifier value, then we need to add or subtract the modifier to/from the dice roll:

if (plusMinusParm != null && modifier != 0) {
    // add or subtract the modifier to each roll
    if (plusMinusParm == 'plus') {
        roll += modifier;
    } else {
        roll -= modifier;
    }
}

Next, we need to put the roll in the correct bucket.  Note that because of the modifier, we could roll less than 1 or more than 6, so we need to handle those values too:

if (roll < 1) {     buckets[0]++; } else if (roll > 5) {
    buckets[6]++;
} else {
    buckets[roll]++;
}

Now, we’ve completed the for loop that loops over the diceCount and we can start putting together the speech phrases that we want Alexa to say in her response.  First, we’ll tackle the buckets so she can say “I rolled 3 ones, 2 twos, a four and 3 sixes.”  Note:  This code snippet uses apostrophes ( ‘ ‘ ) and backward tick marks ( ` ` ) to create the right strings.

var bucketsPhrase = 'I rolled ';
for (let i = 0; i < 7; i++) {     if (buckets[i] > 1) {
        if (i < 6) {
            bucketsPhrase += `${buckets[i]} ${bucketSlots[i]}s, `;
        } else {
            bucketsPhrase += `and ${buckets[i]} ${bucketSlots[i]}es `;
        }
    } else if (buckets[i] == 1) {
        if (i < 6) {
            bucketsPhrase += `a ${bucketSlots[i]}, `;
        } else {
            bucketsPhrase += `and a ${bucketSlots[i]} `;
        }
    }
}

The above code handles the fact that six is the only plural that ends in ‘es’.  And that six is the last number in the set, so put an ‘and’ in front of it.  It also differentiates between ‘a six’ and ‘two sixes’.

The next phrase to put together is the entire speech that we want Alexa to say.  We have two options:  with modifier and without, so the code looks like this:

if (modifier == 0 && target == null) {
    speechOutput = `You said roll ${diceCount} d ${diceSize}. ${bucketsPhrase}.`;
} else if (target == null) {
    speechOutput = `You said roll ${diceCount} d ${diceSize} ${plusMinusParm} ${modifier}. ${bucketsPhrase}.`;
}

Lastly, we return the speechOutput to Alexa:

return (speechOutput);

The Finished Function

Putting it all together, we get the following:

exports.rollSomeDice = function rollSomeDice(diceCountParm, plusMinusParm, modifierParm) {

    var speechOutput = "";
    if (diceCountParm == null) {
        speechOutput = "Sorry, I did not hear that.  Please say something like roll 2 dice.";
        return (speechOutput);
    }
    var diceCount = (isNaN(diceCountParm)) ? null :
        parseInt(diceCountParm);
    const diceSize = 6;
    var modifier = (isNaN(modifierParm)) ? 0 :
        parseInt(modifierParm);

    if (diceCount == null) {
        speechOutput = "Sorry, I did not hear the number of dice to roll.  Please say something like roll 2 dice.";
        return (speechOutput);
    }

    var buckets = [0, 0, 0, 0, 0, 0, 0];
    var bucketSlots = ['zero', 'one', 'two', 'three', 'four', 'five', 'six'];

    for (var i = 0; i < diceCount; i++) {
        let roll = 0;
        roll = Math.floor(Math.random() * diceSize) + 1;

        if (plusMinusParm != null && modifier != 0) {
            // add or subtract the modifier to each roll
            if (plusMinusParm == 'plus') {
                roll += modifier;
            } else {
                roll -= modifier;
            }
        }

        if (roll < 1) {             buckets[0]++;         } else if (roll > 5) {
            buckets[6]++;
        } else {
            buckets[roll]++;
        }
    }

    var bucketsPhrase = 'I rolled ';
    for (let i = 0; i < 7; i++) {         if (buckets[i] > 1) {
            if (i < 6) {
                bucketsPhrase += `${buckets[i]} ${bucketSlots[i]}s, `;
            } else {
                bucketsPhrase += `and ${buckets[i]} ${bucketSlots[i]}es `;
            }
        } else if (buckets[i] == 1) {
            if (i < 6) {
                bucketsPhrase += `a ${bucketSlots[i]}, `;
            } else {
                bucketsPhrase += `and a ${bucketSlots[i]} `;
            }
        }
    }

    if (modifier == 0) {
        speechOutput = `You said roll ${diceCount} dice. ${bucketsPhrase}.`;
    } else if (target == null) {
        speechOutput = `You said roll ${diceCount} dice ${plusMinusParm} ${modifier}. ${bucketsPhrase}.`;
    }

    return (speechOutput);
}

Modify the RollSomeDiceIntentHandler

Now that we have a function that can roll some dice, we need to wire it up to the RollSomeDiceIntentHandler we stubbed out in the basic template code in index.js.

Open index.js, right at the top we’re going to import the function we just created above, like so:

const Alexa = require('ask-sdk-core');
const i18n = require('i18next');
const sprintf = require('i18next-sprintf-postprocessor');
const diceRoller = require("./rollSomeDiceFunction.js");

Next, find the RollSomeDiceIntentHandler.  We’re going to replace the red, bold text in this function:

const RollSomeDiceIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
            handlerInput.requestEnvelope.request.intent.name === 'RollSomeDiceIntent';
    },
    handle(handlerInput) {

        const speechText = 'Hello World!';

        return handlerInput.responseBuilder
            .speak(speechText)
            .withSimpleCard('Roll Some Dice', speechText)
            .getResponse();
    },
};

First, delete the line const speechText = ‘Hello World!’;

Next, we need to pull the Slot values out of the request:

var diceCount = handlerInput.requestEnvelope.request.intent.slots["diceCount"].value;
var plusMinusSlot = handlerInput.requestEnvelope.request.intent.slots["plusMinus"];
var plusMinus = '';
if (plusMinusSlot.resolutions) {
    plusMinus = plusMinusSlot.resolutions.resolutionsPerAuthority[0].values[0].value.name;
}
var modifier = handlerInput.requestEnvelope.request.intent.slots["modifier"].value;

Next, we need to make sure the user supplied values for the Slots.  If not, we will assign a default value:

if (!(diceCount)) {
    diceCount = '1';
}
if (!(plusMinus)) {
    plusMinus = 'plus';
}
if (!(modifier)) {
    modifier = '0';
}

Next, we need to get the speech from the RollSomeDice() function we created earlier.

const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
sessionAttributes.speakOutput = diceRoller.rollSomeDice(diceCount, plusMinus, modifier) + " ... Now, what shall I roll?";
sessionAttributes.repromptSpeech = 'What shall I roll?';

Lastly, we need to return the speech to Alexa:

return handlerInput.responseBuilder
    .speak(sessionAttributes.speakOutput)
    .reprompt(sessionAttributes.repromptSpeech)
    .withSimpleCard('Roll Some Dice', sessionAttributes.speakOutput)
    .getResponse();

The Completed RollSomeDiceIntentHandler

Putting it all together, we get:

const RollSomeDiceIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
            handlerInput.requestEnvelope.request.intent.name === 'RollSomeDiceIntent';
    },
    handle(handlerInput) {

        var diceCount = handlerInput.requestEnvelope.request.intent.slots["diceCount"].value;
        var plusMinusSlot = handlerInput.requestEnvelope.request.intent.slots["plusMinus"];
        var plusMinus = '';
        if (plusMinusSlot.resolutions) {
            plusMinus = plusMinusSlot.resolutions.resolutionsPerAuthority[0].values[0].value.name;
        }
        var modifier = handlerInput.requestEnvelope.request.intent.slots["modifier"].value;

        if (!(diceCount)) {
            diceCount = '1';
        }
        if (!(plusMinus)) {
            plusMinus = 'plus';
        }
        if (!(modifier)) {
            modifier = '0';
        }

        const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
        sessionAttributes.speakOutput = diceRoller.rollSomeDice(diceCount, plusMinus, modifier) + " ... Now, what shall I roll?";
        sessionAttributes.repromptSpeech = 'What shall I roll?';

        return handlerInput.responseBuilder
            .speak(sessionAttributes.speakOutput)
            .reprompt(sessionAttributes.repromptSpeech)
            .withSimpleCard('Roll Some Dice', sessionAttributes.speakOutput)
            .getResponse();
    },
};

Publish the Code to Alexa and AWS Lambda

Now that the RollSomeDice() function and the RollSomeDiceIntentHandler are written, we can publish the skill to Alexa and AWS Lambda.

In VS Code, open the Command Palette (View | Command Palette…).  Type ASK.  Select Deploy the skill.

snip_20181106045852

Wait a moment while the ASK initializes, then select the default profile:

snip_20181106045953

Then select “All”:

snip_20181106050011

Now, it will try, and probably fail, to deploy. If it succeeds, great.  If it fails, you probably need to change the working directory in the Terminal window at the bottom right of VS Code.  This is done with the CD command, like so:

snip_20181106050219

Once you’ve changed the directory to the one with your skill in it, press the up arrow on the keyboard a couple of times to get the “ask deploy” command back.  Then press enter.

If all goes well, a few moments later, your skill will be deployed:

snip_20181106050943

Now that your skill is deployed, we can switch over to the Alexa Developer Console to test it.  If you still have your template project open from earlier, click “Your Skills” in the top left corner.  Then you will see your deployed skill at the top of the list:

snip_20181106051315

Click the Edit link on the far right.  On the Build tab, you should see a little popup saying your build was successful:

snip_20181106051456

Click on the Test tab.  If it’s not enabled, enable testing for this skill:

snip_20181106051713

In the Alexa Simulator, type in “start Roll Some Dice and roll 2 dice”.  Press enter.

snip_20181106051938

Alexa should say that she rolled some dice and list the results:

snip_20181109075156

I just realized, there’s one last step:  Submitting Your Skill for Amazon Approval.  This will be covered in my next blog entry.