Skip to content

Commit dda540a

Browse files
committed
AI framework
- add basic MCTS bot - update tutorial to demonstrate the API
1 parent 40cd4b8 commit dda540a

27 files changed

Lines changed: 1604 additions & 339 deletions

docs/react/boardgameio.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/react/example-2.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@
6767
flow: {
6868
endGameIf: (G, ctx) => {
6969
if (IsVictory(G.cells)) {
70-
return ctx.currentPlayer;
70+
return { winner: ctx.currentPlayer };
71+
}
72+
if (G.cells.filter(c => c === null).length == 0) {
73+
return { draw: true };
7174
}
7275
}
7376
}
@@ -88,8 +91,10 @@
8891

8992
render() {
9093
let winner = '';
91-
if (this.props.ctx.gameover !== undefined) {
92-
winner = <div>Winner: {this.props.ctx.gameover}</div>;
94+
if (this.props.ctx.gameover) {
95+
winner = this.props.ctx.gameover.winner !== undefined ?
96+
<div id="winner">Winner: {this.props.ctx.gameover.winner}</div> :
97+
<div id="winner">Draw!</div>;
9398
}
9499

95100
const cellStyle = {

docs/react/example-3.html

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<style>
6+
body {
7+
padding: 20px;
8+
}
9+
.msg {
10+
position: absolute;
11+
bottom: 0;
12+
left: 20px;
13+
color: #aaa;
14+
font-size: 12px;
15+
margin-bottom: 20px;
16+
}
17+
</style>
18+
</head>
19+
20+
<body>
21+
<div class="msg">interactive (not an image)</div>
22+
<div id="app"></div>
23+
24+
<script type="text/babel">
25+
function IsVictory(cells) {
26+
const positions = [
27+
[0, 1, 2],
28+
[3, 4, 5],
29+
[6, 7, 8],
30+
[0, 3, 6],
31+
[1, 4, 7],
32+
[2, 5, 8],
33+
[0, 4, 8],
34+
[2, 4, 6],
35+
];
36+
37+
for (let pos of positions) {
38+
const symbol = cells[pos[0]];
39+
let winner = symbol;
40+
for (let i of pos) {
41+
if (cells[i] != symbol) {
42+
winner = null;
43+
break;
44+
}
45+
}
46+
if (winner != null) return true;
47+
}
48+
49+
return false;
50+
}
51+
52+
const TicTacToe = BoardgameIO.Game({
53+
setup: () => ({ cells: Array(9).fill(null) }),
54+
55+
moves: {
56+
clickCell(G, ctx, id) {
57+
const cells = [...G.cells];
58+
59+
if (cells[id] === null) {
60+
cells[id] = ctx.currentPlayer;
61+
}
62+
63+
return { ...G, cells };
64+
}
65+
},
66+
67+
flow: {
68+
movesPerTurn: 1,
69+
70+
endGameIf: (G, ctx) => {
71+
if (IsVictory(G.cells)) {
72+
return { winner: ctx.currentPlayer };
73+
}
74+
if (G.cells.filter(c => c === null).length == 0) {
75+
return { draw: true };
76+
}
77+
}
78+
}
79+
});
80+
81+
82+
class TicTacToeBoard extends React.Component {
83+
onClick(id) {
84+
if (this.isActive(id)) {
85+
this.props.moves.clickCell(id);
86+
this.props.events.endTurn();
87+
}
88+
}
89+
90+
isActive(id) {
91+
return this.props.isActive && this.props.G.cells[id] == null;
92+
}
93+
94+
render() {
95+
let winner = '';
96+
if (this.props.ctx.gameover) {
97+
winner = this.props.ctx.gameover.winner !== undefined ?
98+
<div id="winner">Winner: {this.props.ctx.gameover.winner}</div> :
99+
<div id="winner">Draw!</div>;
100+
}
101+
102+
const cellStyle = {
103+
cursor: 'pointer',
104+
border: '1px solid #555',
105+
width: '50px',
106+
height: '50px',
107+
lineHeight: '50px',
108+
textAlign: 'center',
109+
fontFamily: 'monospace',
110+
fontSize: '20px',
111+
fontWeight: 'bold',
112+
};
113+
114+
let tbody = [];
115+
for (let i = 0; i < 3; i++) {
116+
let cells = [];
117+
for (let j = 0; j < 3; j++) {
118+
const id = 3 * i + j;
119+
cells.push(
120+
<td style={cellStyle}
121+
key={id}
122+
onClick={() => this.onClick(id)}>
123+
{this.props.G.cells[id]}
124+
</td>
125+
);
126+
}
127+
tbody.push(<tr key={i}>{cells}</tr>);
128+
}
129+
130+
return (
131+
<div>
132+
<table id="board">
133+
<tbody>{tbody}</tbody>
134+
</table>
135+
{winner}
136+
</div>
137+
);
138+
}
139+
}
140+
141+
var App = BoardgameIO.ReactClient({
142+
board: TicTacToeBoard,
143+
game: TicTacToe,
144+
ai: BoardgameIO.AI({
145+
enumerate: (G, ctx) => {
146+
let moves = [];
147+
for (let i = 0; i < 9; i++) {
148+
if (G.cells[i] === null) {
149+
moves.push({ move: 'clickCell', args: [i] });
150+
}
151+
}
152+
return moves;
153+
}
154+
})
155+
});
156+
157+
158+
ReactDOM.render(<App/>, document.getElementById('app'));
159+
</script>
160+
161+
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
162+
<script src="//unpkg.com/react@next/umd/react.development.js"></script>
163+
<script src="//unpkg.com/react-dom@next/umd/react-dom.development.js"></script>
164+
<script src="boardgameio.min.js"></script>
165+
</body>
166+
167+
</html>

docs/roadmap.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
This is a living document capturing the current areas of focus, and what needs to
44
get done before we are ready for a v1 release.
55

6-
* *Areas that need help are marked with **[help needed]**.*
7-
* *Stuff that [nicolodavis@](https://github.com/nicolodavis) is working on is marked with **[N]**.*
6+
* _Areas that need help are marked with **[help needed]**._
7+
* _Stuff that [nicolodavis@](https://github.com/nicolodavis) is working on is marked with **[N]**._
88

99
### AI framework
1010

@@ -35,14 +35,15 @@ get done before we are ready for a v1 release.
3535
* [ ] add error handling **[help needed]**
3636

3737
* ##### Firebase / Other
38+
3839
* [ ] add support for one more backend (Firebase?) **[help needed]**
39-
40+
4041
### Clients
4142

4243
* ##### Vue
4344

44-
* [ ] add basic Vue support **[help needed]**
45-
* [ ] debug UI implementation in Vue **[help needed]**
45+
* [ ] add basic Vue support **[help needed]**
46+
* [ ] debug UI implementation in Vue **[help needed]**
4647

4748
### Core
4849

docs/tutorial.md

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ npm start
7373
```
7474

7575
Notice that we have a fully playable game that we can
76-
interact with via the Debug UI with just
77-
this little piece of code!
76+
interact with via the Debug UI with just this little piece of code!
7877

7978
?> You can make a move by clicking on `clickCell` on the
8079
Debug UI (or pressing the keyboard shortcut `c`),
@@ -100,16 +99,22 @@ In order to do this, we add a `flow` section to control the
10099
condition to it.
101100

102101
```js
102+
// Return true if `cells` is in a winning configuration.
103103
function IsVictory(cells) {
104-
// Return true if `cells` is in a winning configuration.
104+
...
105+
}
106+
107+
// Return true if all `cells` are occupied.
108+
function IsDraw(cells) {
109+
return G.cells.filter(c => c === null).length == 0;
105110
}
106111

107112
const TicTacToe = Game({
108113
setup: () => ({ cells: Array(9).fill(null) }),
109114

110115
moves: {
111116
clickCell(G, ctx, id) {
112-
const cells = [...G.cells];
117+
const cells = [ ...G.cells ];
113118

114119
// Ensure we can't overwrite cells.
115120
if (cells[id] === null) {
@@ -123,16 +128,19 @@ const TicTacToe = Game({
123128
flow: {
124129
endGameIf: (G, ctx) => {
125130
if (IsVictory(G.cells)) {
126-
return ctx.currentPlayer;
131+
return { winner: ctx.currentPlayer };
132+
}
133+
if (IsDraw(G.cells)) {
134+
return { draw: true };
127135
}
128136
},
129137
},
130138
});
131139
```
132140

133141
!> The `endGameIf` field takes a function that determines if
134-
the game is over. If it returns anything other than `undefined`,
135-
the game ends, and the return value is available at `ctx.gameover`.
142+
the game is over. If it returns anything at all, the game ends and
143+
the return value is available at `ctx.gameover`.
136144

137145
## Render Board
138146

@@ -163,8 +171,13 @@ class TicTacToeBoard extends React.Component {
163171

164172
render() {
165173
let winner = '';
166-
if (this.props.ctx.gameover !== null) {
167-
winner = <div>Winner: {this.props.ctx.gameover}</div>;
174+
if (this.props.ctx.gameover) {
175+
winner =
176+
this.props.ctx.gameover.winner !== undefined ? (
177+
<div id="winner">Winner: {this.props.ctx.gameover.winner}</div>
178+
) : (
179+
<div id="winner">Draw!</div>
180+
);
168181
}
169182

170183
const cellStyle = {
@@ -240,4 +253,79 @@ And there you have it. A basic tic-tac-toe game!
240253
<iframe class='react' src='react/example-2.html' height='850' scrolling='no' title='example' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'></iframe>
241254
```
242255

243-
Editable version on CodePen: [link](https://codepen.io/nicolodavis/full/MEvrjq/)
256+
!> You can press `1` (or click on the button next to `reset`) to reset the
257+
state of the game and start over.
258+
259+
## Add AI
260+
261+
In this section we will show you how easy it is to add a bot that is
262+
capable of playing your game. All you need to do is just tell the
263+
bot how to find legal moves in the game, and it will do the rest.
264+
265+
We shall first modify our flow section by adding a useful option
266+
called `movesPerTurn` to automatically end the turn after one
267+
move has been made. That way, the bot doesn't have to worry about
268+
issuing `endTurn` calls (which, while possible, make the game tree
269+
a bit messier to search).
270+
271+
```js
272+
flow: {
273+
movesPerTurn: 1,
274+
...
275+
}
276+
```
277+
278+
After that, add an AI section to our `Client` call that returns a list
279+
of moves (one per empty cell).
280+
281+
```js
282+
import { AI } from 'boardgame.io';
283+
284+
const App = Client({
285+
game: TicTacToe,
286+
board: TicTacToeBoard,
287+
288+
ai: AI({
289+
enumerate: (G, ctx) => {
290+
let moves = [];
291+
for (let i = 0; i < 9; i++) {
292+
if (G.cells[i] === null) {
293+
moves.push({ move: 'clickCell', args: [i] });
294+
}
295+
}
296+
return moves;
297+
},
298+
}),
299+
});
300+
301+
export default App;
302+
```
303+
304+
That's it! You will notice that you now have two more options in
305+
the **Controls** section (`step` and `simulate`). You can use the
306+
keyboard shortcuts `4` and `5` to trigger them.
307+
308+
Press `5` and just watch your game play by itself!
309+
310+
You can also use a combination of moves that you make yourself
311+
and bot moves (press `4` to have the bot make a move). You can make
312+
some manual moves to get two in a row and then verify that
313+
the bot makes a block, for example.
314+
315+
```react
316+
<iframe class='react' src='react/example-3.html' height='850' scrolling='no' title='example' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'></iframe>
317+
```
318+
319+
!> The bot uses [MCTS](http://www.baeldung.com/java-monte-carlo-tree-search) under the
320+
hood to explore the game tree and find good moves.
321+
322+
The framework will come bundled with a few different bot algorithms, and an advanced
323+
version of MCTS that will allow you to specify a set of objectives to optimize for.
324+
For example, at any given point in the game you can tell the bot to gather resources
325+
in the short term and wage wars in the late stages. You just tell the bot what to do
326+
and it will figure out the right combination of moves to make it happen!
327+
328+
Detailed documentation about all this is coming soon. Adding bots to games for actual
329+
play (as opposed to merely simulating moves) is also in the works.
330+
331+
?> Editable version of the code in this tutorial is available here: [CodePen](https://codepen.io/nicolodavis/full/MEvrjq/)

examples/react/modules/app.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,5 @@ test('victory', () => {
8080
expect(board.props.G).toEqual({
8181
cells: ['0', '0', '0', '1', '1'].concat(Grid(4)),
8282
});
83-
expect(board.props.ctx.gameover).toEqual('0');
83+
expect(board.props.ctx.gameover).toEqual({ winner: '0' });
8484
});

0 commit comments

Comments
 (0)