boardgameio / boardgame.io

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

Game Creation / Lobby API #171

Closed scally closed 6 years ago

scally commented 6 years ago

I've been trying to debug an issue with a multiplayer game this weekend, and encountered the following misunderstanding:

When you provide a playerID to the server today, it must be a 0-indexed number corresponding to the number of players -- [0, 1] for 2 players, for instance, is the only possible space. This appears to be due to this: https://github.com/google/boardgame.io/blob/bb9ff3b6dd6c9f8ea5314d3712d2741b5d8992bc/src/core/flow.js#L563

So, if you provide a playerID outside of the range [0,1], no player can make a move.

I can imagine a few possible solutions:

Otherwise, in a true non-demo multiplayer environment, how would you know which player is 0 and which is 1 without some kind of server-side arbitration?

Stefan-Hanke commented 6 years ago

The framework needs to assume something about players in order to allow working with them. I didn't find anything in multiplayer.md, so I'd imagine we could add a parapraph about this.

There is currently no notion of an game instance id that comes from the server, associating players to a specific running game. Instead, the client is assumed to provide a gameID which fulfills this purpose. There is a paragraph about that in the aforementioned docs. In true non-demo, the client currently is assumed to generate a unique gameID for each separate game instance.

Take this with a grain of salt, I didn't experiment with the code to get this knowledge, I've only read it.

nicolodavis commented 6 years ago

I'll make the documentation clearer about this, but all the playerID/gameID management is currently the responsibility of the user. I'd like to eventually ship a lobby implementation that will do all this for you, but we aren't there yet.

playerID's are '0', '1', etc. as you rightly point out, and deliberately so. Figuring out how to map your notion of a user to these ID's is entirely up to you.

scally commented 6 years ago

@nicolodavis Your response makes sense, but what I'm still a bit lost on is how else to secure the playerID. If it's always deterministic, isn't it pretty easily spoofable?

scally commented 6 years ago

That is, you wouldn't send a session ID to a server with an ID of "1", because it would be super easy to guess the other player IDs and pretend to be them. Without some way of the server identifying you by session, how would you prevent similar tampering?

nicolodavis commented 6 years ago

Yes, it is spoofable at the moment.

100 was one attempt to add some authentication logic. If you have some ideas along this line, we could probably pick this up again.

nicolodavis commented 6 years ago

We need a few things probably:

scally commented 6 years ago

@nicolodavis thanks for hearing me out. 😄 I'll think on this a bit and see if I can sketch something towards it this week.

scally commented 6 years ago

So, I've thought on this a bit.

If the "create game" bits create player secrets, that could work for encryption of one game session.

What I'm then wondering about is if you wanted some kind of persistence of the concept of a player -- to track wins, or leaderboards, or scores -- some kind of persistent state between games. If the game creation steps create a player secret, and it's tied to that individual session, then there's not really a way for someone to be permanently tied to any kind of persisting identity. You could make that player secret something the client holds onto between games, at least, although there's no way to ever switch devices/browsers.

For some multiplayer games, anonymous matchmaking with no persistence might be enough, but there's going to be a lot of use cases where you'd want to let a user's actions carry over past the end of a game session.

The conclusion I come to after these thoughts is that you'd want something sort of like oauth on the server side -- the client would auth through some kind of identity provider and the server would be responsible for reading the secret and then tying that identity to some persistent server-side concept.

That way, the server doesn't have to have a login/password store that would have to be secured, it would just delegate auth to somewhere else, and just care that it has a token to prove you're user XYZ.

Implementation would look like some new actions for logging in, and then an area within the Game object to store Sessions/Lobbies/Leaderboards, with data stripped of anything not pertaining to the current session for the player on the client side, similar to the existing secret system boardgame.io already supports. Action payloads would have to contain the token, which the server would check with the auth provider.

You would still need some kind of zero-config option here for anonymous sessions to preserve the simplicity for people who don't need authentication for players.

@nicolodavis can you think of an easier way to have persistent user identity in this system besides something similar to what I'm describing?

nicolodavis commented 6 years ago

@scally My post above describes how to prevent spoofing, i.e. clients that pretend that they're part of a particular game and execute moves.

What you're describing is a reasonable implementation for a lobby / leaderboard. boardgame.io is designed so that you can integrate it into your own user data without imposing a structure to that data. You can implement your own user authentication system (or use oauth) and then store all the user related data in your own database. boardgame.io just takes over when a game is actually played.

For example, you can use your own auth scheme to determine that a player called Alice has logged in to your website. boardgame.io doesn't even need to know about Alice. Your website implementation then does the following (as part of your lobby implementation):

gameID: "game-0",
numPlayers: 2,
players: {
  "0": { secret: "secret-0" },
  "1": { secret: "secret-1" },
}
gameID: "game-0",
numPlayers: 2,
players: {
  "0": { secret: "secret-0", name: "Alice" },
  "1": { secret: "secret-1", name: "Bob" },
}

The names are just for reference (so we can display them). They're not used for authentication. The framework will then accept moves for Player 0 if the player supplies "secret-0" during play (only Alice can do this).

We can of course provide a bundled lobby implementation that does all of this for users that want true zero config (I would be really excited if someone wants to work on this), but we should make sure that the underlying API is de-coupled enough from it so that others can integrate it with their existing lobby implementations easily.

scally commented 6 years ago

@nicolodavis Is it fair to say then that there's two issues/PR's here? One for game join/creation logic and another for lobbying/persistence?

scally commented 6 years ago

Hmm, I might be misunderstanding this separation a bit. Let me look over your answer some more.

nicolodavis commented 6 years ago

Yes, there are two components which would fit naturally into two PRs.

nicolodavis commented 6 years ago

I think the lobby that we could bundle in should be fairly straightforward, though. No real persistence. Just have the user fill in their username (no password) and have some UI to create games and allow others to join. This will create a good base for people that want to add OAuth or something else on top of that.

scally commented 6 years ago

@nicolodavis I have a super sketchy PR for this to see if this is sort of on the right track for what you were thinking. It creates routes for the two named API actions on a separate Koa app, and then has some non-working code that documents the approach. Please let me know if this is the right direction or not.

https://github.com/google/boardgame.io/pull/189

nicolodavis commented 6 years ago

Some thoughts after looking through your PR (sorry about the delay in getting to this):

There are going to be two distinct "databases" (in reality they will most likely be two tables within one database, but I just want to make the conceptual difference stand out).

DB 1

This holds the game data. This is what we already have today. We even have an API (db.set and db.get) that allows any database backend to be connected in order to persist games. There is one entry in this database for every game that is currently in progress. Since we don't currently have a create game API at the moment, the server just creates a game on demand during the sync here. This is exactly what we need to pull out and facilitate with a proper lobby API instead. In other words, your post handler should have a db.set that adds an entry to this database on create game.

DB 2

This contains lobby metadata. This is what we would use to hold current open invitations to games, for example. It could also hold user data if we want to authenticate users too.

I'll add more comments directly on the PR.

scally commented 6 years ago

@nicolodavis Is your expectation right now that "joining" a game is just adding a named label to an ordinal player? Does it have some other meaning?

nicolodavis commented 6 years ago

That and distributing the secret to the player as well.

scally commented 6 years ago

@nicolodavis If the secret is generated on the lobby system (elsewhere, outside), wouldn't that system - that also authenticated the player, be responsible for giving it to them?

nicolodavis commented 6 years ago

The lobby API we're building (create game and join game) should do everything we expect as far as these operations are concerned. I'm hoping that a lobby implementation will just use these two calls under the hood. Which means that if we don't do all the secret generation here, we're pushing the burden onto the lobby implementation, which should only be concerned with logging in users and such. Note that the secrets we're talking about are different. If the lobby implementation is going to do some user authentication, it will use some secret (a password) to allow the user to login. The secret I'm referring to is generated on demand when a user joins a game. This is separate from the secret used to login. Think of it more like a token that enables the user to play. In fact, we could allow for it to be invalidated in the future (to support kicking players out of games)., so it would be good to keep it decoupled from the secret used to log in the user.

scally commented 6 years ago

@nicolodavis summarizing my understanding:

create_game -> creates a game & a boardgame.io player secret for each player in the game. join_game -> associates the player to a name, returns the boardgame.io player secret in its response

Is this accurate?

nicolodavis commented 6 years ago

Yep!

scally commented 6 years ago

@nicolodavis Still working on this. Were you thinking the secrets would be stored to db2 in your example?

nicolodavis commented 6 years ago

Yeah, that sounds good. Right now, this is what our DB looks like:

gameid-1: {
  G: ...
  ctx: ...
}

gameid-2: {
  G: ...
  ctx: ...
}

...

We could add the game metadata in a parallel key space like this:

gameid-1: {
  players: {
    '0': { secret: '' }
    ...
  }
}

gameid-2: {
  players: {
    '0': { secret: '' }
    ...
  }
}

...

One way to achieve the separation of the key space would be to do something like this:

db.set('gameid', gameState);
db.set('gameid:metadata', metadata);

We could in theory also change our DB API to support the concept of a table, so we can do:

db.set({
  table: 'metadata',
  key: 'gameid',
  value: metadata
});

and depending on whether the implementation under the hood supports tables or not, it could either use our key augmentation trick or actually write to a separate table if that's a possibility.

scally commented 6 years ago

@nicolodavis thanks. I used the gameid:metadata version for now, we can upgrade to something more like the table-based version later perhaps? I updated my PR, could you please take a look and see if you feel that it's going in a good direction? 😄

nicolodavis commented 6 years ago

I'll close this now that we have whatever we discussed on this thread in. I'll open another issue to track future work.