Mazzmn
Created November 15, 2018

Candy Grab!

A race to get the most candy! The Amazon Echo Button is the "spinner", players spin a color or an animal. Some animals will steal candy!

81

Things used in this project

Hardware components

Echo Show (2nd Gen)
Amazon Alexa Echo Show (2nd Gen)
×1
Amazon Alexa Echo Buttons
×1

Software apps and online services

Alexa Skills Kit
Amazon Alexa Alexa Skills Kit
This is node.js and includes use of the Alexa Skills Kit Sound Library
AWS Lambda
Amazon Web Services AWS Lambda

Story

Read more

Code

myCandyGrab.js

JavaScript
Node.js code for the Candy Grab game. Note this requires 'alexa-sdk'
'use strict';

const Alexa = require('alexa-sdk');
const SKILL_NAME = "Candy Grab";
let speechOutput ="";

exports.handler = (event, context) => {
  // standard Alexa Skill Kit initialization
  const alexa = Alexa.handler(event, context);
  alexa.appId = '';
  alexa.registerHandlers(main);
  alexa.execute();
};

const main = {
  'LaunchRequest': function(handlerInput) {

    if(Object.keys(this.attributes).length === 0) {
      this.attributes['sessionProcess'] = 'LaunchRequest';
      this.attributes['timeStamp'] = + new Date();
      this.attributes['timeOutOnce'] = false;

    }
    console.log("starting " + this.attributes['sessionProcess']);

    /*
      For this skill, on launch we'll immediately setup the
      input handler to listen to all attached buttons for 30
      seconds.
      We'll setup two events that each report when buttons are
      pressed down and when they're released up.
      After 60 seconds, we'll get the timeout event.
    */
    this.response._addDirective({
      "type": "GameEngine.StartInputHandler",
      "timeout": 30000,
      "recognizers": {
        "button_down_recognizer": {
          type: "match",
          fuzzy: false,
          anchor: "end",
          "pattern": [{
            "action": "down"
          }]
        }
      },
      "events": {
        "button_down_event": {
          "meets": ["button_down_recognizer"],
          "reports": "matches",
          "shouldEndInputHandler": false
        },
        "timeout": {
          "meets": ["timed out"],
          "reports": "history",
          "shouldEndInputHandler": false
        }
      }
    });

    this.attributes.buttonCount = 0;

    if (this.attributes['sessionProcess'] == 'LaunchRequest') {
          // build 'button down' animation for when the button is pressed
      this.response._addDirective(buildButtonDownAnimationDirective([]));
      this.response.cardRenderer(SKILL_NAME, "Let's play candy grab!  Make four groups of candy, then youngest player, press the Echo Button to start! or say help for more info.");
      this.response.speak("<audio src='soundbank://soundlibrary/magic/amzn_sfx_magic_blast_1x_01'/> Let's play candy grab!  Make four groups of candy, then youngest player, press the Echo Button to start!");
      this.handler.response.response.shouldEndSession = false;

    } 

    this.emit(':responseReady');
  },
  
  
  'GameEngine.InputHandlerEvent': function() {
    console.log('Received game event', JSON.stringify(this.event, null, 2));

    let gameEngineEvents = this.event.request.events || [];
    for (let i = 0; i < gameEngineEvents.length; i++) {

      let buttonId;

      switch (gameEngineEvents[i].name) {
        case 'button_down_event':
          this.attributes['timeStamp'] = + new Date();
          // id of the button that triggered event
          buttonId = gameEngineEvents[i].inputEvents[0].gadgetId;

          if (this.attributes[buttonId + '_initialized'] === undefined) {
            this.attributes.buttonCount += 1;
             this.attributes[buttonId + '_initialized'] = true;
          }

          let colorNumber = randomInteger(1,6);
          delete this.handler.response.response.shouldEndSession;
          let responseText = randomColor(colorNumber);
          if (colorNumber > 4) {
            let responseText2 = responseText.replace(/\<audio.*\>/, '');
            this.response.cardRenderer(SKILL_NAME, responseText2);
          } else {
            this.response.cardRenderer(SKILL_NAME, responseText);
          }
            this.response.speak(' ' + spinnerSays() + " " + responseText) ;
            console.log("color number "+ colorNumber);

          switch (colorNumber) {
            case 1:  
              this.response._addDirective(buildButtonIdleAnimationDirective([buttonId], redAnimation));
              break;
            case 2: 
              this.response._addDirective(buildButtonIdleAnimationDirective([buttonId], blueAnimation));
              break;
            case 3:  
              this.response._addDirective(buildButtonIdleAnimationDirective([buttonId], greenAnimation));
              break;
            case 4:  
              this.response._addDirective(buildButtonIdleAnimationDirective([buttonId], purpleAnimation));
              break;
            case 5:
              this.response._addDirective(buildButtonIdleAnimationDirective([buttonId], breathAnimation));
              break;
            case 6:
              this.response._addDirective(buildButtonIdleAnimationDirective([buttonId], breathAnimation));
              break;
          }

          this.handler.response.response.outputSpeech.playBehavior = 'REPLACE_ALL';
          delete this.handler.response.response.shouldEndSession;
          this.emit(':responseReady');
          break;

        case 'timeout':
          let lasttime = this.attributes['timeStamp'];
          let thistime = + new Date();
          let sincelasttime = thistime - lasttime;

          console.log('timeout case sincelasttime '+ sincelasttime);
          if (sincelasttime < 50000) {
            this.attributes['sessionProcess'] = 'theTimeOut';
            delete this.handler.response.response.shouldEndSession;
            this.emit('LaunchRequest');
          }
          else {
            this.response.speak("Thank you for using Candy Grab! " + sincelasttime);
            this.handler.response.response.shouldEndSession = true;
            this.emit('AMAZON.StopIntent');
          }
          break;
      }
    }
  },

  'AMAZON.HelpIntent': function() {
    console.log('HelpIntent');
    this.handler.response.response.shouldEndSession = false;
    this.attributes['timeStamp'] = + new Date();
    speechOutput = "It's The Candy Grab game! Start with four small piles of candy, a Red, Blue, Green and Purple Pile, (colors don\'t have to match the pile color). "+
    "Youngest player goes first, press the Echo Button to spin...you will spin one of the following: "+
    "1, A random color... if a piece of candy of this color is left in the pile, grab it and put it in your private stash (but don't eat it yet!) " +
    "2, Animal sound...Aww, too bad! no candy when this happens. Or " +
    "3, crash sound...Oh no, Candy Crash!, you have to return 2 pieces of candy! " + 
    "When the candy in the middle is gone, the player with the most candy in their stash wins! ... Remember winner, sharing is good!, Now let's begin!";
    this.attributes['timeStamp'] = + new Date();
    this.response.cardRenderer(SKILL_NAME, "Start with 4 small piles of candy, a Red, Blue, Green and Purple. (colors don\'t have to match the pile color). "+
    "Youngest player goes first, press the Echo Button to spin...you will spin one of the following:  " +
     "1, A random color... if a piece of candy of this color is left in the pile, grab it and put it in your private stash (but don't eat it yet!) " +
    "2, Animal sound...Aww, too bad! no candy when this happens. Or " +
    "3, crash sound...Oh no, Candy Crash!, you have to return 2 pieces of candy! " + 
    "When the candy in the middle is gone, the player with the most candy in their stash wins! ... Remember winner, sharing is good!, Now let's begin!");
    this.response.speak(speechOutput);
    this.emit(':responseReady');
  },
  
  'AMAZON.StopIntent': function() {
    console.log('StopIntent');
    this.response.speak('Good Bye for now from Candy Grab');
    this.response.cardRenderer(SKILL_NAME, "Good Bye for now from Candy Grab.");
    this.response._addDirective(buttonFadeoutAnimationDirective);
    this.emit(':responseReady');
  },
  'AMAZON.CancelIntent': function() {
    console.log('CancelIntent');
    this.response.speak('Alright, canceling');
    this.response._addDirective(buttonFadeoutAnimationDirective);
    this.emit(':responseReady');
  },
  'SessionEndedRequest': function() {
    console.log('SessionEndedRequest');
  },
  'System.ExceptionEncountered': function() {
    console.log('ExceptionEncountered');
    console.log(this.event.request.error);
    console.log(this.event.request.cause);
  },
  'Unhandled': function() {
    console.log('Unhandled');
    const msg = "Sorry, I didn't get that.";
    this.emit(':ask', msg, msg);
  }
};

const buildBreathAnimation = function(fromRgbHex, toRgbHex, steps, totalDuration) {
  const halfSteps = steps / 2;
  const halfTotalDuration = totalDuration / 2;
  return buildSeqentialAnimation(fromRgbHex, toRgbHex, halfSteps, halfTotalDuration)
    .concat(buildSeqentialAnimation(toRgbHex, fromRgbHex, halfSteps, halfTotalDuration));
};

const buildSeqentialAnimation = function(fromRgbHex, toRgbHex, steps, totalDuration) {
  const fromRgb = parseInt(fromRgbHex, 16);
  let fromRed = fromRgb >> 16;
  let fromGreen = (fromRgb & 0xff00) >> 8;
  let fromBlue = fromRgb & 0xff;

  const toRgb = parseInt(toRgbHex, 16);
  const toRed = toRgb >> 16;
  const toGreen = (toRgb & 0xff00) >> 8;
  const toBlue = toRgb & 0xff;

  const deltaRed = (toRed - fromRed) / steps;
  const deltaGreen = (toGreen - fromGreen) / steps;
  const deltaBlue = (toBlue - fromBlue) / steps;

  const oneStepDuration = Math.floor(totalDuration / steps);
  const result = [];

  for (let i = 0; i < steps; i++) {
    result.push({
      "durationMs": oneStepDuration,
      "color": rgb2h(fromRed, fromGreen, fromBlue),
      "intensity": 255,
      "blend": true
    });
    fromRed += deltaRed;
    fromGreen += deltaGreen;
    fromBlue += deltaBlue;
  }

  return result;
};

const rgb2h = function(r, g, b) {
  return '' + n2h(r) + n2h(g) + n2h(b);
};
// number to hex with leading zeroes
const n2h = function(n) {
  return ('00' + (Math.floor(n)).toString(16)).substr(-2);
};

let breathAnimation = buildBreathAnimation('1e90ff', 'b22222', 30, 1200);
let redAnimation = buildBreathAnimation('f44242', 'f44242', 30, 1200);
let blueAnimation = buildBreathAnimation('4b42f4', '4b42f4', 30, 1200);
let greenAnimation = buildBreathAnimation('22b222', '22b222', 30, 1200);
let purpleAnimation = buildBreathAnimation('7441f4', '7441f4', 30, 1200);


// build 'button down' animation directive
// animation will overwrite default 'button down' animation
const buildButtonDownAnimationDirective = function(targetGadgets) {
  return {
    "type": "GadgetController.SetLight",
    "version": 1,
    "targetGadgets": targetGadgets,
    "parameters": {
      "animations": [{
        "repeat": 1,
        "targetLights": ["1"],
        "sequence": [{
          "durationMs": 300,
          "color": "228b22",
          "intensity": 255,
          "blend": false
        }]
      }],
      "triggerEvent": "buttonDown",
      "triggerEventTimeMs": 0
    }
  };
};

// build idle animation directive
const buildButtonIdleAnimationDirective = function(targetGadgets, animation) {
  return {
    "type": "GadgetController.SetLight",
    "version": 1,
    "targetGadgets": targetGadgets,
    "parameters": {
      "animations": [{
        "repeat": 100,
        "targetLights": ["1"],
        "sequence": animation
      }],
      "triggerEvent": "none",
      "triggerEventTimeMs": 0
    }
  };
};

// fadeout animation directive
const buttonFadeoutAnimationDirective = {
  "type": "GadgetController.SetLight",
  "version": 1,
  "targetGadgets": [],
  "parameters": {
    "animations": [{
      "repeat": 1,
      "targetLights": ["1"],
      "sequence": [{
        "durationMs": 1,
        "color": "FFFFFF",
        "intensity": 255,
        "blend": true
      }, {
        "durationMs": 1000,
        "color": "000000",
        "intensity": 255,
        "blend": true
      }]
    }],
    "triggerEvent": "none",
    "triggerEventTimeMs": 0
  }
};

function randomColor(colorNumber) {
  let outText = "";
  switch (colorNumber) {
    case 1:
      outText = outText + " Red. Take one from the Red Pile.";
      break;
    case 2:
      outText = outText + " Blue. Take one of the Blue.";
      break;
    case 3:
      outText = outText + " Green. Take one from the Green Pile.";
      break;
    case 4:
      outText = outText + " Purple. Take one from the Purple Pile.";
      break;
    case 5:
      if (randomInteger(1, 2)==1) {
        outText = outText + "<audio src='soundbank://soundlibrary/animals/amzn_sfx_bear_roar_small_01'/> Watch it, a bear! You have to put 1 piece back. ";
      } else {
        outText = outText + "<audio src='soundbank://soundlibrary/animals/amzn_sfx_dog_med_bark_growl_01'/> Oops, it\'s the watchdog! You don't get a piece this time...";
      }
      break;
    case 6:
      if (randomInteger(1, 3)==1) {
        outText = outText + "<audio src='soundbank://soundlibrary/cartoon/amzn_sfx_boing_long_1x_01'/> Oh no! Candy Crash! You have to put 2 pieces back!";
      } else {
        outText = outText + "<audio src='soundbank://soundlibrary/animals/amzn_sfx_monkey_chimp_01'/> A crazy monkey! You don't get a piece of candy this turn!";
      }
      break;
  }
  return outText + " " + passTheButton();
  
}
function randomInteger(lowest, highest) {
  return Math.floor(Math.random() * (highest - lowest + 1) + lowest);
}

function spinnerSays() {
  let random = randomInteger(1,4);
  let responseString ;
  switch(random) {
    case 1:
      responseString = "Spinning...Spinner says:<audio src='soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_positive_response_01'/>";
      break;
    case 2:
      responseString = "Spinning...<audio src='soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_positive_response_01'/>";
      break;
    case 3:
      responseString = "Here We go!:<audio src='soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_positive_response_01'/>";
      break;
    case 4:
      responseString = "And the spinner says:<audio src='soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_positive_response_01'/>";
      break;
  }
  
  return responseString;
}

function passTheButton() {
  let random = randomInteger(1,4);
  let responseString ;
  switch(random) {
    case 1:
      responseString = " Then pass the spinner button.";
      break;
    case 2:
      responseString = " Then on to the next player.";
      break;
    case 3:
      responseString = " Then pass the button.";
      break;
    case 4:
      responseString = " Then the next player goes.";
      break;
  }
  
  return responseString;
}

en-US.json

JSON
json for the Interaction Model
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "candy grab",
            "intents": [
                {
                    "name": "AMAZON.FallbackIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": [
                        "cancel"
                    ]
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": [
                        "help"
                    ]
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": [
                        "stop"
                    ]
                }
            ],
            "types": []
        }
    }
}

Credits

Mazzmn

Mazzmn

3 projects • 6 followers

Comments