Utiliza la Web API for Games de Alexa

Amazon acaba de anunciar que su nueva Web API for Games de Alexa está disponible. Ésta es una gran noticia, ya que permite desarrollar skills usando todo el potencial de las aplicaciones Web, tales como WebGL, WebAudio, CSS y Javascript, en la creación de juegos para Alexa.

¿Pero cómo se usa? A modo de demostración, les mostraré cómo crear un juego muy simple con esta API, paso a paso.

El juego

El juego consistirá en que se mostrarán en pantalla algunas formas geométricas, y tú deberás tocar la de color rojo. Como lo he dicho, ¡muy simple!

Las skills basadas en aplicaciones web consisten en dos partes que interactúan: la skill de Alexa propiamente dicha, y la aplicación web. La skill se encargará de administrar los comandos de voz, y la lógica del juego, mientras que la aplicación web mostrará las formas en pantalla y gestionará la interacción táctil.

Tu aplicación web

Tienes que tener en cuenta que, a diferencia de una skill “normal”, en que lo único que necesitas es alojar tu función lambda, aquí deberás también alojar tu aplicación web. Para ello necesitarás un servidor. Alexa requiere que las aplicaciones se sirvan con HTTPS, así que puedes utilizar cualquier servidor que posea un certificado válido. Ten en cuenta que, al momento de lanzar la aplicación web al dispositivo, la URL de la aplicación se muestra en pantalla por unos 8 segundos, junto con el ícono de la skill.

Para mantener las cosas sencillas utilizaremos un bucket S3 para alojar nuestra aplicación, que consistirá solamente un archivo html con el código javascript en el mismo archivo. También utilizaremos una librería para manejar el canvas, optamos por Konva (https://konvajs.org/), pero se puede usar prácticamente cualquier librería que desees.

Además, para que la aplicación web se pueda comunicar con la skill, debemos usar la “Alexa JavaScript API”, simplemente agregando en tu documento HMTL el siguiente script:

https://cdn.html.games.alexa.a2z.com/alexa-html/latest/alexa-html.js

Con lo que nuestra página web nos viene quedando de ésta manera:

<!doctype html>
<html>
<head>
    <title>Web API for Games</title>
    <style>
        body {
            margin: 0;
            color: white;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <div id="container"></div>
    <script 
      src="https://cdn.html.games.alexa.a2z.com/alexa-html/latest/alexa-html.js">
    </script>
    <script src="https://unpkg.com/konva@7.0.3/konva.min.js">
    </script>
    <script>
        //aquí el código de la aplicación...
    </script>
</body>
</html>

Ahora es momento de agregar el código de la aplicación web, que por supuesto lo podemos escribir en el mismo archivo html, o en un archivo JS externo.

Lo primero que haremos es crear nuestro canvas, con tres objetos que necesitaremos: un cuadrado, un hexágono y un triángulo. Haremos esto utilizando la librería Konva que mencionamos anteriormente. Puedes ver toda la documentación, demos y tutoriales en este vínculo: https://konvajs.org/

const W = window.innerWidth
const H = window.innerHeight
//definimos el stage
const stage = new Konva.Stage({
    container: 'container',
    width: W,
    height: H
});
//y un layer, al cual le agregamos tres formas
let layer = new Konva.Layer();
stage.add(layer);

let shape1 = new Konva.RegularPolygon({
    x: W / 6,
    y: H / 2,
    sides: 4,
    radius: (H / 3),
    rotation: 45,
    fill: '',
    stroke: 'black',
    strokeWidth: 4,
    name: "cuadrado"
});
layer.add(shape1);

let shape2 = new Konva.RegularPolygon({
    x: W / 2,
    y: H / 2,
    sides: 6,
    radius: (H / 3),
    fill: '',
    stroke: 'black',
    strokeWidth: 4,
    name: "hexagono"
});

layer.add(shape2)

let shape3 = new Konva.RegularPolygon({
    x: 5 * W / 6,
    y: H / 2,
    sides: 3,
    radius: (H / 3),
    fill: '',
    stroke: 'black',
    strokeWidth: 4,
    name: "triangulo"
});
layer.add(shape3)
layer.draw();

Una vez que tenemos las formas definidas, declaramos una función y la asignamos como callback para cuando tocamos uno de los objetos en pantalla.

function shapeClickHandler(e) {
    //por ahora, la funcion solamente imprime el color de la forma que se ha tocado
    //esto lo modificaremos luego para que se comunique con la skill
    console.log(e.target.fill())
}

shape1.on('tap', shapeClickHandler)
shape2.on('tap', shapeClickHandler)
shape3.on('tap', shapeClickHandler)

Ahora, debemos crear el objeto Alexa con el API SDK, mediante la instrucción

var client; //declaramos un objeto para referenciar al cliente
Alexa.create({ version: '1.0' })
    .then((args) => {
        const {
            alexa,
            message
        } = args;
        //guardamos la referencia al cliente
        client = alexa
        //aquí inicializamos el cliente
    })
    .catch(error => {
        console.error('failed to initialize')
    });

Finalmente, debemos que configurar este objeto “cliente”, registrando los callbacks para los diversos eventos que puede manejar. Por ejemplo, puedes registrar funciones que se ejecuten cuando llega un mensaje, cuando el dispositivo comienza a hablar, cuando termina la locución, cuando se abre o cierra el micrófono o cuando el cliente finaliza la inicialización.

Nosotros declararemos solamente una función para manejar los mensajes que nos llegan de la skill, pintando las formas con los colores que nos indique la skill:

function processAlexaMessage(message) {
    if (message.intent == "SetColors") {
        shape1.fill(message.colors[0]);
        shape2.fill(message.colors[1])
        shape3.fill(message.colors[2])
        layer.draw()
    }
}

Llamaremos a esta funcion cuando llegue cualquier mensaje de la skill, aún el de inicialización de la aplicación. Para esto agregamos en la inicialización, con lo que nuestro código queda así:

Alexa.create({ version: '1.0' })
    .then((args) => {
        const {
            alexa,
            message
        } = args;
        //guardamos la referencia al cliente
        client = alexa
        //aquí inicializamos el cliente
        processAlexaMessage(message) //procesa el mensaje de inicialización
        //asigna la funcion como callback para los mensajes recibidos
        client.skill.onMessage(processAlexaMessage);
    })
    .catch(error => {
        console.error('failed to initialize')
    });

Finalmente, modificamos la funcion “shapeClickHandler”, para enviar a la skill los datos de la forma que se tocó en pantalla, de la siguiente manera:

function shapeClickHandler(e) {
    if (client != null) {
        client.skill.sendMessage({
            intent: "AnswerIntent",
            shape: e.target.getAttr('name'),
            color: e.target.fill()
        });
    }
}

Una vez que tenemos la aplicación web terminada, la subimos al bucket S3 (asegurándonos de darle permiso de lectura público), y anotamos la URL de dicho archivo.

Tu skill

Ahora es el turno de programar nuestra skill para que interactúe con la aplicación web que acabamos de desarrollar. No voy a documentar todo el código de la skill, ya que no deja de ser una skill como otras que ya sabes cómo desarrollar. Solamente mostraré los pasos necesarios para que la skill lance la aplicación web, envíe y responda los mensajes.

Antes que nada, necesitamos declarar que nuestra skill declare la nueva interfaz ALEXA_PRESENTATION_HTML. Esto se puede hacer desde la solapa “Interfaces” de la consola de desarrollo, activando “Alexa Web API for Games”, o, si utilizas la interfaz CLI, deberás modificar tu skill manifest (skill.json), agregando en el apartado “interfaces”, el objeto

{  
    "type": "ALEXA_PRESENTATION_HTML"
}

Para arrancar la aplicación web, enviaremos la directiva “Alexa.Presentation.HTML.Start” en el handler del LaunchRequest. En esta directiva indicaremos la URL completa de la aplicación web (que anotamos del bucket S3), y además enviaremos los colores iniciales para las formas

handlerInput.responseBuilder.addDirective({
    type: "Alexa.Presentation.HTML.Start",
    data: {
        "intent": "SetColors",
        "colors": ["green", "red", "blue"]
    },
    request: {
        uri: "https://mi-web-app.com/webapp.html",
        method: "GET"
    },
    configuration: {
        "timeoutInSeconds": 300
    }
});

Esto cargará la aplicación en el dispositivo. ¡Ya tienes tu aplicación corriendo!

La aplicación envía mensajes a la skill…

Cuando toques una forma en la pantalla, la aplicación web ejecutará la funcion “shapeClickHandler” y enviará un mensaje a la skill con un request de ésta forma:

{
    type: 'Alexa.Presentation.HTML.Message',
    requestId: 'amzn1.echo-api.request.21512c3b-18e4-4497-8275-f18e0c70c3b8',
    timestamp: '2020-07-24T16:32:18Z',
    locale: 'es-ES',
    message: {
        intent: "AnswerIntent",
        shape: "cuadrado",
        color: "green"
    }
}

Con lo que debemos declarar un handler para estos requests y actuar según lo que nos diga el mensaje:

AnswerIntentHandler: {
    canHandle(handlerInput) {
        const request = handlerInput.requestEnvelope.request
        return request.type === 'Alexa.Presentation.HTML.Message'
            && request.message.intent === 'AnswerIntent';
    },
    handle(handlerInput) {
        let speakOutput = ""
        if (handlerInput.requestEnvelope.request.message.color == "red") 
            speakOutput = "Correcto"
        else 
            speakOutput = "Lo siento";

        return handlerInput.responseBuilder
                .speak(speakOutput)
                .getResponse();
    }
}

…y la skill le responde a la aplicación.

Cuando la skill necesita comunicar un evento a la aplicación web, debe enviar una directiva “Alexa.Presentation.HTML.HandleMessage” con el mensaje apropiado.

Por ejemplo, si queremos que nuestra aplicación cambie los colores de las formas agregamos esta directiva antes de devolver la respuesta:

handlerInput.responseBuilder.addDirective({
    "type": "Alexa.Presentation.HTML.HandleMessage",
    "message": {
        "intent": "SetColors",
        "colors": ["red", "blue", "green"]
    }
})

Con lo que nuestro handler quedará así:

AnswerIntentHandler: {
    canHandle(handlerInput) { 
        const request = handlerInput.requestEnvelope.request
        return request.type === 'Alexa.Presentation.HTML.Message'
            && request.message.intent === 'AnswerIntent';
    },
    handle(handlerInput) {
        let speakOutput = ""
        if (handlerInput.requestEnvelope.request.message.color == "red") 
            speakOutput = "Correcto"
        else 
            speakOutput = "Lo siento";

        handlerInput.responseBuilder.addDirective({
            "type": "Alexa.Presentation.HTML.HandleMessage",
            "message": {
                "intent": "SetColors",
                "colors": ["red", "blue", "green"]
            }
        })

        return handlerInput.responseBuilder
                .speak(speakOutput)
                .getResponse();
    }
}

Entonces, declarando este request handler en el SkillBuilder de tu skill, ya podrás responder a los eventos que envíe tu aplicación web. A partir de aquí, puedes agregar eventos, mensajes y lógica a tu aplicación como quieras.

¿Cuándo se cierra la aplicación?

A diferencia de una skill normal, mientras está cargada la aplicación web en pantalla, la sesión quedará abierta, hasta que el usuario salga de la skill diciendo por ejemplo: “Alexa, salir”, o hasta que la skill envíe una directiva para una interfaz diferente de Alexa.Presentation.HTML (por ejemplo, si enviamos una pantalla APL para el dispositivo). En este último caso, se cerrará la aplicación web, pero no necesariamente la sesión de la skill, según el valor de shouldEndSession que contenga la respuesta.

¡No se puede hacer cualquier cosa!

En primer lugar, ¡tu skill debe ser un juego! Si no, no pasará la certificación.
No puedes enviar más de dos mensajes por segundo entre la aplicación web y la skill.
Si bien el dispositivo tiene grandes capacidades, no todas ellas están expuestas, y hay cosas que no podrás hacer:

  • utilizar geolocalización
  • utilizar la cámara o el micrófono
  • utilizar alert(), prompt() o confirm() en JavaScript
  • cargar un archivo local con urls de tipo file://
  • utilizar la API para contenido local
  • utilizar WebSQL
  • utilizar Local Storage
  • acceder a contenido HTTP (solamente se puede usar HTTPS)

Las cookies, datos de formulario y la historia solo están disponibles durante la directiva Start, luego se eliminan estos datos.

¡Sigue leyendo!

Aquí encontrarás toda la documentación relevante (en inglés):