This project allows four players to compete against each other in a quiz game. It combines the best features of both Alexa and MINDSTORMS, leveraging the voice features of Alexa and the mechanical and sensor features of MINDSTORMS. Alexa asks questions and players have to use MINDSTORMS controllers to respond.
Alexa recognizes the first player to buzz in using the MINDSTORMS controllers and allows the player to answer the question. If the player answers the question correctly, they earn a point. Alexa serves as the host and will keep asking five questions. The winner is the player with the most points at the end of five questions.
This Quiz Game application is very versatile. The questions can easily be changed to refresh the game when needed. It can be used by adults or children, at home or in the classroom. You can make the game harder or easier as well depending upon the audience. In the sample game provided, I created a country capitals quiz.
Alexa plays the role of the Game Host. She starts the game by asking questions. She picks randomly from a large database of questions entered ahead of time. The screen of the Echo Show is also used to display the answers. Players use their Controller to indicate that they would like to answer the question. Alexa checks to see who has buzzed in first and lets that player know. Then the player verbally tells Alexa the answer. Alexa checks the answer and keeps track of the score for each player.
Watch this video to see how it works.
Hardware Overview:This project deliberately requires a very simple build which makes it project easy to reproduce with minimal parts: 4 touch sensors need to be connected to a MINDSTORMS EV3 brick using EV3 cables. The touch sensors are cleverly designed as colorful controllers that fit neatly in the palm of your hand. Build instructions are provided for easy duplication. Follow the instructions and build four identical controllers. You can connect any controller into any of the four EV3 sensor ports.
To start, I chose a quiz game template when creating the skill. I wanted to take the single player quiz game that was provided and use a LEGO MINDSTORMS EV3 to make it into a physical multiplayer game.
While the quiz game provides a useful starting frame, the EV3-Alexa Quiz Game was extended in three significant ways: EV3 Integration, Multiplayer Scoring, and Game Interaction. I go through each of these steps below.
EV3 Integration. When the program starts it connects to the EV3 as follows:
const request = handlerInput.requestEnvelope;
const { apiEndpoint, apiAccessToken } = request.context.System;
const apiResponse = await Util.getConnectedEndpoints(apiEndpoint, apiAccessToken);
if ((apiResponse.endpoints || []).length === 0) {
return handlerInput.responseBuilder
.speak(`I couldn't find an EV3 Brick connected to this Echo device. Please check to make sure your EV3 Brick is connected, and try again.`)
.getResponse();
}
// Store the gadget endpointId to be used in this skill session
const endpointId = apiResponse.endpoints[0].endpointId || [];
Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);
// Set skill duration to 5 minutes (ten 30-seconds interval)
Util.putSessionAttribute(handlerInput, 'duration', 30);
// Set the token to track the event handler
const token = handlerInput.requestEnvelope.request.requestId;
Util.putSessionAttribute(handlerInput, 'token', token);
The above code both establishes the connection and does some setup to support custom events from the EV3.
Unlike the standard quiz template, which just asks a question and waits for an answer, the EV3 version must also incorporate a stage where players "buzz" in to answer the question.
To request to the EV3 to run the game controllers. I added the below code where questions are read aloud by Alexa
let endpointId = attributes.endpointId || [];
let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'ask',
});
This sends and 'ask' command to the EV3. In response, the EV3 runs the below python code.
def on_custom_mindstorms_gadget_control(self, directive):
"""
Handles the Custom.Mindstorms.Gadget control directive.
:param directive: the custom directive with the matching namespace and name
"""
try:
payload = json.loads(directive.payload.decode("utf-8"))
print("Control payload: {}".format(payload), file=sys.stderr)
control_type = payload["type"]
if control_type == "ask":
self._ask()
except KeyError:
print("Missing expected parameters: {}".format(directive), file=sys.stderr)
The above python code receives the message, parses it and then invokes the _ask function below.
def _ask(self):
print("Entering ASK", file=sys.stderr)
self.leds.set_color("LEFT", "GREEN")
self.leds.set_color("RIGHT", "GREEN")
time.sleep(2.5)
self.leds.set_color("LEFT", "RED", 1)
self.leds.set_color("RIGHT", "RED", 1)
p1 = False
p2 = False
p3 = False
p4 = False
while True:
if self.p1.is_released:
p1 = True
if self.p2.is_released:
p2 = True
if self.p3.is_released:
p3 = True
if self.p4.is_released:
p4 = True
if self.p1.is_pressed and p1:
self._send_event(EventName.ANSWER, {'player': 1})
break
if self.p2.is_pressed and p2:
self._send_event(EventName.ANSWER, {'player': 2})
break
if self.p3.is_pressed and p3:
self._send_event(EventName.ANSWER, {'player': 3})
break
if self.p4.is_pressed and p4:
self._send_event(EventName.ANSWER, {'player': 4})
break
self.leds.set_color("LEFT", "GREEN")
self.leds.set_color("RIGHT", "GREEN")
print("Exiting ASK", file=sys.stderr)
The python code changes the color of the LEDs from green to red when it begins checking the controller buttons. It also clears 4 flag variables (p1, p2, p3, p4) that are used to ensure that the buttons are released before they are pressed. The remainder of the function is a loop that checks each controller to see if it has been pressed. The first time a button is pressed, a custom event checks each game controller to see if the button is pressed. The ID of the first controller to be pressed is sent back to Alexa as a Custom.Mindstorms.Gadget event of type ANSWER is sent to the Alexa with a payload as the player ID that pressed the button.
On the Alexa side, all user answers are ignored until the custom event is received from the EV3. This is done by setting a flag (attributes.buzzed) to false when the question is asked. This flag is then set to true when the EV3 button pressed event is received.
const EventsReceivedRequestHandler = {
// Checks for a valid token and endpoint.
canHandle(handlerInput) {
let { request } = handlerInput.requestEnvelope;
console.log('Request type: ' + Alexa.getRequestType(handlerInput.requestEnvelope));
if (request.type !== 'CustomInterfaceController.EventsReceived') return false;
const attributesManager = handlerInput.attributesManager;
let sessionAttributes = attributesManager.getSessionAttributes();
let customEvent = request.events[0];
// Validate event token
if (sessionAttributes.token !== request.token) {
console.log("Event token doesn't match. Ignoring this event");
return false;
}
// Validate endpoint
let requestEndpoint = customEvent.endpoint.endpointId;
if (requestEndpoint !== sessionAttributes.endpointId) {
console.log("Event endpoint id doesn't match. Ignoring this event");
return false;
}
return true;
},
handle(handlerInput) {
const attributes = handlerInput.attributesManager.getSessionAttributes();
console.log("== Received Custom Event ==");
let customEvent = handlerInput.requestEnvelope.request.events[0];
let payload = customEvent.payload;
let name = customEvent.header.name;
let speechOutput;
if (name === 'Answer') {
let player = parseInt(payload.player);
attributes.activePlayer = player
attributes.buzzed = true
let speechOutput = "Player " + payload.player + " buzzed first, what is your answer";
return handlerInput.responseBuilder
.speak(speechOutput, "REPLACE_ALL")
.withShouldEndSession(false)
.getResponse();
} else {
speechOutput = "Event not recognized. Awaiting new command.";
}
return handlerInput.responseBuilder
.speak(speechOutput + BG_MUSIC, "REPLACE_ALL")
.getResponse();
}
};
In addition to setting the attributes.buzzed flag, the above code also announces the player that buzzed first and prompts for an answer.
Multiplayer Scoring. Another significant addition to the code is the tracking of individual player scores. The scores are stored in the quizScore array in the Session Attributes for the Alexa skill.
if (isCorrect) {
speakOutput = "Good Job Player " + attributes.activePlayer + ".";
attributes.quizScore[attributes.activePlayer] += 1;
handlerInput.attributesManager.setSessionAttributes(attributes);
} else {
speakOutput = "Wrong, player " + attributes.activePlayer + ".";
}
When the player (attributes.activePlayer) gets a correct answer, their score is increased by 1. In addition, the Alexa verbal feedback announces the player that got (or missed) the score. After each question, the Alexa uses the below code to create a message with the current score.
function getCurrentScore(score, counter) {
return `After ${counter} turns, Player 1 has ${score[1]} points. 2 has ${score[2]} points. 3 has ${score[3]} points and Player 4 has ${score[4]} points. <break time="0.5s"/>`;
}
In addition, at the end of the game, I create a message that announces the winner and ties.
function getFinalScore(score, counter) {
let winners = []
let max = Math.max(...score)
let message = 'Your game is over. '
message += 'The max score was ' + max + '. '
if (score[1] === max) {
winners.push("1")
}
if (score[2] === max) {
winners.push("2")
}
if (score[3] === max) {
winners.push("3")
}
if (score[4] === max) {
winners.push("4")
}
if (winners.length === 1) {
message += 'The winner was Player ' + winners.shift()
} else {
message += 'The winners were Players '
while (winners.length > 1) {
message += winners.shift() + ", "
}
message += " and " + winners.shift()
}
return message
}
In the above code, I first determine the winning score and then identify which players had this score. If more than one player had the winning score, I use a loop to announce the tie score.
Game Interaction. The final major addition was creating the set of questions for my game. I choose to use country capitals for my quiz. The first step was creating an array with the country names, capitals, and country code abbreviations. This looks like:
const data = [
{CountryName: 'Republic of Albania', Abbreviation: 'al', Capital: 'Tirana'},
{CountryName: 'People\'s Democratic Republic of Algeria', Abbreviation: 'dz', Capital: 'Algiers'},
{CountryName: 'Principality of Andorra', Abbreviation: 'ad', Capital: 'Andorra la Vella'},
...
{CountryName: 'Republic of Zimbabwe', Abbreviation: 'zw', Capital: 'Harare'},
{CountryName: 'Plurinational State of Bolivia', Abbreviation: 'bo', Capital: 'La Paz'}
];
The abbreviations are used to name the files that store the flag image for each country. The flags are stored at the URL https://raw.githubusercontent.com/arvindseshan/EV3-Alexa-Quiz-Game/master/flags/{2}.jpg where the {2} is replaced by the 2 letter country abbreviation.
In addition, the question generation code needed to be modified to only ask questions about the capitals.
Finally, the skill Intents need to be modified to reflect this changed game. The user only tells Alexa the name of the capital for each answer. As a result, the AnswerIntent was modified to only take {Capital} as an utterance. I initially used AMAZON.City as the slot type for Capitals. However, this did not work as well as I expected. Looking over Amazon CloudWatch logs, I was able to identify that the Alexa was using a much wider vocabulary in recognizing the utterances. For example, the city "Dakar" was being recognized as "the car". This would score players as getting a question incorrect even if they spoke the correct city name. To eliminate this issue, I created a custom Slot Type with just the list of capitals that I use in the game. This significantly improved the recognition quality and made the game more playable.
For users that wish to change the questions for the game, the above data array needs to be changed, the askQuestion function needs to be modified to ask the type of questions desired and the AnswerIntent Interaction Model needs to be modified to identify the type of answers expected.
Recreating the Project:The hackster.io LEGO MINDSTORMS Voice Challenge is the starting point for these instructions. Modifications to the basic instructions provided by hackster.io are indicated in each step when necessary.
1. Start by following the instructions available on this page: https://www.hackster.io/alexagadgets/lego-mindstorms-voice-challenge-setup-17300f. Use it to install ev3dev on your Mindstorms EV3 device and Visual Studio Code on your computer.
2. Register your Alexa Gadget using the instructions available on this page: https://www.hackster.io/alexagadgets/lego-mindstorms-voice-challenge-setup-17300f
3. Download the code for Quiz Game from the linked GitHub repository. In the alexaplot.ini file, fill in the values for amazonId and alexaGadgetSecret with the values from step 2.
4. Build four Quiz Game controllers using the PDF for build instructions. You can connect the four controllers into any of the four sensor ports on the EV3 Brick as indicated in the Schematics File.
5. Next, you will have to complete an Amazon Skill. To create a skill, follow the instructions available at https://www.hackster.io/alexagadgets/lego-mindstorms-voice-challenge-mission-3-4ed812#toc-create-your-alexa-skill-2. Name your skill "Quiz Game" or whatever you want to call it. Choose a name you like since it is the name you will use to make Alexa run the skill. Custom Interface Controller will need to be enabled for this project.
6. You will now need to define the skill interaction model as you did for Mission 3 of the LEGO MINDSTORMS Voice Challenge. Instructions can be found here: https://www.hackster.io/alexagadgets/lego-mindstorms-voice-challenge-mission-3-4ed812#toc-define-the-skill-interaction-model-4. In the Alexa Developer Console, under Interaction Model, and click on JSON Editor. Cut and paste the model.json file provided in the linked GitHub repository for Quiz Game. After pasting the JSON into the Alexa skill JSON Editor, click Save Model, and then Build Model presented at the top of the console interface.
7. For this step, you have to implement the skill logic. You would have done a similar process in Mission 3/4 of the Voice Challenge ( https://www.hackster.io/alexagadgets/lego-mindstorms-voice-challenge-mission-3-4ed812?#toc-implementing-the-skill-logic-5.) Now, you will need to replace the files in Voice Challenge instructions with files from the EV3-Alexa-Quiz-Game/skill-nodejs/lambda directory in GitHub. Step 1: Click on Code in the top navigation bar of the Alexa Developer Console. Step 3: Create a new file by clicking the New File icon in the upper-left of the Code Editor, and fill in the path and file name as /lambda/common.js. Step 3: Copy the contents of index.js, package.json, util.js and common.js from the GitHub repository into each of the files of the Alexa skill - replacing the content of each file.
8. Next, select Deploy in the upper-right of the Alexa Skill Code Editor and wait for the deployment process to complete.
9. Open VS Code and open the code folder from the GitHub repository. Using the instructions from step 1 above, you will "send your workspace" to the EV3 brick. Now, use the EV3DEV DEVICE BROWSER part of the Explorer panel, right-click on the quiz-game.py file and choose Run from the displayed menu.
10. Once the program starts, you should hear your EV3 Brick play some tones, and see a prompt in the debug console telling you the friendly name of your gadget, which will look like Gadget000
. Now your gadget is in pairing mode.
11. You're now ready to pair your EV3 Brick to your Echo device. This pairing process will depend on whether or not your Echo device has a screen. See these instructions for how to pair the devices.
12. Yay! You are done. You should be able to say "Alexa play Quiz Game" (replace Quiz Game with the name you gave the skill - if you chose something different in step 5). Have fun learning about country capitals and challenging your friends, family, and schoolmates!
Comments