Skip to content

Instantly share code, notes, and snippets.

@germanviscuso
Last active November 17, 2020 17:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save germanviscuso/bce91a9cdc5e3e06b1417b1916859a01 to your computer and use it in GitHub Desktop.
Save germanviscuso/bce91a9cdc5e3e06b1417b1916859a01 to your computer and use it in GitHub Desktop.
Sí mi Capitán - Alexa Games workshop
const APLHomeCardRequestInterceptor = {
process(handlerInput) {
const withSimpleCard = handlerInput.responseBuilder.withSimpleCard;
const withStandardCard = handlerInput.responseBuilder.withStandardCard;
function withSimpleAPLCard(cardTitle, cardContent){
if(supportsAPL(handlerInput)){
handlerInput.responseBuilder.addDirective({
type: 'Alexa.Presentation.APL.RenderDocument',
version: '1.0',
document: APLDoc,
datasources: {
templateData: {
"header": cardTitle,
"text": cardContent,
// default background for simple card
"backgroundSmall": "https://s3-eu-west-1.amazonaws.com/happybirthday-alexa/papers_720x480_card_small.png",
"backgroundLarge": "https://s3-eu-west-1.amazonaws.com/happybirthday-alexa/papers_1200x800_card_large.png"
}
}
})
}
withSimpleCard(cardTitle, cardContent);
return handlerInput.responseBuilder;
}
function withStandardAPLCard(cardTitle, cardContent, smallImageUrl, largeImageUrl){
if(supportsAPL(handlerInput)){
handlerInput.responseBuilder.addDirective({
type: 'Alexa.Presentation.APL.RenderDocument',
version: '1.0',
document: APLDoc,
datasources: {
templateData: {
"header": cardTitle,
"text": cardContent,
"backgroundSmall": smallImageUrl,
"backgroundLarge": largeImageUrl
}
}
})
}
withStandardCard(cardTitle, cardContent, smallImageUrl, largeImageUrl);
return handlerInput.responseBuilder;
}
handlerInput.responseBuilder.withSimpleCard = (...args) => withSimpleAPLCard(...args);
handlerInput.responseBuilder.withStandardCard = (...args) => withStandardAPLCard(...args);
}
}
function supportsAPL(handlerInput){
const {supportedInterfaces} = handlerInput.requestEnvelope.context.System.device;
return supportedInterfaces['Alexa.Presentation.APL'];
}
function deviceType(handlerInput){
if(supportsAPL(handlerInput)){
const {Viewport} = handlerInput.requestEnvelope.context;
const resolution = Viewport.pixelWidth + 'x' + Viewport.pixelHeight;
switch(resolution){
case "480x480": return "EchoSpot";
case "960x480": return "EchoShow5";
case "1024x600": return "EchoShow";
case "1200x800": return "FireHD8";
case "1280x800": return "EchoShow2";
case "1920x1080": return "FireTV";
case "1920x1200": return "FireHD10";
default: return "unknown";
}
} else {
return "screenless";
}
}
const APLDoc =
{
"type": "APL",
"version": "1.0",
"import": [
{
"name": "alexa-styles",
"version" : "1.0.0"
},
{
"name": "alexa-layouts",
"version" : "1.0.0"
},
{
"name": "alexa-viewport-profiles",
"version" : "1.0.0"
}
],
"resources": [
{
"when": "${viewport.shape == 'round'}",
"dimensions": {
"myTextTopPadding": "0dp"
}
},
{
"when": "${@viewportProfile == @hubLandscapeSmall || @viewportProfile == @hubLandscapeMedium || @viewportProfile == @hubLandscapeLarge || @viewportProfile == @tvLandscapeXLarge}",
"dimensions": {
"myTextTopPadding": "50dp"
}
}
],
"styles": {},
"layouts": {
"CentralLayout": {
"description": "A basic central screen layout with an image and a text",
"parameters": [
{
"name": "image",
"type": "string"
},
{
"name": "text",
"type": "string"
}
],
"items": [
{
"type": "Container",
"width": "100vw",
"height": "100vh",
"alignItems": "center",
"justifyContent": "center",
"item": [
{
"type": "Image",
"source": "${image}",
"width": "50vw",
"height": "50vh"
},
{
"type": "Text",
"text": "${text}",
"color" : "#FFFFFF",
"textAlign": "center",
"style" : "textStyleDisplay4",
"paddingTop" : "@myTextTopPadding"
}
]
}
]
}
},
"mainTemplate": {
"parameters": [
"payload"
],
"items": [
{
"type": "Container",
"items": [
{
"type": "Image",
"source": "${viewport.shape == 'round' || !payload.templateData.backgroundLarge ? payload.templateData.backgroundSmall : payload.templateData.backgroundLarge}",
"scale": "best-fill",
"width": "100vw",
"height": "100vh",
"opacity": 0.8,
"filters": [
{
"type": "Blur",
"radius": "5dp"
}
]
},
{
"type": "Container",
"position": "absolute",
"width": "100vw",
"height": "100vh",
"items": [
{
"type": "AlexaHeader",
"headerTitle": "${payload.templateData.header}"
},
{
"when": "${viewport.shape == 'round'}",
"type": "Container",
"width": "100vw",
"height": "60vh",
"alignItems": "center",
"justifyContent": "center",
"items": [
{
"type": "CentralLayout",
"image": "${viewport.shape == 'round' || !payload.templateData.backgroundLarge ? payload.templateData.backgroundSmall : payload.templateData.backgroundLarge}",
"text": "${payload.templateData.text}"
}
]
},
{
"when": "${@viewportProfile == @hubLandscapeSmall || @viewportProfile == @hubLandscapeMedium || @viewportProfile == @hubLandscapeLarge || @viewportProfile == @tvLandscapeXLarge}",
"type": "Container",
"width": "100vw",
"height": "90vh",
"alignItems": "center",
"justifyContent": "center",
"items": [
{
"type": "CentralLayout",
"image": "${viewport.shape == 'round' || !payload.templateData.backgroundLarge ? payload.templateData.backgroundSmall : payload.templateData.backgroundLarge}",
"text": "${payload.templateData.text}"
}
]
}
]
}
]
}
]
}
}
module.exports = {
APLHomeCardRequestInterceptor
}
{
"interactionModel": {
"languageModel": {
"invocationName": "sí mi capitán",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "AMAZON.YesIntent",
"samples": []
},
{
"name": "AMAZON.NoIntent",
"samples": [
"no quiero"
]
}
],
"types": []
}
}
}
// 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');
const modules = require('./modules');
var persistenceAdapter = getPersistenceAdapter();
const LaunchRequestHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
},
handle(handlerInput) {
const {attributesManager} = handlerInput;
const sessionAttributes = attributesManager.getSessionAttributes();
let vueltas = sessionAttributes['vueltas'];
let speechText;
if(vueltas){
speechText = 'Hola otra vez! Eres muy valiente en continuar. ';
} else {
speechText = 'Bienvenido a sí mi capitán! ';
sessionAttributes['id'] = 1;
sessionAttributes['reputacion'] = 50;
sessionAttributes['tesoro'] = 50;
sessionAttributes['vueltas'] = 0;
}
const module = getModule(sessionAttributes['id']);
speechText += module.question;
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.withStandardCard('Sí mi Capitán', module.question, module.image) // <--
.getResponse();
}
};
const YesHandler = {
canHandle(handlerInput){
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'AMAZON.YesIntent';
},
handle(handlerInput){
const {attributesManager} = handlerInput;
const sessionAttributes = attributesManager.getSessionAttributes();
let vueltas = sessionAttributes['vueltas'];
sessionAttributes['vueltas'] = vueltas !== undefined ? vueltas + 1 : 1;
const moduleId = sessionAttributes['id'];
const module = getModule(moduleId);
const nextModule = getNextModule(moduleId);
if(nextModule) sessionAttributes['id'] = nextModule.id;
let speechText;
calculateGameVariables(sessionAttributes, module.yes.variable1, module.yes.variable2);
if(sessionAttributes['reputacion'] === 100 || sessionAttributes['reputacion'] === 0 || sessionAttributes['tesoro'] === 100 || sessionAttributes['tesoro'] === 0) {
speechText = 'Tu reputación es de ' + sessionAttributes['reputacion'] + ' y tu tesoro de ' + sessionAttributes['tesoro'] + '. Lamentablemente no has sobrevivido al viaje! Inténtalo otra vez! Hasta la próxima!';
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
}
speechText = module.yes.answer;
module.yes.warning ? speechText += module.yes.warning : speechText;
if(module.audio){
speechText += module.audio;
}
if(nextModule) {
speechText += nextModule.question;
handlerInput.responseBuilder.reprompt(speechText)
}
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
}
}
const NoHandler = {
canHandle(handlerInput){
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'AMAZON.NoIntent';
},
handle(handlerInput){
const {attributesManager} = handlerInput;
const sessionAttributes = attributesManager.getSessionAttributes();
let vueltas = sessionAttributes['vueltas'];
sessionAttributes['vueltas'] = vueltas ? vueltas + 1 : 0;
const moduleId = sessionAttributes['id'];
const module = getModule(moduleId);
const nextModule = getNextModule(moduleId);
if(nextModule) sessionAttributes['id'] = nextModule.id;
let speechText;
calculateGameVariables(sessionAttributes, module.no.variable1, module.no.variable2);
if(sessionAttributes['reputacion'] === 100 || sessionAttributes['reputacion'] === 0 || sessionAttributes['tesoro'] === 100 || sessionAttributes['tesoro'] === 0) {
speechText = 'Tu reputación es de ' + sessionAttributes['reputacion'] + ' y tu tesoro de ' + sessionAttributes['tesoro'] + '. Lamentablemente no has sobrevivido al viaje! Inténtalo otra vez! Hasta la próxima!';
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
}
speechText = module.no.answer;
module.no.warning ? speechText += module.no.warning : speechText;
if(module.audio){
speechText += module.audio;
}
if(nextModule) {
speechText += nextModule.question;
handlerInput.responseBuilder.reprompt(speechText)
}
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
}
}
const HelpIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent';
},
handle(handlerInput) {
const speechText = 'Solo tienes que contestar si o no durante el juego. Elige con sabiduría!';
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.getResponse();
}
};
const CancelAndStopIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'
|| handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent');
},
async handle(handlerInput) {
const {attributesManager} = handlerInput;
const sessionAttributes = attributesManager.getSessionAttributes();
const speechText = `Has durado ${sessionAttributes['vueltas']} turnos. Tu reputación fue de ${sessionAttributes['reputacion']} y tu tesoro de ${sessionAttributes['tesoro']}. Al salir te has caído por la borda así que tendrás que volver a empezar! Hasta la próxima!`;
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
}
};
const SessionEndedRequestHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === '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 handlerInput.requestEnvelope.request.type === 'IntentRequest';
},
handle(handlerInput) {
const intentName = handlerInput.requestEnvelope.request.intent.name;
const speechText = `You just triggered ${intentName}`;
return handlerInput.responseBuilder
.speak(speechText)
//.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.message}`);
const speechText = `Hubo un error. por favor inténtalo otra vez.`;
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.getResponse();
}
};
function getModule(id) {
return modules.game.filter(function(i) { return i.id === id; })[0];
}
function getNextModule(id) {
const module = getModule(id);
if(module.targets.length === 0) return null;
const nextTarget = module.targets[Math.floor(Math.random() * module.targets.length)];
return getModule(nextTarget);
}
function calculateGameVariables(sessionAttributes, reputacionDif, tesoroDif) {
sessionAttributes['reputacion'] += reputacionDif;
sessionAttributes['tesoro'] += tesoroDif;
console.log(sessionAttributes);
}
function getPersistenceAdapter() {
const {S3PersistenceAdapter} = require('ask-sdk-s3-persistence-adapter');
return new S3PersistenceAdapter({
bucketName: process.env.S3_PERSISTENCE_BUCKET
});
}
const LoadAttributesRequestInterceptor = {
async process(handlerInput) {
const {attributesManager, requestEnvelope} = handlerInput;
if(requestEnvelope.session['new']){ //is this a new session? this check is not enough if using auto-delegate
const persistentAttributes = await attributesManager.getPersistentAttributes() || {};
//copy persistent attribute to session attributes
attributesManager.setSessionAttributes(persistentAttributes);
}
}
};
const SaveAttributesResponseInterceptor = {
async process(handlerInput, response) {
if(!response) return; // avoid intercepting calls that have no outgoing response due to errors
const {attributesManager, requestEnvelope} = handlerInput;
const sessionAttributes = attributesManager.getSessionAttributes();
const shouldEndSession = (typeof response.shouldEndSession === "undefined" ? true : response.shouldEndSession); //is this a session end?
if(shouldEndSession || requestEnvelope.request.type === 'SessionEndedRequest') { // skill was stopped or timed out
attributesManager.setPersistentAttributes(sessionAttributes);
await attributesManager.savePersistentAttributes();
}
}
};
// This handler 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()
.addRequestHandlers(
LaunchRequestHandler,
YesHandler,
NoHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
SessionEndedRequestHandler,
IntentReflectorHandler) // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
.addRequestInterceptors(
require('./aplcard').APLHomeCardRequestInterceptor,
LoadAttributesRequestInterceptor)
.addResponseInterceptors(SaveAttributesResponseInterceptor)
.addErrorHandlers(
ErrorHandler)
.withPersistenceAdapter(persistenceAdapter)
.lambda();
{
"game": [
{
"id": 1,
"image": "https://media.tacdn.com/media/attractions-splice-spp-674x446/06/6f/3a/32.jpg",
"audio": "<audio src=\"soundbank://soundlibrary/nature/amzn_sfx_ocean_wave_1x_01\"/>",
"question": "Un noble capitán os ofrece la liberación de las mazmorras que os mantienen prisionero. a cambio debeis embarcaros en un viaje de dudoso destino hacia tierras desconocidas. Os embarcáis hacia la fortuna y el peligro? ",
"yes": {
"variable1": 10,
"variable2": 0,
"answer": "Las damas del lugar cuchichean al verte pasar entre los fornidos marineros que se dirijen a bordo. Has ganado 10 puntos de reputación. ",
"warning": "Cuidado, que si tu reputación llega a 100 el capitán verá amenazada su autoridad y te ejecutará lanzándote por la borda. "
},
"no": {
"variable1": -10,
"variable2": 0,
"answer": "Tus compañeros de celda no pueden creer que hayas dejado pasar la oportunidad de ser libre. Has perdido 10 puntos de reputación. ",
"warning": "Cuidado, que si tu reputación llega a 0 dejarás de existir para el resto de los mortales y te desvanecerás en el olvido. "
},
"targets": [2, 3]
},
{
"id": 2,
"image": "https://media.tacdn.com/media/attractions-splice-spp-674x446/06/6f/3a/32.jpg",
"audio": "<audio src=\"soundbank://soundlibrary/nature/amzn_sfx_ocean_wave_1x_02\"/>",
"question": "La vida a bordo es muy dura. La carabela solo ofrece hambre, sudor y pestilencia. Un marinero ex pirata se ha quedado dormido y ves la oportunidad de quitarle sus raciones sin que lo note. Tienes mucha hambre! Cojes las raciones? ",
"yes": {
"variable1": -10,
"variable2": 10,
"answer": "Al cojer las raciones tu tesoro se ha incrementado en 10 puntos pero, desafortunadamente, te han visto, y ya no inspiras el mismo respeto. Tu reputación ha caído en 10 puntos. ",
"warning": "Cuidado, que si tu reputación llega a 0 el capitán te lanzará por la borda por inservible. Y, si bien es bueno acumular riquezas, si tu tesoro llega a 100 el capitán te hará ejecutar para quedarse con tu botín. "
},
"no": {
"variable1": 10,
"variable2": -10,
"answer": "Tu nobleza te engrandece, pero tu tripa empieza a hacer ruido. Pierdes 10 puntos de tesoro por falta de comida. Los marineros ven tu honestidad, y por ello, ganas 10 puntos de reputación. ",
"warning": "Cuidado, que si tu reputación llega a 100 el capitán verá amenazada su autoridad y te ejecutará lanzándote por la borda. "
},
"targets": [3, 4]
},
{
"id": 3,
"image": "https://media.tacdn.com/media/attractions-splice-spp-674x446/06/6f/3a/32.jpg",
"audio": "<audio src=\"soundbank://soundlibrary/nature/amzn_sfx_ocean_wave_on_rocks_1x_01\"/>",
"question": "Empiezas a pensar que no ha sido tan buena idea embarcarte. Acabas de darte cuenta que sufres mareos con el vaivén de las olas! Tómas la medicina para no vomitar? ",
"yes": {
"variable1": 10,
"variable2": -10,
"answer": "La medicina te ha costado. Pierdes 10 puntos de tesoro pero ganas 10 de reputación ya que no has dado un espectáculo regando de vómito a tus camaradas. ",
"warning": ""
},
"no": {
"variable1": -10,
"variable2": 10,
"answer": "Has vendido la medicina al marinero más flojo del barco, por lo que tu tesoro se ha incrementado en 10 puntos. Desafortunadamente, has regado la cubierta de vómito, por lo que tus camaradas ya no te ven con los mismos ojos. Has perdido 10 puntos de reputación. ",
"warning": ""
},
"targets": [2, 4]
},
{
"id": 4,
"image": "https://media.tacdn.com/media/attractions-splice-spp-674x446/06/6f/3a/32.jpg",
"audio": "<audio src=\"soundbank://soundlibrary/nature/amzn_sfx_ocean_wave_on_rocks_1x_02\"/>",
"question": "Mientras te toca el turno de timonel, oyes una melodía exquisita a estribor. Apenas puedes controlarte para no seguir el canto de las sirenas. Alertas a los demás sobre el peligro? ",
"yes": {
"variable1": 10,
"variable2": 0,
"answer": "La voz de alarma ha disuadido a las sirenas y han desaparecido. La tripulación está aliviada y te lo agradece. Has ganado 10 puntos de reputación, pero aun dudas de lo que hubiera pasado al seguir el canto celestial. Quizá hubieras aprendido a entonar? ",
"warning": "Cuidado, que si tu reputación llega a 100, el capitán verá una amenaza de motín por parte de tus seguidores y te servirá de alimento a los tiburones. "
},
"no": {
"variable1": 0,
"variable2": 10,
"answer": "Las sirenas eran benévolas y te han enseñado a cantar. Como acompañamiento te han regalado una flauta. Tu tesoro crece en 10 puntos. ",
"warning": "Cuidado. Si tu tesoro llega a 100 puntos el capitán confiscará tus bienes para enriquecerse e inventará una excusa para justificar tu muerte. "
},
"targets": [2, 3, 5]
},
{
"id": 5,
"image": "https://media.tacdn.com/media/attractions-splice-spp-674x446/06/6f/3a/32.jpg",
"audio": "<audio src=\"soundbank://soundlibrary/human/amzn_sfx_crowd_cheer_med_01\"/>",
"question": "Tu sufrimiento a bordo por meses no ha sido en vano. A lo lejos se divisa la costa y tiene pinta de ser un nuevo mundo. La costa está compuesta de acantilados peligrosos. Te anímas a desembarcar? ",
"yes": {
"variable1": 0,
"variable2": 0,
"answer": "El resultado de tu temeraria acción deberá esperar al siguiente capítulo del juego. Por ahora has ganado, pero quién sabe que te depara el futuro. Hasta la próxima! ",
"warning": ""
},
"no": {
"variable1": 0,
"variable2": 0,
"answer": "El resultado de tu temeraria acción deberá esperar al siguiente capítulo del juego. Por ahora has ganado, pero quién sabe que te depara el futuro. Hasta la próxima! ",
"warning": ""
},
"targets": []
}
]
}
{
"name": "hello-world",
"version": "0.9.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": "^2.3.0",
"ask-sdk-s3-persistence-adapter": "^2.3.0",
"aws-sdk": "^2.326.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment