boardgame.io icon indicating copy to clipboard operation
boardgame.io copied to clipboard

Using setup data for Local games

Open julienroulle opened this issue 4 years ago • 9 comments

As of now, the setup function of the Game object can take as an argument a setupData object, which is given by the multiplayer server.

I was wondering if we could have a similar behaviour for Local games?

Here are my 2 ideas on how to add that to the framework:

  • Passing a setupData object to the Client (preferred way)
var GameClient = Client({
        game: Game,
        board: Board,
        multiplayer: Local(),
        setupData: {}
});
  • Passing a setupData object to the Game (not sure how though, maybe as a prop?)
var myGame = Game;
Game.setupData = {}
var GameClient = Client({
        game: ,
        board: Board,
        multiplayer: Local(),
});

Let me know what you guys think. I'd be happy to work on it as soon as I get the go :)

julienroulle avatar Mar 06 '20 15:03 julienroulle

I just thought through a few options and will lead with the simplest, but below I’ve also included some thoughts on how this might otherwise be implemented, which might also help show that the simplest approach could be the best.

Simplest

You can achieve this without any changes to the library 🎉. In local circumstances like these, you can wrap your game definition itself as a function and do something like:

const Game = customData => ({
  name: 'my-game',
  setup: ctx => {
    // use customData
  }
})

const GameClient = Client({
  game: Game(localSetupData)
})

Even better, you can abstract this to a default wrapper for any game definition to provide setupData:

import myGame from './my-game.js'

function wrapGameDefinition (game) {
  return setupData => ({
    ...game,
    setup: ctx => game.setup(ctx, setupData)
  })
}

const wrappedGameDef = wrapGameDefinition(myGame)
const setupData = { /* local data */ }
const game = wrappedGameDef(setupData)

Given this is possible, is fairly straightforward, and requires no library changes, it might be preferable. (Maybe documenting this pattern could be a good idea if it’s a common requirement.)

If you’re still thinking it would be useful to have a setupData option. Here are some more thoughts:

Read even more…

Adding a setupData option

Of the two options you suggested, I think something like the first would be preferable as it doesn’t add a special field to the game definition that applies only on the client. Going that route, here are a few thoughts.

Implementation

You need to get your setupData from the client, to the InitializeGame call site, which will be either in the Master class’s sync method (if using Local()) or (more simply) in the Client itself (when multiplayer is falsy).

It’s a bit fiddly, but definitely possible.

Concept

For clients with multiplayer: undefined || false, passing setupData directly to the client seems like a reasonable approach, because you have a single client managing everything. However, when using the Local transport factory as in your code examples, there’s actually a check in the code to see if a corresponding master already exists on the page in which case subsequent calls to Local use an existing master, not a new instance. This way, several clients can share a single master.

Realising that, it would make most sense for setupData to be passed to Local not to the Client and then to document the pattern as:

const transportFactory = Local({
  setupData: { /* ... */ }
})

const Client1 = Client({
  multiplayer: transportFactory
})

const Client2 = Client({
  multiplayer: transportFactory
})

Nevertheless, this could be buggy if people started to try calling Local repeatedly with different setupData and expecting it to be respected.

delucis avatar Mar 06 '20 17:03 delucis

Thanks @delucis,

I learnt a lot by reading your propositions and you sure know the framework better than I do! I like the wrapper option, makes me able to use the classic Game object for online games and the wrapper for local games.

Nonetheless, i think that having a similar behaviour between local and online would be needed at some point for the lib

julienroulle avatar Mar 06 '20 18:03 julienroulle

@julienroulle No problem! I may have spent a few too many hours reading through the code over the past months…

I agree that consistency is a good aim. How exactly are you using the Local class? To manage multiple views for pass-and-play or something?

The difficulty is that the Client calls the Game helper with the game definition passed to it and hands the result to the multiplayer implementation, so an “automatic” solution using something like the wrapper internally would currently need to happen at the Client level. That runs into the weird thing of having to set setupData on each client even though it has to be the same for each of them. (The same applies with passing the game definition.) In a way it would be nice if the client could get its game definition from a multiplayer implementation if in use.

delucis avatar Mar 06 '20 18:03 delucis

@delucis I am using this for Local AI play, in a 1v1 boardgame, where the user can choose it's color, the first player, and the time (blitz mode).

Since I instantiate only one client I don't really mind doing this wrapper since I don't have to check that the setup was made correctly for the second player.

But in the end I'm still a bit confused as of why the setup happens on the Client level and not directly into the Game. Maybe it could be possible to create a "Local API" for game creation? Or maybe abstract the Lobby implementation that would work both for local and online multiplayer?

julienroulle avatar Mar 06 '20 18:03 julienroulle

Oh right, I’m not so familiar with the AI side of things.

The lobby design is a pretty significant complexity to add for local game management where you don’t need most of the abstractions it gives you (waiting for other players, authentication, etc.). There probably is some refactoring that might improve the separation of concerns, but it would be pretty significant if you consider that more than half of the options for the Client are potentially shared between clients: game, debug, numPlayers, multiplayer, gameID and enhancer could all potentially be set on some local API class and only playerID and credentials on the client.

delucis avatar Mar 06 '20 20:03 delucis

Hi! I am trying to allow user customization for https://github.com/freeboardgames/FreeBoardGames.org ... I don't like the proposed solution of wrapping the game in local mode, because our games can work in several modes and that would mean two different variables/places to access in each case...

Are you okay if we go with the proposed solution of adding it to both Local() (for multiplayer running in a single browser) and Client() (for local-only games) ?

vdfdev avatar Dec 22 '20 21:12 vdfdev

I can implement this for us :)

vdfdev avatar Dec 22 '20 21:12 vdfdev

@flamecoals We could add that sure.

I’m not familiar with your codebase, but this shouldn’t require two variables necessarily to use the wrapping approach. Your local clients could handle this, something along the lines of:

import { Client } from 'boardgame.io/react';

const gameWithSetupData = (game, setupData) => ({
  ...game,
  setup: ctx => game.setup(ctx, setupData)
});

const ClientWithSetupData = ({ game, setupData, ...props }) => {
  props = {
    ...props,
    game: gameWithSetupData(game, setupData),
  };
  return <Client {...props} />;
};

At that point you will basically be using setupData on the local clients as you are proposing to add here.

delucis avatar Dec 22 '20 22:12 delucis

Oh! I didn't get that it was that easy. So I can do that instead, thanks :)

vdfdev avatar Dec 23 '20 00:12 vdfdev