Building a Card Playing Twitter Bot: storing and retrieving game state to/from AWS DynamoDB

I recently built a Twitter Bot that plays Blackjack. Here’s my previous posts so far if you are catching up:

Since interaction with the Bot is via Twitter, there will be an unknown length of time between when a player choses to reply and interact with the bot which could be seconds, minutes or days. We therefore need to store the gameplay state for each player, retrieve it on the next interaction from the user and store it again after the response from the bot while we wait for the next player interaction.

The bot can support any number of players in parallel. For each bot/player interaction we need to store the following:

  • the player’s twitter handle
  • current cards remaining in the deck
  • cards in the player’s hand
  • cards in the bot’s hand
  • who’s turn is it next

In a ‘traditional’ data model we could easily model this with some relational tables, but with DynamoDB as a key/value / document based datastore, since we only need to store one interaction state per user, we can store this whole data structure in a single row keyed by the user’s Twitter handle.

DynamoDB’s supported types of scalar values (number, string), and set types (collections of scalar types) allow us to store everything we need in the game state (I did consider the document type, persisting JSON documents), but for retrieving game state values in an easy to use format this didn’t appear as useful and straightforward as scalar types and sets).

Browsing the table in DynamoDB using the AWS console, here’s what the schema currently looks like for a completed game:

AWS DynamoDB offers 3 different APIs for interacting with tables and your data, described in the docs here: low level, document, high level object mapper. With the AWS Java SDK APIs, using Java POJO classes to represent your data together with the DynamoDB Object Mapper APIs is probably the simplest of the 3 approaches, and will feel familiar if you’ve used JPA, Hibernate or other object/relational type mappers before.

Similar to JPA, the AWS DynamoDB APIs provide a number of annotations to map Java Pojos to your DynamoDB tables and columns. Here’s the Pojo class that represents the game state:

[code language=”Java”]
@DynamoDBTable(tableName = "twitterbot-blackjack")
public class PlayerGameState {

private String twitterhandle;
private Hand playerHand;
private Hand botHand;
private Deck deck;

}
[/code]

twitterhandle is the key, the annotation for the key column looks like this:

[code language=”Java”]

@DynamoDBHashKey
public String getTwitterhandle() {
return this.twitterhandle;
}

[/code]

deck, playerHand and botHand are all collections of Card. As a ‘sub-document’ type used by each of these other collections of Cards, the type is annotated with @DynamoDBDocument (instead of @DynamoDBTable):

[code language=”Java”]

@DynamoDBDocument
public class Card {

private Suit suit;
private CardName name;
private int pointsValue;


}
[/code]

DynamoDB supports maps of scalar values, so these fit well for representing a deck of cards, and the player’s hands of cards. If a single Card is a map of values, a collection of Cards is a map of Cards, so a map of maps. To map these more complex structures, a DynamoDB Type Converter is needed to tell the DynamoDB api how to map the structure to the table and back:

[code language=”Java”]
@DynamoDBDocument
public class Hand {

private List<Card> cards = new ArrayList<>();

@DynamoDBTypeConverted(converter = ListOfCardsToMapOfMapsConverter.class)
public List<Card> getCards(){
return this.cards;
}

}
[/code]

Next up I’ll describe how these Type Converters are used in more detail, and we’ll look at storing an retrieving from a DynamoDB table.

Building a Card Playing Twitter Bot: when AWS Lambdas timeout

node.js AWS Lambdas have a default timeout of 3 seconds when you create a new Lambda via the AWS Console. This can be increased with the slider in the Console, but if you keep the default and you get a timeout, you’ll see this error in CloudWatch:

2018-06-14T01:16:56.953Z 9f86... Task timed out after 3.00 seconds

From looking at the logs, my Blackjack card playing Twitter bot Lambda typically executes in around 2 secs, so the default of 3  secs probably doesn’t have enough wiggle room for unexpected slowdowns, and while testing I’ve seen this error a couple of times. When it does time out, it seems the execution just stops in the middle of whatever it was currently doing at that point, which leads to some unexpected results. In the case of this Twitter bot, the execution is being triggered via a 5 minute Cloudwatch event, so the only way I know it’s failed is to look at the logs (it’s not called via a webpage, so there’s no error being returned to the caller). Increasing the timeout to 10 secs seems to work fine for this particular Lambda.

Remember you are billed per GB-s of execution time, so keep an eye on how long your Lambdas execute for. If you’re aiming for low cost, then quicker execution times are obviously better.

Building a Card Playing Twitter Bot: gameplay dialog

I’ve built a couple of other Twitter bots, I have @kevinhookebot which generates random tweets generated from a trained ML model:

and I have a product name generator which generates humorous product names using the Tracery template library:

For my next project I’m thinking about what it would involve to build a multiplayer card game playing bot, a simple card game to get started, like Blackjack. The Twitter REST apis I’ve used so far will be reusable for this project, but the interesting parts are the interaction between a player and the bot, the game logic, and the persistence of game state (each of which I’ll discuss in future posts).

I’ve been thinking about the interaction for the gamevand think it will look something like this:

Player: @blackjackcard deal

Bot: @player bot deals you 4 Clubs and 7 Spades. Reply hit or stick

Player: @blackjackcard hit

Bot: @player bot deals you 4 Hearts. You now have 4 Clubs, 7 Spades, 4 Hearts. Reply hit or stick

Player: @blackjackcard stick

Bot: @player the bot currently has 3 Hearts, 9 Clubs, and takes a card

Bot: @player the bot takes 10 Diamonds and now has 3 Hearts, 9 Clubs, 10 Diamonds. Bust! You win!

The interesting part of the gameplay interaction is that there’s only 3 commands:

  • deal: start a game (get dealt your initial two cards)
  • hit: get dealt another card
  • stick: keep current hard

This makes the options that the bot needs to handle pretty simple. Next up, I’ll talk about persisting the game state to AWS DynamoDB.