Skip to the content
The room

Documentation · Game SDK

Player Two's game SDK

A game hosted on Player Two gives its players trophies, XP and levels, stats, leaderboards, cloud saves and presence through their one Player Two account. Add one script tag. Back to the documentation.

  1. Your game asksIt calls the SDK: p2.trophies.unlock("first-win"). It never holds the player’s sign-in.
  2. Player Two's host carries itThe player's page around the game, or the Player Two app, sends it with the play session only it knows.
  3. The player sees itA trophy's toast over the game, their level, their friends seeing what they play.
On this page

Quick start

Put the script on your game’s page and call PlayerTwo.init() once, early. It answers with who is playing and every call below.

index.html
<script src="https://playertwo.lol/sdk/v1.js"></script>
<script>
  PlayerTwo.init()
    .then(async (p2) => {
      console.log(p2.player.name, "level", p2.player.level);
      await p2.trophies.unlock("first-win");
    })
    .catch((e) => {
      /* not on Player Two (e.code is "not-on-player-two"): the game plays without trophies or saves */
    });
</script>

Where it works

The services are for games Player Two hosts: a web build kept here (one .html, or a .zip of your export), or downloads players get from Player Two and play in its app. A game played on its own site gets them in Player Two’s player once its developer turns on Player Two Connect, with a signed identity its own servers can trust.

  • On Player Two (PlayerTwo.env is "p2"): your game runs in Player Two’s player, at its own address on the games host (hosting), where Player Two puts this script in its page for you. The SDK says hello to the page around it, takes a private channel from it, and every call goes over that channel. In the Player Two app, the same calls go to the app’s local bridge.
  • On localhost ("local"): a stand-in answers every call from memory, so you can build without Player Two.
  • Anywhere else ("disabled"): init() rejects after 1.5 seconds with code not-on-player-two, and your game plays on without services. Or, with { required: true }, the page says “Play this on Player Two” with a link, and stops.

Building without Player Two

On localhost the stand-in knows no trophies unless you tell it. Give it yours, and it refuses a key that isn’t among them, as Player Two would:

const p2 = await PlayerTwo.init({
  local: { trophies: [{ key: "first-win", name: "First win", tier: 1 }] },
});
// on localhost: answered from memory; on Player Two: the real thing
// local: true (or { force: true, trophies }) uses the stand-in wherever the game runs

Every call

Every call returns a promise. A refusal rejects with an Error that has a code and a status.

init

const p2 = await PlayerTwo.init({ required: false, timeout: 1500 });
p2.player  // { id, handle, name, picture, creature, level }  id: theirs for your game only
p2.game    // { slug, title }
p2.test    // true while its developer (or an admin) plays: see Test mode
p2.demo    // true in a demo: demos earn no trophies
p2.params  // the invite params the player came with, or null
PlayerTwo.env  // "p2" | "local" | "disabled"

The player’s id is the same every time for your game, and different in every other game, so games can’t line up their players with each other’s. Their handle and name are public. Player Two never tells a game a player’s email or wallet.

me

const { player, game, session, test, demo, from } = await p2.me();
// from: "web" (Player Two's player) or "desktop" (the app)

Trophies

const { trophies, xp, level } = await p2.trophies.list();
// each: { key, name, how, tier, xp, hidden, art, earned: { at, n } | null, share, rarity }
// a hidden one the player hasn't earned: name, how and art are empty

const won = await p2.trophies.unlock("flawless-duel");
// { trophy, new, n, xp: { gained, total }, level, complete? }
// new: false if they had it; n: which player to earn it they are (No. 1 is the first)
// complete: the completion trophy, earned with this one
// xp.pending: XP that waits until your game counts toward levels

The player’s host shows the trophy arriving over your game; your game doesn’t have to. Define trophies in your studio (each game’s card, Trophies) or through your agent: a key your code uses, a name, how to earn it, a tier, hidden or not, its sign, optionally a stat and target, optionally “only my server can give it”.

Stats

await p2.stats.add("duels-won");        // by 1
await p2.stats.add("gold", -50);         // or any whole number
await p2.stats.set("highest-floor", 12);
const { stats } = await p2.stats.get();  // { "duels-won": 3, ... }
const floor = await p2.stats.get("highest-floor");
// set and add answer { stats, unlocked }: a trophy waiting on a stat is earned
// by itself when the stat reaches its target, and comes back in unlocked

Leaderboards

const s = await p2.scores.submit("time-attack", 83400);
// { value, best, new, previous, rank, total }: only a player's best is kept

await p2.scores.list("time-attack");                       // the top 10
await p2.scores.list("time-attack", { around: "me" });     // around the player
await p2.scores.list("time-attack", { friends: true, limit: 25 });
// { board, rows: [{ rank, value, at, player: { id, handle, name, picture }, me? }], total, me }

Define boards in your studio: higher or lower is better, a number, a time in milliseconds or points, the lowest and highest score it takes, how many seconds a player waits between scores, and whether only your server sends its scores (trusted). A player who keeps off leaderboards shows as “A player”.

Saves

const { slots } = await p2.saves.list();        // [{ slot, size, version, at }]
const save = await p2.saves.get("slot-1");       // .json(), .text(), .bytes(), .version
await p2.saves.put("slot-1", { level: 3 }, { version: save.version });
await p2.saves.delete("slot-1");
// put takes text, an object (kept as JSON) or bytes, up to 1 MB
// { version }: refused (409, e.version is the one there now) if another device saved since

A slot that holds a save is only written over once your game has read it in this session (get, or list), so a game starting blank on a new device can’t wipe a player’s progress. A new slot needs no read.

Presence

p2.presence.set("Chapter 3, The Docks");   // up to 80 characters
p2.presence.set(null);                       // just the game's name
p2.presence.set("In a party", { join: { room: "482913" } });  // friends get Join

The player’s friends see that they are playing your game, and this line, unless the player keeps their activity private. Time played counts by itself while the game is open. With join, they also get Join beside it, which opens your game with those params (invites).

Identity and invites

const token = await p2.identity();   // who is playing, signed for your own servers (ten minutes)
const link = await p2.invite({ room: "482913" });   // playertwo.lol/game/<your game>?join=...
p2.params                               // the params the player came with, or null

An identity is a JWT signed with Player Two’s key, for your game only, which your servers check before they trust who it names; an invite is a link to your game on Player Two that starts it with the params. Both, and how to carry your own accounts and multiplayer over: Player Two Connect.

Loading and gameplay

p2.loading.done();     // the game is ready to play
p2.gameplay.start();   // a round, a level, a run begins
p2.gameplay.stop();    // ... and ends, or the game pauses

Player Two’s player and its app use these for what they show over the game. Nothing is kept of them yet.

The overlay

const off = p2.on("overlay", (open) => (open ? pause() : resume()));
// on the web: Player Two's bar over the game; in the app: its window over the game

When a call is refused

Codes a refusal carries
CodeStatusWhat it means
not-on-player-twoinit only: no Player Two around the game.
signed-out401The player isn't signed in (a demo played signed out): play on without services.
too-soon409Nothing is earned in a session's first ten seconds.
unknown-trophy404No trophy by that key, or it is retired.
server-only403Only your server gives this trophy.
demo403Trophies come with the full game.
revoked403Player Two took this trophy back from this player.
read-first409Read the slot before saving over it.
version409Another device saved in between; e.version is the one there now.
full409Eight save slots (or 200 stats) already.
too-big413A save is up to 1 MB.
out-of-range422A score outside the board's bounds.
cooldown429Too soon after this player's last score on this board.
trusted403Only your server sends this board's scores.
not-connected409init only: a game on its own site whose developer hasn't turned on Connect.

The rules

Trophies

  • 1 to 100 a game. Keys are lowercase letters, numbers and dashes, up to 40; names up to 40 characters; how to earn one up to 120.
  • Tiers 1 to 4 are worth 10, 25, 50 and 100 XP, and a game’s add up to 1,000 XP at most. Player Two adds the completion trophy (key complete, 200 XP) once a game has one: it is earned by holding every other trophy that isn’t retired.
  • An unlock needs a live session at least ten seconds old, and a session earns twenty a minute at most. Demos earn none.
  • Every trophy is a relic, a small sculpture of cubes built from its sign: up to 16 by 16 cells you draw in the studio (or pick), sent by an agent as rows of # and . (..#../.###./#####). Without one, Player Two picks one by its key; the completion trophy is a crown until you give it your game’s own sign. Its cubes are cut from the finish of how rare it turns out to be, from titanium for Common to pearl mended with gold for Divine.
  • Once a player has earned a trophy, its key, tier, stat, target and server-only setting stay; its name, how to earn it, sign and whether it is hidden can change. It can be retired (nobody earns it from then on; those who have it keep it), never deleted.
  • The first player to earn a trophy is No. 1, and every unlock keeps its number.

XP and levels

A player’s level comes from the XP of every trophy they hold, from every game that counts, and from Player Two’s own trophies. XP from level n to n+1 is 100 × n/10 rounded up: 100 a level up to 11, 200 a level up to 21, and so on.

A game’s trophies count toward levels once it is listed and 20 players have played it ten minutes or more each, or once Player Two says so. Until then they are earned and shown, and their XP waits; it arrives when the game starts counting. Player Two can take a game’s trophies out of levels (an XP farm, say), and take back what a cheating player earned.

Rarity

Each trophy’s share of the game’s players (everyone who has played it, not counting tests) is worked out daily, in hundredths of a percent (share: 425 is 4.25%), and named from Player Two’s ladder: Common at 50% and over, Rare 20%, Epic 10%, Legendary 5%, Mythic 1%, and Divine under 1%. Until a game has 10 players, every trophy is New.

Limits

What a game, a session and a player may do
WhatLimit
Trophies100 a game, 1,000 XP of tiers 1 to 4
Unlocks20 a minute per session
Stats200 per player per game, 20 in one call, 120 calls a minute; whole numbers
Leaderboards20 a game; 60 scores a minute per session, and each board's own wait
Saves8 slots per player per game, 1 MB each; 60 saves a minute per session
Reading240 reads a minute per session (trophies, stats, scores, saves)
A game key1,200 requests a minute

Test mode

You play your own game in test mode, and so do Player Two’s admins: p2.test is true. What you earn is kept apart, unnumbered and without XP; your scores show only to you. In your studio, each game’s Testing section wipes your test unlocks, stats and scores, so you can play it again as a new player would.

Your game's server

Game keys

Make a key for your game’s own server in your studio (Game keys). It starts p2g_, is shown once, and only a hash of it is kept; revoke it there at any time. Keep it on your server: never in your game.

What a player can fake

Its calls

Who is playing, from the session your game's client sends you
curl https://playertwo.lol/api/v1/sdk/server/session \
  -H "Authorization: Bearer p2g_..." \
  -H "x-player-session: p2s_..."
# { live, player: { id, handle, name, picture, level }, game, session, test, demo, from, startedAt, beatAt }
A trophy, server-only ones too
curl -X POST https://playertwo.lol/api/v1/sdk/server/trophies/tournament-winner \
  -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \
  -d '{ "player": "<the player's id for your game>" }'
A score, on any board (a trusted board takes them only this way)
curl -X POST https://playertwo.lol/api/v1/sdk/server/boards/ranked \
  -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \
  -d '{ "player": "<the player's id for your game>", "value": 1840 }'
Whether an identity your game's client sends is good, and who it names
curl -X POST https://playertwo.lol/api/v1/sdk/server/identity \
  -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \
  -d '{ "token": "<from p2.identity()>" }'
# { valid, player: { id, handle, name, picture, level }, kind, live, exp }

Your game’s client gets its session token only on a computer (the app hands it to the game); on the web it stays in Player Two’s page, so a web game’s server learns who is playing from an identity the client sends it (p2.identity(), checked against Player Two’s keys or with the call above), and names players by their id for the game.

On a computer

The local bridge

The Player Two app starts your game with three settings in its environment: P2_BRIDGE (a local address with a secret, alive while the game runs), P2_TICKET (its launch ticket) and P2_GAME (its address on Player Two). The same calls go to the bridge as JSON over HTTP: POST ${P2_BRIDGE}/call with { "method": "trophies.unlock", "args": ["first-win"] }, answered as on the web. A game made with web technology on a computer passes the address to init({ bridge }), or sets P2_BRIDGE on globalThis.

Launch tickets

Each time the app starts a game it asks for a launch ticket: a JWT signed RS256 by Player Two, naming the player (their id for your game), the game, the build, the computer and when it runs out, 14 days on, so the game starts offline for two weeks. Player Two’s public keys are at /.well-known/p2-keys.json. Check the ticket and refuse to start without one, or mark your game DRM-free.

Checking tickets in your engine

The helpers for Unity (C#), Godot (GDScript) and plain HTTP come with the Player Two app; this part will show how to use them.

The requests underneath

Each call becomes one request to Player Two, made by the player’s host with the play session (Authorization: Bearer p2s_...). They answer any site, without cookies.

Calls and their requests
CallRequest
me()GET /api/v1/sdk/me
trophies.list()GET /api/v1/sdk/trophies
trophies.unlock(key)POST /api/v1/sdk/trophies/<key>
stats.get() · set · addGET · POST /api/v1/sdk/stats { set } | { add }
scores.submit · listPOST · GET /api/v1/sdk/boards/<key>
saves.list()GET /api/v1/sdk/saves
saves.get · put · deleteGET · PUT · DELETE /api/v1/sdk/saves/<slot>
presence.set(line, { join })POST /api/v1/sdk/beat { status, join }
identity()GET /api/v1/sdk/identity
invite(params)POST /api/v1/sdk/invite { params }
loading.done · gameplay.start · stopPOST /api/v1/sdk/event { event }