Showing posts with label Ghost. Show all posts
Showing posts with label Ghost. Show all posts

Tuesday, January 22, 2019

Ghost Game, Part 2: Voice-Enabling with Alexa

Note: If you don't see proper syntax highlighting of code, change https to http in URL.

I few weeks ago, I wrote about a father-daugher project in which I and my college student daughter Susan collaborated on a web site 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 Alexa Skill (as they're called), and how it turned out. I have not published my game--it isn't fully debugged yet, and there is already a Ghost Game skill on the Alexa marketplace (not mine). My main purpose was learning how to do an Alexa Skill, and that's what I accomplished.

The End Result

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.

Session 1: Human makes word M-E-M-O-R-Y
Session 2: Computer bluffs, challenged by human. Human bluffs, challenged by computer.
Session 3: Human makes a word game doesn't know, teaches it a new word.
Session 4: Full game

Here's a visual rendering of a dialog with the game (bolded words spoken by human).

Human: Alexa, open Ghost David.
Alexa: Welcome to Ghost. To hear the rules, say help. I'll start. "M".
Human: "E"
Alexa; "M"
Human: "O"
Alexa: "R."
Human: "Y"
Alexa: "Memory. That's a word! I win. Youre a G. Let's start a new game. You go first."

Human: "T"
Alexa: "A"
Human: "R"
Alexa: "I challenge you. What's your word?"
Human: "My word was 'tarnish'."
Alexa: I see. You win, and I'm a G. Congratulations. Should I add this word to my dictionary? Tarnish. T-A-R-N-I-S-H
Human: Add Word
Alexa: Thanks. I have learned the word 'tarnish'.
Alexa: Let's start a new game. You can start.

Adapting a Web Site to be an Alexa Skill

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:

  • Instead of typing a letter to take a turn, the user will speak a letter: "C"
  • Instead of clicking a That's a Word button, the user will say "That's a word."
  • Instead of clicking the I Challenge You button, the user can say "I challenge you."
  • Instead of clicking the New Game button, the user can say "New Game."
Since this is a spoken interface, we'll also add some new action phrases:
  • If the user forgets what the current word is, they can ask "What is word?"
  • If the user forgets the rules for Ghost, they can say "help" or "What are the rules?"
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:
  1. 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.
  2. 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. 
  3. 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:
  1. A skill definition named Ghost, invoked with the name "Ghost David".
  2. 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. 
  3. 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);
    }
};


Sunday, December 9, 2018

Ghost : A Father-Daughter Project, Part 1

Over the last Thanksgiving break, our oldest daughter Susan came home from college for a few days. She is studying web design and marketing, but has found herself more and more becoming a front-end UX designer. She's been spending a lot of time working on HTML/CSS/JavaScript projects with great success.

While we had a few days together, we decided it would be fun to collaborate on a project that would be useful for her coursework: a program that learns. She suggested implementing the game of GHOST that our family has always played, and that's what we did. The game is here.

Rules of GHOST

In case you're not familiar with it, GHOST is a game well-suited for car trips, sitting around the dinner table, or any other time you have 2 to N people sitting around with nothing to do. It's a spelling game, and if you make a valid word of 3 letters or more, you've lost the round. The first time you lose a round, you're a "G". The next time you lose a round, you're a "GH". Once you get all the way to "GHOST", you're out of the game. Here's a sample sequence or two to give you the idea:

Player 1: K
Player 2: N
Player 3: O
Player 1: C
Player 2: K
Player 3: "That's a word. You're a G."

Player 1: P
Player 2: A
Player 1: C
Player 2: I
Player 1: N
Player 2: A
Player 1: "I challenge you. What was your word?"
Player 2: "Ah, you got me. I didn't want to go out on PACING, so I bluffed. I'm a G-H-O."

Although GHOST is a simple game, there are nuances and strategy to it. Programming a competent player is not as simple to implement programatically as you might think.

Our Game

Our version of Ghost will be a two-player edition, human against computer.

Since Susan has done front-end development but not back-end or database work, I volunteered to put together a starting point program, and write the back-end code to her specifications. Since I spend much of my time with ASP.NET, I created a sample ASP.NET Model-View-Controller (MVC) project, and added web.config and code to connect to a SQL Server database.

Since this game is supposed to learn, it most definitely needs a database so it can grow its word list as it plays. This is just the simplest of databases: one table named Words containing one colum, Word. We initially seeded the word list with a couple dozen entries, but ever since the list has been growing as a result of game play. At the time of this writing, it has over 1400 words. We could of course license a dictionary, but that would defeat the learning purpose of this exercise.


Our first objective was to implement basic game play in order to arrive at a functional game, although not a very smart one. The basic algorithm for game play is this:

Basic Gameplay Flowchart (click to enlarge)

When it's the human's turn, he or she has 3 options:

1. Play: press a letter key to continue the word.
2. Challenge: click a Challenge button.
3. That's a Word: click a That's a Word button.


The Challenge and That's a Word buttons aren't visible unless at least 3 letters have been played.

Initially the human player goes first. In each new round, the game will flip who the starting player is.

Flow for Human Plays a Letter

When the human presses a letter key, the letter is added to the current word in play. Non-letter keys are ignored.

Next, a check is made to see whether the human player has just completed a word: if they have, they have lost the round. This is done with a call to the back end to look up the current word-in-play against the Ghost word list.

SELECT word FROM words WHERE word=@round

If the word is found in the word list, the game informs the player and a loss is scored for them. The player gets the next letter in G-H-O-S-T, and if T has been reached the game is over.


If the word was not found in the word list, the computer needs to make a turn. A query is made of the word list for winning words. Winning words that begin with the current word-in-play and are also the right length (odd or even) such that the computer will win if the word is played out. For example, let's say the current word in play is B A C. That means a winning word must begin with BAC and also be an odd-number of characters. The search for winning words would inlude BACON but not BACK or BACCARAT. If one or more winning words are found, one is randomly selected and the next letter is played.

A good algorithm for selecting a winning word took some thought and experimentation. The example query below shows how a winning word is selected if the human player went first. The first WHERE clause ensures the word selected begins with the word-in-play. The second clause ensures the word selected is longer than the word-in-play. The third clause ensures the selected word, when played out, will result in the human player making a complete word and not the computer. The ORDER BY clause tells SQL Server to select a random order.

SELECT TOP 1 Word from Words 
WHERE word LIKE @round + '%' 
AND LEN(word)>LEN(@round) 
AND ((LEN(word) % 2)=0) 
ORDER BY NEWID()

The above query is actually augmented further, because we don't want to target a winning word only to discover we accidentally made a losing word on the way; for example, planning to play P to pursue the word ZIPPER would be a mistake because ZIP is a losing word. To achieve this, more WHERE clauses are added to the query to ensure the computer does not select any word that could result in a losing position.

If no winning words are found, then the computer must either challenge the user or make a bluff. We came up with this rule: if the word-in-play is 3 letters or more in length and there are no losing words in the word list, a challenge is made. The human player can then admit they were bluffing, or tell the computer their word. If the human player was bluffing, a loss is scored for them. If a word is provided, the game adds the word to its word list and scores a loss for itself.



If not challenging, then the computer must bluff. Initiallly a random letter was selected for bluffing in our game, but that was often too obvious a bluff in game play, with nonsense letter combinations. Susan came up with the idea of scanning the word list for the next letter to play. This results in more credible letter sequences. The bluff letter is played.

Flow for Human Clicks Challenge Button

The human player can click a Challenge button if they don't believe the computer is making a valid word.

The game looks for a word in its word list that begins with the current word-in-play. If found, the user is informed what the word is, and then someone loses the round. Usually this is the human player, except in the case where the computer's word has been fully played: in that case, the computer has made a complete word and loses the round.


If the computer was bluffing (no matching word in the word list), it admits it was bluffing and takes a loss.


Flow for Human Clicks That's a Word Button

The human player can click a That's a Word button to indicate the computer has made a complete word. Since the Ghost game is designed to learn as it plays, it trusts the human player to be truthful (and a good speller), and adds the word to its word list database. Now it knows the new word for future play.


Of course, trusting the human player comes with risks. That's the reason for the next section we'll discuss, Administration.

Administration

Our game has a page for viewing the word list. If you add administrator credentials, this page also allows adding and removing words. This is important because our game trusts the human player in learning new words. If there's a typographic error, or a disallowed word (like a proper name), or profanity, we want to be able to correct it or remove it.


The back end of administrative functions are simple DELETE and INSERT queries to the database.

Summary

Well, that's our game--so far. From a learning / intelligence perspective, Ghost can:

  • Learn new words
  • Distinguish between potential winning and losing words
  • Bluff convincingly

Susan is next going to re-do my placeholder front-end with her own UX design, which I'm sure will be highly creative. We'll cover that in a future Part 2.

I am greatly enjoying teaming up with my daughter on a project--something we haven't done since those middle school science project days that now seem so long ago.

Next: Ghost Game, Part 2: Voice-Enabling for Alexa