room.io v2.0.1
room.io
A real-time communication framework for creating client-server games on the web.
This repository exposes the Node.js server. The client part is a convinient (but not mandatory) way to connect to your room.io server. It is provided in a separated repository: room.io-client.
Motivation
room.io
lets you focus on what's important: your game. It encapsulates for you the boring boilerplate code of room creation, player connection/disconnection or game settings. All that with security built-in to avoid unauthorized players to mess your server up.
It uses socket.io for the messages transport. You game runs on the server and client browsers connect to it via a websocket bidirectional channel.
Installation
# with npm
npm install room.io
# with yarn
yarn add room.io
Quick Start
Standalone
const { createServer } = require('room.io')
const server = createServer({
gameClass: Game,
minPlayers: 2,
maxPlayers: 6
})
server.run()
With an HTTP server
You can attach the room.io
server to a Node.js HTTP server.
const httpServer = require('http').createServer()
const { createServer } = require('room.io')
const server = createServer({
gameClass: Game,
minPlayers: 2,
maxPlayers: 6
})
server.run({ httpServer })
With Express
Express applications can integrate as request handlers for HTTP servers. As such you can connect the room.io
server to an Express app.
const app = require('express')()
const httpServer = require('http').createServer(app)
const { createServer } = require('room.io')
const server = createServer({
gameClass: Game,
minPlayers: 2,
maxPlayers: 6
})
server.run({ httpServer })
API
Create a room.io server
room.io
exposes a single createServer
function. Calling it with a configuration object returns a room.io
server instance. Then calling the run
method runs the server on the provided port.
const { createServer } = require('room.io')
// Create the room.io server
const server = createServer({
gameClass: Game,
minPlayers: 3,
maxPlayers: 10,
maxNameLength: 10,
actions: [
{ name: 'toggleLight', inputValidator: (input) => (typeof input === 'string') }
],
playerDataValidator: (data) => !!data && (data?.avatar instanceof Number),
roomSettingsValidator: (settings) => !!settings && (typeof settings?.lightCount === 'string'),
roomSettingsChecker: (settings, players) => settings.lightCount <= players.length,
logger: {
level: 'info'
}
})
// Run the server
server.run({ port: 3000 })
gameClass
(Mandatory)
A reference to your game class on the server side.
minPlayers
and maxPlayers
(Mandatory)
The minimum and maximum (inclusive) number of players per game.
maxNameLength
(Optional)
The maximum length (inclusive) for player names. Longer names are truncated to this value. You can enforce a length limit on the client side.
actions
(Optional)
The list of actions necessary for your game. An action consists of a name and an optional input-validator function.
If you have a toggleLight
action for instance, whenever a client sends the event toggleLight
the server looks for a method toggleLight
on the room game instance and calls it. If an input-validator function is provided, it is called before that and passed the client input. If the validator returns a falsy value, the action is not executed and an error is sent back to the calling client.
playerDataValidator
(Optional)
A function run against the payload sent by players when trying to update their own player data. Must return a boolean value telling if the payload is in the valid form.
roomSettingsValidator
(Optional)
A function run against the payload sent by the host player when trying to update the room settings. Must return a boolean value telling if the payload is in the valid form.
roomSettingsChecker
(Optional)
A function run when the game instance is starting. It receives 2 parameters: roomSettings
(current room settings) and players
(current players with theirpublicId
, name
and data
) and must return a boolean value telling if the game can start with the current room settings. You can use it to ensure the room settings chosen by the host are valid for the current number of players for example.
logger
(Optional)
A configuration object for the server logger. Supported keys are level
for the log level and defaultMeta
for the default metadata added to each log.
Game class
The game class contains your game logic, it represents an instance of your game running on your server. It can be any JavaScript function or ES6 class and has to expose a contructor (it is instanciated with the new
operator) and an init
method.
When the host player launches the game, a new instance of your game class is created and its method init
is called with a initialization object containing context data.
class Game {
constructor() {}
init({
players // the list of players in the room. In the form { publicId, name, data }
host // the publicId of the host player
settings // the room settings
roomCode // the room code
roomPusher // a RoomPusher instance. Can be used to push messages to players
logger // the room logger, used to log messages on the server
})
{
// Initialize your game here
}
}
Actions
Actions are used to update your game state. They are the interface between client players and the game running in the room.
In addition to the init
method, your game class must expose methods named after every actions
declared in the server config. Actions take a single object as parameter containing the requester playerId
and data
as such:
toggleLight({
playerId // public id of the action requester
data // optional payload
})
{
// Your game logic
}
Return response
Actions can send back a response to the caller. Simply return an object with a response
key containing whatever data you want to return. Returns must be synchronous. If you need to perform asynchronous actions, use the RoomPusher to send another message to client(s) when your action is done.
talk({ playerId, data})
{
// do stuff with data
return { response: `You just said ${data}` }
}
Note: the response must be a simple type or a serializable datastructure.
Return error
Actions can also send back an error to the caller. Your game UI should prevent your players from accidentaly do an action they are not supposed to do, but still always check for access rights and data validity as UIs can be hijacked.
To send back an error, simply return an object with an error
key containing your error. Returns must be synchronous too.
rollDice({ playerId }) {
if (!this.isMyTurnToPlay(playerId))
{
return { error: { code: 'E_NOT_YOUR_TURN' } }
}
// ...
}
Notes:
- The error content must be a simple type or a serializable datastructure.
room.io
sends low-level errors that are always in the form:{ code: 'err_xxx' }
. You can stick to the same formalism for your custom errors, but that is not mandatory. Seeroom.io-client
for the full list ofroom.io
errors.
RoomPusher
When a game instance is created, the server provides a RoomPusher instance to the Game in the init
call. This object can be used to send messages from the server to the players.
The pushTo
method sends a event to a specific player (public id), optionally with a payload.
say({ playerId, data }) {
const { targetId, message } = data
// Transmit the message to the target
this.roomPusher.pushTo(targetId, 'incomingMessage', {
from: playerId,
message
})
}
The pushToAll
method sends a event to every players in the room, optionally with a payload.
yell({ playerId, data }) {
const { message } = data
// Transmit the message to everyone
this.roomPusher.pushToAll('incomingMessage', {
from: playerId,
message
})
}
Note:
pushToAll
always sends to every player, including the action requester ifpushToAll
is called in an action.
The pushError
method sends an error to every players in the room, optionally with a payload.
rollDice({ playerId }) {
if (!this.isMyTurnToPlay(playerId))
{
// Warn everyone that playerId is trying to cheat
this.roomPusher.pushError('E_CHEAT_DETECTED', { playerId })
return
}
// ...
}
Logging
room.io
uses winston to log messages on the server.
Use the logger.level
setting from the server configuration to determine the logging level from the following supported values (default is info
):
Name | Level | Description |
---|---|---|
server | 0 | Logs at the server level. Displayed in green. |
room | 1 | Logs affecting the room and the game. Displayed in magenta. |
error | 2 | Error logs. Displayed in red. |
warn | 3 | Warning logs. Displayed in yellow. |
info | 4 | Basic info logs. Displayed in white. |
debug | 5 | Debug logs that can be used during development. Displayed in white. |
Logs of level greater than than the chosen
logger.level
are not shown.
Log transport
If NODE_ENV is set to 'development', logs are only displayed in the server console with a readable colored syntax. On non-development environments, logs are additionally stored in a server/server.log
file in a json format.
Use the logger.defaultMeta
setting from the server configuration to add your custom metadata to these json file logs.
Testing
TODO