Skip to content

Commit ef8df65

Browse files
committed
add ability for Local multiplayer mode to run bots
1 parent b0a5175 commit ef8df65

9 files changed

Lines changed: 204 additions & 52 deletions

File tree

docs/documentation/tutorial.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,11 @@ const TicTacToe = {
266266
}
267267
```
268268

269-
After that, add an AI section to our `Client` call that returns a list
270-
of moves (one per empty cell).
269+
After that, add an `ai` section to the game config:
271270

272271
```js
273-
const App = Client({
274-
game: TicTacToe,
275-
board: TicTacToeBoard,
272+
const TicTacToe = {
273+
...
276274

277275
ai: {
278276
enumerate: (G, ctx) => {
@@ -285,9 +283,7 @@ const App = Client({
285283
return moves;
286284
},
287285
},
288-
});
289-
290-
export default App;
286+
};
291287
```
292288

293289
That's it! Now that you can visit the AI section of the Debug Panel:

examples/react-web/src/tic-tac-toe/game.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ const TicTacToe = {
6363
return { draw: true };
6464
}
6565
},
66+
67+
ai: {
68+
enumerate: G => {
69+
let r = [];
70+
for (let i = 0; i < 9; i++) {
71+
if (G.cells[i] === null) {
72+
r.push({ move: 'clickCell', args: [i] });
73+
}
74+
}
75+
return r;
76+
},
77+
},
6678
};
6779

6880
export default TicTacToe;

examples/react-web/src/tic-tac-toe/singleplayer.js

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,16 @@ import { Client } from 'boardgame.io/react';
1111
import { Debug } from 'boardgame.io/debug';
1212
import TicTacToe from './game';
1313
import Board from './board';
14+
import { Local } from 'boardgame.io/multiplayer';
15+
import { MCTSBot } from 'boardgame.io/ai';
1416

1517
const App = Client({
1618
game: TicTacToe,
1719
board: Board,
1820
debug: { impl: Debug },
19-
ai: {
20-
enumerate: G => {
21-
let r = [];
22-
for (let i = 0; i < 9; i++) {
23-
if (G.cells[i] === null) {
24-
r.push({ move: 'clickCell', args: [i] });
25-
}
26-
}
27-
return r;
28-
},
29-
},
21+
multiplayer: Local({
22+
bots: { '1': MCTSBot },
23+
}),
3024
});
3125

3226
const Singleplayer = () => (

src/ai/ai.test.js

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,27 +97,28 @@ describe('Step', () => {
9797
endIf(G) {
9898
if (G.moved) return true;
9999
},
100-
},
101100

102-
ai: {
103-
enumerate: () => [{ move: 'clickCell' }],
101+
ai: {
102+
enumerate: () => [{ move: 'clickCell' }],
103+
},
104104
},
105105
});
106106

107-
const bot = new RandomBot({ enumerate: client.ai.enumerate });
107+
const bot = new RandomBot({ enumerate: client.game.ai.enumerate });
108108
expect(client.getState().G).toEqual({ moved: false });
109109
await Step(client, bot);
110110
expect(client.getState().G).toEqual({ moved: true });
111111
});
112112

113113
test('does not crash on empty action', async () => {
114114
const client = Client({
115-
game: {},
116-
ai: {
117-
enumerate: () => [],
115+
game: {
116+
ai: {
117+
enumerate: () => [],
118+
},
118119
},
119120
});
120-
const bot = new RandomBot({ enumerate: client.ai.enumerate });
121+
const bot = new RandomBot({ enumerate: client.game.ai.enumerate });
121122
await Step(client, bot);
122123
});
123124

@@ -133,14 +134,14 @@ describe('Step', () => {
133134
turn: {
134135
activePlayers: { currentPlayer: 'stage' },
135136
},
136-
},
137137

138-
ai: {
139-
enumerate: () => [{ move: 'A' }],
138+
ai: {
139+
enumerate: () => [{ move: 'A' }],
140+
},
140141
},
141142
});
142143

143-
const bot = new RandomBot({ enumerate: client.ai.enumerate });
144+
const bot = new RandomBot({ enumerate: client.game.ai.enumerate });
144145
expect(client.getState().G).not.toEqual({ moved: true });
145146
await Step(client, bot);
146147
expect(client.getState().G).toEqual({ moved: true });

src/client/client.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ export const createMoveDispatchers = createDispatchers.bind(null, 'makeMove');
8080
class _ClientImpl {
8181
constructor({
8282
game,
83-
ai,
8483
debug,
8584
numPlayers,
8685
multiplayer,
@@ -90,7 +89,6 @@ class _ClientImpl {
9089
enhancer,
9190
}) {
9291
this.game = Game(game);
93-
this.ai = ai;
9492
this.playerID = playerID;
9593
this.gameID = gameID;
9694
this.credentials = credentials;

src/client/debug/ai/AI.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@
4040
}
4141
4242
let bot;
43-
if (client.ai) {
43+
if (client.game.ai) {
4444
bot = new MCTSBot({
4545
game: client.game,
46-
enumerate: client.ai.enumerate,
46+
enumerate: client.game.ai.enumerate,
4747
iterationCallback,
4848
});
4949
bot.setOpt('async', true);
@@ -56,7 +56,7 @@
5656
const botConstructor = bots[selectedBot];
5757
bot = new botConstructor({
5858
game: client.game,
59-
enumerate: client.ai.enumerate,
59+
enumerate: client.game.ai.enumerate,
6060
iterationCallback,
6161
});
6262
bot.setOpt('async', true);

src/client/react.js

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,7 @@ import { Client as RawClient } from './client';
3030
* UNDO and REDO.
3131
*/
3232
export function Client(opts) {
33-
let {
34-
game,
35-
numPlayers,
36-
loading,
37-
board,
38-
multiplayer,
39-
ai,
40-
enhancer,
41-
debug,
42-
} = opts;
33+
let { game, numPlayers, loading, board, multiplayer, enhancer, debug } = opts;
4334

4435
// Component that is displayed before the client has synced
4536
// with the game master.
@@ -85,7 +76,6 @@ export function Client(opts) {
8576

8677
this.client = RawClient({
8778
game,
88-
ai,
8979
debug,
9080
numPlayers,
9181
multiplayer,

src/client/transport/local.js

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ export function LocalMaster(game) {
4141
return master;
4242
}
4343

44+
/**
45+
* Returns null if it is not a bot's turn.
46+
* Otherwise, returns a playerID of a bot that may play now.
47+
*/
48+
export function GetBotPlayer(state, bots) {
49+
if (state.ctx.stage) {
50+
for (const key of Object.keys(bots)) {
51+
if (key in state.ctx.stage) {
52+
return key;
53+
}
54+
}
55+
} else if (state.ctx.currentPlayer in bots) {
56+
return state.ctx.currentPlayer;
57+
}
58+
59+
return null;
60+
}
61+
4462
/**
4563
* Local
4664
*
@@ -58,27 +76,70 @@ export class LocalTransport extends Transport {
5876
* @param {string} numPlayers - The number of players.
5977
* @param {string} server - The game server in the form of 'hostname:port'. Defaults to the server serving the client if not provided.
6078
*/
61-
constructor({ master, store, gameID, playerID, gameName, numPlayers }) {
79+
constructor({
80+
master,
81+
bots,
82+
game,
83+
store,
84+
gameID,
85+
playerID,
86+
gameName,
87+
numPlayers,
88+
}) {
6289
super({ store, gameName, playerID, gameID, numPlayers });
6390

6491
this.master = master;
92+
this.game = game;
6593
this.isConnected = true;
94+
95+
if (game && game.ai && bots) {
96+
this.bots = {};
97+
98+
for (const playerID in bots) {
99+
const bot = bots[playerID];
100+
this.bots[playerID] = new bot({
101+
game,
102+
enumerate: game.ai.enumerate,
103+
seed: game.seed,
104+
});
105+
}
106+
}
66107
}
67108

68109
/**
69110
* Called when another player makes a move and the
70111
* master broadcasts the update to other clients (including
71112
* this one).
72113
*/
73-
onUpdate(gameID, state, deltalog) {
114+
async onUpdate(gameID, state, deltalog) {
74115
const currentState = this.store.getState();
75116

76117
if (gameID == this.gameID && state._stateID >= currentState._stateID) {
77118
const action = ActionCreators.update(state, deltalog);
78119
this.store.dispatch(action);
120+
121+
if (this.bots) {
122+
const newState = this.store.getState();
123+
const botPlayer = GetBotPlayer(newState, this.bots);
124+
if (botPlayer !== null) {
125+
setTimeout(async () => {
126+
await this.makeBotMove(newState, botPlayer);
127+
}, 100);
128+
}
129+
}
79130
}
80131
}
81132

133+
async makeBotMove(state, playerID) {
134+
const botAction = await this.bots[playerID].play(state, playerID);
135+
await this.master.onUpdate(
136+
botAction.action,
137+
state._stateID,
138+
this.gameID,
139+
botAction.action.payload.playerID
140+
);
141+
}
142+
82143
/**
83144
* Called when the client first connects to the master
84145
* and requests the current game state.
@@ -149,7 +210,7 @@ export class LocalTransport extends Transport {
149210
}
150211

151212
const localMasters = new Map();
152-
export function Local() {
213+
export function Local(opts) {
153214
return transportOpts => {
154215
let master;
155216

@@ -160,6 +221,10 @@ export function Local() {
160221
localMasters.set(transportOpts.gameKey, master);
161222
}
162223

163-
return new LocalTransport({ master, ...transportOpts });
224+
return new LocalTransport({
225+
master,
226+
bots: opts && opts.bots,
227+
...transportOpts,
228+
});
164229
};
165230
}

0 commit comments

Comments
 (0)