Skip to content

Commit

Permalink
Initial sample checkin of Roller Skill
Browse files Browse the repository at this point in the history
  • Loading branch information
Stevenic committed Apr 17, 2017
1 parent 0a0ac96 commit 10544d1
Show file tree
Hide file tree
Showing 4 changed files with 558 additions and 0 deletions.
270 changes: 270 additions & 0 deletions Node/demo-RollerSkill/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/*-----------------------------------------------------------------------------
Roller is a dice rolling skill that's been optimized for speech.
-----------------------------------------------------------------------------*/

var restify = require('restify');
var builder = require('botbuilder');
var ssml = require('./ssml');

// Setup Restify Server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
console.log('%s listening to %s', server.name, server.url);
});

// Create chat connector for communicating with the Bot Framework Service
var connector = new builder.ChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
});

// Listen for messages from users
server.post('/api/messages', connector.listen());

/**
* Create your bot with a function to receive messages from the user.
* - This function will be called anytime the users utterance isn't
* recognized.
*/
var bot = new builder.UniversalBot(connector, function (session) {
// Just redirect to our 'HelpDialog'.
session.replaceDialog('HelpDialog');
});

/**
* This dialog sets up a custom game for the bot to play. It will
* ask the user how many sides they want the dice to have and then
* how many should be rolled. Once it's built up the game structure
* it will pass it to a seperate 'PlayGameDialog'.
*
* We've added a triggerAction() to this dialog that lets a user say
* something like "I'd like to roll some dice" to start the dialog.
* We're using a RegEx to match the users input but we could just as
* easily use a LUIS intent.
*/
bot.dialog('CreateGameDialog', [
function (session) {
// Initialize game structure.
// - dialogData gives us temporary storage of this data in between
// turns with the user.
var game = session.dialogData.game = {
type: 'custom',
sides: null,
count: null,
turns: 0
};

/**
* Ask for the number of sides.
*
* You can pass an array of choices to be matched. These will be shown as a
* numbered list by default. This list will be shown as a numbered list which
* is what we want since we have so many options.
*
* - value is what you want returned via 'results.response.entity' when selected.
* - action lets you customize the displayed label and for buttons what get sent when clicked.
* - synonyms let you specify alternate things to recognize for a choice.
*/
var choices = [
{ value: '4', action: { title: '4 Sides' }, synonyms: 'four|for|4 sided|4 sides' },
{ value: '6', action: { title: '6 Sides' }, synonyms: 'six|sex|6 sided|6 sides' },
{ value: '8', action: { title: '8 Sides' }, synonyms: 'eight|8 sided|8 sides' },
{ value: '10', action: { title: '10 Sides' }, synonyms: 'ten|10 sided|10 sides' },
{ value: '12', action: { title: '12 Sides' }, synonyms: 'twelve|12 sided|12 sides' },
{ value: '20', action: { title: '20 Sides' }, synonyms: 'twenty|20 sided|20 sides' },
];
builder.Prompts.choice(session, 'choose_sides', choices, {
speak: speak(session, 'choose_sides_ssml')
});
},
function (session, results) {
// Store users input
// - The response comes back as a find result with index & entity value matched.
var game = session.dialogData.game;
game.sides = Number(results.response.entity);

/**
* Ask for number of dice.
*
* - We can use gettext() to format a string using a template stored in our
* localized prompts file.
* - The number prompt lets us pass additional options to say we only want
* intergers back and what's the min & max value that's allowed.
*/
var prompt = session.gettext('choose_count', game.sides);
builder.Prompts.number(session, prompt, {
speak: speak(session, 'choose_count_ssml'),
minValue: 1,
maxValue: 100,
intergerOnly: true
});
},
function (session, results) {
// Store users input
// - The response is already a number.
var game = session.dialogData.game;
game.count = results.response;

/**
* Play the game we just created.
*
* We can use replaceDialog() to end the current dialog and start a new
* one in its place. We can pass arguments to dialogs so we'll pass the
* 'PlayGameDialog' the game we created.
*/
session.replaceDialog('PlayGameDialog', { game: game });
}
]).triggerAction({ matches: [
/(roll|role|throw|shoot).*(dice|die|dye|bones)/i,
/new game/i
]});

/**
* This dialog is our main game loop. We'll store the game structure in
* session.conversationData so that should the user say "roll again" we
* can just re-roll the same set of dice again.
*/
bot.dialog('PlayGameDialog', function (session, args) {
// Get current or new game structure.
var game = args.game || session.conversationData.game;
if (game) {
// Generate rolls
var total = 0;
var rolls = [];
for (var i = 0; i < game.count; i++) {
var roll = Math.floor(Math.random() * game.sides) + 1;
if (roll > game.sides) {
// Accounts for 1 in a million chance random() generated a 1.0
roll = game.sides;
}
total += roll;
rolls.push(roll);
}

// Format roll results
var results = '';
var multiLine = rolls.length > 5;
for (var i = 0; i < rolls.length; i++) {
if (i > 0) {
results += ' . ';
}
results += rolls[i];
}

// Render results using a card
var card = new builder.HeroCard(session)
.subtitle(game.count > 1 ? 'card_subtitle_plural' : 'card_subtitle_singular', game)
.buttons([
builder.CardAction.imBack(session, 'roll again', 'Roll Again'),
builder.CardAction.imBack(session, 'new game', 'New Game')
]);
if (multiLine) {
//card.title('card_title').text('\n\n' + results + '\n\n');
card.text(results);
} else {
card.title(results);
}
var msg = new builder.Message(session).addAttachment(card);

// Determine bots reaction for speech purposes
var reaction = 'normal';
var min = game.count;
var max = game.count * game.sides;
var score = total/max;
if (score == 1.0) {
reaction = 'best';
} else if (score == 0) {
reaction = 'worst';
} else if (score <= 0.3) {
reaction = 'bad';
} else if (score >= 0.8) {
reaction = 'good';
}

// Check for special craps rolls
if (game.type == 'craps') {
switch (total) {
case 2:
case 3:
case 12:
reaction = 'craps_lose';
break;
case 7:
reaction = 'craps_seven';
break;
case 11:
reaction = 'craps_eleven';
break;
default:
reaction = 'craps_retry';
break;
}
}

// Build up spoken response
var spoken = '';
if (game.turn == 0) {
spoken += session.gettext('start_' + game.type + '_game_ssml') + ' ';
}
spoken += session.gettext(reaction + '_roll_reaction_ssml');
msg.speak(ssml.speak(spoken));

// Incrment number of turns and store game to roll again
game.turn++;
session.conversationData.game = game;

/**
* Send card and bots reaction to user.
*/
msg.inputHint(builder.InputHint.acceptingInput);
session.send(msg).endDialog();
} else {
// User started session with "roll again" so let's just send them to
// the 'CreateGameDialog'
session.replaceDialog('CreateGameDialog');
}
}).triggerAction({ matches: /(roll|role|throw|shoot) again/i });

/**
* Listen for the user to ask to play craps.
*
* While you can use a triggerAction() to start a dialog, you sometimes just want
* to either send a message when a user says something or start an existing dialog
* with some arguments. You can use a cusomAction() to recognize something the user
* says without tampering with the dialog stack. In our case what we want to do is
* call 'PlayGameDialog' with a pre-defined game structure.
*/
bot.customAction({
matches: /(play|start).*(craps)/i,
onSelectAction: function (session, args, next) {
// The user could be in another dialog so clear the dialog stack first
// to make sure we end that task.
session.clearDialogStack().beginDialog('PlayGameDialog', {
game: { type: 'craps', sides: 6, count: 2, turn: 0 }
})
}
});

/**
* Every bot should have a help dialog. Ours will use a card with some buttons
* to educate the user with the options available to them.
*/
bot.dialog('HelpDialog', function (session) {
var card = new builder.HeroCard(session)
.title('help_title')
.buttons([
builder.CardAction.imBack(session, 'roll some dice', 'Roll Dice'),
builder.CardAction.imBack(session, 'play craps', 'Play Craps')
]);
var msg = new builder.Message(session)
.speak(speak(session, 'help_ssml'))
.addAttachment(card)
.inputHint(builder.InputHint.acceptingInput);
session.send(msg).endDialog();
}).triggerAction({ matches: /help/i });

/** Helper function to wrap SSML stored in the prompts file with <speak/> tag. */
function speak(session, prompt) {
var localized = session.gettext(prompt);
return ssml.speak(localized);
}
65 changes: 65 additions & 0 deletions Node/demo-RollerSkill/locale/en/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"choose_sides": "__Number of Sides__",
"choose_sides_ssml": [
"How many sides on the dice?",
"Pick your poison.",
"All the standard sizes are supported."
],
"choose_count": "How many %d sided dice should I roll?",
"choose_count_ssml": [
"How many dice? Choose a number from 1 to a hundred.",
"How many dice? The more the merrier",
"How many dice?"
],
"card_title": "I Rolled",
"card_subtitle_singular": "using a %(sides)d-sided die",
"card_subtitle_plural": "rolled %(count)d dice using a %(sides)d-sided die",
"start_custom_game_ssml": [
"Lets do this!",
"Here we go.",
"Are you ready for this?"
],
"start_craps_game_ssml": [
"I love vegas.",
"Good luck."
],
"craps_lose_roll_reaction_ssml": [
"You crapped out.",
"Oh no you lost."
],
"craps_seven_roll_reaction_ssml": "You win with a lucky seven!",
"craps_eleven_roll_reaction_ssml": "Nice, an eleven you win!",
"craps_retry_roll_reaction_ssml": [
"Roll again.",
"Try again you want a seven or eleven.",
"Roll again and try not to get a 2, 3, or 12."
],
"best_roll_reaction_ssml": [
"That was a perfect roll!",
"Awesome!",
"You can't get better than that."
],
"good_roll_reaction_ssml": [
"That's pretty good.",
"There you go.",
"That should help."
],
"normal_roll_reaction_ssml": [
"That's not too bad",
"It could have been worse.",
"That was a decent roll."
],
"bad_roll_reaction_ssml": [
"ouch",
"I hope your luck improves.",
"I think I saw that coming.",
"That's not good."
],
"worst_roll_reaction_ssml": [
"wow... just wow...",
"oh no. That was horrible.",
"You can't do better. Oh wait you can."
],
"help_title": "Roller Options",
"help_ssml": "I'm roller, the dice rolling bot. You can say 'roll some dice' or play one of the games I know how to play."
}
15 changes: 15 additions & 0 deletions Node/demo-RollerSkill/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "rollerskill",
"version": "1.0.0",
"description": "Sample BotBuilder skill for speech enabled Bot Framework channels.",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"botbuilder": "^3.8.0-beta2",
"restify": "^4.3.0"
}
}
Loading

0 comments on commit 10544d1

Please sign in to comment.