Asobi - turn-based game framework - server
A multiplayer turn-based game server for Node.js.
Installation
npm install @basementuniverse/asobi-server
Setup
See jsonpad.md for instructions on setting up JSONPad.
Usage
import { AsobiServer, Game } from '@basementuniverse/asobi-server';
const server = new AsobiServer({
jsonpadServerToken: '<YOUR JSONPAD TOKEN>',
jsonpadGamesList: '<YOUR JSONPAD LIST PATHNAME>',
jsonpadPlayersList: '<YOUR JSONPAD LIST PATHNAME>',
jsonpadRateLimit: 150,
mode: 'turns',
minPlayers: 2,
maxPlayers: 2,
joinTimeLimit: null,
turnTimeLimit: null,
roundTimeLimit: null,
gameTimeLimit: null,
gameSchema: {
type: 'object',
},
playerSchema: {
type: 'object',
},
moveSchema: {
type: 'object',
},
hooks: {
setup: (api: Express): void => {
},
createGame: async (game: Game): Promise<Game> => {
return game;
},
joinGame: async (game: Game, player: Player): Promise<Game> => {
return game;
},
move: async (game: Game, player: Player, move: Move): Promise<Game> => {
return game;
},
round: async (game: Game): Promise<Game> => {
return game;
},
finishGame: async (game: Game): Promise<Game> => {
return game;
},
},
});
server.start();
Types
type Game = {
id: string;
status: GameStatus;
startedAt: Date | null;
finishedAt: Date | null;
lastEventType:
| 'game-created'
| 'player-joined'
| 'player-moved'
| 'timed-out'
| 'game-finished';
lastEventData: any;
numPlayers: number;
players: Player[];
moves: Move[];
round: number;
state: any;
startsAt?: Date | null;
finishesAt?: Date | null;
turnFinishesAt?: Date | null;
roundFinishesAt?: Date | null;
[key: string]: any;
};
type Player = {
id: string;
name: string;
status: PlayerStatus;
state?: any;
hiddenState?: any;
[key: string]: any;
};
type Move = {
playerId: string;
movedAt: Date;
data?: any;
};
enum GameStatus {
WAITING_TO_START = 'waiting_to_start',
STARTED = 'started',
FINISHED = 'finished',
}
enum PlayerStatus {
WAITING_FOR_TURN = 'waiting_for_turn',
TAKING_TURN = 'taking_turn',
FINISHED = 'finished',
}
A note about hidden player state
For some games, we might want to hide certain parts of a player's state data from other players. For example, in a card game, we might want to hide a player's hand from their opponents.
To achieve this, in the createGame
, joinGame
, and move
hooks, for each player in the game's players
array we can include a hiddenState
property in the player object.
If this property is present in the game data when the hook returns, it will be removed.
The current player (the player who is starting the game, joining the game, or currently taking their turn) will still be able to see their own hidden state in responses.
Note that all hidden player state will be hidden from event handler parameters, so if you need to access or modify hidden state when handling a realtime event, you will need to re-fetch the game state using the client's fetchState()
method. This method takes a player token as an argument, which ensures that a player's hidden state can only be viewed by that player.
Error handling
You can throw AsobiServerError
inside hooks to return an error to the client.
import { AsobiServerError } from '@basementuniverse/asobi-server';
throw new ServerError(
'Something went wrong...',
400
);
Endpoints
It is recommended to use the client library to interact with the server, but here are the endpoints that the server exposes in case you want to interact with it directly:
Create a new game
POST {SERVER_URL}/create-game
Request payload:
{
// The first player's name
// Optional; if not provided, we will use "Player 1"
"playerName": "Player 1",
// Freeform player data for Player 1
"playerData": {},
// Initial data for the game
"gameData": {}
}
Response payload:
{
// The game object (see Types above)
"game": {
// ...
},
// An identification token for Player 1
// Player 1 should use this token when making moves
"token": "..."
}
Join an existing game
POST {SERVER_URL}/join-game/{GAME_ID}
Request payload:
{
// The player's name
// Optional; if not provided, we will use "Player N"
// (where N is the next available player index)
"playerName": "Player 2",
// Freeform player data for Player 2
"playerData": {}
}
Response payload:
{
// The game object (see Types above)
"game": {
// ...
},
// An identification token for the joining player
// The joining player should use this token when making moves
"token": "..."
}
Make a move
POST {SERVER_URL}/move/{GAME_ID}
Headers:
Authorization: Bearer {PLAYER_TOKEN}
Request payload:
{
// Freeform move data
"moveData": {
// ...
}
}
Response payload:
{
// The game object (see Types above)
"game": {
// ...
}
}
Fetch game state for a specific player
This endpoint is useful for fetching the current game state with hidden player state attached for the specified player.
GET {SERVER_URL}/state/{GAME_ID}
Headers:
Authorization: Bearer {PLAYER_TOKEN}
Response payload:
{
// The game object (see Types above)
"game": {
// ...
}
}
Fetch a list of games, or fetch a specific game
The client communicates directly with JSONPad (via the JSONPad SDK) when fetching a list of games or a specific game.
Check out the JSONPad API documentation for more information on available endpoints and their parameters.
Realtime updates
The client uses WebSockets (via the JSONPad Realtime SDK) to listen for updates to the games list.
Check out the JSONPad Realtime documentation for more information.