
Farm, Build, Fight! The survival RPG you never knew you need
How two brothers working full-time jobs made a full-scale game in their free time using Amazon Q Developer
- Most Visual Assets (including player, enemies, world assets) by Kenmi
- Virtual Mobile Keyboard (for mobile players) by Lilly Games
- Gamer Font by memesbruh03
- Sound Effects from Pixabay
- Background Music generated by Suno.ai
- The input to Q Chat is very dynamic and fits the programming life cycle. Besides writing your prompt, Q Chat refers to the currently open file in the IDE. If you select a specific piece of code, it will refer to it in particular. You can also start your prompt with @workspace for Q Chat to refer to your entire code environment!
- The Q Chat output is not only high-quality on its own but provides links that refer to more sources and discussions for further exploration. This was very helpful, mostly when Q Chat introduced us to new APIs and concepts.
- Q Chat is an expert in all AWS-related subjects, so writing code that uses or integrates with AWS SDK was incredibly straightforward and much simpler than anticipated.
- Most importantly - the Q Chat results are stunningly accurate. We could implement features like character movement, health system, game logic, projectile mechanics, day-night cycle, and more with just a single query. We also found it helpful to provide a small starting point for Q Chat to generate perfect results in requests where they initially did not fit our needs (This is demonstrated in the example below).
- AWS Lambda: Handles the NPC logic and processes the prompts
- AWS Bedrock: Powers our NPCs' intelligence with the Nova Lite model
- AWS API Gateway: Invokes the Lambda function with the player's current game state in the request body
- Cost-Efficiency: Compared to other available models, Nova Lite offers great performance at a fraction of the cost.
- Quick Response Time: Perfect for real-time gaming interactions.
- Consistent Output: Provides reliable, controlled responses without going off-track.
- While Amazon Nova requires a specific schema for its prompts, Amazon Q helped us format it correctly and quickly.
- Player's current status (health, money, inventory)
- World state (day count, existing towers and crops)
- Shop inventory
- Last round activity
- Upcoming threats
- Additional relevant data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json
import boto3
import os
import logging
def create_context_prompt(game_state):
"""
Creates a context-aware prompt for the store advisor NPC based on game state
"""
# Get all data from the correct game state
player_status = game_state.get('playerStatus', {})
inventory = player_status.get('inventory', {})
world_status = player_status.get('worldStatus', {})
shops = world_status.get('shops', {})
last_round = player_status.get('lastRoundActivity', {})
next_enemies = player_status.get('nextRoundEnemies', {})
previous_responses = player_status.get('previousResponses', [])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
context = f"""You are a wise merchant NPC who gives strategic advice to players who are warriors, and survivels.
IMPORTANT: You must respond with EXACTLY ONE SHORT SENTENCE (8-15 words).
Your advice should be direct and concise.
VERY IMPORTANT: Provide advice that is distinctly different from "{last_response}". Avoid suggesting anything similar to it!
Player Status:
Health: {player_status.get('health', 0)}/12
Money: {player_status.get('money', 0)} gold
Owned crops: {crops_owned}
Owned materials: {materials_owned}
World Status:
{world_status}
Shops Inventory:
{shops_inventory}
Seeds Available:
{available_seeds}
Materials Available:
{available_materials}
Tool Upgrades Available:
{tool_upgrades}
Utility Upgrades Available:
{utility_upgrades}"""
try:
# Generate context-aware system prompt
system_prompt = create_context_prompt(event)
logger.info("Generated system prompt")
# Prepare the Bedrock request payload with Nova schema
payload = {
"system": [{"text": system_prompt}],
"messages": [{
"role": "user",
"content": [{"text": "Give one short sentence of advice to the player."}]
}],
"inferenceConfig": {
"temperature": 0.7,
"max_new_tokens": 100,
"stopSequences": ["\n"]
}
}
- Encouraging players based on their gaming history
- Playful banter
- Welcoming messages
- Economic status priorities
- Specific threat warnings (like those pesky chickens!)
- Defensive needs
- Available upgrades tracking
- All other session-related data
- "Hello rookie, I hope you'll survive day one this time!"
- "Hello there, let's see if you can beat that 5-day record!"
- "It's you again! I hope you won't lose to that skeleton this time!"
- "Better harvest those tomatoes before the chicken squad arrives!"
- "Those tower upgrades won't buy themselves - time to plant some pumpkins!"
- "With that many orcs coming, you might want to reinforce your defences!"
- Allow players to choose their own usernames.
- Support email verification for account security.
- Enable username-based logins (instead of email).
- Ensure everything is secure and scalable.
- Amazon Cognito: For user pool management and authentication.
- DynamoDB: To map usernames to emails.
- API Gateway: To create RESTful API endpoints.
- Lambda: To handle backend logic.
- User Submits Registration: Players enter their email, username, and password. This data is sent in the request body to the API Gateway.
- Lambda Processes Registration: A Lambda function receives the data and performs validation.
1
2
3
4
5
6
7
8
9
10
11
12
response = cognito.sign_up(
ClientId=CLIENT_ID,
Username=email,
Password=password,
SecretHash=secret_hash,
UserAttributes=[
{
'Name': 'email',
'Value': email
}
]
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if is_email(username):
email = username
username = get_user_by_email(email)
if not username:
return create_response(404, {'success': False, 'message': 'Email not found'})
else:
email = get_user_email(username)
if not email:
return create_response(404, {'success': False, 'message': 'Username not found'})
secret_hash = get_secret_hash(email)
response = cognito.initiate_auth(
ClientId=CLIENT_ID,
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': email,
'PASSWORD': password,
'SECRET_HASH': secret_hash
}
)
- The player enters a username, email, and password.
- Lambda checks for existing usernames or emails.
- If an email exists but isn’t verified, the old registration is cleaned up.
- The player is registered with Cognito using their email.
- The username-email mapping is stored in DynamoDB.
- A verification code is sent to the player.
- The player verifies their email.
- They’re ready to play!
- Days Survived: The primary score is the number of days the player survives.
- Time Played: In the event of a tie (which is likely), the time taken to complete the game serves as the tiebreaker.
- Username
- Days survived
- Time taken to finish the game
- The player finishes the game and submits their score.
- The Lambda function checks DynamoDB for the player’s existing record.
- If the new score is better, it updates the record in DynamoDB.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
player_name = request_body.get('playerName')
days_survived = request_body.get('daysSurvived')
time_taken = request_body.get('timeTaken')
if not query_response['Items']:
# No existing record, create new one
should_update = True
print("No existing records found, will create new")
else:
# Find the best record for this player
best_record = min(query_response['Items'],
key=lambda x: (-int(x['Days']), safe_decimal_conversion(x['timeTaken'])))
print(f"Best existing record: {best_record}")
best_days = int(best_record['Days'])
best_time = safe_decimal_conversion(best_record['timeTaken'])
if (days_survived > best_days or
(days_survived == best_days and time_taken_decimal < best_time)):
should_update = True
print("New score is better, will update")
# Delete all existing records for this player
for record in query_response['Items']:
table.delete_item(Key={'playerName': player_name, 'Days': record['Days']} )
if should_update:
# Create new record
timestamp = datetime.now().isoformat()
new_record = {
'playerName': player_name,
'Days': str(days_survived),
'timeTaken': time_taken_decimal,
'timestamp': timestamp
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Scan the table to get all records
response = table.scan()
items = response['Items']
# Convert items to the format needed by the scoreboard
scoreboard_items = []
for item in items:
scoreboard_items.append({
'playerName': item['playerName'],
'Days': int(item['Days']),
'timeTaken': item['timeTaken']})
# Sort by Days (descending) and then by time taken (ascending)
sorted_items = sorted(
scoreboard_items,
key=lambda x: (-int(x['Days']), x['timeTaken']))
# Take top 50 scores only (changed from 10)
top_scores = sorted_items[:50]
- Cognito for authentication (only registered users can access saved data from the cloud)
- API Gateway for REST endpoints
- Lambda for serverless computing
- DynamoDB to store the data.
- Amazon S3 (Simple Storage Service): We chose S3 as our primary storage solution because it provides highly reliable and scalable object storage, making it perfect for hosting our WebGL game build files, HTML assets, and website resources.
- Amazon CloudFront: CloudFront serves as our content delivery network (CDN), significantly enhancing our game's performance worldwide. One of CloudFront's standout features is its automatic file compression, which reduces load times for our WebGL build.
- AWS Certificate Manager (ACM): ACM provides SSL/TLS certificates at no additional cost, ensuring all communications between players and our servers are securely encrypted.
- Amazon Route 53: Route 53 does more than just provide our custom domain. Its intelligent DNS routing automatically directs players to the nearest CloudFront edge location, optimizing their gaming experience.