boardgameio / boardgame.io

State Management and Multiplayer Networking for Turn-Based Games
https://boardgame.io
MIT License
10.03k stars 709 forks source link

CLient side setup #538

Open pociej opened 4 years ago

pociej commented 4 years ago

I have the case where initial setup is generated client side, based on things that server is completly not aware of. Problem is that setup is defined on Game object that is imported by both client and server so only way to have dynamic client side generated setup is to have artificial setSetup move and call it. Isn't better to expose setup api to be pure client side and then overwrite the server state if called?

delucis commented 4 years ago

setup has to be run on the server to ensure it is consistent and predictable. If you need some additional user input to correctly set up a game, you can use the setupData argument to your setup function (docs).

Using the Lobby REST API, you can send this setupData along with your game-creation request (docs). (This is not currently supported out-of-the-box by the React Lobby implementation, but you’d presumably need some custom UI for input in any case.)

Currently, setupData has to be sent to the /create endpoint, which initialises the game state (source). This makes it impossible for each player to provide setup data before initialisation, because the game is created before any players have joined. Is that the kind of thing you’re talking about @pociej?

For now, I don’t think having a setup phase at the beginning of a game is bad practice — it allows you to rely on all of boardgame.io’s guarantees, such as validating moves, state mutations etc.

For the future, @nicolodavis, what do you think about delaying game state initialisation until after the last player has joined? That would allow the /join endpoint to receive player-specific setupData to stash in gameMetadata, which could be consumed by CreateGame once the final seat is taken.

pociej commented 4 years ago

@delucis Sorry that i didn't properly describe the full case. I opened two issues one after another and i had context of #537 when was posting this. You are 100% right that in case of real multiplayer game it has to be set server side to be sure of data consistency between player. However in case i described in #537 there is only one player. But we still have to use multiplayer just with numPlayers : 1. In this case i would like to be able to setup only client side. I like your idea of having player specific setupData. Currently i solve this problem by creating a bit artifical move setup which is called on client when data are available.

delucis commented 4 years ago

@pociej If you’re storing game state in a database somewhere (so players can save games etc.), I think you’d still be better off generating and validating that state on the server, rather than posting the state object directly from the client. You can do that either with a setup move or with setupData, depending on your exact needs.

If there’s only one player, you can already use setupData to achieve what you need: have the client provide the extra information needed for setup before requesting to create the game on the server. Here’s a method adapted from the lobby connection implementation:

const lobbyUrl = 'https://...'
const gameName = 'my-game'
const numPlayers = 1

async function create(setupData) {
  try {
    const resp = await fetch(`${lobbyUrl}/${gameName}/create`, {
      method: 'POST',
      body: JSON.stringify({
        numPlayers,
        setupData,
      }),
      headers: { 'Content-Type': 'application/json' },
    });
    if (resp.status !== 200) throw new Error(`HTTP status ${resp.status}`);
  } catch (error) {
    throw new Error(
      `failed to create room for ${gameName} ('${error}')`
    );
  }
}

Pass whatever data you need to this create method, then use setupData in your game setup function, e.g.:

function setup (ctx, setupData) {
  const G = {}
  if (setupData) G.setupData = true
  return G
}

You might want to immediately POST a request to the /join endpoint if the game is created successfully to automatically add a player to the games they create.

nicolodavis commented 4 years ago

For the future, @nicolodavis, what do you think about delaying game state initialisation until after the last player has joined? That would allow the /join endpoint to receive player-specific setupData to stash in gameMetadata, which could be consumed by CreateGame once the final seat is taken.

Sorry about missing this thread earlier. What's an example of per-player setup data in a real game? Are we talking about true metadata (like a player name or badge) or something that has a meaning within the game?

I'm definitely open to supporting the use-case if we can find some compelling examples.

delucis commented 4 years ago

I don't have a super concrete example myself. Aside from metadata, I can think of something where players might need to choose a class of character or a strategy that might change setup. I'd be interested in other people's needs. I don't think it's super urgent as it can always be handled using setup phases/moves instead but I wanted to mention it in the other thread in case it was a consideration for other lobby changes.

nicolodavis commented 4 years ago

I see. It makes sense to have players bring in their own metadata if this choice of character class or some other game aspect happens outside the view that boardgame.io handles (for example in a custom lobby component).

On the other hand, it's easy enough to have players make the choice inside the game itself like you mentioned. Perhaps the difficulty that arises is the fact that these moves logically belong to a "pre-game" phase.

On a related note, we have toyed around with the idea of a "start game" event before (analogous to "start phase"). I wonder if that makes these use-cases more naturally implementable.

delucis commented 4 years ago

To be fair, what would such a start game event/phase do that a handmade setup phase couldn't? I think you're right that it's partly confusing because people think of these setup tasks as “pre-game”. Maybe it's just a case of some documentation of that pattern, e.g. a more advanced tutorial than the tic-tac-toe one that uses a setup phase and perhaps stages seeing as neither of those are covered by the introductory tutorial. It's simple enough to toggle a game view based on the current phase so even if a setup phase needs custom UI it's fairly straightforward to do.

(As a sidenote: In the fork of the lobby server I'm using, I did end up initialising game state after all players have joined so I could pull player nicknames from the metadata into the game state. I did that for logging messages that should say things like “Nickname did X”. In theory, I could use some kind of template substitution to store the ID in game logic and replace it with the nickname from metadata on client, but this made my life easier at the time.)

delucis commented 4 years ago

One other option would be to have setupData arguments accepted by both the create and join endpoints. The server could create a game (without yet initialising state) with some data that impacts what players can then choose in the lobby when joining, then the join action accepts player-scoped data. Once the final player joins, the game would be initialised with the setupData and the player-specific data.

An example use case:

  1. You can create a game with different character sets

  2. You can join the game choosing from the selected character set

nicolodavis commented 4 years ago

I'll spend more time digging into the lobby implementation as I try to convert it to Svelte, so I'll have a more informed opinion after that. My intuition tells me that choices that the players make on the lobby screen should be handled outside boardgame.io, with the game given the eventual setupData when it is finally instantiated after all players have joined.

Choices made once the game has started (using a "pre-game" phase) can be handled by boardgame.io quite easy without any changes. It just requires documenting the pattern like you say.