
Putting All My Hobbies into A Single Project (Boosted: A Propulsion Game)
How Amazon Q helped me build my dream project in a programming language I didn't know existed.
- Lambda
- Cognito
- API Gateway
- DynamoDB (which I ended up not using)
- IAM
- Cloud Watch
- and Amplify
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
import {
CognitoIdentityProviderClient,
InitiateAuthCommand,
} from "@aws-sdk/client-cognito-identity-provider";
import crypto from 'crypto';
import { MongoClient } from 'mongodb';
const cognito = new CognitoIdentityProviderClient();
const client = new MongoClient(process.env.MONGODB_URI);
const calculateSecretHash = (username) => {
const message = username + process.env.COGNITO_CLIENT_ID;
const hmac = crypto.createHmac('SHA256', process.env.COGNITO_CLIENT_SECRET);
return hmac.update(message).digest('base64');
};
export const handler = async (event) => {
try {
const body = JSON.parse(event.body);
let { username, password } = body;
if (!username || !password) {
return {
statusCode: 400,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: 'Username and password are required'
})
};
}
const command = new InitiateAuthCommand({
ClientId: process.env.COGNITO_CLIENT_ID,
AuthFlow: 'USER_PASSWORD_AUTH',
AuthParameters: {
'USERNAME': username,
'PASSWORD': password,
'SECRET_HASH': calculateSecretHash(username)
}
});
const response = await cognito.send(command);
1
2
3
4
const confirmCommand = new AdminConfirmSignUpCommand({
UserPoolId: USER_POOL_ID,
Username: username
});
- /signup
- /signin
- /leaderboard
- /validateToken
- /refreshToken
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
const result = await collection.aggregate([
{ $match: { username: username } },
{
$project: {
tracks: { $objectToArray: "$tracks" }
}
},
{
$project: {
_id: 0,
keyValues: {
$arrayToObject: {
$map: {
input: "$tracks",
as: "track",
in: {
k: "$$track.k",
v: "$$track.v.time"
}
}
}
}
}
},
{ $replaceRoot: { newRoot: "$keyValues" } }
]).toArray();
1
2
3
4
5
6
7
8
if current_request_type == 'signin':
if FileAccess.file_exists(BEST_SCORES_LOCATION):
var dir = DirAccess.open("user://")
dir.remove("*****.save")
var file = FileAccess.open(BEST_SCORES_LOCATION, FileAccess.WRITE)
var trackTimes = response.get('trackTimes')
file.store_line(JSON.stringify(trackTimes))
file.close()
- Be simple: for the observer, the gameplay must look interesting and the goal at hand should be obvious, if not, there's a big chance it would deter potential players.
- Have Replayability: the game needed to be replayable, it had to be something you'd want to come back to.
- Be Competitive: players needed to have a way of competing with each other, this will make the game more interesting, we as humans have an innate desire to compete with each other, and most games leverage this idea.
- Last but not least, there needed to be No Dying!, just good times!
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
signal carousel_changed(index)
var items = []
var current_index = 0
# Configuration
var spacing = 800
var center_scale = Vector2(1.2, 1.2)
var side_scale = Vector2(0.6, 0.6)
var transition_time = 0.3
func _ready():
GameManager.setup_sound_buttons()
for data in GameManager.tracks_data:
var track = track_container.instantiate()
var best_time = '__'
if GameManager.get_best_time(data['track_identifier']) != -1:
best_time = '%0.2f' %GameManager.get_best_time(data['track_identifier'])
track.get_node('MarginContainer/VBoxContainer/Labels/Best').text = 'Best: ' + best_time
track.get_node('MarginContainer/VBoxContainer/Labels/TrackName').text = data['track_display_name']
track.get_node('MarginContainer/VBoxContainer/Buttons/Play').pressed.connect(func(): SceneManager.change_scene(data['track_identifier']))
track.get_node('MarginContainer/VBoxContainer/Buttons/Leaderboard').pressed.connect(func(): _on_leaderboard_button_pressed(data['track_identifier']))
var preview_image = load(data['preview_image_path'])
track.get_node('TrackPreviewImage').texture = preview_image
var font_color = data['track_name_font_color']
track.get_node('MarginContainer/VBoxContainer/Labels/TrackName').modulate = font_color
carousel_container.add_child(track)
# Get all carousel items
items = carousel_container.get_children()
# Initializing each item
for item in items:
item.size = Vector2(700,420)
# Start all items at normal scale
item.scale = Vector2(1, 1)
# Centering items
item.position = -item.size / 2
# Set initial positions
update_carousel()
func _process(_delta) -> void:
if Input.is_action_just_pressed('left_button'):
previous_item()
if Input.is_action_just_pressed('right_button'):
next_item()
func _on_leaderboard_button_pressed(track_name: String):
var leaderboard_scene = preload('res://scenes/ui/Leaderboard.tscn').instantiate()
leaderboard_scene.track_name = track_name
OverlayManager.push_overlay_node(leaderboard_scene)
print(leaderboard_scene.track_name)
func update_carousel():
for i in range(items.size()):
var item = items[i]
var relative_index = i - current_index
# Calculating target position
var target_position = Vector2(
relative_index * spacing - item.size.x / 2,
-item.size.y / 2
)
# Center item is scaled to be larger, rest are scaled down
var target_scale
if relative_index == 0:
target_scale = center_scale
item.z_index = 1
else:
target_scale = side_scale
item.z_index = 0
var tween = create_tween()
tween.set_parallel(true) # To play the transitions together, more natural
# Position tween
tween.tween_property(item, "position", target_position, transition_time)\
.set_trans(Tween.TRANS_CUBIC)\
.set_ease(Tween.EASE_OUT)
# Scale tween
tween.tween_property(item, "scale", target_scale, transition_time)\
.set_trans(Tween.TRANS_CUBIC)\
.set_ease(Tween.EASE_OUT)
# The further the item, the more faded it should be (may need improving)
var distance = abs(relative_index)
var target_alpha = 1.0 if distance <= 1 else 0.5
tween.tween_property(item, "modulate:a", target_alpha, transition_time)
carousel_changed.emit(current_index)
func next_item():
if current_index < items.size() - 1:
current_index += 1
update_carousel()
func previous_item():
if current_index > 0:
current_index -= 1
update_carousel()
func _on_next_track_button_pressed():
next_item()
func _on_previous_track_button_pressed():
previous_item()
1
2
3
4
5
6
func _setup_country_options():
var countries = GameManager.country_codes.keys()
for country in countries:
var flag = load("res://assets/flags/%s.png" % GameManager.country_codes[country])
signup_country.add_icon_item(flag, country)
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
func createTimeHash(time: float) -> String:
var combined = str(time) + track_name + env.TIME_HASH_SECRET
var timeHash = combined.sha256_text()
return timeHash
func upload_score():
var access_token = AuthManager.access_token
var id_token = AuthManager.id_token
if !access_token or !id_token:
return
var headers = [
"Content-Type: application/json",
"Authorization: Bearer " + access_token
]
var time = formatTime(time_elapsed)
track_name = track_name.to_lower()
var body = {
"id_token": id_token,
"trackName": track_name,
"time": time,
"timeHash": createTimeHash(time)
}
var json_body = JSON.stringify(body)
var error = http_request.request(
API_URL + "/leaderboard",
headers,
HTTPClient.METHOD_POST,
json_body
)
1
2
3
4
5
6
7
8
9
10
11
12
13
const verifyTimeHash = (receivedHash, time, trackName) => {
const combined = `${time}${trackName}${process.env.TIME_HASH_SECRET}`;
const checkHash = crypto.createHash('sha256').update(combined).digest('hex');
return checkHash === receivedHash;
};
const isHashValid = verifyTimeHash(timeHash, time, trackName);
if (!isHashValid) {
return {
statusCode: 400,
body: JSON.stringify({ message: "Invalid score" }),
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var sfx_enabled: bool
var sounds = {
"button_sfx_1": preload("res://assets/audio/sfx/button/button_sfx_1.wav"),
"button_sfx_2": preload("res://assets/audio/sfx/button/button_sfx_2.wav"),
"button_sfx_3": preload("res://assets/audio/sfx/button/button_sfx_3.wav"),
}
func play_button_sfx():
if not sfx_enabled:
return
var audio = AudioStreamPlayer.new()
var sound_name = sounds.keys()[randi_range(0, sounds.size() - 1)]
audio.stream = sounds[sound_name]
add_child(audio)
audio.play()
audio.finished.connect(audio.queue_free)
- More music; for now I only made one soundtrack which is played across the entire game, but it'd be a nice touch to have multiple unique soundtracks, one for each map/track.
- More tracks; Right now, the game has three short tracks (which I intentionally made short to keep players engaged), but I plan on incorporating more.
- Publish on Mobile; The game is currently only available on the web, but I plan to also publish on the AppStore and Playstore, as mobile was my original intended target.
- Player-Designed Tracks; Giving players the ability to create their own tracks and share them opens the door for unlimited content.