This Issue is about sharing some insights on making a Boardgame using NetplayJS.
The no-server approach, as @rameshvarun is kind enough to host a public server here (https://netplayjs.varunramesh.net/), and the simplicity of the API, as well as the ease of Matchmaking (just a click and a QR), drew me to this library, and I really want to thank @rameshvarun for his work.
The Canvas
So, first things first, the game would be plain HTML, so I minimized and threw away the canvas element. Additionally, one can bring their own canvas (answering this one:
https://github.com/rameshvarun/netplayjs/issues/80), as the constructor can also be like this:
I first moved and then totally threw my canvas away and the lib didn't fall apart:
// Create a ghost game (no Canvas), that is only used
// to sync the data and also for net discovery.
NetPlayJSGame.canvasSize = { width: 0, height: 0 };
[...]
// (Re)Move the NetPlayJS ghost canvas
$("canvas").each((i, c) => {
console.log(c)
c.id = `netplayjs-canvas-${i}`;
$('#netplayjs').remove(c);
// $('#netplayjs').append(c);
});
Game state
This lib works fundamentally different than a board game! It only syncs inputs (key-presses, touches) between the 2 browsers, so anything else is not synced (e.g variables).
Luckily there is a serialization engine that I only found to work through the RollbackWrapper of the game. My serialization implementation:
class NetPlayJSGame extends netplayjs.Game {
[...]
serialize() {
return JSON.stringify(this.G)
}
deserialize(value) {
let newG = JSON.parse(value);
if (!this._isHost())
this.G = newG
}
The G object is a JSON serializable (data only) Object that carries stuff around in the same style as boardgame.io. Stuff like cards in player's hand, board positions, turns, player names, scores are all stored in this same object.
Sharing such object between 2 browsers would need an authoritative source. A source of truth.
The Host and passing messages
Looks like every time a NetplayJS game starts, it chooses one browser to become the host. I found out that this host never accepts the serialized objects of the client. So it is the authoritative for the state and all GameState manipulation should happen ONLY in the Host's tick().
I found the host like this:
class NetPlayJSGame extends netplayjs.Game {
[...]
_getOwnPlayerID(){
for(let i = 0; i < this.players.length; i++)
if (this.players[i]['isLocal']) return i;
return null;
}
_isHost(){
//if I'm not a Client - I'm HOST
return !this.players[this._getOwnPlayerID()].isClient()
}
And I used _isHost() in my tick()
class NetPlayJSGame extends netplayjs.Game {
[...]
tick(playerInputs) {
[...] // stuff that runs in both Host and Client
/*
The rest of the game loop
is run only by the Host
Client only gets the G updates
*/
if (!this._isHost()) return;
// Process the "Events" from all players and do "moves" accordingly
for (const [player, input] of playerInputs.entries()) {
Object.keys(input.keysPressed).forEach((event)=>{
[...]
But, if it's a boardgame, played through clicking, what kinds of events is the Host getting?
function passToHost(key) {
const body = document.body;
body.dispatchEvent(new KeyboardEvent("keydown", { key }));
}
Funny thing is that calling this function with ANY string will make the Host think that such button is pressed:
passToHost("thiskeydoesntexist 123");
The above code will make the Host's tick consume the thiskeydoesntexist 123 string. So I created some basic event "commands" like:
passToHost("DRAW 2"); // will make the Host add 2 cards to my hand
and parse the "commands" when processing the playerInputs parameter in Host's tick(), along with the playerID who sent the "command". Finally, every consumed "command" should be deleted in the tick(), as the passToHost does not create any keyup event:
class NetPlayJSGame extends netplayjs.Game {
[...]
tick(playerInputs) {
[...]
for (const [player, input] of playerInputs.entries()) {
Object.keys(input.keysPressed).forEach((event)=>{
// Parse everything
const eventTokens = event.split(" ")
const eventAction = EVENTS[eventTokens[0]]
const eventParameter = eventTokens[1]
// use eventAction and eventParameter to change the Gamestate (this.G)
[...]
// delete the keypress
delete input.keysPressed[event] // remove the event from the inputs
});
So, finally, to attach an event to an HTML outside the NetplayJS objects, it's as simple as:
// using JQuery
$('#deck').click(() => {
passToHost("DRAW 1"); // this is evaluated in Host's tick()
});
Drawing stuff (without a Canvas)
This is very simple as there is a built-in draw function with access to the Gamestate (as this.G):
class NetPlayJSGame extends netplayjs.Game {
[...]
draw(){
// needed to find out who's view is drawn
// E.g whether cards are drawn face up or down
const playerID = this._getOwnPlayerID()
// Create HTML elements to the DOM as one does...
[...]
}
Performance
As the game does not draw or does not need any synchronization magic we can run easy with:
NetPlayJSGame.timestep = 1000 / 10; // Our game runs slowly as it is a boardgame
NetPlayJSGame.deterministic = true;
NetPlayJSGame.highDPI = false;
let GameObjectWrapper = new netplayjs.RollbackWrapper(NetPlayJSGame)
GameObjectWrapper.start();
The LockstepWrapper does not seem to work with serialization for me, but I assume it would be better on performance.
Security...
This game can be tampered with only by the Host by altering the GameState (G), through the browser console. Some checks can be set in tick() for both Host and Client, so it can at least be detected if there is any bogus change to the GameState by any player.
The impossible issue is that both players have full access to the GameState through the Browser Console, allowing them to see hidden parts, such as player hands in a card game. There is no way around this without a server component. Again, boardgame.io covers this: https://boardgame.io/documentation/#/secret-state
So here you have it. How I made NetplayJS to work for a Boardgame!
This Issue is about sharing some insights on making a Boardgame using NetplayJS. The no-server approach, as @rameshvarun is kind enough to host a public server here (
https://netplayjs.varunramesh.net/
), and the simplicity of the API, as well as the ease of Matchmaking (just a click and a QR), drew me to this library, and I really want to thank @rameshvarun for his work.The Canvas
So, first things first, the game would be plain HTML, so I minimized and threw away the canvas element. Additionally, one can bring their own canvas (answering this one: https://github.com/rameshvarun/netplayjs/issues/80), as the constructor can also be like this:
I first moved and then totally threw my canvas away and the lib didn't fall apart:
Game state
This lib works fundamentally different than a board game! It only syncs inputs (key-presses, touches) between the 2 browsers, so anything else is not synced (e.g variables).
Luckily there is a serialization engine that I only found to work through the
RollbackWrapper
of the game. My serialization implementation:The
G
object is a JSON serializable (data only) Object that carries stuff around in the same style as boardgame.io. Stuff like cards in player's hand, board positions, turns, player names, scores are all stored in this same object. Sharing such object between 2 browsers would need an authoritative source. A source of truth.The Host and passing messages
Looks like every time a NetplayJS game starts, it chooses one browser to become the host. I found out that this host never accepts the serialized objects of the client. So it is the authoritative for the state and all GameState manipulation should happen ONLY in the Host's
tick()
.I found the host like this:
And I used
_isHost()
in mytick()
But, if it's a boardgame, played through clicking, what kinds of events is the Host getting?
The fake Keyboard Events
I used this code here, based on this answer of @hciam (https://github.com/rameshvarun/netplayjs/issues/85#issuecomment-2128473208).
Funny thing is that calling this function with ANY string will make the Host think that such button is pressed:
The above code will make the Host's tick consume the
thiskeydoesntexist 123
string. So I created some basic event "commands" like:and parse the "commands" when processing the
playerInputs
parameter in Host'stick()
, along with theplayerID
who sent the "command". Finally, every consumed "command" should be deleted in thetick()
, as thepassToHost
does not create anykeyup
event:So, finally, to attach an event to an HTML outside the NetplayJS objects, it's as simple as:
Drawing stuff (without a Canvas)
This is very simple as there is a built-in
draw
function with access to the Gamestate (asthis.G
):Performance
As the game does not draw or does not need any synchronization magic we can run easy with:
The
LockstepWrapper
does not seem to work with serialization for me, but I assume it would be better on performance.Security...
This game can be tampered with only by the Host by altering the GameState (
G
), through the browser console. Some checks can be set intick()
for both Host and Client, so it can at least be detected if there is any bogus change to the GameState by any player. The impossible issue is that both players have full access to the GameState through the Browser Console, allowing them to see hidden parts, such as player hands in a card game. There is no way around this without a server component. Again,boardgame.io
covers this: https://boardgame.io/documentation/#/secret-stateSo here you have it. How I made NetplayJS to work for a Boardgame!