Repository: boardgameio/boardgame.io (ts)

Issue:
Boards/bots not in sync after update to v0.38.0
The following (using react client TicTacToe example with multilplayer local bots) shows two boards syncing every turn from player + bot in v0.37.2. Perfect! 

```
...
const GameBoardClient = Client({
  game: TicTacToe,
  multiplayer: Local(
    { bots: { "1" : RandomBot } }
  ),
  board: Board,
});
...
```

```
...
<GameBoardClient playerID="0" matchID="match1" />  
<GameBoardClient playerID="1" matchID="match1" />
...
```


Starting with v0.38.0 this is completely broken. Turns not synced to the other board + bot does not to change currentPlayer so that gameplay is broken. Ive seen other issues related to changing currentPlayer manually because this does not work by default - but it worked using v0.37.2. I tested multiple versions up to 40.0 the issue is definitely between v0.37.2...v0.38.0.

Additionally: The bot worked with only one board in v0.37.2 also because it changes the current player automatically like a bot should...

The issue must be here:
https://github.com/boardgameio/boardgame.io/compare/v0.37.2...v0.38.0

Pull request description:
fix(local): Use shared local master with bots
This returns a shared local master from the Local transport even when `bots` is passed in the options to `Local`. #553 changed this to avoid  out-of-date bots persisting, but in the process broke scenarios where  several clients should remain in sync on a single page (see #800). This  implements a more careful check, returning a shared master as long as  the `bots` options passed are *equal* to each other (via simple object  equality).

The reliance on object identity could still represent a confusing  gotcha, so in the future it might be better to offer a more explicit  API for getting a new clean transport. Moving the global map of master  instances *inside* the function scope of `Local`, could do this —  calling `Local` would always return a new transport layer that could  be shared around — but would potentially break current usage where  repeatedly calling `Local` would still connect to the same master.

Fix #800

Patch:
diff --git a/src/client/transport/local.ts b/src/client/transport/local.ts
index 0f2b6b5..ffea3fc 100644
--- a/src/client/transport/local.ts
+++ b/src/client/transport/local.ts
@@ -43,7 +43,7 @@ export function GetBotPlayer(state: State, bots: Record<PlayerID, any>) {
 
 interface LocalMasterOpts {
   game: Game;
-  bots: Record<PlayerID, any>;
+  bots?: Record<PlayerID, any>;
 }
 
 /**
@@ -234,19 +234,32 @@ export class LocalTransport extends Transport {
   }
 }
 
-const localMasters = new Map();
-export function Local(opts?: Pick<LocalMasterOpts, 'bots'>) {
+/**
+ * Global map storing local master instances.
+ */
+const localMasters: Map<
+  Game,
+  { master: LocalMaster; bots: LocalMasterOpts['bots'] }
+> = new Map();
+
+/**
+ * Create a local transport.
+ */
+export function Local({ bots }: Pick<LocalMasterOpts, 'bots'> = {}) {
   return (transportOpts: TransportOpts) => {
+    const { gameKey, game } = transportOpts;
     let master: LocalMaster;
 
-    if (localMasters.has(transportOpts.gameKey) && !opts) {
-      master = localMasters.get(transportOpts.gameKey);
-    } else {
-      master = new LocalMaster({
-        game: transportOpts.game,
-        bots: opts && opts.bots,
-      });
-      localMasters.set(transportOpts.gameKey, master);
+    if (localMasters.has(gameKey)) {
+      const instance = localMasters.get(gameKey);
+      if (instance.bots === bots) {
+        master = instance.master;
+      }
+    }
+
+    if (!master) {
+      master = new LocalMaster({ game, bots });
+      localMasters.set(gameKey, { master, bots });
     }
 
     return new LocalTransport({ master, ...transportOpts });


Test patch:
diff --git a/src/client/transport/local.test.js b/src/client/transport/local.test.js
index 54b1846..1fa0ea2 100644
--- a/src/client/transport/local.test.js
+++ b/src/client/transport/local.test.js
@@ -10,6 +10,7 @@ import { createStore } from 'redux';
 import { LocalTransport, LocalMaster, Local, GetBotPlayer } from './local';
 import { makeMove, gameEvent } from '../../core/action-creators';
 import { CreateGameReducer } from '../../core/reducer';
+import { ProcessGameConfig } from '../../core/game';
 import { InitializeGame } from '../../core/initialize';
 import { Client } from '../client';
 import { RandomBot } from '../../ai/random-bot';
@@ -121,6 +122,33 @@ describe('GetBotPlayer', () => {
   });
 });
 
+describe('Local', () => {
+  test('transports for same game use shared master', () => {
+    const gameKey = {};
+    const game = ProcessGameConfig(gameKey);
+    const transport1 = Local()({ game, gameKey });
+    const transport2 = Local()({ game, gameKey });
+    expect(transport1.master).toBe(transport2.master);
+  });
+
+  test('transports use shared master with bots', () => {
+    const gameKey = {};
+    const game = ProcessGameConfig(gameKey);
+    const bots = {};
+    const transport1 = Local({ bots })({ game, gameKey });
+    const transport2 = Local({ bots })({ game, gameKey });
+    expect(transport1.master).toBe(transport2.master);
+  });
+
+  test('transports use different master for different bots', () => {
+    const gameKey = {};
+    const game = ProcessGameConfig(gameKey);
+    const transport1 = Local({ bots: {} })({ game, gameKey });
+    const transport2 = Local({ bots: {} })({ game, gameKey });
+    expect(transport1.master).not.toBe(transport2.master);
+  });
+});
+
 describe('LocalMaster', () => {
   const game = {};
   const master = new LocalMaster({ game });