about a father-daugher project in which I and my college student daughter Susan collaborated on a
implementing the game of GHOST. This is one of those games your family might have played when you were in the car or sitting around the table or a campfire: you say letters in turn until somebody makes a word, and then they lose the round. We'd next planned to have her design a user experience during Christmas break, but we didn't get around to it. This month, I got the idea to voice-enable Ghost for Amazon Alexa (accessible on the Echo, Echo Dot, FireStick, the Alexa App on phones, and lots of other places).
I had been looking for an excuse to create a voice-assistant application for Alexa, and decided a speech front-end for GHOST would be the perfect project since the back-end already existed. This post describes how I went about creating my
(as they're called), and how it turned out. I have not published my game--it isn't fully debugged yet, and
(not mine). My main purpose was learning how to do an Alexa Skill, and that's what I accomplished.
Before we go into how I created the skill, let's take a look at the end result. Below are some audio clips of actual gameplay on Alexa. Take a listen.
Here's a visual rendering of a dialog with the game (bolded words spoken by human).
Our GHOST web site had a pretty simple user interface: the word-in-progress letters were displayed. Users could type a letter to take a turn, and also had a few buttons they could click such as I Challenge You, That's a Word, and New Game. This should translate pretty readily to spoken phrases:
And, when the computer challenge the human, we'll need to process their response:
- They state their word: "My word was tofu."
- They admit they were bluffing: "I was bluffing" or "you got me."
Architecture
In terms of the components we need to create, an
Alexa Custom Skill usually has two (or more) parts:
- There's the skill defnition, which you create and configure in the Amazon Developer portal. This includes:
• invocation name: official name of skill used to open it.
• utterances: speech patterns to recognize.
• intents: groups of utterances that express the same intended action.
• slots: variables of various types that can occur in intents. You can define your own custom slot types by providing a list of values--such as letters of the alphabet.
- A cloud service, usuallly implemented as an AWS Lambda Function (a server-less computing component). This can be written in a variety of languages, including Node.js (JavaScript) or Python.
- Lastly, your cloud service may in turn need to talk to something in the outside world, such as an API or service. For us, that's the existing back-end server functions of the Ghost web site.
In the case of Ghost, these components are:
- A skill definition named Ghost, invoked with the name "Ghost David".
- A Node.js AWS Lambda function. This function has to do the same things the Ghost web site's front-end does: implement the game logic and make calls to the back end server. The JavaScript logic that was originally in the Ghost web site's front end is replicated here.
- The original Ghost web site's server will respond to the Lambda function, providing functionality such as playing a turn; checking the validity of a word; responding to a player challenge; and adding new words. The server queries and updates the word list SQL Server database. The Lambda function will be calling actions like /Home/Play/word, /Home/ValidWord/word, and /Home/Challenge/word.
Architecture: Alexa and GHOST Web Site Share a Common Back-End
This entire skill was written in Amazon portal interfaces: the Skill definition in the developer portal, and the JavaScript code for the Lambda Function in the AWS portal. No desktop tools required!
Skill Definition
A skill definition is configured in the Amazon developer portal. In the Intents section you specify intended user actions. Some of the intents for Ghost are "My Letter Is...", "I Challenge You", and "That's a Word".
Skills Developer Portal
For each intent, you can specify a collection of utterances. This allows you to cover variations in what a user might say. Your utterances can contain variable placeholder such as {Letter}, called slots. A slot is passsed to the Lambda Function. Thus, when someone says "My letter is B" to Ghost, that utterance will be recognized as intent MyLetterIsIntent. That will be passed to the back end with slot Letter set to "B". The back end will then have the opportunity to exercise logic and decide on a suitable response.
Intent
These slots can be of different types. There are many pre-defined slot types, such as a date or city. However, you can also define your own custom slots, where you provide a list of values. That is what was done for the Letter slot. The values define include not only A-Z, but also Alpha-Zulu: the NATO alphabet, a useful fallback if Alexa can't understand a single letter. Thus, I can say "B" or "Bravo": both will be understood as B by the Lambda Function.
Slot Values
On the Endpoint page, the skill is linked to the AWS Lambda Function. The Lambda function's ARN ID is stored in the skill's configuration. You can also associate the skill ID in the Lambda function's configuration which is recommended.
Skill Endpoint
Lambda Function
The Lambda Function has functions for announcing a welcome message, and for responding to intents. Let's look at some of them.
The method getWelcomeResponse provides the initial spoken prompts when the game is started. We also create a structure of session variables here: we will need to maintain state throughout the sesson in order to track the word-in-play, who went first, and player scores. In this function the computer also takes the first turn, so there is an HTTP call to the server to get a letter to play. The letter is included in the spoken response. A callback passes the response back to the skill, and the user hears something like the following (the initial letter is random each time):
"Welcome to Ghost. To hear the rules, say help. I'll start. P."
// --------------- Functions that control the skill's behavior -----------------------
// getWelcomeResponse: intiial greeting to user. Initialize session attributes. Make first turn.
function getWelcomeResponse(callback) {
// If we wanted to initialize the session to have some attributes we could add those here.
let sessionAttributes = {
inChallenge: false, // true if in a challenge.
gameInSession: true, // true if a game is in-progress.
turn: true, // true = human player turn, false = computer player turn
word: '', // current partial word in play.
saveWord: '', // saved word before last player move
startPlayer: "0", // 0: computer player starts, 1: human player starts.
humanGhost: 0, // human player's loss count toward being a G-H-O-S-T.
computerGhost: 0, // computer player's loss count toward being a G-H-O-S-T.
unrecognizedLetterCount: 0, // number of times we've failed to understand the human's spoken letter.
newWord: '' // new word we learned from human
};
const cardTitle = 'Welcome';
sessionAttributes.startPlayer = "0"; // 0: computer goes first, 1: human goes first
var speechOutput = 'Welcome to Ghost. To hear the rules, say help. I\'ll start. ';
var repromptText = 'Say a letter';
var shouldEndSession = false;
httpGet('Home/Play/' + sessionAttributes.word + '?sp=' + sessionAttributes.startPlayer, (letter) => {
letter = trimLetter(letter);
if (letter != null) {
speechOutput = speechOutput + '"' + letter + '".';
sessionAttributes.word = sessionAttributes.word + letter;
}
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
});
}
One important method is onIntent, whose purpose is to invoke a handler function for an intent based its name. For example, the intent type MyLetterIsIntent is handled by the function humanPlay.
/**
* Called when the user specifies an intent for this skill.
*/
function onIntent(intentRequest, session, callback) {
console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);
const intent = intentRequest.intent;
const intentName = intentRequest.intent.name;
// Dispatch to your skill's intent handlers
if (intentName === 'MyLetterIsIntent') {
humanPlay(intent, session, callback);
} else if (intentName === 'WhatIsWordIntent') {
whatIsWord(intent, session, callback);
} else if (intentName === 'ThatsAWordIntent') {
thatsAWord(intent, session, callback);
} else if (intentName==='NewGameIntent') {
newGame(intent, session, callback);
} else if (intentName==='IChallengeYouIntent') {
humanChallenge(intent, session, callback);
} else if (intentName==='MyWordIsIntent') {
humanMyWordIs(intent, session, callback);
} else if (intentName==='UndoIntent') {
undoIntent(intent, session, callback);
} else if (intentName==='BluffingIntent') {
humanBluffing(intent, session, callback);
} else if (intentName==='AddNewWordIntent') {
addNewWord(intent, session, callback);
} else if (intentName==='HelpIntent') {
helpIntent(intent, session, callback);
} else if (intentName==='DebugIntent') {
debugIntent(intent, session, callback);
} else if (intentName === 'AMAZON.HelpIntent') {
getWelcomeResponse(callback);
} else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
handleSessionEndRequest(callback);
} else if (intentName === 'AMAZON.FallbackIntent') {
handleFallbackIntent(intent, session, callback)
} else {
console.log('intentName: ' + intentName);
throw new Error('Invalid intent');
}
}
the humanPlay function is one of the larger functions. It handles the human game play, where the human has spoken a letter. The letter can come in a few different ways, including a NATO alphabet word, so a function named trimLetter is employed to identify the letter and return a value like "A", "B", etc. The letter is added to the current word, and then evaluated.
If the user has made a valid word, determined by asking the server to check its word list, then the human has lost. Their GHOST count is incremented, and they are told they are now a G, or a G-H, etc. A new round starts.
"Stone. That's a word. I win! Let's start a new game. You go first."
Otherwise, the computer attempts to play, by passing the current word to the server and asking it to play. If the response is a letter, it is played (spoken) and added to the current word. If however the server responds with "?", a challenge is issued.
// -------------------- Human Plays Letter --------------------
// A letter was played. Set letter in session and prepares speech to reply to the user.
function humanPlay(intent, session, callback) {
const cardTitle = intent.name;
const letterSlot = intent.slots.Letter;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
sessionAttributes.saveWord = sessionAttributes.word;
console.log('humanPlay 02 saveWord: ' + sessionAttributes.saveWord);
if (!sessionAttributes.gameInSession) {
speechOutput = "To start a new game, say 'New Game'.";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
if (sessionAttributes.inChallenge) {
speechOutput = "Please respond to the challenge. Either tell me what your word was, or admit you were bluffing. Or say 'New Game' to start a new game..";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
if (letterSlot) {
var letter = trimLetter(letterSlot.value);
if (letter===null) {
speechOutput = "I'm not sure what your letter is.";
sessionAttributes.unrecognizedLetterCount++;
if (sessionAttributes.unrecognizedLetterCount==2)
speechOutput += " Please try again, or say a word from the NATO alphabet like Alpha or Bravo.";
else
speechOutput += " Please try again.";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
else {
console.log('Human played: ' + letter + ' | current word: ' + sessionAttributes.word);
sessionAttributes.unrecognizedLetterCount = 0;
//sessionAttributes = createLetterAttributes(letter);
sessionAttributes.word = sessionAttributes.word + letter;
// Human play - check for a completed word
if (sessionAttributes.word.length >= 3) {
httpGet('Home/ValidWord/' + sessionAttributes.word, (res) => {
if (res=='true') {
sessionAttributes.humanGhost++;
speechOutput = sessionAttributes.word + ". That\'s a word! I win! You\'re a " + score(sessionAttributes.humanGhost) + ". Let's start a new game. You go first.";
sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
sessionAttributes.word = '';
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
});
}
// Computer play
httpGet('Home/Play/' + sessionAttributes.word + '?sp=' + sessionAttributes.startPlayer, (res) => {
res = trimLetter(res);
if (res==='?') { // challenge
speechOutput = 'I challenge you! What\'s your word?';
}
else {
console.log('Computer played: ' + letter + ' | current word: ' + sessionAttributes.word);
speechOutput = speechOutput + '"' + res + '".';
sessionAttributes.word = sessionAttributes.word + res;
}
// If the user either does not reply to the welcome message or says something that is not
// understood, they will be prompted again with this text.
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
});
}
} else {
speechOutput = "There is a data problem in the skill: letter slot not found.";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
}
humanPlay function
The user has other options besides playing a letter. The human can say "I challenge you", in which case the humanChallenge function is run. If the program had a valid word, it will spell it out and score a loss for the human. Otherwise, it will admit it was bluffing and score a loss for itself.
// -------------------- Human Challenges Computer --------------------
// Human challenges computer. Tell them our word, or admit we were bluffing.
function humanChallenge(intent, session, callback) {
const cardTitle = intent.name;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
httpGet('Home/Challenge/' + sessionAttributes.word, (res) => {
if (res==='^' || res==='"^"') {
sessionAttributes.computerGhost++;
speechOutput ="You got me - I was bluffing. That makes me a " + score(sessionAttributes.computerGhost) + ". Let's start a new game. You go first.";
sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
sessionAttributes.word = '';
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
else {
sessionAttributes.humanGhost++;
speechOutput = "My word was " + res + ". " + spellWord(res) + ". I win! You\'re a " + score(sessionAttributes.humanGhost) + ". Let's start a new game. You go first.";
sessionAttributes.startPlayer = "1";
sessionAttributes.word = '';
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
});
}
humanChallenge function
Test and Debug
How do you test this kind of application? From the Developer Portal, there's an Alexa Simulator; but if you have an actual device on the same account then you'll be able to call up your skill even though it hasn't been officially published.
How do you debug an Alexa Skill? Well, all you have to do is add console.log(text); statements in your Lambda Function's JavaScript. The output will be captured in CloudWatch logs. Every time you conduct an Alexa Session, there's a CloudWatch log you can inspect. These logs are also where error detail will be found if your Lambda Function has an unhandled exception. In the log excerpt below, the "Human played..." and "Computer played..." lines were added by console.log statements.
CloudWatch Logs
Post-Mortem: Rapid development, Prolonged Debugging
Ghost was my very first attempt at developing an Alexa skill. To give you some idea of how rapidly you can learn Alexa Skills, here's what I experienced in my first 3 days:
1. Saturday afternoon. I went to Best Buy and purchased an Echo Dot for $24.
2. I went through the
Color Picker quickstart. I registered as a developer with Amazon. Some of the instructions in the quickstart were hard to follow, because some of the developer site screens and AWS screens had changed since the tutorial was written. But I stayed with it, and in a couple of hours I was able to say "Open Color Picker" to my Echo Dot and tell it my favorite color. I chose the Node.js version (JavaScript), but there are also versions of this quickstart where the Lambda Function is written in other languages such as Python.
3. Now feeling empowered, I worked on a derivative of Color Picker that would search my blog. To achieve that, I had to look up how to do an HTTP request from a Node.js Lambda function. In another couple of hours, I had this project working. I now felt ready and empowered to do something real.
4. On Sunday, I started work on my Ghost Alexa skill, again using Color Picker as my starting point.
5. By end of day Monday, I was finished with the basic implementation: I could play Ghost with my Echo Dot. All that remains is thorough testing and debug.
If you're going to work on an Alexa Skill of any depth, you're likely going to find like me that initial development goes pretty quickly, but you can expect to spend quite a bit of time on test and debug before your skill is ready for prime time. There are a number of reasons for this, chief of which is the surprise factor: the user may not say things you expect them to, or what they say may be misunderstood. It took some time to learn the nuances of effectively communicating this way (you might call this "elastic communication"). If you want your Alexa Skill to be smarter than a voicemail menu, then you'll need to invest some effort--but it will be worth it in the usability of what you end up with. All in all, I'm pleased with how quickly I became functional in this new area.
Will I publish Ghost? Perhaps, after I finish debugging and refining it.
Source Code
Below is the full source code to the Alexa Skill and the Node.js Lambda Function.
Alexa Skill
{
"interactionModel": {
"languageModel": {
"invocationName": "ghost david",
"intents": [
{
"name": "AMAZON.FallbackIntent",
"samples": []
},
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": [
"exit game",
"exit",
"stop game",
"quit game",
"stop",
"quit",
"end game"
]
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "MyLetterIsIntent",
"slots": [
{
"name": "Letter",
"type": "LIST_OF_LETTERS"
}
],
"samples": [
"Let's try {Letter}",
"{Letter}",
"My letter is {Letter}"
]
},
{
"name": "WhatIsWordIntent",
"slots": [],
"samples": [
"What is the current word",
"What is current word",
"what's the word again",
"what's the word so far",
"tell me word",
"What's the current word",
"What's the word",
"Say word",
"What is the word",
"What is word"
]
},
{
"name": "ThatsAWordIntent",
"slots": [],
"samples": [
"You lose",
"That is a word",
"I win",
"You made a word",
"Tha'ts a word"
]
},
{
"name": "NewGameIntent",
"slots": [],
"samples": [
"redo",
"start over",
"start new game",
"new game"
]
},
{
"name": "IChallengeYouIntent",
"slots": [],
"samples": [
"challenge",
"Tell me your word",
"What is your word",
"I give up",
"I challenge you"
]
},
{
"name": "MyWordIsIntent",
"slots": [
{
"name": "ClaimedWord",
"type": "ANY_WORD"
}
],
"samples": [
"My word was {ClaimedWord}",
"My word is {ClaimedWord}"
]
},
{
"name": "DebugIntent",
"slots": [],
"samples": [
"log debug",
"log variables",
"debug log"
]
},
{
"name": "UndoIntent",
"slots": [],
"samples": [
"do over",
"take it back",
"undo my letter",
"change my letter",
"change my last turn",
"backspace",
"undo",
"undo my last turn"
]
},
{
"name": "HelpIntent",
"slots": [],
"samples": [
"i need the rule",
"explain",
"what do I do now",
"what do I do",
"what are the rules",
"what commands do you know",
"help"
]
},
{
"name": "BluffingIntent",
"slots": [],
"samples": [
"busted",
"Oops",
"I have no word",
"I had no word",
"You caught me I was bluffing",
"You caught me",
"I didn't have a word",
"You got me I was bluffing",
"You got me",
"I was bluffing"
]
},
{
"name": "AddNewWordIntent",
"slots": [],
"samples": [
"add word",
"add new word",
"yes add new word",
"yes add word",
"yes"
]
}
],
"types": [
{
"name": "LIST_OF_LETTERS",
"values": [
{
"name": {
"value": "zulu"
}
},
{
"name": {
"value": "yankee"
}
},
{
"name": {
"value": "x-ray"
}
},
{
"name": {
"value": "whiskey"
}
},
{
"name": {
"value": "victor"
}
},
{
"name": {
"value": "uniform"
}
},
{
"name": {
"value": "tango"
}
},
{
"name": {
"value": "sierra"
}
},
{
"name": {
"value": "romeo"
}
},
{
"name": {
"value": "quebec"
}
},
{
"name": {
"value": "papa"
}
},
{
"name": {
"value": "oscar"
}
},
{
"name": {
"value": "november"
}
},
{
"name": {
"value": "mike"
}
},
{
"name": {
"value": "lima"
}
},
{
"name": {
"value": "kilo"
}
},
{
"name": {
"value": "juliett"
}
},
{
"name": {
"value": "india"
}
},
{
"name": {
"value": "hotel"
}
},
{
"name": {
"value": "golf"
}
},
{
"name": {
"value": "foxtrot"
}
},
{
"name": {
"value": "echo"
}
},
{
"name": {
"value": "delta"
}
},
{
"name": {
"value": "alpha"
}
},
{
"name": {
"value": "charlie"
}
},
{
"name": {
"value": "bravo"
}
},
{
"id": "LETTER_Z",
"name": {
"value": "z",
"synonyms": [
"zulu"
]
}
},
{
"id": "LETTER_Y",
"name": {
"value": "y",
"synonyms": [
"Yankee"
]
}
},
{
"id": "LETTER_X",
"name": {
"value": "x",
"synonyms": [
"X-Ray"
]
}
},
{
"id": "LETTER_W",
"name": {
"value": "w",
"synonyms": [
"Whiskey"
]
}
},
{
"id": "LETTER_V",
"name": {
"value": "v",
"synonyms": [
"Victor"
]
}
},
{
"id": "LETTER_U",
"name": {
"value": "u",
"synonyms": [
"Uniform"
]
}
},
{
"id": "LETTER_T",
"name": {
"value": "t",
"synonyms": [
"Tango"
]
}
},
{
"id": "LETTER_S",
"name": {
"value": "s",
"synonyms": [
"Sierra"
]
}
},
{
"id": "LETTER_R",
"name": {
"value": "r",
"synonyms": [
"Romeo"
]
}
},
{
"id": "LETTER_Q",
"name": {
"value": "q",
"synonyms": [
"Quebec"
]
}
},
{
"id": "LETTER_P",
"name": {
"value": "p",
"synonyms": [
"Papa"
]
}
},
{
"id": "LETTER_O",
"name": {
"value": "o",
"synonyms": [
"Oscar"
]
}
},
{
"id": "LETTER_N",
"name": {
"value": "n",
"synonyms": [
"November"
]
}
},
{
"id": "LETTER_M",
"name": {
"value": "m",
"synonyms": [
"Mike"
]
}
},
{
"id": "LETTER_L",
"name": {
"value": "l",
"synonyms": [
"Lima"
]
}
},
{
"id": "LETTER_K",
"name": {
"value": "k",
"synonyms": [
"Kilo"
]
}
},
{
"id": "LETTER_J",
"name": {
"value": "j",
"synonyms": [
"Juliett"
]
}
},
{
"id": "LETTER_I",
"name": {
"value": "i",
"synonyms": [
"India"
]
}
},
{
"id": "LETTER_H",
"name": {
"value": "h",
"synonyms": [
"Hotel"
]
}
},
{
"id": "LETTER_G",
"name": {
"value": "g",
"synonyms": [
"Golf"
]
}
},
{
"id": "LETTER_F",
"name": {
"value": "f",
"synonyms": [
"Foxtrot"
]
}
},
{
"id": "LETTER_E",
"name": {
"value": "e",
"synonyms": [
"Echo"
]
}
},
{
"id": "LETTER_D",
"name": {
"value": "d",
"synonyms": [
"Delta"
]
}
},
{
"id": "LETTER_C",
"name": {
"value": "c",
"synonyms": [
"Charlie"
]
}
},
{
"id": "LETTER_B",
"name": {
"value": "b",
"synonyms": [
"Bravo"
]
}
},
{
"id": "LETTER_A",
"name": {
"value": "a",
"synonyms": [
"alpha"
]
}
}
]
},
{
"name": "ANY_WORD",
"values": [
{
"name": {
"value": "Alpha"
}
}
]
}
]
}
}
}
Lambda Function
'use strict';
var http = require('http');
//---------- G H O S T ----------
// Ghost game AWS Lambda function. This function pairs with the Alexa skill David Ghost.
// The Ghost game for Alexa has 3 components:
// 1. An Alexa skill named Ghost David (unpublished).
// 2. AWS Lambda function (this code), which takes the place of the web site JavaScript at http://ghost-susanpallmann.com.
// 3. The ASP.NET MVC web site back-end at http://ghost-susanpallmann.com. see http://davidpallmann.blogspot.com/2018/12/ghost-father-daughter-project-part-1.html
//
// To output to the CloudWatch logs for this function, simply add console.log('message'); statements to the code.
/**
* This sample demonstrates a simple skill built with the Amazon Alexa Skills Kit.
* The Intent Schema, Custom Slots, and Sample Utterances for this skill, as well as
* testing instructions are located at http://amzn.to/1LzFrj6
*
* For additional samples, visit the Alexa Skills Kit Getting Started guide at
* http://amzn.to/1LGWsLG
*/
// --------------- Helpers that build all of the responses -----------------------
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
return {
outputSpeech: {
type: 'PlainText',
text: output,
},
card: {
type: 'Simple',
title: `SessionSpeechlet - ${title}`,
content: `SessionSpeechlet - ${output}`,
},
reprompt: {
outputSpeech: {
type: 'PlainText',
text: repromptText,
},
},
shouldEndSession,
};
}
function buildResponse(sessionAttributes, speechletResponse) {
return {
version: '1.0',
sessionAttributes,
response: speechletResponse,
};
}
// --------------- Functions that control the skill's behavior -----------------------
// getWelcomeResponse: intiial greeting to user. Initialize session attributes. Make first turn.
function getWelcomeResponse(callback) {
// If we wanted to initialize the session to have some attributes we could add those here.
let sessionAttributes = {
inChallenge: false, // true if in a challenge.
gameInSession: true, // true if a game is in-progress.
turn: true, // true = human player turn, false = computer player turn
word: '', // current partial word in play.
saveWord: '', // saved word before last player move
startPlayer: "0", // 0: computer player starts, 1: human player starts.
humanGhost: 0, // human player's loss count toward being a G-H-O-S-T.
computerGhost: 0, // computer player's loss count toward being a G-H-O-S-T.
unrecognizedLetterCount: 0, // number of times we've failed to understand the human's spoken letter.
newWord: '' // new word we learned from human
};
const cardTitle = 'Welcome';
sessionAttributes.startPlayer = "0"; // 0: computer goes first, 1: human goes first
var speechOutput = 'Welcome to Ghost. To hear the rules, say help. I\'ll start. ';
var repromptText = 'Say a letter';
var shouldEndSession = false;
httpGet('Home/Play/' + sessionAttributes.word + '?sp=' + sessionAttributes.startPlayer, (letter) => {
letter = trimLetter(letter);
if (letter != null) {
speechOutput = speechOutput + '"' + letter + '".';
sessionAttributes.word = sessionAttributes.word + letter;
}
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
});
}
// -------------------- Human Plays Letter --------------------
// A letter was played. Set letter in session and prepares speech to reply to the user.
function humanPlay(intent, session, callback) {
const cardTitle = intent.name;
const letterSlot = intent.slots.Letter;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
sessionAttributes.saveWord = sessionAttributes.word;
console.log('humanPlay 02 saveWord: ' + sessionAttributes.saveWord);
if (!sessionAttributes.gameInSession) {
speechOutput = "To start a new game, say 'New Game'.";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
if (sessionAttributes.inChallenge) {
speechOutput = "Please respond to the challenge. Either tell me what your word was, or admit you were bluffing. Or say 'New Game' to start a new game..";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
if (letterSlot) {
var letter = trimLetter(letterSlot.value);
if (letter===null) {
speechOutput = "I'm not sure what your letter is.";
sessionAttributes.unrecognizedLetterCount++;
if (sessionAttributes.unrecognizedLetterCount==2)
speechOutput += " Please try again, or say a word from the NATO alphabet like Alpha or Bravo.";
else
speechOutput += " Please try again.";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
else {
console.log('Human played: ' + letter + ' | current word: ' + sessionAttributes.word);
sessionAttributes.unrecognizedLetterCount = 0;
//sessionAttributes = createLetterAttributes(letter);
sessionAttributes.word = sessionAttributes.word + letter;
// Human play - check for a completed word
if (sessionAttributes.word.length >= 3) {
httpGet('Home/ValidWord/' + sessionAttributes.word, (res) => {
if (res=='true') {
sessionAttributes.humanGhost++;
speechOutput = sessionAttributes.word + ". That\'s a word! I win! You\'re a " + score(sessionAttributes.humanGhost) + ". Let's start a new game. You go first.";
sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
sessionAttributes.word = '';
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
});
}
// Computer play
httpGet('Home/Play/' + sessionAttributes.word + '?sp=' + sessionAttributes.startPlayer, (res) => {
res = trimLetter(res);
if (res==='?') { // challenge
speechOutput = 'I challenge you! What\'s your word?';
}
else {
console.log('Computer played: ' + letter + ' | current word: ' + sessionAttributes.word);
speechOutput = speechOutput + '"' + res + '".';
sessionAttributes.word = sessionAttributes.word + res;
}
// If the user either does not reply to the welcome message or says something that is not
// understood, they will be prompted again with this text.
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
});
}
} else {
speechOutput = "There is a data problem in the skill: letter slot not found.";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
}
// Return human or computer loss score as a phrase in the form of G-H-O...
function score(ghostScore) {
switch(ghostScore) {
case 0:
return "";
case 1:
return "G";
case 2:
return "G-H";
case 3:
return "G-H-O";
case 4:
return "G-H-O-S";
default:
case 5:
return "G-H-O-S-T";
}
}
// --------------- Start New Game -----------------------
function newGame(intent, session, callback) {
// If we wanted to initialize the session to have some attributes we could add those here.
let sessionAttributes = session.attributes;
const cardTitle = 'Welcome';
sessionAttributes.startPlayer = "0"; // 0: computer goes first, 1: human goes first
sessionAttributes.word = '';
var speechOutput = 'New game. I\'ll start. ';
var repromptText = 'Say a letter';
var shouldEndSession = false;
httpGet('Home/Play/' + sessionAttributes.word + '?sp=' + sessionAttributes.startPlayer, (letter) => {
letter = trimLetter(letter);
if (letter != null) {
speechOutput = speechOutput + '"' + letter + '".';
sessionAttributes.word = sessionAttributes.word + letter;
}
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
});
}
function handleSessionEndRequest(callback) {
const cardTitle = 'Session Ended';
const speechOutput = 'Thank you for playng Ghost. Be seeing you!';
// Setting this to true ends the session and exits the skill.
const shouldEndSession = true;
callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}
// -------------------- Human Asks for Word --------------------
// Tell the user the current word, by spelling it out. e.g. "The current word is S-T-R-E".
function whatIsWord(intent, session, callback) {
const cardTitle = intent.name;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
speechOutput = "The current word is: ";
if (sessionAttributes.word === '') {
speechOutput = speechOutput + ' empty';
}
else {
for (var w = 0; w < sessionAttributes.word.length; w++) {
speechOutput = speechOutput + sessionAttributes.word.substring(w, w+1) + '. ';
}
}
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
// Input not understood. Say something.
function handleFallbackIntent(intent, session, callback) {
const cardTitle = intent.name;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
speechOutput = "It\'s your turn to say a letter.";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
// -------------------- Human Says Computer Made a Word --------------------
// Accept human's word for it, and accept a loss. TOOD: add new word to vocabulary.
function thatsAWord(intent, session, callback) {
const cardTitle = intent.name;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
sessionAttributes.computerGhost++;
speechOutput = "Congratulations, you beat me. Now I'm a " + score(sessionAttributes.computerGhost) + ". Why don't you start a new word?";
sessionAttributes.word = '';
sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
// -------------------- Human Challenges Computer --------------------
// Human challenges computer. Tell them our word, or admit we were bluffing.
function humanChallenge(intent, session, callback) {
const cardTitle = intent.name;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
httpGet('Home/Challenge/' + sessionAttributes.word, (res) => {
if (res==='^' || res==='"^"') {
sessionAttributes.computerGhost++;
speechOutput ="You got me - I was bluffing. That makes me a " + score(sessionAttributes.computerGhost) + ". Let's start a new game. You go first.";
sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
sessionAttributes.word = '';
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
else {
sessionAttributes.humanGhost++;
speechOutput = "My word was " + res + ". " + spellWord(res) + ". I win! You\'re a " + score(sessionAttributes.humanGhost) + ". Let's start a new game. You go first.";
sessionAttributes.startPlayer = "1";
sessionAttributes.word = '';
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
});
}
// -------------------- Human Challenges Computer --------------------
// Human admis they were bluffing after being challenged by computer.
function humanBluffing(intent, session, callback) {
const cardTitle = intent.name;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
sessionAttributes.humanGhost++;
speechOutput = "Sorry, you lose. You're now a " + score(sessionAttributes.humanGhost) + " Let's start a new game. You go first.";
sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
sessionAttributes.word = '';
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
// -------------------- Human Suppies Word upon Challenge --------------------
// Human claims they had a valid word after being challenged.
function humanMyWordIs(intent, session, callback) {
const cardTitle = intent.name;
var claimedWordSlot = intent.slots.ClaimedWord;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
if (claimedWordSlot && claimedWordSlot.value) {
var claimedWord = claimedWordSlot.value;
sessionAttributes.newWord = claimedWordSlot.value;
if (sessionAttributes.newWord && sessionAttributes.word) {
sessionAttributes.newWord = sessionAttributes.newWord.toUpperCase();
var existingWord = sessionAttributes.word.toUpperCase();
var index = sessionAttributes.newWord.indexOf(existingWord);
if (index != 0) {
speechOutput = "I'm sorry, but that word doesn't match the word in play. The word in play is " + spellWord(sessionAttributes.word) + ".";
}
else {
if (claimedWord.toLowerCase()===sessionAttributes.word.toLowerCase()) {
sessionAttributes.humanGhost++;
speechOutput = sessionAttributes.word + "You just made a complete word so I win! You\'re a " + score(sessionAttributes.humanGhost) + ". Let's start a new game. You go first.";
sessionAttributes.word = '';
sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
}
else {
sessionAttributes.computerGhost++;
speechOutput = "I see. You win, and I'm a " + score(sessionAttributes.computerGhost) + ". Congratulations. Should I add this word to my dictionary? " + claimedWord + '. ' + spellWord(claimedWordSlot.value) + " Say 'Add Word' to confirm.";
}
}
}
}
else {
speechOutput = "Can you say your word again please?";
}
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
// Human confirms, Yes add new word.
function addNewWord(intent, session, callback) {
const cardTitle = intent.name;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
if (sessionAttributes.newWord != '') {
speechOutput = "Thanks. I have learned the word '" + sessionAttributes.newWord + "'. Let's start a new word. You can start.";
sessionAttributes.startPlayer = "1";
httpGet('Home/AddWord/' + sessionAttributes.newWord, (letter) => {
sessionAttributes.newWord = '';
sessionAttributes.word = '';
sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
});
}
else {
speechOutput = "Sorry, I don\'t understand. Please say a letter.";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
}
// Undo last turn (Alexa may have misunderstood letter)
function undoIntent(intent, session, callback) {
const cardTitle = intent.name;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
if (sessionAttributes.word==='') {
speechOutput = "There\'s nothing to undo: the current word is empty.";
}
else {
sessionAttributes.word = sessionAttributes.saveWord;
speechOutput = "I undid your last turn. The current word is: " + spellWord(sessionAttributes.word);
}
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
// Help - explain the rules of Ghost and available commands.
function helpIntent(intent, session, callback) {
const cardTitle = intent.name;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
speechOutput = "Let's play Ghost! Each player takes a turn adding a letter. If you make a real word of 3 letters or more, you're a G. Next time that happens, you're a G-H. When you get to G-H-O-S-T you're out. If you don't think a valid word can be made, you can challenge the other player.";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
// Render speech phrase to spell out a word a letter a time.
function spellWord(word) {
let speechOutput = '';
if (word === '') {
speechOutput = speechOutput + ' empty';
}
else {
for (var w = 0; w < word.length; w++) {
speechOutput = speechOutput + word.substring(w, w+1) + '. ';
}
}
return speechOutput;
}
// output key variables to log
function debugIntent(intent, session, callback) {
const cardTitle = intent.name;
let sessionAttributes = session.attributes;
let repromptText = 'Please say a letter to continue making a word.';
let shouldEndSession = false;
let speechOutput = '';
console.log('sessionAttributes.word: ' + sessionAttributes.word + ', startPlayer: ' + sessionAttributes.startPlayer);
speechOutput = "Variables have been logged";
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
// Determine the letter from input that may be quoted or may be a NATO alphabetic code like Alpha / Bravo / etc. Return just the letter.
// NOTE: ? is recognized as a letter, as that response is given by the back end for a challenge during computer play.
function trimLetter(letter) {
if (!letter) return null;
letter = letter.toUpperCase();
if (letter.indexOf('?') != -1) return '?';
if (letter.indexOf('A') != -1) return 'A';
if (letter.indexOf('B') != -1) return 'B';
if (letter.indexOf('C') != -1) return 'C';
if (letter.indexOf('D') != -1) return 'D';
if (letter.indexOf('E') != -1) return 'E';
if (letter.indexOf('F') != -1) return 'F';
if (letter.indexOf('G') != -1) return 'G';
if (letter.indexOf('H') != -1) return 'H';
if (letter.indexOf('I') != -1) return 'I';
if (letter.indexOf('J') != -1) return 'J';
if (letter.indexOf('K') != -1) return 'K';
if (letter.indexOf('L') != -1) return 'L';
if (letter.indexOf('M') != -1) return 'M';
if (letter.indexOf('N') != -1) return 'N';
if (letter.indexOf('O') != -1) return 'O';
if (letter.indexOf('P') != -1) return 'P';
if (letter.indexOf('Q') != -1) return 'Q';
if (letter.indexOf('R') != -1) return 'R';
if (letter.indexOf('S') != -1) return 'S';
if (letter.indexOf('T') != -1) return 'T';
if (letter.indexOf('U') != -1) return 'U';
if (letter.indexOf('V') != -1) return 'V';
if (letter.indexOf('W') != -1) return 'W';
if (letter.indexOf('X') != -1) return 'X';
if (letter.indexOf('Y') != -1) return 'Y';
if (letter.indexOf('Z') != -1) return 'Z';
return null;
}
// --------------- Events -----------------------
/**
* Called when the session starts.
*/
function onSessionStarted(sessionStartedRequest, session) {
console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}
/**
* Called when the user launches the skill without specifying what they want.
*/
function onLaunch(launchRequest, session, callback) {
console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);
// Dispatch to your skill's launch.
getWelcomeResponse(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}`);
const intent = intentRequest.intent;
const intentName = intentRequest.intent.name;
// Dispatch to your skill's intent handlers
if (intentName === 'MyLetterIsIntent') {
humanPlay(intent, session, callback);
} else if (intentName === 'WhatIsWordIntent') {
whatIsWord(intent, session, callback);
} else if (intentName === 'ThatsAWordIntent') {
thatsAWord(intent, session, callback);
} else if (intentName==='NewGameIntent') {
newGame(intent, session, callback);
} else if (intentName==='IChallengeYouIntent') {
humanChallenge(intent, session, callback);
} else if (intentName==='MyWordIsIntent') {
humanMyWordIs(intent, session, callback);
} else if (intentName==='UndoIntent') {
undoIntent(intent, session, callback);
} else if (intentName==='BluffingIntent') {
humanBluffing(intent, session, callback);
} else if (intentName==='AddNewWordIntent') {
addNewWord(intent, session, callback);
} else if (intentName==='HelpIntent') {
helpIntent(intent, session, callback);
} else if (intentName==='DebugIntent') {
debugIntent(intent, session, callback);
} else if (intentName === 'AMAZON.HelpIntent') {
getWelcomeResponse(callback);
} else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
handleSessionEndRequest(callback);
} else if (intentName === 'AMAZON.FallbackIntent') {
handleFallbackIntent(intent, session, callback)
} else {
console.log('intentName: ' + intentName);
throw new Error('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 cleanup logic here
}
// Retrieve a web page from the server. Query passed in as a parameter.
function httpGet(query, callback) {
var options = {
host: 'ec2-18-224-78-19.us-east-2.compute.amazonaws.com',
path: '/' + query,
method: 'GET',
};
var req = http.request(options, res => {
res.setEncoding('utf8');
var responseString = "";
//accept incoming data asynchronously
res.on('data', chunk => {
responseString = responseString + chunk;
});
//return the data when streaming is complete
res.on('end', () => {
//console.log('httpGet 02');
//console.log(responseString);
//console.log('httpGet 03');
callback(responseString);
});
});
req.end();
}
// --------------- Main handler -----------------------
// Route the incoming request based on type (LaunchRequest, IntentRequest,
// etc.) The JSON body of the request is provided in the event parameter.
exports.handler = (event, context, callback) => {
try {
console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);
/**
* Uncomment this if statement and populate with your skill's application ID to
* prevent someone else from configuring a skill that sends requests to this function.
*/
/*
if (event.session.application.applicationId !== 'amzn1.echo-sdk-ams.app.[unique-value-here]') {
callback('Invalid Application ID');
}
*/
if (event.session.new) {
onSessionStarted({ requestId: event.request.requestId }, event.session);
}
if (event.request.type === 'LaunchRequest') {
onLaunch(event.request,
event.session,
(sessionAttributes, speechletResponse) => {
callback(null, buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === 'IntentRequest') {
onIntent(event.request,
event.session,
(sessionAttributes, speechletResponse) => {
callback(null, buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === 'SessionEndedRequest') {
onSessionEnded(event.request, event.session);
callback();
}
} catch (err) {
callback(err);
}
};