# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ Purpose Increments the current_value field for the given player_id and achievement_id in the player_achievements table. If current_value == max_value defined in game_achievements, the earned column is set to true. This is a player facing Lambda function and used in-game. """ import os import boto3 import botocore from gamekithelpers import handler_request, handler_response, ddb ddb_client = boto3.client('dynamodb') ddb_game_table = ddb.get_table(os.environ.get('ACHIEVEMENTS_TABLE_NAME')) ddb_player_table = ddb.get_table(os.environ.get('PLAYER_ACHIEVEMENTS_TABLE_NAME')) def _update_current_value_request(player_id, achievement_id, max_value, increment_by): """ Create the DynamoDB update_item parameter request for incrementing the current_value attribute """ now = ddb.timestamp() return { 'Key': { 'player_id': player_id, 'achievement_id': achievement_id }, 'ReturnValues': 'ALL_NEW', 'ExpressionAttributeNames': { '#current_value': 'current_value', '#earned': 'earned', '#created_at': 'created_at', '#updated_at': 'updated_at' }, 'ExpressionAttributeValues': { ':max_value': max_value, ':increment_by': increment_by, ':created_at': now, ':updated_at': now, ':zero': 0, ':false_value': False, }, 'ConditionExpression': 'attribute_not_exists(player_id) or ' '(attribute_exists(player_id) and current_value < :max_value)', 'UpdateExpression': 'SET #current_value = if_not_exists(current_value, :zero) + :increment_by, ' '#earned = if_not_exists(earned, :false_value), ' '#created_at = if_not_exists(created_at, :created_at), #updated_at = :updated_at' } def _update_earned_request(player_id, achievement_id, current_value, max_value): """ Create the DynamoDB update_item parameter request to update earned attribute """ now = ddb.timestamp() return { 'Key': { 'player_id': player_id, 'achievement_id': achievement_id }, 'ReturnValues': 'ALL_NEW', 'ExpressionAttributeNames': { '#current_value': 'current_value', '#earned': 'earned', '#updated_at': 'updated_at', '#earned_at': 'earned_at' }, 'ExpressionAttributeValues': { ':current_value': current_value, ':max_value': max_value, ':earned': True, ':updated_at': now, ':earned_at': now, ':true_value': True }, 'ConditionExpression': 'current_value >= :max_value and earned <> :true_value', 'UpdateExpression': 'SET #current_value = :current_value, #earned = :earned, #earned_at = :earned_at, #updated_at = :updated_at' } def _get_achievement(achievement_id): try: response = ddb_game_table.get_item(**ddb.get_item_request_param({'achievement_id': achievement_id})) achievement = ddb.get_response_item(response) except botocore.exceptions.ClientError as err: print(f"Error retrieving achievement_id: {achievement_id}. Error: {err}") raise err return achievement def _increment_player_achievement(player_id, achievement_id, increment_by, max_value): player_achievement = None try: response = ddb_player_table.update_item(**_update_current_value_request(player_id, achievement_id, max_value, increment_by)) player_achievement = ddb.get_update_response_item(response) player_achievement['max_value'] = max_value except ddb_client.exceptions.ConditionalCheckFailedException: # ignore condition expression failure pass except botocore.exceptions.ClientError as err: print(f"Error updating player_id: {player_id}, achievement_id: {achievement_id}. Error: {err}") raise err return player_achievement def _attempt_unlock(player_id, achievement_id, current_value, max_value): player_achievement = None try: response = ddb_player_table.update_item(**_update_earned_request(player_id, achievement_id, current_value, max_value)) player_achievement = ddb.get_update_response_item(response) player_achievement['max_value'] = max_value except ddb_client.exceptions.ConditionalCheckFailedException: # ignore condition expression failure pass except botocore.exceptions.ClientError as err: print(f"Error attempting to unlock player_id: {player_id}, achievement_id: {achievement_id}. Error: {err}") raise err return player_achievement def _get_player_achievement(player_id, achievement_id, max_value): try: response = ddb_player_table.get_item(**ddb.get_item_request_param( {'player_id': player_id, 'achievement_id': achievement_id}, True)) player_achievement = ddb.get_response_item(response) player_achievement['max_value'] = max_value except botocore.exceptions.ClientError as err: print(f"Error attempting to unlock player_id: {player_id}, achievement_id: {achievement_id}. Error: {err}") raise err return player_achievement def lambda_handler(event, context): """ This is the lambda function handler. """ handler_request.log_event(event) # Get player_id from requestContext player_id = handler_request.get_player_id(event) if player_id is None: return handler_response.response_envelope(401) # Get achievement_id from path achievement_id = event.get('pathParameters').get('achievement_id') if achievement_id is None: return handler_response.invalid_request() # Get increment_by from body body = handler_request.get_body_as_json(event) if body is None: return handler_response.invalid_request() increment_by = body.get('increment_by', 0) if increment_by <= 0: return handler_response.invalid_request() # Get achievement achievement = _get_achievement(achievement_id) if achievement is None: return handler_response.response_envelope(404) max_value = achievement['max_value'] # If the achievement is hidden, return invalid request is_hidden = achievement.get('is_hidden', True) if is_hidden: return handler_response.response_envelope(404) # Increment player achievement player_achievement = _increment_player_achievement(player_id, achievement_id, increment_by, max_value) current_value = increment_by if player_achievement is not None: current_value = player_achievement['current_value'] current_value = min(current_value, max_value) # Attemp to unlock achievement player_achievement = _attempt_unlock(player_id, achievement_id, current_value, max_value) if player_achievement is None: # conditional updates might have failed but we need to return the player achievement object player_achievement = _get_player_achievement(player_id, achievement_id, max_value) else: player_achievement['newly_earned'] = True # Merge achievement with player achievement achievement.update(player_achievement) return handler_response.response_envelope(200, None, achievement)