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:
Make sure you’ve installed Open Game Backend as described here.
Create a new directory. Open a terminal in this directory.
Run the following command:
What did this do?
This will create a backend.json
file in your directory with some default
settings.
In the same terminal, run:
See documentation on the module.json
config here.
What did this do?
This will create a directory modules/my_leaderboard
and updated
backend.json
to include the module. The important files we’ll look at are
modules/my_leaderboard/db/schema.prisma
, modules/my_leaderboard/scripts/
,
and modules/my_leaderboard/tests/
.
Edit your modules/my_leaderboard/db/schema.prisma
file to look like this:
What's going on here?
model Scores
creates a new table that will store all of the scores on the leadboardid
is the unique identifier for each score.
String
is the type of the column@id
indicates that this is the primary key of the table@default(uuid())
indicates that this column will be automatically set to a UUID when a new row is created@db.Uuid
indicates that this is stored as UUID type. UUIDs are universally unique (unlike integers) and take a small amount of storage (unlike strings).createdAt
is the time the score was created
DateTime
is the type of the column@default(now())
indicates that this column will be automatically set to the current time when a new row is created@db.Timestamp
indicates that this is stored as a timestamp type. This is a good way to store dates and times in a database.userId
is the user who submitted the score
String
is the type of the column@db.Uuid
indicates that this is stored as UUID type.score
is the score itself
Int
is the type of the column@@index([score])
creates an index on the score
column.
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.
submit_score
scriptWe’re going to create a submit_score
script that will:
Create script
In the same terminal, run:
What did this do?
This will create a new file modules/my_leaderboard/scripts/submit_score.ts
:
This command also udpated modules/my_leaderboard/module.json
to include the new script:
What's going on here?
import { ScriptContext } from "../module.gen.ts";
imports the ScriptContext
.
module.gen.ts
file is generated by Open Game Backend to provide a type-safe interface for the database (see schema.prisma
), calling other modules, and reading the module config.export interface Request {}
and export interface Response {}
define the input and output of the script.
export async function run(...)
is the main function of the script.
Add dependencies & make public
Update the modules/my_leaderboard/module.json
file to look like this:
What did this do?
user
and rate_limit
as dependencies. This will allow us to authenticate the user and rate limit calls to the script.public: true
for the submit_score
script so that anyone can call it.Update request & response
Open modules/my_leaderboard/scripts/submit_score.ts
and update Request
and Response
to look like this:
What did this do?
This will allow the user to authenticate using userToken
and submit a score using score
.
Throttle requests
At the top the run
function in modules/my_leaderboard/scripts/submit_score.ts
file, add code to throttle requests:
What did this do?
This will rate limit the script to 1 request every 15 seconds. If the rate limit is exceeded, a rate_limit_exceeded
error will be thrown. See the rate limit docs for more details.
Validate user token
Then authenticate the user:
What did this do?
This will authenticate the user and throw an error if the token is invalid. See the token docs for more details.
Insert score
Then insert the score in to the database:
What did this do?
This inserts the score into the database.
id
and createdAt
will be automatically set by the database because of the @default
annotations in the schema.
Query rank
Finally, query the score’s rank:
What did this do?
This will return the number of scores that are greater than (gt
) the user’s score. This is the user’s rank on the leaderboard.
We add 1 to the rank, since the 1st place will have 0 scores greater than it.
Full submit_score.ts source code
For more information about querying databases, see Prisma’s documentation on querying data.
get_top_scores
scriptCreate script
In the same terminal, run:
Make public
Open modules/my_leaderboard/module.json
and update the get_top_scores
script to be public:
Update request & response
Open modules/my_leaderboard/scripts/get_top_scores.ts
and update Request
and Response
to look like this:
What did this do?
This will allow the user to request the top count
scores and return an array of Score
objects.
Throttle requests
At the top the run
function in modules/my_leaderboard/scripts/get_top_scores.ts
file, add code to throttle the requests:
Query scores
Then query the top scores:
What did this do?
take
limits the number of scores returned to req.count
.orderBy
orders the scores by score
in descending order.select
limits the columns returned to userId
, createdAt
, and score
.Convert rows
Finally, convert the database rows in to Score
objects:
What did this do?
This converts the database rows into the Score
objects we defined in the Response
.
We use toISOString
to convert the createdAt
of type Date
into a string.
Full get_top_scores.ts source code
Start development server
In the same terminal, run:
Migrate database
You will be prompted to apply your schema changes to the database. Name the migration init (this name doesn’t matter):
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.
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
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:
Run test
In the same terminal, run:
You should see this output once complete:
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:
Make sure you’ve installed Open Game Backend as described here.
Create a new directory. Open a terminal in this directory.
Run the following command:
What did this do?
This will create a backend.json
file in your directory with some default
settings.
In the same terminal, run:
See documentation on the module.json
config here.
What did this do?
This will create a directory modules/my_leaderboard
and updated
backend.json
to include the module. The important files we’ll look at are
modules/my_leaderboard/db/schema.prisma
, modules/my_leaderboard/scripts/
,
and modules/my_leaderboard/tests/
.
Edit your modules/my_leaderboard/db/schema.prisma
file to look like this:
What's going on here?
model Scores
creates a new table that will store all of the scores on the leadboardid
is the unique identifier for each score.
String
is the type of the column@id
indicates that this is the primary key of the table@default(uuid())
indicates that this column will be automatically set to a UUID when a new row is created@db.Uuid
indicates that this is stored as UUID type. UUIDs are universally unique (unlike integers) and take a small amount of storage (unlike strings).createdAt
is the time the score was created
DateTime
is the type of the column@default(now())
indicates that this column will be automatically set to the current time when a new row is created@db.Timestamp
indicates that this is stored as a timestamp type. This is a good way to store dates and times in a database.userId
is the user who submitted the score
String
is the type of the column@db.Uuid
indicates that this is stored as UUID type.score
is the score itself
Int
is the type of the column@@index([score])
creates an index on the score
column.
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.
submit_score
scriptWe’re going to create a submit_score
script that will:
Create script
In the same terminal, run:
What did this do?
This will create a new file modules/my_leaderboard/scripts/submit_score.ts
:
This command also udpated modules/my_leaderboard/module.json
to include the new script:
What's going on here?
import { ScriptContext } from "../module.gen.ts";
imports the ScriptContext
.
module.gen.ts
file is generated by Open Game Backend to provide a type-safe interface for the database (see schema.prisma
), calling other modules, and reading the module config.export interface Request {}
and export interface Response {}
define the input and output of the script.
export async function run(...)
is the main function of the script.
Add dependencies & make public
Update the modules/my_leaderboard/module.json
file to look like this:
What did this do?
user
and rate_limit
as dependencies. This will allow us to authenticate the user and rate limit calls to the script.public: true
for the submit_score
script so that anyone can call it.Update request & response
Open modules/my_leaderboard/scripts/submit_score.ts
and update Request
and Response
to look like this:
What did this do?
This will allow the user to authenticate using userToken
and submit a score using score
.
Throttle requests
At the top the run
function in modules/my_leaderboard/scripts/submit_score.ts
file, add code to throttle requests:
What did this do?
This will rate limit the script to 1 request every 15 seconds. If the rate limit is exceeded, a rate_limit_exceeded
error will be thrown. See the rate limit docs for more details.
Validate user token
Then authenticate the user:
What did this do?
This will authenticate the user and throw an error if the token is invalid. See the token docs for more details.
Insert score
Then insert the score in to the database:
What did this do?
This inserts the score into the database.
id
and createdAt
will be automatically set by the database because of the @default
annotations in the schema.
Query rank
Finally, query the score’s rank:
What did this do?
This will return the number of scores that are greater than (gt
) the user’s score. This is the user’s rank on the leaderboard.
We add 1 to the rank, since the 1st place will have 0 scores greater than it.
Full submit_score.ts source code
For more information about querying databases, see Prisma’s documentation on querying data.
get_top_scores
scriptCreate script
In the same terminal, run:
Make public
Open modules/my_leaderboard/module.json
and update the get_top_scores
script to be public:
Update request & response
Open modules/my_leaderboard/scripts/get_top_scores.ts
and update Request
and Response
to look like this:
What did this do?
This will allow the user to request the top count
scores and return an array of Score
objects.
Throttle requests
At the top the run
function in modules/my_leaderboard/scripts/get_top_scores.ts
file, add code to throttle the requests:
Query scores
Then query the top scores:
What did this do?
take
limits the number of scores returned to req.count
.orderBy
orders the scores by score
in descending order.select
limits the columns returned to userId
, createdAt
, and score
.Convert rows
Finally, convert the database rows in to Score
objects:
What did this do?
This converts the database rows into the Score
objects we defined in the Response
.
We use toISOString
to convert the createdAt
of type Date
into a string.
Full get_top_scores.ts source code
Start development server
In the same terminal, run:
Migrate database
You will be prompted to apply your schema changes to the database. Name the migration init (this name doesn’t matter):
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.
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
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:
Run test
In the same terminal, run:
You should see this output once complete: