Are you a fan of Ten Pin Bowling? This new Alexa skill uses Echo Buttons to bring the game to life!
Step 1 - Voice User InterfaceBuilding a skill requires modeling the audio options for a user to speak into their device. This starts with identifying the name of the skill, then providing details on player names and game modes. This needs to be modeled, including identifying intents (features) of the game as well as what types of phrases someone may use to play.
First, use the developer console on the Amazon Developer website to create a new skill. Each intent gets identified, including the sample utterances that a user may speak as well as custom slots that allow the game to parse out key attributes like player names.
There are two main paths within the intent model. The initial game setup captures assigning the player names, then initiating the actual gameplay. Once the game begins, the players will bowl each frame, and can at any time either check scores, or request to save the game for later.
These are different sets of custom intents that need to be modeled for all of this functionality. Here's a VUI diagram depicting the main paths that we would expect a user to choose.
Once these requests are modeled, we can then begin to develop the backend rules for creating responses.
Step 2 - Develop Game LogicThe next step is to write the logic that encodes the rules of the game. This is done with NodeJS hosted by a Lambda function on AWS. Each intent that is modeled in the prior step will trigger an event that invokes the Lambda function.
It's up to each intent to ensure that the user can appropriately execute the function. For example, within the logic for processing the Bowl intent, a check is made that validates that at least one player has been setup in the game.
Enable Basic Bowling Rules
The basic gameplay follows traditional ten pin bowling scoring rules. If you're not familiar with the game, here are the basic rules.
- The game is broken out into ten frames. Ten pins are setup to knock down in each frame, and the bowler gets to balls to knock them down.
- The objective to "win" is to score more points than the other players.
- If ten pins are knocked down in the first roll, the frame is marked as a "Strike".
- If ten pins are knocked down with two rolls, the frame is marked as a "Spare".
- The player gets points for each pin knocked down, and frames are additive.
- There are bonus points for a Strike or a Spare. Strike = 10 + the next two rolls, Spare = 10 + the next roll.
- Special logic handles the tenth frame. If the bowler gets either a Strike or Spare, a third roll is awarded.
This logic is all written into the Lambda function.
Create different Outcomes for each Turn
To vary the outcome of each roll, there is logic that varies based on a random number generator, and the nature of the roll. Here's the logic.
// these are all of the potential outcomes of an initial roll
const firstBallOutcomes = [
{ "score": 10, "throwLocation": "Right down the middle. ", "pins":[null], "hook": false },
{ "score": 10, "throwLocation": "Sweeping hook from the right. ", "pins":[null], "hook": true },
{ "score": 10, "throwLocation": "Sweeping hook from the left. ", "pins":[null], "hook": true },
{ "score": 10, "throwLocation": "Slight shift to the left. ", "pins":[null], "hook": false },
{ "score": 10, "throwLocation": "Slight shift to the right. ", "pins":[null], "hook": false },
{ "score": 9, "throwLocation": "Right down the middle. ", "pins":[5], "spareThreshold": 80, "hook": false },
{...},
{ "score": 6, "throwLocation": "Drifting to the left. ", "pins":[6, 7, 9, 10], "spareThreshold": 20, "hook": false },
{ "score": 6, "throwLocation": "Drifting to the right. ", "pins":[4, 7, 8, 10], "spareThreshold": 20, "hook": false }
];
// generate a random outcome that includes the number of pins knocked down on the first roll
let possibleThrows = [];
for (let i = 0 ; i < firstBallOutcomes.length; i++) {
let outcome = {};
outcome.scenario = i;
if (this.attributes['buttonDelta']) {
// check if the outcome in the array is associated with a hook roll
if (this.attributes['buttonDelta'] > hookThreshold) {
if (firstBallOutcomes[i].hook) {
possibleThrows.push(outcome);
}
} else {
if (!firstBallOutcomes[i].hook) {
possibleThrows.push(outcome);
}
}
} else {
possibleThrows.push(outcome);
}
}
// randomly pick from the array of possible outcomes, then set the index
const outcomeGenerator = Math.floor(Math.random() * possibleThrows.length);
const ballGenerator = possibleThrows[outcomeGenerator].scenario;
console.log("Frame: " + this.attributes['frame'] + " Generator: " + ballGenerator);
The syntax then takes attributes from the scenario, and embeds them into the audio response.
Using Directives and initiating the Game Engine
If you're not familiar with directives within Alexa, it's a core concept needed when building your skill. For example, to add more advanced features like Gadgets and Displays. Here is where they fit into the json structure for the response object. Note that directives are an array, so you can have multiple in a single response.
{
"version": "1.0",
"sessionAttributes": {},
"respose": {
"outputSpeech": {};
"card": {};
"reprompt": {};
"directives": [
],
"shouldEndSession": true
}
}
To communicate with the Game Engine, you first need to start it by adding a directive. This directive is called a roll call, and supplies the metadata for the Game Engine to begin receiving and interpreting data.
{
"type": "GameEngine.StartInputHandler",
"timeout": 90000,
"recognizers": {
"button_down_recognizer": {
"type": "match",
"fuzzy": false,
"anchor": "end",
"pattern": [{
"action": "down"
}]
},
"button_up_recognizer": {
"type": "match",
"fuzzy": false,
"anchor": "end",
"pattern": [{
"action": "up"
}]
}
},
"events": {
"button_down_event": {
"meets": ["button_down_recognizer"],
"reports": "matches",
"shouldEndInputHandler": false
},
"button_up_event": {
"meets": ["button_up_recognizer"],
"reports": "matches",
"shouldEndInputHandler": false
},
"timeout": {
"meets": ["timed out"],
"reports": "history",
"shouldEndInputHandler": true
}
}
}
Within the roll call are additional sets of attributes. The first is a simple timeout parameter measured in milliseconds. For this skill I have it set to 90 seconds, the maximum allowed. If the game engine doesn't receive any input for this duration, it times out, and triggers an event for the skill to handle. The parts of the object are the recognizers and events.
The recognizers object in the roll call sets the patterns for which the device will be looking for. In this case, the device is attempting to match a single down or up action.
The events object in the roll call instructs the Game Engine to initiate requests when these patterns are met. The event name specified in this will be passed to the lambda function.
Enable Saving the Game
Games that have many turns should assume that a user may want to take a break. To accomplish this, the Alexa SDK can persist the session attributes into a DynamoDB table.
With more extensive Alexa skills, it's great to incorporate Echo Buttons to handle repetitive user interaction. To play a complete game of bowling requires rolling up to 23 balls per player. Buttons give the option of a user pressing them to initiate the roll versus an utterance like "Bowl".
Using buttons within a skill requires pairing and identification of the gadget identifiers. This starts by initiating a roll-call within the custom skill that allows buttons to be registered for the game (more detail in previous step).
Once the buttons are registered, they can be used within gameplay as events, just like audible utterances. For example, during the registration process, the player is prompted to press a button. Upon doing so, that button is registered with the game engine, and the unique identifier (gadget Id) is saved in the session for future processing.
Leverage Multiple Button Events
When a button is pressed, there are two different events that are invoked. First is a button down event, second is a button up event. Each of these events will execute the lambda function for the skill, and create separate responses. We can add a new dimension to the buttons by determining how long a button has been held down based on the timestamps of these two events.
To add an element of skill to the game, I've added the concept of throwing a straight ball versus a hook depending on how long the button was pressed. If the button was pressed for more than 400ms, it will add a hook. This enriches the gameplay as distinct outcomes are created for straight vs. curved throws.
The time difference is based on the timestamps from the button, not when the messages were received by the lambda function. When the first timestamp is received, it is persisted in the session, and the response back to the device does not contain an audio payload. It is the response from the button up event that furthers the gameplay.
Step 4 - Sound IntegrationOne of the iconic parts of bowling is the sounds. If you're like me, hearing pins crashing drives much of the excitement of the game.
To add sounds, need to leverage SSML (Speech Synthesis Markup Language) with the responses to the device. Within the SSML there are different tags that indicate source of audio files.
I'm using the AWS S3 service to host MP3 files that simulate the pins crashing. Here's what the syntax looks like that shows how to tag location of the endpoint.
speechOutput = speechOutput +
"<audio src='https://s3.amazonaws.com/bowlingskill/bowlingIntro.mp3'/>" +
"If you have Echo Buttons, please press a button to begin setting up the game. " +
"If not, just let me know the name of the first player. For example, say player one is James. ";
There are many sounds that you can use that are hosted by Amazon. Here is the sound library from Amazon, and to leverage them, it's still SSML. Example below of the "Gameshow Neutral Response" clip.
if (firstBallOutcomes[ballGenerator].pins.length === 1) {
speechOutput = speechOutput + "Still standing is the " +
firstBallOutcomes[ballGenerator].pins[0] + " pin. " +
"<audio src='soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_neutral_response_01'/>";
}
Step 5 - Display IntegrationWe're building a "voice first" user experience, however it's nice to augment it with some visuals. There are two different ways we can do this.
Leverage Cards on a Companion App
The Alexa App on the users mobile device can be a good way. In bowling, this can be around sharing the score to the users. Here's the syntax needed in the Lambda function.
// send a card to the companion app
let scoreSummary = "";
// display player one score
scoreSummary = "Frame: " + this.attributes['frame'] + "\n ";
scoreSummary = scoreSummary + this.attributes['playerNameOne'] +
" score: " + this.attributes['playerScoreOne'] + "\r";
// if virtual player exists, display score for them
if (this.attributes['ghostGame']) {
scoreSummary = scoreSummary + this.attributes['ghostName'] + " score: " +
this.attributes['ghostScore'] + "\r";
}
this.response.cardRenderer(scoreCardTitle, scoreSummary);
When formatting \n provides a line break, while \r generates a carriage return. Here's what the end result is when a score update is requested.
Optional Image Display for devices like the Echo Show
For users that have a device with a display, a background image can be displayed that matches the game. This is done by sending an extra directive in the response object. Here's the syntax required using BodyTemplate1.
const builder = new Alexa.templateBuilders.BodyTemplate1Builder();
const template = builder.setTitle(startupTitle)
.setBackgroundImage(makeImage(startupImage))
.setTextContent(makePlainText(skillName))
.build();
// render the response based on if the device has a screen
if (this.event.context.System.device.supportedInterfaces.Display) {
console.log("this was requested by an Echo Show");
this.response.speak(resumeGameInstruction)
.listen(resumeGameInstruction).renderTemplate(template);
} else {
this.response.speak(resumeGameInstruction)
.listen(resumeGameInstruction);
}
this.emit(':responseReady');
Step 6 - Multi Player ModeTen Pin Bowling can be played in single or multi-player mode. Identification of the number of players is handled up front when the game is setup, and handled by the intent model. There is the option of having up to four players, and the order of players is determined during setup.
Just like in single player mode, the current score and player data is saved in the session attributes. In addition, a determination is made for which player is currently bowling is recorded in the session.
Managing Multiple Buttons
Having buttons gives options to leverage the different colors that the LEDs can make to facilitate gameplay. In a multi-player game, red indicates who's turn it is, and blue is the default value.
Keeping track of which button belongs to each player is the responsibility of the lambda function, and how it manages session data. Upon registration of each of the players, the unique identifier (gadget Id) is saved within the session. This provides the link between the player name and the device.
During gameplay, directives are passed back in the response object for each of the gadgets with instructions on the specific color (blue or red) for the LEDs to display. Each button illuminates the color based on instructions passed by the game engine.
A fun option that I've built into this game is the addition of virtual players. To differentiate the user experience, there's a great new feature within Alexa that allows voices other than the default "Alexa" voice that we're all familiar with.
It's based on the Polly service from AWS that provides lifelike voices similar to Alexa. I have incorporated three different players (Matthew, Salli, Joey), each have a different voice that are generated by Polly. The game logic is coded such that the three players provide different levels of difficulty, and are "unlocked" as the user advances in the game.
Incorporating Polly into a custom skill just requires SSML syntax indicating to change the voice being used. There's nothing required within the Alexa SDK or the console.
For example, the first level in the virtual bowling league is Matthew. Here's what the response object looks like that the Lambda function generates. The tagging instructs Alexa to leverage Polly to override the native voice for an introduction, pause for a second, then use the native Alexa voice to give instructions.
<ssml>
<voice>Hello my name is Matthew. Let me grab my bowling bag and a turkey sandwich, then it's game on.</voice>
<break time="1s"/>
Press your button to start the match.
</ssml>
There are also new intents required to build out in the voice model for this as this feature has been published as an ISP (In-Skill Purchase). If the user utters "What can I buy?", they are presented an option around purchasing the bowling league add-on, which if invoked will delegate the voice control over to Alexa that handles the actual purchase. Amazon also records the purchase for future sessions as the skill can check if this has already been purchased through an API call.
To create an ISP, you need to create a product that can be associated with your skill. This is created in json notation, then deployed to the skill through the Alexa CLI. For specific instructions, follow this helpful blog post, and the entitlement that I've created for the virtual bowling league is below.
{
"version": "1.0",
"type": "ENTITLEMENT",
"referenceName": "league",
"publishingInformation": {
"locales": {
"en-US": {
"name": "Virtual Bowling League",
"summary": "This enables you to add a virtual player to compete against in a one-on-one bowling league.",
"description": "If you want a challenge, join the bowling league where you can compete against other virtual players.",
"smallIconUri": "https://s3.amazonaws.com/bowlingskill/images/bowling108x108.png",
"largeIconUri": "https://s3.amazonaws.com/bowlingskill/images/bowling512x512.png",
"examplePhrases": [
"Alexa, purchase bowling league edition."
],
"keywords": [
"League",
"Bowling League",
"Competition"
],
"customProductPrompts": {
"purchasePromptDescription": "With this version of the game, you will be able to play in a virtual bowling league against Alexa generated characters.",
"boughtCardDescription": "Congratulations! You can now play with virtual characters in a bowling league."
}
}
},
"distributionCountries": [
"US"
],
"pricing": {
"amazon.com": {
"releaseDate": "2018-11-01T00:00Z",
"defaultPriceListing": {
"price": 1.99,
"currency": "USD"
}
}
},
"taxInformation": {
"category": "SOFTWARE"
}
},
"privacyAndCompliance": {
"locales": {
"en-US": {
"privacyPolicyUrl": "https://s3.amazonaws.com/bowlingskill/privacy.html"
}
}
},
"testingInstructions": "To purchase, just say Join Bowling League. This allows you to play against Alexa generated players.",
"purchasableState": "PURCHASABLE"
}
Step 8 - Publishing to Skill StoreTen Pin Bowling is available in both the US and UK skill stores. Given that buttons are still new, I've made them optional. This needs to get configured on the Alexa skills kit on the distribution tab. Below is a screenshot of how to apply the correct settings.
The description of the skill and how to use the buttons needs to called out in detail.
SummaryButtons are an excellent way of making games more exciting, and can be incorporated into many skills. When doing so, make sure and leverage the great colors of the LED's as they also can provide cues in the gameplay.
Comments