Crash Course
What you’ll learn
We’re going to build a simple leaderboard module from scratch. Users will be able to authenticate, submit leaderboard scores, and retrieve the top scores.
This will teach you how to build a simple leaderboard module from scratch & all related Open Game Backend concepts including:
- Create a module
- Setup your database
- Write scripts
- Write a test
Make sure you’ve installed Open Game Backend as described here.
Step 1: Create project
Create a new directory. Open a terminal in this directory.
Run the following command:
opengb init
Step 2: Create module
In the same terminal, run:
opengb create module my_leaderboard
See documentation on the module.yaml
config here.
Step 3: Write database schema
Edit your modules/my_leaderboard/db/schema.prisma
file to look like this:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Scores {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now()) @db.Timestamp
userId String @db.Uuid
score Int
@@index([score])
}
For more information about writng database schemas, see Prisma’s documentation on data modelling.
Open Game Backend is powered by PostgreSQL. Learn about why we chose PostgreSQL here.
Step 4: Write submit_score
script
We’re going to create a submit_score
script that will:
- Throttle requests
- Validate user token
- Insert score
- Query score rank
Create script
In the same terminal, run:
opengb create script my_leaderboard submit_score
Add dependencies & make public
Update the modules/my_leaderboard/module.yaml
file to look like this:
dependencies:
users: {}
rate_limit: {}
scripts:
submit_score:
public: true
errors: {}
Update request & response
Open modules/my_leaderboard/scripts/submit_score.ts
and update Request
and Response
to look like this:
export interface Request {
userToken: string;
score: number;
}
export interface Response {
rank: number;
}
Throttle requests
At the top the run
function in modules/my_leaderboard/scripts/submit_score.ts
file, add code to throttle requests:
export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
await ctx.modules.rateLimit.throttlePublic({ requests: 1, period: 15 });
// ...
}
Validate user token
Then authenticate the user:
export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
// ...
const validate = await ctx.modules.users.authenticateUser({ userToken: req.userToken });
///
}
Insert score
Then insert the score in to the database:
export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
// ...
await ctx.db.scores.create({
data: {
userId: validate.userId,
score: req.score,
},
});
// ...
}
Query rank
Finally, query the score’s rank:
export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
// ...
const rank = await ctx.db.scores.count({
where: {
score: { gt: req.score },
},
});
return {
rank: rank + 1,
};
}
For more information about querying databases, see Prisma’s documentation on querying data.
Step 5: Create get_top_scores
script
Create script
In the same terminal, run:
opengb create script my_leaderboard get_top_scores
Make public
Open modules/my_leaderboard/module.yaml
and update the get_top_scores
script to be public:
scripts:
get_top_scores:
public: true
Update request & response
Open modules/my_leaderboard/scripts/get_top_scores.ts
and update Request
and Response
to look like this:
export interface Request {
count: number;
}
export interface Response {
scores: Score[];
}
export interface Score {
userId: string;
createdAt: string;
score: number;
}
Throttle requests
At the top the run
function in modules/my_leaderboard/scripts/get_top_scores.ts
file, add code to throttle the requests:
export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
await ctx.modules.rateLimit.throttlePublic({});
// ...
}
Query scores
Then query the top scores:
export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
// ...
const rows = await ctx.db.scores.findMany({
take: req.count,
orderBy: { score: "desc" },
select: { userId: true, createdAt: true, score: true },
});
// ...
}
Convert rows
Finally, convert the database rows in to Score
objects:
export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
// ...
const scores = [];
for (const row of rows) {
scores.push({
userId: row.userId,
createdAt: row.createdAt.toISOString(),
score: row.score,
});
}
return { scores };
}
Step 6: Start development server
Start development server
In the same terminal, run:
opengb dev
Migrate database
You will be prompted to apply your schema changes to the database. Name the migration init (this name doesn’t matter):
Migrate Database develop (my_leaderboard)
Prisma schema loaded from schema.prisma
Datasource "db": PostgreSQL database "my_leaderboard", schema "public" at "host.docker.internal:5432"
? Enter a name for the new migration: › init
Success
You’ve now written a full module from scratch. You can now generate an SDK to use in your game or publish it to a registry.
Step 7 (optional): Test module
Tests are helpful for validating your module works as expected before running in to the issue down the road. Testing is optional, but strongly encouraged.
Create test
opengb create test my_leaderboard e2e
e2e
stands for “end to end” test. E2E tests simulate real-world scenarios involving multiple parts of a system. These tend to be comprehensive and catch the most bugs.
Write test
Update modules/my_leaderboard/tests/e2e.ts
to look like this:
import { test, TestContext } from "../_gen/test.ts";
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
import { faker } from "https://deno.land/x/[email protected]/mod.ts";
test("e2e", async (ctx: TestContext) => {
// Create user & token to authenticate with
const { user } = await ctx.modules.users.createUser({});
const { token } = await ctx.modules.users.createUserToken({
userId: user.id,
});
// Create some scores
const scores = [];
for (let i = 0; i < 10; i++) {
const score = faker.random.number({ min: 0, max: 100 });
await ctx.modules.myLeaderboard.submitScore({
userToken: token.token,
score: score,
});
scores.push(score);
}
// Get top scores
scores.sort((a, b) => b - a);
const topScores = await ctx.modules.myLeaderboard.getTopScores({ count: 5 });
assertEquals(topScores.scores.length, 5);
for (let i = 0; i < 5; i++) {
assertEquals(topScores.scores[i].score, scores[i]);
}
});
Run test
In the same terminal, run:
opengb test
You should see this output once complete:
...test logs...
----- output end -----
e2e ... ok (269ms)
ok | 1 passed | 0 failed (280ms)