In the previous article we converted our very simple app into a version of Rock, Paper, Scissors, Lizard, Spock. In this article we will refactor our game, and then add scoring so the app will keep track of your wins and losses (initially it will be per session, and eventually we will have it keep track for all time).
As we stated in the previous article, our if else engine for determining game winners was very ugly. Let’s replace it
with an array based engine, that will clean it up, reduce the amount of code, and remove a ton of hard coded strings. First we will create a pseudo enum with Object.freeze that has all of the 10 possible combinations of two people playing Rock, Paper, Scissors, Lizard, Spock (place this at the top of the rpsls.js file, after the options):
If you haven’t seen Object.freeze before it is a new feature added to JavaScript 5.1, that allows us to force immutable objects (in case you hadn’t noticed in the past, const isn’t really const as
you can always add to or remove items from an array or object, with Object.freeze you will get an exception thrown if you try to mutate a frozen object). Javascript doesn’t really have enums, but this is pretty close. Next we are going to create an array of text outcomes based on our ‘enumeration’ (this also goes near the top of the rpsls.js file):
This way instead of duplicating our results like we previously were, we can use the same text for either computer beating the player, or losing to the player. Of course we could freeze this text too, as it won’t change, but for reasons only known to the author at the time he wrote the code, we have decided not to.
Next, instead of repeating strings throughout the code (and having the chance of misspelling scissors as scisors or sissors somewhere), we turn all of the user choices into another frozen Object Collection (place this at the top of the rpsls.js file as well):
Next we create out choice matrix, this basically produces all the possible choices and contains their output. The users choice is the first part of the key, and the computer’s choice the second. Each element in the collection contains an outcome which is a string, and win – a boolean that indicates whether the player won or lost. We are placing this matrix again near the top of the rpsls.js file:
Now we can greatly simplify our returnResults code:
To see exactly what this looks like, you can look at the tag: PART_3_STEP_1.
Nothing here is particularly complicated, and it really has nothing to do with Google Actions so let’s skip along to our next change.
Next we are going to add per session scoring, as well as a bit of state so that we can provide better responses when we don’t get
an answer we want. Our game state is going to be an enumeration, and there are basically 3 states: Initial, StartGame (when we are
waiting for the user to choose Rock/Paper/Scissors/Lizard/Spock), and EndGame when we are waiting to see if they want to play again. Let’s
create our version of an enumeration and place it at the top of the rpsls.js file:
Next we need to keep the state somewhere, and this is done by adding the state to to every ask method (and askWIthList method). What is the state variable?
It can be any JSON serializable object (i.e. you can’t shove something in that has circular structure, and an Object will lose any of its methods, but basically you
can put any structure into the state object). If at any time during the interaction you don’t pass the state, it will revert to null
so remember you have to set it everywhere, even if you don’t change it! The other thing to realize about
state is that you are passing it back to the Google Servers and the Google Servers are passing it back to you with every request, so even though
you can store fairly large objects in it, it may not be the most efficient way of doing things (you might want to store large objects
in a local database instead).
The first place to add the state is in our mainIntentHandler. Since this is only executed when a user begins their conversation with our game, we
can completely reset the state (including setting their wins and losses for this conversation to 0 – we will use these wins and losses later in the article).
the only change in this function is the addition of the state at the end:
becomes:
Now we have to go and add state to all the rest of our Ask Commands. And to add state (unless we want it to revert every time), we
have to get the state, which we do through the app method getDialogState.
So our readInstructions function (removing the ssml content) becomes:
We set the state of the state variable (OK, perhaps a bit confusing, maybe we should have called it gameState), to be INITIAL when we
reading the instructions. To start the game we do the same thing, except set the state to START_GAME:
It is in returnResults that things really change:
The main change is that we update the state’s win and losses depending upon whether the play won or lost, and we tell the
user their current win/loss state when telling them the outcome of the game.
The last change we make is to the textIntentHandler, because we now know the state of the game, we can handle invalid users
much more intelligently, and give the user better feedback:
So, as you can see, we can tailor our results based upon our state. Also note that we don’t pass state to the app.tell method as
that ends the conversation, and state is only kept around during the conversation. So everything to this point is available
at the tag PART_3_STEP_2. Lets get our Simulator up and
running again, so to do that we probably have to restart ngrok, get our new ngrok address:
And then go to https://console.actions.google.com, go to the Overview of our project, and hit
“Test Draft”. And now our app should be able to keep score:
You will notice that state is kept in variable called ‘conversationToken’, but we don’t have to worry about that as the
Google Actions Node SDK takes care of all of that for us.
But what if we want to keep a running total of our wins and losses for all of our conversations with the Google Assistant?
Then we need to switch from state to user storage. The userStorage is available on the app object, and is maintained
from conversation to conversation. It has the same restrictions as the state does in what can be serialized on it,
and you should remember that anything you store in there will be kept (presumably) for ever (until you delete it)
and will always be sent to your service so remember to clean up items you don’t need. It is important to note (not that we would ever approach that size) that the serialized JSON for the userStorage is limited to 10K.
For our game, we can switch from using state to userStorage very easily. All we have to do is change a few lines of code, first in the middle of the returnResults function, were we set the dialog state, change the way scoring is tracked from:
to
also we should remove the setting of the original win and loss state in the main intent handler, so that:
becomes:
Now when we restart app, we see that our request object instead of storing wins and losses in the conversationToken, they are now stored in userStorage.
You can also see that I am apparently very good at Rock/Paper/Scissors/Lizard/Spock with a seven and one record! So what else could we possibly do to extend our Rock, Paper, Scissors, Lizard, Spock game? Well, we know how good I am at the
game because I posted a chunk of JSON, but that is obviously no way to share it with our friends. To do that we need a site were we can have accounts were our scores are visible beyond the Google Assistant, and to the entire web. To do that we will have to
Create a website with a login, a leader board, a sign up page, etc.
Link that account to our Google Assistant Account through OAuth
Profit!
But that will have to wait for the next (and probably final article) in this series on Google Home and the Google Assistant.