Skip to content

Instantly share code, notes, and snippets.

@pkarthikr
Created April 16, 2020 11:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pkarthikr/2c11fe4eaf9f7fc28241d9898714ea4b to your computer and use it in GitHub Desktop.
Save pkarthikr/2c11fe4eaf9f7fc28241d9898714ea4b to your computer and use it in GitHub Desktop.
Day 4 - Alexa Skills Summer Camp
/*
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* 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 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();
{
"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"
}
}
/*
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* 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