Using boardgame.io

Using boardgame.io

You can embed boardgame.io, an open source game engine for turn based games, in the Tabletop Playground JavaScript environment. If you already have a game implemented using boardgame.io, this allows you to use Tabletop Playground as visualization and networking layer, while you can reuse all the game logic you already have!

If you create a new game in Tabletop Playground, using boardgame.io can help you to keep track of the game state and rules. And when you use boardgame.io to create your package, you can easily port your game to be playable in a web browser by connecting to a different frontend.

Using boardgame.io together with Tabletop Playground means that you won’t use some of the functionality that boardgame.io offers: the multiplayer and lobby features are taken care of by Tabletop Playground, and your JavaScript code only runs on the host. You also don’t need the React bindings, instead you’ll be writing code to use Tabletop Playground as your view layer!

Set up boardgame.io

This tutorial will assume that you have some JavaScript knowledge and you are familiar with how scripting in Tabletop Playground works (see Scripting Basics). In order to set up boardgame.io for your package, you first need to make sure that you have npm installed. Then open up a command line in your package’s “Scripts” folder and enter:

npm install boardgame.io

This will install boardgame.io and it’s dependencies in the “node_modules” folder in your scripts directory. You now have boardgame.io available in the scripts for your package.

Create the game logic

As an example of how boardgame.io can be used, we’ll adapt the tic-tac-toe tutorial at https://boardgame.io/documentation/#/tutorial to work within Tabletop Playground. You can download the completed package at mod.io: https://tabletopplayground.mod.io/tic-tac-toe-boardgameio

For the game logic, we can use the code from the tutorial almost unchanged, we only need to change the first line from import to require and use module.exports instead of export:

import { INVALID_MOVE } from 'boardgame.io/core';

const TicTacToe = {
  setup: () => ({ cells: Array(9).fill(null) }),

  turn: {
    moveLimit: 1,
  },

  moves: {
    clickCell: (G, ctx, id) => {
      if (G.cells[id] !== null) {
        return INVALID_MOVE;
      }
      G.cells[id] = ctx.currentPlayer;
    },
  },

  endIf: (G, ctx) => {
    if (IsVictory(G.cells)) {
      return { winner: ctx.currentPlayer };
    }
    if (IsDraw(G.cells)) {
      return { draw: true };
    }
  },
};

// Return true if `cells` is in a winning configuration.
function IsVictory(cells) {
  const positions = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];

  const isRowComplete = row => {
    const symbols = row.map(i => cells[i]);
    return symbols.every(i => i !== null && i === symbols[0])
  };

  return positions.map(isRowComplete).some(i => i === true);
}

// Return true if all `cells` are occupied.
function IsDraw(cells) {
  return cells.filter(c => c === null).length === 0;
}

module.exports = TicTacToe;

This gives us the basic mechanics of a tic-tac-toe game and a check whether the game is over and who has won.

Connect with Tabletop Playground

Instead of writing a view layer for a browser, we now connect the game logic to Tabletop Playground objects. For this example, we can use a board with snap points, and two card stacks to represent the X and O symbols that players can place.

Simple tic-tac-toe in Tabletop Playground

To connect these objects with the game logic represented in boardgame.io, we create a global script. First, we import the Tabletop Playground API, our TicTacToe object, and the boardgame.io client. Then we initialize the client object:

const {world, globalEvents} = require('@tabletop-playground/api');
const { TicTacToe } = require('./Game');
const { Client } = require('boardgame.io/client');

client = Client({ game: TicTacToe });
client.start();

Now we need to get the information about player actions to boardgame.io. Since we created a board with snap points, the relevant event is onSnapped: when an X or O card is snapped to the board, a player has placed a mark. We add the onSnapped callback new objects of the card types – new objects are created when taking a card from a stack.

o_id = world.getObjectById('O Cards').getTemplateId();
x_id = world.getObjectById('X Cards').getTemplateId();

function snapped(obj, player, snap) {
    client.moves.clickCell(snap.getIndex());
};

globalEvents.onObjectCreated.add(function(obj) {
    if (obj.getTemplateId() === o_id || obj.getTemplateId() === x_id) {
        obj.onSnapped.add(snapped);
    }
});

Finally, we can subscribe to state updates from boardgame.io to react to events in the game. For example, we can show a message when the game is over:

client.subscribe(function (state) {
    if (state.ctx.gameover) {
        if (state.ctx.gameover.winner !== undefined) {
            console.log("Winner: " + state.ctx.gameover.winner);
        }
        else {
            console.log("Draw");
        }
    }
})

And that’s it! With just a few lines of code, we’ve connected an existing boardgame.io game to Tabletop Playground. There’s still a lot that could be improved, of course. Most importantly, actions that players can take are usually not restricted in Tabletop Playground, so the scripts could check whether a player action is valid and react appropriately if it is not. For the tic-tac-toe game, this could mean moving a card back to the stack if it is snapped to an invalid position or dropped somewhere else.

Note that recent versions of boardgame.io use the library nanoid, which is not compatible with the JavaScript environment in Tabletop Playground. It will cause an error to be printed on the console when you import the boardgame.io client (“Cannot use import statement outside a module”). Apart from that message, everything should still work fine though, the parts of boardgame.io where nanoid is called are not required when used within Tabletop Playground.

Table of Contents