Alexa Gadget Skill API: Let’s Make a Game

In this post of the Alexa Gadget Skill API series, we create a real game for Alexa and Echo Buttons. I figured that I would create a game for my 18 month old to play with. I figure that he can tell the difference between lit up and unlit buttons. With this in mind, I create a game I called Whack-a-button. The game will randomly light up anywhere from 1-2 buttons at a time. The object is to press the lit buttons. Every time you press the right button you get one point. If you press an unlit button, you lose a point.

In the first two parts of this series we explored input handlers and setting light animations on the gadgets. We will pick up the work done in those two posts to create the Whack-a-button game.

Setting the Scene

It took me some time to build the code for this post. Getting something basic up and going was simple. Even now, I feel there are some rough edges to this. In particular, I had some problems with the input event handlers and the data I was receiving from them. This is due to one of two factors: either there is a bug in the simulator version of the input handlers or my input handler JSON is buggy. I’ll give more details when we get to that part.

My goal with this post was to create a framework in which the user can ask to play a specific game, but there is one skill that support all of these games. I assumed that we would have a game object in the session attributes that would store the type of game and the current state. Each game would have a different state representation. When a game is started, all input calls are delegated to that game’s code. The code decides whether what is to be done given an internal state and it decides when the game ends. At that point, we can kick it back to some menu for deciding the game. For the purpose of this post, I have only implemented one game but the pattern works for multiple games. Here’s a classy Visio diagram of the overall approach.

  1. The game starts with the Launch state.
  2. In this state we ask the user if they would like to play a game or ask them which game they want to play if we support more than one game.
  3. The user responds with a game selection.
  4. The internal state of our skill moves into the in game state…
  5. And we initialize the game. In this case, it is just Whack-a-button. The diagram illustrates the interface we expect each game to implement. We call the interface IGameTurn, because we create a new instance at each user input.
  6. The game delegates to theRollCall functionality first, as we need to make sure that the buttons are correctly identified before the game starts.
  7. The RollCall, sends its input handler…
  8. The user pressed the buttons and RollCall finished…
  9. And passes control back into the game by using the resumeAfterRollCall() call.
  10. The game initializes itself and sends the first input handler to the user. In our sample code, this will be a confirmation to press any button to get started.
  11. At this point, any input event should be delegated over to the game handle() method. We also assume that any AMAZON.HelpIntent or AMAZON.CancelIntent will be handled by the game’s help() or cancel() methods.
  12. The game responds to incoming events as long as it lasts.
  13. The game transitions to a PostGameState in which the user can restart the game or ask for their score.
  14. The user can exit the skill or restart the game.

Show Us The Code!

There is a lot of new code in this post, and I’m going to do my best to walk through it. As always, feel free to skip ahead and jump into the Github repo yourself.

At the center of everything is the IGameTurn interface. Each game must implement this functionality.


export interface IGameTurn {
    initialize(): Response;
    handle(): Response;
    help(): Response;
    cancel(): Response;
    postGameSummary(): Response;
    resumeAfterRollCall(): Response;
}

When the game is first created, we call initialize(). Initialize should invoke the RollCall functionality. Once RollCall is done, the resumeAfterRollCall() call is made. We begin in the InLaunchStateHandler. If the user responds with AMAZON.YesIntent to playing the game, we call:


if (req.intent.name === "AMAZON.YesIntent") {
    const game = new WhackabuttonGame(handlerInput);
    return game.initialize();
}

initialize() is defined as:


public initialize(): Response {
    const game = new GameState();
    game.currentGame = GameType.WhackaButton;
    game.data = new WhackState();

    GameHelpers.setState(this.handlerInput, game);
    return RollCall.initialize(this.handlerInput, WHACKABUTTON_NUM_OF_BUTTONS);
}

GameState is defined as follows. Note that for each method, it resolves the right IGameTurn instance based on the selected game type.


export class GameState {
    public currentGame: GameType;
    public data: any;

    public static deleteState(handlerInput: HandlerInput): void {
        const sessionAttr = handlerInput.attributesManager.getSessionAttributes();
        delete sessionAttr.game;
        handlerInput.attributesManager.setSessionAttributes(sessionAttr);
    }

    public static setInLaunchState(handlerInput: HandlerInput, val: boolean): void {
        const sessionAttr = handlerInput.attributesManager.getSessionAttributes();
        sessionAttr.inLaunch = val;
        handlerInput.attributesManager.setSessionAttributes(sessionAttr);
    }

    public static setInPostGame(handlerInput: HandlerInput, val: boolean): void {
        const sessionAttr = handlerInput.attributesManager.getSessionAttributes();
        sessionAttr.inPostGame = val;
        handlerInput.attributesManager.setSessionAttributes(sessionAttr);
    }

    public static getGameState(handlerInput: HandlerInput): GameState {
        const sessionAttr = handlerInput.attributesManager.getSessionAttributes();
        const game = sessionAttr.game;
        return new GameState(game);
    }

    constructor(obj?: GameState) {
        this.currentGame = GameType.None;
        if (obj) {
            this.currentGame = obj.currentGame;
            this.data = obj.data;
        }
    }

    public reinit(handlerInput: HandlerInput): Response {
        const gameTurn = this.resolveGameTurn(handlerInput);
        return gameTurn.initialize();
    }

    public resumeGameFromRollcall(handlerInput: HandlerInput): Response {
        const gameTurn = this.resolveGameTurn(handlerInput);
        return gameTurn.resumeAfterRollCall();
    }

    public cancel(handlerInput: HandlerInput): Response {
        const gameTurn = this.resolveGameTurn(handlerInput);
        return gameTurn.cancel();
    }

    public help(handlerInput: HandlerInput): Response {
        const gameTurn = this.resolveGameTurn(handlerInput);
        return gameTurn.help();
    }

    public handleInput(handlerInput: HandlerInput): Response {
        const gameTurn = this.resolveGameTurn(handlerInput);
        return gameTurn.handle();
    }

    private resolveGameTurn(handlerInput: HandlerInput): IGameTurn {
        switch (this.currentGame) {
            case GameType.WhackaButton:
                return new WhackabuttonGame(handlerInput);
            default:
                throw new Error("Unsupported game type.");
        }
    }

}

export enum GameType {
    None,
    WhackaButton
}

At this point, RollCall takes over. Any request from the user hits the RollCallHandler. We change the RollCall‘s handleDone() method to the following:


const gameState = GameState.getGameState(handlerInput);
handlerInput.responseBuilder
    .addDirective(blackOutUnusedButtons)
    .addDirective(lightUpSelectedButtons);
return gameState.resumeGameFromRollcall(handlerInput);

For the Whack-a-button game, the resumeAfterRollCall() method looks as follows:


public resumeAfterRollCall(): Response {
    const gameState = GameHelpers.getState(this.handlerInput, new WhackState());
    const whackState = gameState.data;
    whackState.waitingOnConfirmation = true;
    whackState.pushAndTrimHandler(this.handlerInput.requestEnvelope.request.requestId);
    GameHelpers.setState(this.handlerInput, gameState);

    const confirmationInputHandler = this.generateConfirmationHandler(GameHelpers.getAvailableButtons(this.handlerInput));

    const resp = LocalizedStrings.whack_start();
    this.handlerInput.responseBuilder
        .speak(resp.speech)
        .reprompt(resp.reprompt)
        .addDirective(confirmationInputHandler);
    return this.handlerInput.responseBuilder.getResponse();
}

We initialize a new game state, set some Whack-a-button specific state and ask the user to confirm when they are ready to start. The confirmation occurs by having the user press any of the selected buttons. That is the input handler that the this.generateConfirmationHandler(...) method generates.

At this point, control will flow into the InGameHandler. If there is a game object set and we receive either an InputHandlerEvent, a AMAZON.StopIntent, AMAZON.CancelIntent or AMAZON.HelpInent, we delegate the action to the current game. Here is the code for the handler.


export class InGameHandler implements RequestHandler {
    canHandle(handlerInput: HandlerInput): boolean {
        const sessionAttr = handlerInput.attributesManager.getSessionAttributes();
        const result = !sessionAttr.inPostGame &&
            !sessionAttr.inRollcall &&
            sessionAttr.game &&
            (handlerInput.requestEnvelope.request.type === "GameEngine.InputHandlerEvent"
                || handlerInput.requestEnvelope.request.type === "IntentRequest");

        console.log(`InGameHandler: ${result}`);
        return result;
    }

    handle(handlerInput: HandlerInput): Response {
        console.log("executing in game state handler");
        const gameState = GameState.getGameState(handlerInput);
        if (handlerInput.requestEnvelope.request.type === "GameEngine.InputHandlerEvent") {
            return gameState.handleInput(handlerInput);
        } else if (handlerInput.requestEnvelope.request.type === "IntentRequest") {
            const intent = handlerInput.requestEnvelope.request.intent;

            if (intent.name === "AMAZON.CancelIntent" || intent.name === "AMAZON.StopIntent") {
                return gameState.cancel(handlerInput);
            } else if (intent.name === "AMAZON.HelpIntent") {
                return gameState.help(handlerInput);
            } else if (intent.name === "AMAZON.StopIntent") {
                return handlerInput.responseBuilder
                    .speak(LocalizedStrings.goodbye().speech)
                    .withShouldEndSession(true)
                    .getResponse();
            } else {
                // empty response for anything else  that comes in during game play
                return handlerInput.responseBuilder.getResponse();
            }
        }
        throw new Error("Unexpected event type. Not supported in roll call.");

    }
}

Now For the Real Stuff

When an event comes in, it’ll be an indication for our game to begin.

  1. If we game has been going for longer than GAME_DURATION_SECONDS, we finish by responding with the user’s score.
  2. We begin a turn by randomly select some buttons that we want the user to select.
  3. We set buttons the user should press to a random color, not black.
  4. Buttons the user should not press have their color set to black.
  5. We generate a new input handler with a timeout between MIN_TIME_TO_PRESS and MAX_TIME_TO_PRESS.
  6. If the user presses a black button, we deduct the score and indicate they did something wrong.
  7. If the user presses a button she was supposed to press, we increase the score. If there are buttons left, we wait for those buttons to be pressed, otherwise we go back to step 1 for a new turn.

Selecting a random set of buttons and preparing the input handlers looks as follows:


// we select buttons randomly for the next turn
const shuffle = Utilities.shuffle(btns.slice(0));
const num = Utilities.randInt(1, shuffle.length);
console.log(`generating input handler with ${num} buttons.`);

const buttonsInPlay = shuffle.slice(0, num);
const buttonsNotInPlay = btns.filter(p => !buttonsInPlay.some(p1 => p1 === p));
console.log(`${buttonsInPlay.length} buttons in play for next turn: ${JSON.stringify(buttonsInPlay)}. ` +
        `Not in play: ${JSON.stringify(buttonsNotInPlay)}`);

// assign a random time duration to the turn, but make sure we don't go past the max game duration
const timeTilEnd = whackState.timeInMsUntilEnd();
console.log(`${timeTilEnd}ms left until end`);
const turnDuration = Math.min(Utilities.randInt(MIN_TIME_TO_PRESS, MAX_TIME_TO_PRESS), timeTilEnd);
whackState.expectedEvents = buttonsInPlay;
whackState.pushAndTrimHandler(this.handlerInput.requestEnvelope.request.requestId);
whackState.lastHandlerStartTime = moment().utc().format(Utilities.DT_FORMAT);
whackState.lastHandlerLength = turnDuration;

// generate the input handler
const startHandler = this.generateInputHandlerTemplate(btns, turnDuration);

// turn off buttons not assigned to this turn and turn on buttons assigned to the turn
const turnOffEverything = SetLightDirectiveBuilder.setLight(
    SkillAnimations.rollCallFinishedUnused(), buttonsNotInPlay.map(p => p.gadgetId));
const setLight = SetLightDirectiveBuilder.setLight(
    SkillAnimations.lightUpWhackaButton(turnDuration), buttonsInPlay.map(p => p.gadgetId));

 

I struggled with the right way to model the input handlers and the complexity of the code probably increased as a function of this; I blame myself and not fully understanding the rules of how Alexa reports events. My first approach was to create one input handler for the entirety of the game, but this would not work well with the MAX_TIME_TO_PRESS concept; I want there to be a time pressure involved. I could also not use the input handler’s shouldEndInputHandler functionality; if the current turn requires more than one button to be pressed, the same handler should be able to generate the two events. If I had one handler that looked for button down events anchored to anywhere and reported the matches, the reported match would always be the first match. Why does this matter? Well, I want to see the latest event and its timestamp so I can make sure I verify if I handled that. If I used the input handler below, any time I pressed the button once I would receive two calls into my endpoint and the timestamp on the input event would be the same. Here is the input handler directive (gadgetId set to something easier to read).


{
    "type": "GameEngine.StartInputHandler",
    "proxies": [],
    "recognizers": {
        "btn1": {
            "type": "match",
            "anchor": "anywhere",
            "fuzzy": false,
            "pattern": [
                {
                    "action": "down",
                    "gadgetIds": [
                        "A"
                    ]
                }
            ]
        },
        "btn2": {
            "type": "match",
            "anchor": "anywhere",
            "fuzzy": false,
            "pattern": [
                {
                    "action": "down",
                    "gadgetIds": [
                        "B"
                    ]
                }
            ]
        }
    },
    "events": {
        "failed": {
            "meets": [
                "timed out"
            ],
            "reports": "history",
            "shouldEndInputHandler": true
        },
        "btn1": {
            "shouldEndInputHandler": false,
            "meets": [
                "btn1"
            ],
            "reports": "matches"
        },
        "btn2": {
            "shouldEndInputHandler": false,
            "meets": [
                "btn2"
            ],
            "reports": "matches"
        }
    },
    "timeout": 7708
}

And the two requests sent to my skill.


{
    "type": "GameEngine.InputHandlerEvent",
    "requestId": "amzn1.echo-api.request.ee60ad56-56a0-4b73-b4f5-48a7bee715b7",
    "timestamp": "2018-10-11T15:39:52Z",
    "locale": "en-US",
    "originatingRequestId": "amzn1.echo-api.request.a0b25097-030e-465c-9454-0c0e1caa0386",
    "events": [
        {
            "name": "btn1",
            "inputEvents": [
                {
                    "gadgetId": "A",
                    "timestamp": "2018-10-11T15:39:52.324Z",
                    "color": "000000",
                    "feature": "press",
                    "action": "down"
                }
            ]
        }
    ]
}

{
    "type": "GameEngine.InputHandlerEvent",
    "requestId": "amzn1.echo-api.request.8c4ab5ab-8580-4c7b-994e-598c35e192c5",
    "timestamp": "2018-10-11T15:39:52Z",
    "locale": "en-US",
    "originatingRequestId": "amzn1.echo-api.request.a0b25097-030e-465c-9454-0c0e1caa0386",
    "events": [
        {
            "name": "btn1",
            "inputEvents": [
                {
                    "gadgetId": "A",
                    "timestamp": "2018-10-11T15:39:52.324Z",
                    "color": "000000",
                    "feature": "press",
                    "action": "down"
                }
            ]
        }
    ]
}

 

Note everything is the same EXCEPT the originatingRequestId. So it sounds like I need to start tracking the timestamp of the latest event. It is not enough to use the request’s timestamp, since it doesn’t provide millisecond resolution. One could easily generate two real buttons presses within a second of each other. So… I decided I’ll track the latest input event timestamp and will only consider event if their input event is after my latest event timestamp. BUT, I also need to send a new input handler directive anytime an event comes in, because of then fact that matches reports the first input event only.

Ok enough cryptic text. Let’s see the code. Here is the code that selects the relevant events and the latest timestamp.


export function getEventsAndMaxTimeSince(
    events: services.gameEngine.InputHandlerEvent[],
    lastEvent: moment.Moment,
    timeoutEventName: string)
    : { maxTime: moment.Moment, events: Array } {
    if (events.some(p => p.name! === timeoutEventName)) {
        return { maxTime: moment.utc(lastEvent), events: [timeoutEventName] };
    }
    const mapped = events
        .map(p => {
            const temp = p.inputEvents!.map(p1 => moment(p1.timestamp!).utc().valueOf());
            const max = moment.utc(Math.max.apply({}, temp));
            const diff = max.diff(lastEvent, "ms");
            console.log(`temp: ${JSON.stringify(temp)}`);
            console.log(`max: ${max.format(Utilities.DT_FORMAT)}`);
            return { max: max.valueOf(), maxMoment: max, diff: diff, name: p.name! };
        });

    console.log(`Mapping events last update${lastEvent.format(Utilities.DT_FORMAT)}: \n${JSON.stringify(mapped, null, 2)}`);
    const filtered = mapped.filter(p => p.diff > 0);
    let globalMax = Math.max.apply({}, filtered.map(p => p.max));
    if (!globalMax || isNaN(globalMax) || !isFinite(globalMax)) {
        console.log(`setting global max to ${lastEvent.valueOf()}`);
        globalMax = lastEvent.valueOf();
    }
    const resultGlobalMax = moment.utc(globalMax);
    console.log(`GLOBAL MAX ${resultGlobalMax.format(Utilities.DT_FORMAT)}`);

    const array = filtered.map(p => p.name);
    const result = { maxTime: resultGlobalMax, events: array };
    console.log(`returning result\n${JSON.stringify(result)}`);
    return result;
}

We get the constituent input event timestamps, select the maximum value, select the events whose maximum value is after the current latest value and then return those event names and the new maximum timestamp. If the event is a timeout event, we simply return as we have to generate a new turn.

Once we have the relevant events handy, we increase the score if we get an expected event, otherwise we increase the bad count.


private processRelevantEvents(relevantEvents: string[], whackState: WhackState): { good: string[], bad: string[] } {
    console.log(`received events ${JSON.stringify(relevantEvents)}`);
    const result: { good: string[], bad: string[] } = {
        good: [],
        bad: []
    };

    relevantEvents.forEach(evName => {
        // check if we are expecting this event
        const index = whackState.expectedEvents.findIndex(val => val.name === evName);
        if (index > -1) {
            // if we are, great. increase score and remove event from expected list.
            console.log(`increasing good`);
            result.good.push(whackState.expectedEvents[index].gadgetId);
            whackState.good++;
            whackState.expectedEvents.splice(index, 1);
        } else {
            // otherwise, increase bad count.
            console.log(`increasing bad.`);
            console.log(`still expecting number of buttons ${whackState.expectedEvents.length}`);
            result.bad.push(evName);
            whackState.bad++;
        }
    });

    return result;
}

If a the user has any buttons left, we simply turn off any good buttons that were pressed, and we add a voice response if there were any bad buttons pressed.


let rb = this.handlerInput.responseBuilder;
if (hasBad) {
    rb.speak(LocalizedStrings.whack_bad_answer().speech);
}

// need to turn off all good pressed buttons
if (goodPressedButtons.length > 0) {
    rb = rb.addDirective(SetLightDirectiveBuilder.setLight(SkillAnimations.rollCallFinishedUnused(), goodPressedButtons));
}
return rb.getResponse();

Deeper and Deeper

Another effect of the issue with the input handler we presented below, is that the code above needs to generate a new input handler. The entire method looks as follows:


private buttonsOutstanding(
    whackState: WhackState,
    hasBad: boolean,
    goodPressedButtons: string[],
    btns: GameButton[]): Response
{
    console.log(`responding with acknowledgment and new handler; more buttons remaining`);

    const now = moment.utc();
    const turnDuration = whackState.lastHandlerLength - (now.diff(whackState.lastHandlerStartTime, "ms"));
    whackState.lastHandlerStartTime = now.format(Utilities.DT_FORMAT);
    whackState.lastHandlerLength = turnDuration;
    whackState.pushAndTrimHandler(this.handlerInput.requestEnvelope.request.requestId);

    const startHandler = this.generateInputHandlerTemplate(btns, turnDuration);
    let rb = this.handlerInput.responseBuilder.addDirective(startHandler);
    if (hasBad) {
        rb.speak(LocalizedStrings.whack_bad_answer().speech);
    }

    // need to turn off all good pressed buttons
    if (goodPressedButtons.length > 0) {
        rb.addDirective(SetLightDirectiveBuilder.setLight(SkillAnimations.rollCallFinishedUnused(), goodPressedButtons));
    }
    return rb.getResponse();
}

Amazon recommends that the skill ensures that the input event requests are coming from the right originatingRequestId, since requests might come in late. The code that does this utilizes the lastHandlerIds property on the WhackState. The reason we use a list instead of one value is that if we press button 1 and button 2 one right after the other, the handler for button 1 would send a new input handler and reset the lastHandlerId, rendering the event from button 2 as junk. So we store the last two handlerIds


let ev = inputHandlerEvent;
if (!whackState.lastHandlerIds.some(p => p === ev.originatingRequestId)) {
    console.warn(`SKIPPING MESSAGE.\nLAST HANDLER IDs: \n${JSON.stringify(whackState.lastHandlerIds, null, 2)}`
        + `\nORIGINATING REQUEST ID: ${ev.originatingRequestId}`);
    return this.handlerInput.responseBuilder.getResponse();
}

For completeness, this is what the WhackState type looks like.


class WhackState {
    public startTime: string | undefined;
    public good: number = 0;
    public bad: number = 0;
    public turn: number = 0;
    public waitingOnConfirmation: boolean = false;

    public expectedEvents: Array = [];
    public lastEventTime: string | undefined;
    public lastHandlerIds: Array = [];
    public lastHandlerStartTime: string | undefined;
    public lastHandlerLength: number = 0;

    public initGame(): void {
        console.log(`initializing game. start time ${moment.utc(this.startTime).format(Utilities.DT_FORMAT)}`);

        this.waitingOnConfirmation = false;
        this.expectedEvents = [];
        this.bad = 0;
        this.good = 0;
        this.startTime = moment.utc().format(Utilities.DT_FORMAT);
        this.lastEventTime = this.startTime;
    }

    public pushAndTrimHandler(reqId: string): void {
        this.lastHandlerIds.push(reqId);
        while (this.lastHandlerIds.length > WHACKABUTTON_NUM_OF_BUTTONS + 2) {
            this.lastHandlerIds.shift();
        }
    }

    public timeInMsUntilEnd(): number {
        const now = moment.utc();
        const start = moment.utc(this.startTime);
        const end = start.add(GAME_DURATION_SECONDS, "s");
        const diff = end.diff(now, "ms");
        return diff;
    }


    public timeSinceStarted(): number {
        const now = moment.utc();
        const start = moment.utc(this.startTime);
        const diff = now.diff(start, "s");
        console.log(`it has been ${diff} seconds since the game started.`);
        return diff;
    }

}

Wrapping The Game Up

What happens when the game is done? We check for the time elapsed anytime user input or a time out request comes in. If the game has lasted long enough, we send the result, transition to the InLaunchStateHandler and ask the user if they want to play again.


private finish(handlerInput: HandlerInput, finish: boolean): Response {
    const whackState = GameHelpers.getState(handlerInput, new WhackState()).data;
    GameState.setInPostGame(handlerInput, true);

    let resp = LocalizedStrings.whack_summary({
        score: whackState.good - whackState.bad,
        good: whackState.good,
        bad: whackState.bad
    });
    if (finish) {
        resp = LocalizedStrings.whack_finish({
            score: whackState.good - whackState.bad,
            good: whackState.good,
            bad: whackState.bad
        });
    }

    const turnOffEverything = SetLightDirectiveBuilder.setLight(
        SkillAnimations.rollCallFinishedUnused());

    return handlerInput.responseBuilder
        .speak(resp.speech)
        .reprompt(resp.reprompt)
        .addDirective(turnOffEverything)
        .getResponse();
}

At this point the user can either restart the game, ask for their score (added a ScoreIntent to support this) or exit out. The PostGameStateHandler is implements this logic.


export class PostGameStateHandler implements RequestHandler {
    canHandle(handlerInput: HandlerInput): boolean {
        const sessionAttr = handlerInput.attributesManager.getSessionAttributes();
        const issupportedintent = handlerInput.requestEnvelope.request.type === "IntentRequest"
            && ["AMAZON.YesIntent",
                "AMAZON.NoIntent",
                "StartGameIntent",
                "ScoreIntent"]
                .some(p => p === (handlerInput.requestEnvelope.request).intent.name);
        return sessionAttr.inPostGame && issupportedintent;
    }

    handle(handlerInput: HandlerInput): Response {
        console.log("executing in post game state handler");

        if (handlerInput.requestEnvelope.request.type === "IntentRequest") {
            const req = handlerInput.requestEnvelope.request as IntentRequest;
            if (req.intent.name === "AMAZON.YesIntent" || req.intent.name === "StartGameIntent") {
                GameState.deleteState(handlerInput);
                const game = new WhackabuttonGame(handlerInput);
                GameState.setInPostGame(handlerInput, false);
                return game.initialize();
            } else if (req.intent.name === "AMAZON.NoIntent") {
                GameState.deleteState(handlerInput);
                GameState.setInPostGame(handlerInput, false);
                return handlerInput.responseBuilder
                    .speak(LocalizedStrings.goodbye().speech)
                    .getResponse();
            } else if(req.intent.name === "ScoreIntent") {
                return new WhackabuttonGame(handlerInput).postGameSummary();
            }
        }

        const donotresp = LocalizedStrings.donotunderstand();
        return handlerInput.responseBuilder
            .speak(donotresp.speech)
            .reprompt(donotresp.reprompt)
            .getResponse();
    }
}

How Did It Go?

Building this was a lot of fun but the development process was much more complicated than I expected. The number of events and semantics of the requests that Alexa sends are rather confusing, so there is a bit of a learning curve. The simulator isn’t great at helping debug some of this as the timeout and button presses do not show the JSON inside the simulator, so trying to figure out bugs was an exercise in diving into CloudWatch and figuring it out. I’ve seen inconsistent animation behavior; sometimes my animations wouldn’t play at all in the buttons. Sometimes although the input events seems to show up in the simulator, they never flow into the skill, either from the simulator or the real buttons. It would have helped to have unit tests but… you know how it goes when playing with a new tech.

As an exploratory exercise this was fairly successful. Let’s see how Teddy enjoyed the game.

As always, you can find the code in the Github repo. Enjoy!