Skip to content

Commit

Permalink
AI framework
Browse files Browse the repository at this point in the history
- add basic MCTS bot
- update tutorial to demonstrate the API
  • Loading branch information
darthfiddler committed Jun 4, 2018
1 parent 40cd4b8 commit dda540a
Show file tree
Hide file tree
Showing 27 changed files with 1,604 additions and 339 deletions.
2 changes: 1 addition & 1 deletion docs/react/boardgameio.min.js

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions docs/react/example-2.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@
flow: {
endGameIf: (G, ctx) => {
if (IsVictory(G.cells)) {
return ctx.currentPlayer;
return { winner: ctx.currentPlayer };
}
if (G.cells.filter(c => c === null).length == 0) {
return { draw: true };
}
}
}
Expand All @@ -88,8 +91,10 @@

render() {
let winner = '';
if (this.props.ctx.gameover !== undefined) {
winner = <div>Winner: {this.props.ctx.gameover}</div>;
if (this.props.ctx.gameover) {
winner = this.props.ctx.gameover.winner !== undefined ?
<div id="winner">Winner: {this.props.ctx.gameover.winner}</div> :
<div id="winner">Draw!</div>;
}

const cellStyle = {
Expand Down
167 changes: 167 additions & 0 deletions docs/react/example-3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<!DOCTYPE html>
<html>

<head>
<style>
body {
padding: 20px;
}
.msg {
position: absolute;
bottom: 0;
left: 20px;
color: #aaa;
font-size: 12px;
margin-bottom: 20px;
}
</style>
</head>

<body>
<div class="msg">interactive (not an image)</div>
<div id="app"></div>

<script type="text/babel">
function IsVictory(cells) {
const positions = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];

for (let pos of positions) {
const symbol = cells[pos[0]];
let winner = symbol;
for (let i of pos) {
if (cells[i] != symbol) {
winner = null;
break;
}
}
if (winner != null) return true;
}

return false;
}

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

moves: {
clickCell(G, ctx, id) {
const cells = [...G.cells];

if (cells[id] === null) {
cells[id] = ctx.currentPlayer;
}

return { ...G, cells };
}
},

flow: {
movesPerTurn: 1,

endGameIf: (G, ctx) => {
if (IsVictory(G.cells)) {
return { winner: ctx.currentPlayer };
}
if (G.cells.filter(c => c === null).length == 0) {
return { draw: true };
}
}
}
});


class TicTacToeBoard extends React.Component {
onClick(id) {
if (this.isActive(id)) {
this.props.moves.clickCell(id);
this.props.events.endTurn();
}
}

isActive(id) {
return this.props.isActive && this.props.G.cells[id] == null;
}

render() {
let winner = '';
if (this.props.ctx.gameover) {
winner = this.props.ctx.gameover.winner !== undefined ?
<div id="winner">Winner: {this.props.ctx.gameover.winner}</div> :
<div id="winner">Draw!</div>;
}

const cellStyle = {
cursor: 'pointer',
border: '1px solid #555',
width: '50px',
height: '50px',
lineHeight: '50px',
textAlign: 'center',
fontFamily: 'monospace',
fontSize: '20px',
fontWeight: 'bold',
};

let tbody = [];
for (let i = 0; i < 3; i++) {
let cells = [];
for (let j = 0; j < 3; j++) {
const id = 3 * i + j;
cells.push(
<td style={cellStyle}
key={id}
onClick={() => this.onClick(id)}>
{this.props.G.cells[id]}
</td>
);
}
tbody.push(<tr key={i}>{cells}</tr>);
}

return (
<div>
<table id="board">
<tbody>{tbody}</tbody>
</table>
{winner}
</div>
);
}
}

var App = BoardgameIO.ReactClient({
board: TicTacToeBoard,
game: TicTacToe,
ai: BoardgameIO.AI({
enumerate: (G, ctx) => {
let moves = [];
for (let i = 0; i < 9; i++) {
if (G.cells[i] === null) {
moves.push({ move: 'clickCell', args: [i] });
}
}
return moves;
}
})
});


ReactDOM.render(<App/>, document.getElementById('app'));
</script>

<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="//unpkg.com/react@next/umd/react.development.js"></script>
<script src="//unpkg.com/react-dom@next/umd/react-dom.development.js"></script>
<script src="boardgameio.min.js"></script>
</body>

</html>
11 changes: 6 additions & 5 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
This is a living document capturing the current areas of focus, and what needs to
get done before we are ready for a v1 release.

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

### AI framework

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

* ##### Firebase / Other

* [ ] add support for one more backend (Firebase?) **[help needed]**

### Clients

* ##### Vue

* [ ] add basic Vue support **[help needed]**
* [ ] debug UI implementation in Vue **[help needed]**
* [ ] add basic Vue support **[help needed]**
* [ ] debug UI implementation in Vue **[help needed]**

### Core

Expand Down
108 changes: 98 additions & 10 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ npm start
```

Notice that we have a fully playable game that we can
interact with via the Debug UI with just
this little piece of code!
interact with via the Debug UI with just this little piece of code!

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

```js
// Return true if `cells` is in a winning configuration.
function IsVictory(cells) {
// Return true if `cells` is in a winning configuration.
...
}

// Return true if all `cells` are occupied.
function IsDraw(cells) {
return G.cells.filter(c => c === null).length == 0;
}

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

moves: {
clickCell(G, ctx, id) {
const cells = [...G.cells];
const cells = [ ...G.cells ];

// Ensure we can't overwrite cells.
if (cells[id] === null) {
Expand All @@ -123,16 +128,19 @@ const TicTacToe = Game({
flow: {
endGameIf: (G, ctx) => {
if (IsVictory(G.cells)) {
return ctx.currentPlayer;
return { winner: ctx.currentPlayer };
}
if (IsDraw(G.cells)) {
return { draw: true };
}
},
},
});
```

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

## Render Board

Expand Down Expand Up @@ -163,8 +171,13 @@ class TicTacToeBoard extends React.Component {

render() {
let winner = '';
if (this.props.ctx.gameover !== null) {
winner = <div>Winner: {this.props.ctx.gameover}</div>;
if (this.props.ctx.gameover) {
winner =
this.props.ctx.gameover.winner !== undefined ? (
<div id="winner">Winner: {this.props.ctx.gameover.winner}</div>
) : (
<div id="winner">Draw!</div>
);
}

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

Editable version on CodePen: [link](https://codepen.io/nicolodavis/full/MEvrjq/)
!> You can press `1` (or click on the button next to `reset`) to reset the
state of the game and start over.

## Add AI

In this section we will show you how easy it is to add a bot that is
capable of playing your game. All you need to do is just tell the
bot how to find legal moves in the game, and it will do the rest.

We shall first modify our flow section by adding a useful option
called `movesPerTurn` to automatically end the turn after one
move has been made. That way, the bot doesn't have to worry about
issuing `endTurn` calls (which, while possible, make the game tree
a bit messier to search).

```js
flow: {
movesPerTurn: 1,
...
}
```

After that, add an AI section to our `Client` call that returns a list
of moves (one per empty cell).

```js
import { AI } from 'boardgame.io';

const App = Client({
game: TicTacToe,
board: TicTacToeBoard,

ai: AI({
enumerate: (G, ctx) => {
let moves = [];
for (let i = 0; i < 9; i++) {
if (G.cells[i] === null) {
moves.push({ move: 'clickCell', args: [i] });
}
}
return moves;
},
}),
});

export default App;
```

That's it! You will notice that you now have two more options in
the **Controls** section (`step` and `simulate`). You can use the
keyboard shortcuts `4` and `5` to trigger them.

Press `5` and just watch your game play by itself!

You can also use a combination of moves that you make yourself
and bot moves (press `4` to have the bot make a move). You can make
some manual moves to get two in a row and then verify that
the bot makes a block, for example.

```react
<iframe class='react' src='react/example-3.html' height='850' scrolling='no' title='example' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'></iframe>
```

!> The bot uses [MCTS](http://www.baeldung.com/java-monte-carlo-tree-search) under the
hood to explore the game tree and find good moves.

The framework will come bundled with a few different bot algorithms, and an advanced
version of MCTS that will allow you to specify a set of objectives to optimize for.
For example, at any given point in the game you can tell the bot to gather resources
in the short term and wage wars in the late stages. You just tell the bot what to do
and it will figure out the right combination of moves to make it happen!

Detailed documentation about all this is coming soon. Adding bots to games for actual
play (as opposed to merely simulating moves) is also in the works.

?> Editable version of the code in this tutorial is available here: [CodePen](https://codepen.io/nicolodavis/full/MEvrjq/)
2 changes: 1 addition & 1 deletion examples/react/modules/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,5 @@ test('victory', () => {
expect(board.props.G).toEqual({
cells: ['0', '0', '0', '1', '1'].concat(Grid(4)),
});
expect(board.props.ctx.gameover).toEqual('0');
expect(board.props.ctx.gameover).toEqual({ winner: '0' });
});

0 comments on commit dda540a

Please sign in to comment.