
Build a Real-Time Quiz Game: Amplify Gen 2 & Amazon Bedrock Tutorial
Learn how to build a real-time quiz application with React Native to support both iOS and Android
We will not be focusing on the UI components in this article. You can check the source code for those details.
npx create-expo-app amplify_quiz -t expo-template-blank-typescript
npm create amplify@latest -y
npx ampx sandbox
auth/resource.ts
file and update it as follows:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineAuth } from "@aws-amplify/backend";
export const auth = defineAuth({
loginWith: {
email: {
verificationEmailSubject: "Welcome to quiz app! Verify your email!",
verificationEmailBody: (code) => `Here is your verification code: ${code()}`,
verificationEmailStyle: "CODE",
},
},
userAttributes: {
preferredUsername: {
mutable: true,
required: true,
},
},
});
1
2
3
4
5
6
7
8
9
npm add \
@aws-amplify/ui-react-native \
@aws-amplify/react-native \
aws-amplify \
@react-native-community/netinfo \
@react-native-async-storage/async-storage \
react-native-safe-area-context \
react-native-get-random-values \
react-native-url-polyfill
App.tsx
file and update it as follows: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
import React, { useEffect, useState } from 'react';
import { Button, View, StyleSheet, SafeAreaView, Text } from 'react-native';
import { Amplify } from 'aws-amplify';
import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native';
import outputs from './amplify_outputs.json';
import { fetchUserAttributes } from 'aws-amplify/auth';
Amplify.configure(outputs);
const SignOutButton: React.FC = () => {
const { signOut } = useAuthenticator();
return (
<View style={styles.signOutButton}>
<Button title="Sign Out" onPress={signOut} />
</View>
);
};
const HomeScreen: React.FC<{ username: string }> = ({ username }) => {
return (
<View style={styles.contentContainer}>
<Text style={styles.welcomeText}>Welcome {username}!</Text>
<Button title="Search Game" onPress={() => {}} />
</View>
);
};
const App: React.FC = () => {
const [username, setUsername] = useState<string>('');
useEffect(() => {
const fetchUsername = async (): Promise<void> => {
const userAttributes = await fetchUserAttributes();
const fetchedUsername = userAttributes?.preferred_username ?? '';
setUsername(fetchedUsername);
};
void fetchUsername();
}, []);
return (
<Authenticator.Provider>
<Authenticator>
<SafeAreaView style={styles.container}>
<SignOutButton />
<HomeScreen username={username} />
</SafeAreaView>
</Authenticator>
</Authenticator.Provider>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
signOutButton: {
alignSelf: 'flex-end',
},
contentContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
welcomeText: {
fontSize: 18,
fontWeight: 'bold',
},
});
export default App;
fetchUserAttributes
function returns the attributes of the user that you provided<Authenticator>
component controls the authentication flow. Shows the app related pages when user sign in.useAuthenticator
hook has many capabilities alongside withsignOut
function.
data/resource.ts
file and update it like the following: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
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
const schema = a.schema({
Question: a.customType({
question: a.string().required(),
options: a.string().required().array().required(),
correctAnswer: a.string().required(),
category: a.string().required(),
}),
Game: a
.model({
playerOneId: a.string().required(),
playerTwoId: a.string().required(),
questions: a.ref("Question").required().array().required(),
currentQuestion: a.integer().default(0),
playerOneScore: a.integer().default(0),
playerTwoScore: a.integer().default(0),
})
.authorization((allow) => [allow.authenticated()]),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: "userPool",
},
});
Todo
table and create a GamePool
table. Now you can update the HomeScreen
component with the following to check the status of the game search: 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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
import { Schema } from "./amplify/data/resource";
import { generateClient } from "aws-amplify/data";
const client = generateClient<Schema>();
const HomeScreen: React.FC<HomeScreenProps> = ({ username }) => {
const [gameState, setGameState] = useState<GameState>("idle");
const [currentQuestion, setCurrentQuestion] = useState<number>(-1);
const [game, setGame] = useState<Schema["Game"]["type"]>();
const handleSearchGame = async (): Promise<void> => {
try {
const currentGames = await client.models.Game.list({
filter: {
playerTwoId: {
eq: "notAssigned",
},
},
});
if (currentGames.data.length > 0) {
await client.models.Game.update({
id: currentGames.data[0].id,
playerTwoId: username,
});
setGameState("found");
client.models.Game.observeQuery({
filter: {
id: {
eq: currentGames.data[0].id,
},
},
}).subscribe(async (observedGame) => {
if (observedGame.items[0].questions.length > 0) {
setGameState("quiz");
setGame(observedGame.items[0]);
}
if (observedGame.items[0].currentQuestion !== currentQuestion) {
setCurrentQuestion(
(observedGame.items[0].currentQuestion ?? 0) + 1
);
}
});
const updatedGame = await client.models.Game.update({
id: currentGames.data[0].id,
questions: [
{
question: "Which country won the FIFA World Cup in 2022?",
options: ["Brazil", "France", "Argentina", "Germany"],
correctAnswer: "Argentina",
category: "Soccer",
},
{
question: "In which sport would you perform a 'slam dunk'?",
options: ["Volleyball", "Tennis", "Basketball", "Cricket"],
correctAnswer: "Basketball",
category: "Basketball",
},
{
question:
"How many players are there on a standard ice hockey team?",
options: ["5", "6", "7", "8"],
correctAnswer: "6",
category: "Ice Hockey",
},
{
question:
"In which Olympic sport might you use the 'Fosbury Flop' technique?",
options: ["Swimming", "Diving", "High Jump", "Gymnastics"],
correctAnswer: "High Jump",
category: "Athletics",
},
{
question:
"Which Grand Slam tennis tournament is played on clay courts?",
options: [
"Wimbledon",
"US Open",
"Australian Open",
"French Open",
],
correctAnswer: "French Open",
category: "Tennis",
},
],
});
if (updatedGame.data) {
setGame(updatedGame.data);
}
setGameState("quiz");
} else {
setGameState("searching");
const newGame = await client.models.Game.create({
playerOneId: username,
playerTwoId: "notAssigned",
questions: [],
});
client.models.Game.observeQuery({
filter: {
id: {
eq: newGame.data?.id,
},
},
}).subscribe((observedGame) => {
if (observedGame.items[0].questions.length > 0) {
setGameState("quiz");
setGame(observedGame.items[0]);
} else if (observedGame.items[0].playerTwoId !== "notAssigned") {
setGameState("found");
}
if (observedGame.items[0].currentQuestion !== currentQuestion) {
setCurrentQuestion(
(observedGame.items[0].currentQuestion ?? 0) + 1
);
}
});
}
} catch (error) {
console.error("Error searching for game:", error);
setGameState("error");
}
};
const renderContent = (): JSX.Element => {
switch (gameState) {
case "idle":
return (
<>
<Text style={styles.welcomeText}>Welcome {username}!</Text>
<Button title="Search Game" onPress={handleSearchGame} />
</>
);
case "searching":
return (
<>
<Text style={styles.quizText}>Searching for a game now</Text>
<ActivityIndicator style={styles.activityIndicator} size="large" />
</>
);
case "found":
return (
<>
<Text style={styles.quizText}>
Questions are getting generated now...
</Text>
<ActivityIndicator style={styles.activityIndicator} size="large" />
</>
);
case "quiz":
if (!game) return <Text>Loading game...</Text>;
const question = game.questions[currentQuestion];
if (currentQuestion === game.questions.length) {
return (
<>
<Text style={styles.quizText}>Quiz is over!</Text>
<Text style={styles.quizText}>
{game.playerOneScore === game.playerTwoScore
? "It's a tie!"
: (game.playerOneScore ?? 0) > (game.playerTwoScore ?? 0)
? `${
game.playerOneId === username ? "You" : game.playerOneId
} won with ${game.playerOneScore} points!`
: `${
game.playerTwoId === username ? "You" : game.playerTwoId
} won with ${game.playerTwoScore} points!`}
</Text>
</>
);
}
return (
<>
<Text>{question?.question}</Text>
{question?.options.map((option) => (
<Button
key={option}
title={option}
onPress={() => {
if (option === question.correctAnswer) {
if (game.playerOneId === username) {
client.models.Game.update({
id: game.id,
playerOneScore: (game.playerOneScore ?? 0) + 10,
currentQuestion,
});
} else {
client.models.Game.update({
id: game.id,
playerTwoScore: (game.playerTwoScore ?? 0) + 10,
currentQuestion,
});
}
} else {
client.models.Game.update({
id: game.id,
currentQuestion,
});
}
}}
/>
))}
</>
);
case "error":
return <Text style={styles.welcomeText}>There is an error.</Text>;
default:
return <Text>Unknown state</Text>;
}
};
return <View style={styles.contentContainer}>{renderContent()}</View>;
};
- When you open the game, you see a welcome screen with your username and a button to search for a game.
- When you press the "Search Game" button, the game looks for another player who is also searching for a game.
- If it finds another player waiting, you join their game. If not, you create a new game and wait for someone else to join.
- While you're waiting, the screen shows a message that it's searching or that questions are being generated.
- Once two players are matched and the questions are ready, the quiz begins.
- The quiz has 5 questions about different sports topics. Each question has four possible answers to choose from.
- Both players see the same question at the same time and can select their answer.
- If you choose the correct answer, you get 10 points. If you're wrong, you don't lose any points.
- After both players have answered or time runs out, the game moves to the next question.
- This continues until all 5 questions have been answered.
- At the end of the quiz, the game shows the final scores and announces the winner (or a tie if both players have the same score).
- Throughout the game, the screen updates to show your current question, options to select, and at the end, the final results.
const client = generateClient<Schema>();
This line will generate the client helper libraries and references through your data schema.- The
list
function will get all the items related to a model. For filtering the models, you can use predicates to define rules about your data.
1
2
3
4
5
6
7
const currentGames = await client.models.Game.list({
filter: {
playerTwoId: {
eq: "notAssigned",
},
},
});
- The
update
function updates a property of the object by matching it through an id.
1
2
3
4
await client.models.Game.update({
id: currentGames.data[0].id,
playerTwoId: username,
});
- The
create
will do an game object creation.
1
2
3
4
5
const newGame = await client.models.Game.create({
playerOneId: username,
playerTwoId: "notAssigned",
questions: [],
});
- The
observeQuery
will listen to the object with pre-defined filters to act on it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
client.models.Game.observeQuery({
filter: {
id: {
eq: newGame.data?.id,
},
},
}).subscribe((observedGame) => {
if (observedGame.items[0].questions.length > 0) {
setGameState("quiz");
setGame(observedGame.items[0]);
} else if (observedGame.items[0].playerTwoId !== "notAssigned") {
setGameState("found");
}
if (observedGame.items[0].currentQuestion !== currentQuestion) {
setCurrentQuestion(
(observedGame.items[0].currentQuestion ?? 0) + 1
);
}
});
Amplify AI is at Developer Preview now, you can check the RFC about it over GitHub.
data/resource.ts
file and update it with the following: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
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
const schema = a.schema({
generateQuestions: a
.generation({
aiModel: a.ai.model("Claude 3.5 Sonnet"),
systemPrompt: `
You are a quiz question generator.
Create exactly 10 questions, evenly distributed across the categories from the following list [Sport, General Culture, Movies, Art, History]. Ensure the questions are evenly distributed in different difficulty levels.
Requirements for each question:
- The questions should be in English.
- Return the result as a JSON list containing JSON objects.
- Return the question with the JSON key 'question'.
- Include 4 different answer options, with the JSON key 'options', each a string.
- Specify 1 correct answer, with the JSON key 'correctAnswer', in string format.
- Return the category with the JSON key 'category'.
- The returned JSON will only have keys and values from the information from the mentioned before. Do not add any explanatory messages or statements such as 'Here is a JSON containing your trip', so user can take the JSON string and play around with it.
- Questions should not be repeated.
`,
})
.arguments({
description: a.string(),
})
.returns(a.ref("Question").required().array().required())
.authorization((allow) => allow.authenticated()),
Question: a.customType({
question: a.string().required(),
options: a.string().required().array().required(),
correctAnswer: a.string().required(),
category: a.string().required(),
}),
Game: a
.model({
playerOneId: a.string().required(),
playerTwoId: a.string().required(),
questions: a.ref("Question").required().array().required(),
currentQuestion: a.integer().default(0),
playerOneScore: a.integer().default(0),
playerTwoScore: a.integer().default(0),
})
.authorization((allow) => [allow.authenticated()]),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: "userPool",
},
});
generation
capability is a single synchronous request-response API. A generation route is just an AppSync Query. Once you save the file, it will automatically start deployment process about it. Make sure your AWS account has access to the model you wish to use. You can do that by going in to the Bedrock console and requesting access.
App.tsx
file, remove the lines between 79-125, which create the mocking part of the question generation. Add the following code instead:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const result = await client.generations.generateQuestions({
description: "",
});
if (result.errors) {
setGameState("error");
return;
}
const updatedGame = await client.models.Game.update({
id: currentGames.data[0].id,
questions: result.data as Schema["Question"]["type"][],
});
if (updatedGame.data) {
setGame(updatedGame.data);
}
generateQuestions
function from Amazon Bedrock to generate the content. Now, if you run the application, you should be able to see that the game is working:Any opinions in this post are those of the individual author and may not reflect the opinions of AWS.