Add users to a game

The first access pattern you address in this module is adding new users to a game.

When adding a new user to a game, you need to:

  • Confirm that there are not already 50 players in the game (each game can have a maximum of 50 players).
  • Confirm that the user is not already in the game.
  • Create a new UserGameMapping entity to add the user to the game.
  • Increment the people attribute on the Game entity to track how many players are in the game.

Note that accomplishing all of these things requires write actions across the existing Game entity and the new UserGameMapping entity as well as conditional logic for each of the entities. This is the kind of operation that is a perfect fit for DynamoDB transactions because you need to work on multiple entities in the same request, and you want the entire request to succeed or fail together.

In the code you downloaded, a join_game.py script is in the application/ directory. The function in that script uses a DynamoDB transaction to add a user to a game.

import boto3
from entities import Game, UserGameMapping

dynamodb = boto3.client('dynamodb')

GAME_ID = "c6f38a6a-d1c5-4bdf-8468-24692ccc4646"

USERNAME = 'vlopez'

def join_game_for_user(game_id, username):
    try:
        resp = dynamodb.transact_write_items(
            TransactItems=[
                {
                    "Put": {
                        "TableName": "battle-royale",
                        "Item": {
                            "PK": {"S": "GAME#{}".format(game_id) },
                            "SK": {"S": "USER#{}".format(username) },
                            "game_id": {"S": game_id },
                            "username": {"S": username }
                        },
                        "ConditionExpression": "attribute_not_exists(SK)",
                        "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
                    },
                },
                {
                    "Update": {
                        "TableName": "battle-royale",
                        "Key": {
                            "PK": { "S": "GAME#{}".format(game_id) },
                            "SK": { "S": "#METADATA#{}".format(game_id) },
                        },
                        "UpdateExpression": "SET people = people + :p",
                        "ConditionExpression": "people <= :limit",
                        "ExpressionAttributeValues": {
                            ":p": { "N": "1" },
                            ":limit": { "N": "50" }
                        },
                        "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
                    }
                }
            ]
        )
        print("Added {} to game {}".format(username, game_id))
    return True

except Exception as e:
    print("Could not add user to game")

join_game_for_user(GAME_ID, USERNAME)

In this script’s join_game_for_user function, the transact_write_items() method performs a write transaction. This transaction has two operations.

In the transaction’s first operation, you use a Put operation to insert a new UserGameMapping entity. As part of that operation, you specify a condition that the SK attribute should not exist for this entity. This ensures that an entity with this PK and SK doesn’t already exist. If such an entity did already exist, that would mean this user already joined the game.

The second operation is an Update operation on the Game entity to increment the people attribute by one. As part of this operation, you add a conditional check that the current value of people is not greater than 50. As soon as 50 people join a game, the game is full and ready to begin.

Run this script with the following command in your terminal:

python application/join_game.py

The output in your terminal should indicate that the user was added to the game.

Added vlopez to game c6f38a6a-d1c5-4bdf-8468-24692ccc4646

Note that if you try to run the script again, the function fails. User vlopez has been added to the game already, so trying to add the user again does not satisfy the conditions you specified.

The addition of DynamoDB transactions greatly simplifies the workflow around complex operations like these. Without transactions, this would have required multiple API calls with complex conditions and manual rollbacks in the event of conflicts. Now, you can implement such complex operations with fewer than 50 lines of code.