Created
April 16, 2020 11:44
-
-
Save pkarthikr/2c11fe4eaf9f7fc28241d9898714ea4b to your computer and use it in GitHub Desktop.
Day 4 - Alexa Skills Summer Camp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
* SPDX-License-Identifier: Apache-2.0 | |
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
*/ | |
const sdk = require('@alexa-games/skills-gameon-sdk'); | |
const settings = require('./settings.js'); | |
const defaultClient = new sdk.SkillsGameOnApiClient(); | |
const generator = sdk.PlayerProfileGeneratorBuilder.getGenerator({ | |
locale: 'en-US', | |
avatarBaseUrl: settings.gameAvatarBaseUrl, | |
numberOfUniqueAvatars: settings.numberOfUniqueAvatars | |
}); | |
/** | |
* Initializes a new player with added profile info (name, avatar, color) | |
* | |
* @param {SkillsGameOnApiClient} [client=defaultClient] | |
* @returns {AugmentedPlayer} | |
*/ | |
async function newPlayer(client = defaultClient) { | |
let alexaPlayer = await client.initializeNewAugmentedPlayer({ | |
gameApiKey: settings.gameOnApiKey, | |
appBuildType: settings.appBuildType, | |
playerProfileGenerator: generator | |
}); | |
await client.enterTournamentForPlayer({ | |
tournamentId: settings.tournamentId, | |
player: alexaPlayer | |
}); | |
return alexaPlayer; | |
} | |
/** | |
* Looks up the player profile by the external player id | |
* @param {String} externalPlayerId | |
* @returns {PlayerProfile} | |
*/ | |
function lookupPlayerProfile(externalPlayerId) { | |
const profile = generator.getPlayerProfileFromId(externalPlayerId); | |
return profile; | |
} | |
/** | |
* Enter the match for a player. Assumes match and tournament are persistent / eternal. | |
* This is the most simple use case. If you are implementing recurring leaderboards e.g. daily or monthly, | |
* it is recommended to use SkillsGameOnApiClient.getTournamentsByTitle to retrieve the ephemeral tournamentId | |
* and SkillsGameOnApiClient.getMatchListForPlayer to retrieve the ephemeral matchId. | |
* | |
* @param {Player} alexaPlayer | |
* @param {SkillsGameOnApiClient} [client=defaultClient] | |
* @returns {EnterMatchResponse} | |
*/ | |
async function enterMatch(alexaPlayer, client = defaultClient) { | |
return await client.enterMatchForPlayer({ | |
matchId: settings.matchId, | |
player: alexaPlayer | |
}); | |
} | |
/** | |
* Submits score for player. Ensures session has not expired before submission. | |
* NOTE: stats can also be submitted in the sdk method, but we are not showcasing that here. | |
* | |
* @param {Player} alexaPlayer | |
* @param {Number} score | |
* @param {SkillsGameOnApiClient} [client=defaultClient] | |
* @returns {Player} | |
*/ | |
async function submitScore(alexaPlayer, score, client = defaultClient) { | |
await client.submitScoreForPlayer({ | |
matchId: settings.matchId, | |
submitScoreRequest: { score }, | |
player: alexaPlayer, | |
ensureMatchEntered: true | |
}); | |
return alexaPlayer; | |
} | |
/** | |
* Retrieves the player's PlayerScore. The PlayerScore is scoped to a particular matchId and contains the | |
* players rank, score, and ordinalRank e.g. first, second, third, etc. | |
* | |
* @param {Player} alexaPlayer | |
* @param {SkillsGameOnApiClient} [client=defaultClient] | |
* @returns {PlayerScore} | |
*/ | |
async function getPlayerScore(alexaPlayer, client = defaultClient) { | |
return await client.getPlayerScore( | |
settings.matchId, | |
alexaPlayer); | |
} | |
/** | |
* Refresh a player session by retrieving a new sessionId and sessionApiKey from GameOn. | |
* If the session has not expired, then do nothing. | |
* | |
* @param {Player} alexaPlayer | |
* @param {SkillsGameOnApiClient} [client=defaultClient] | |
* @returns {Player} | |
*/ | |
async function refreshPlayerSession(alexaPlayer, client = defaultClient) { | |
alexaPlayer = await client.refreshPlayerSession({ | |
gameApiKey: settings.gameOnApiKey, | |
appBuildType: settings.appBuildType, | |
player: alexaPlayer | |
}); | |
return alexaPlayer; | |
} | |
/** | |
* Retrieve a rendered leaderboard APL document. This function assumes that you always want the score and rank | |
* stored with GameOn. If you configure the leaderboard to only persist the best score, but want to display how the | |
* player performed in this particular instance, you can use the SkillsGameOnApiClient.renderLeaderboard and pass in | |
* an AugmentedPlayer with the desired PlayerScore. | |
* | |
* @param {Player} alexaPlayer | |
* @param {SkillsGameOnApiClient} [client=defaultClient] | |
* @returns Alexa APL document directive | |
*/ | |
async function getLeaderboard(alexaPlayer, client = defaultClient) { | |
const leaderboard = await client.getCombinationLeaderboards({ | |
matchId: settings.matchId, | |
topScoresLimit: settings.topNleaderboardItemCount, | |
playerNeighborsLimit: settings.playerNeighborsCount, | |
player: alexaPlayer | |
}); | |
const currentScore = await client.getPlayerScore( | |
settings.matchId, | |
alexaPlayer); | |
alexaPlayer.score.ordinalRank = currentScore.ordinalRank; | |
alexaPlayer.score.rank = currentScore.rank; | |
alexaPlayer.score.score = currentScore.score; | |
const renderOptions = { backgroundImageUrl: settings.leaderboardBackgroundImageUrl }; | |
return sdk.renderLeaderboard(alexaPlayer, leaderboard, renderOptions, generator); | |
} | |
module.exports = { | |
newPlayer, | |
lookupPlayerProfile, | |
submitScore, | |
getPlayerScore, | |
enterMatch, | |
getLeaderboard, | |
refreshPlayerSession | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2). | |
// Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management, | |
// session persistence, api calls, and more. | |
const Alexa = require('ask-sdk-core'); | |
const skillData = require('skillData.js'); | |
const persistenceAdapter = require('ask-sdk-s3-persistence-adapter'); | |
const sdk = require('@alexa-games/skills-gameon-sdk'); | |
const GameOn = require('./gameOn.js'); | |
const apiClient = new sdk.SkillsGameOnApiClient(); | |
const LaunchRequestHandler = { | |
canHandle(handlerInput) { | |
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'; | |
}, | |
async handle(handlerInput) { | |
const data = getLocalizedData(handlerInput.requestEnvelope.request.locale); | |
let speakOutput = ""; | |
const prompt = data["QUESTION"]; | |
let persistentAttributes = await handlerInput.attributesManager.getPersistentAttributes(); | |
console.log(persistentAttributes.FIRST_TIME); | |
let dataToSave = {}; | |
let player = persistentAttributes.PLAYER; | |
if(persistentAttributes.FIRST_TIME === undefined && player === undefined){ | |
player = await GameOn.newPlayer(); | |
dataToSave = { | |
"FIRST_TIME": false, | |
"PLAYER": player | |
} | |
save(handlerInput, dataToSave, null); | |
speakOutput = data["WELCOME_MESSAGE"]+ data["QUESTION"]; | |
} else { | |
speakOutput = data["RETURNING_USERS_WELCOME"] + data["QUESTION"]; | |
player = await GameOn.refreshPlayerSession(player); | |
dataToSave = { | |
"PLAYER": player | |
} | |
save(handlerInput, dataToSave, null); | |
} | |
return handlerInput.responseBuilder | |
.speak(speakOutput) | |
.reprompt(prompt) | |
.getResponse(); | |
} | |
}; | |
function save(handlerInput, attributesToSave, attributesToDelete) { | |
return new Promise((resolve, reject) => { | |
handlerInput.attributesManager.getPersistentAttributes() | |
.then((attributes) => { | |
for (let key in attributesToSave) { | |
attributes[key] = attributesToSave[key]; | |
} | |
if (null !== attributesToDelete) { | |
attributesToDelete.forEach(function (element) { | |
delete attributes[element]; | |
}); | |
} | |
handlerInput.attributesManager.setPersistentAttributes(attributes); | |
return handlerInput.attributesManager.savePersistentAttributes(); | |
}) | |
.catch((error) => { | |
reject(error); | |
}); | |
}); | |
} | |
function getLocalizedData(locale){ | |
return skillData[locale]; | |
} | |
const RiddleIntentHandler = { | |
canHandle(handlerInput) { | |
return (Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' | |
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'RiddleIntent') || | |
(Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' | |
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.YesIntent') ; | |
}, | |
handle(handlerInput) { | |
const speakOutput = 'Here is the riddle for today. '; | |
const data = getLocalizedData(handlerInput.requestEnvelope.request.locale); | |
// Homework : Find the number of the current day and get the corresponding question. | |
const speechOutput = speakOutput + data["QUESTIONS"][0]; | |
const dataToSave = { | |
"RIGHT_ANSWER": data["ANSWERS"][0] | |
} | |
handlerInput.attributesManager.setSessionAttributes(dataToSave); | |
const reprompt = data["QUESTIONS"][0] + " " + data["ANSWER_MESSAGE"]; | |
return handlerInput.responseBuilder | |
.speak(speechOutput) | |
.reprompt(reprompt) | |
.getResponse(); | |
} | |
}; | |
const AnswerIntentHandler = { | |
canHandle(handlerInput) { | |
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' | |
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'AnswerIntent'; | |
}, | |
async handle(handlerInput) { | |
const data = getLocalizedData(handlerInput.requestEnvelope.request.locale); | |
const userAnswer = handlerInput.requestEnvelope.request.intent.slots.answer.resolutions.resolutionsPerAuthority[0].values[0].value.name; | |
const sessionAttributes = handlerInput.attributesManager.getSessionAttributes(); | |
const correctAnswer = sessionAttributes.RIGHT_ANSWER; | |
let speakOutput = ''; | |
let persistentAttributes = await handlerInput.attributesManager.getPersistentAttributes(); | |
// player = await GameOn.refreshPlayerSession(player); | |
let player = persistentAttributes.PLAYER; | |
if(correctAnswer === userAnswer){ | |
let number = Math.floor(Math.random() * 6) + 1 | |
await GameOn.submitScore(player, 354); | |
const leaderboardDirective = await GameOn.getLeaderboard(player); | |
const playerScore = await GameOn.getPlayerScore(player); | |
console.log("Score"+JSON.stringify(playerScore)); | |
console.log(leaderboardDirective); | |
speakOutput = "Correct Answer. You get X points"; | |
speakOutput = "Correct Answer. You get X points"; | |
} else { | |
speakOutput = "Wrong Answer. You only have x chances remaining." | |
} | |
return handlerInput.responseBuilder | |
.speak(speakOutput) | |
.reprompt(speakOutput) | |
.getResponse(); | |
} | |
}; | |
const HelpIntentHandler = { | |
canHandle(handlerInput) { | |
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' | |
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'; | |
}, | |
handle(handlerInput) { | |
const speakOutput = 'You can say hello to me! How can I help?'; | |
return handlerInput.responseBuilder | |
.speak(speakOutput) | |
.reprompt(speakOutput) | |
.getResponse(); | |
} | |
}; | |
const CancelAndStopIntentHandler = { | |
canHandle(handlerInput) { | |
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' | |
&& (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent' | |
|| Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent'); | |
}, | |
handle(handlerInput) { | |
const speakOutput = 'Goodbye!'; | |
return handlerInput.responseBuilder | |
.speak(speakOutput) | |
.getResponse(); | |
} | |
}; | |
const SessionEndedRequestHandler = { | |
canHandle(handlerInput) { | |
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest'; | |
}, | |
handle(handlerInput) { | |
// Any cleanup logic goes here. | |
return handlerInput.responseBuilder.getResponse(); | |
} | |
}; | |
// The intent reflector is used for interaction model testing and debugging. | |
// It will simply repeat the intent the user said. You can create custom handlers | |
// for your intents by defining them above, then also adding them to the request | |
// handler chain below. | |
const IntentReflectorHandler = { | |
canHandle(handlerInput) { | |
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'; | |
}, | |
handle(handlerInput) { | |
const intentName = Alexa.getIntentName(handlerInput.requestEnvelope); | |
const speakOutput = `You just triggered ${intentName}`; | |
return handlerInput.responseBuilder | |
.speak(speakOutput) | |
//.reprompt('add a reprompt if you want to keep the session open for the user to respond') | |
.getResponse(); | |
} | |
}; | |
// Generic error handling to capture any syntax or routing errors. If you receive an error | |
// stating the request handler chain is not found, you have not implemented a handler for | |
// the intent being invoked or included it in the skill builder below. | |
const ErrorHandler = { | |
canHandle() { | |
return true; | |
}, | |
handle(handlerInput, error) { | |
console.log(`~~~~ Error handled: ${error.stack}`); | |
const speakOutput = `Sorry, I had trouble doing what you asked. Please try again.`; | |
return handlerInput.responseBuilder | |
.speak(speakOutput) | |
.reprompt(speakOutput) | |
.getResponse(); | |
} | |
}; | |
// The SkillBuilder acts as the entry point for your skill, routing all request and response | |
// payloads to the handlers above. Make sure any new handlers or interceptors you've | |
// defined are included below. The order matters - they're processed top to bottom. | |
exports.handler = Alexa.SkillBuilders.custom() | |
.withPersistenceAdapter( | |
new persistenceAdapter.S3PersistenceAdapter({bucketName: process.env.S3_PERSISTENCE_BUCKET}) | |
) | |
.addRequestHandlers( | |
LaunchRequestHandler, | |
RiddleIntentHandler, | |
AnswerIntentHandler, | |
HelpIntentHandler, | |
CancelAndStopIntentHandler, | |
SessionEndedRequestHandler, | |
IntentReflectorHandler, // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers | |
) | |
.addErrorHandlers( | |
ErrorHandler, | |
) | |
.lambda(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "hello-world", | |
"version": "1.1.0", | |
"description": "alexa utility for quickly building skills", | |
"main": "index.js", | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"author": "Amazon Alexa", | |
"license": "ISC", | |
"dependencies": { | |
"ask-sdk-core": "^2.6.0", | |
"ask-sdk-model": "^1.18.0", | |
"aws-sdk": "^2.326.0", | |
"ask-sdk-s3-persistence-adapter": "^2.0.0", | |
"@alexa-games/skills-gameon-sdk": "^0.1.0" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
* SPDX-License-Identifier: Apache-2.0 | |
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
*/ | |
// Should be stored securely with KMS or as a secure environment variable on your lambda | |
// See https://github.com/alexa-games/skills-gameon-sdk-js/blob/master/README.md#gameon-api-secret-management | |
const gameOnApiKey = 'your-api-key'; | |
// Preferable to store the following settings as AWS Lambda environment variables | |
const matchId = process.env.matchId || 'your-match-id'; | |
const tournamentId = process.env.tournamentId || 'your-tournament-id'; | |
// Required for GameOn. Value must be set to 'development' or 'release' | |
const appBuildType = process.env.appBuildType || 'development'; | |
// Base url for the player avatars. See https://github.com/alexa-games/skills-gameon-sdk-js/blob/master/README.md#avatar-generation | |
// Cannot be empty string. Passing in any other value will allow the leaderboard to render, | |
// but will display blank placeholders. | |
const gameAvatarBaseUrl = process.env.gameAvatarBaseUrl || 'https://picture-url-you-want-to-paste.com'; | |
// Background image for the leaderboard template | |
// Recommended minimum size: 1280x800px | |
// Cannot be empty string. Passing in any other value will allow the leaderboard to render, | |
// but will display a blank white background | |
const leaderboardBackgroundImageUrl = process.env.leaderboardBackgroundImageUrl || 'https://image-of-background.com'; | |
// Top n places to show on the leaderboard | |
const topNleaderboardItemCount = process.env.topNleaderboardItemCount || 5; | |
// Number of players to render before and after current player | |
const playerNeighborsCount = process.env.playerNeighborsCount || 1; | |
// Number of avatars that have been generated | |
// See https://github.com/alexa-games/skills-gameon-sdk-js/blob/master/README.md#avatar-generation | |
const numberOfUniqueAvatars = process.env.numberOfUniqueAvatars || 50; | |
module.exports = { | |
matchId, | |
tournamentId, | |
appBuildType, | |
gameOnApiKey, | |
gameAvatarBaseUrl, | |
leaderboardBackgroundImageUrl, | |
topNleaderboardItemCount, | |
playerNeighborsCount, | |
numberOfUniqueAvatars | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment