diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 000000000..77d7e8001 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,43 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: './docs' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/CNAME b/docs/CNAME index aa51e1bbf..b3cf502dc 100644 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -boardgame.io \ No newline at end of file +bg.oc.is \ No newline at end of file diff --git a/docs/documentation/api/Context.md b/docs/documentation/api/Context.md new file mode 100644 index 000000000..8c4300ea5 --- /dev/null +++ b/docs/documentation/api/Context.md @@ -0,0 +1,105 @@ +# Context + +Here are some examples on what the `ctx` Context parameter contains: + +## Simple Beginning Game + +This would be the context of a simple two player game without phases or stages that has just started: + +```js +{ + /** Number of players in the game */ + numPlayers: 2, + /** List of player IDs in play order */ + playOrder: ["A", "B"], + /** Current position in the play order */ + playOrderPos: 0, + /** + * If there are stages, this is an Object containing all currently active players with their playerIds as fields and the stage they are in as content. + * + * Will be `null` if there are no stages. + * */ + activePlayers: null, + /** playerID of the player that is acting right now */ + currentPlayer: "A", + /** Number of moves that have been taken in the game already */ + numMoves: 0, + /** Turn number of the current turn */ + turn: 0, + /** + * Contains the return value of the `endIf` function in the `Game` object if the game is over. + * + * Is empty if the game is not over. + */ + phase: null, +} +``` + +## Simple In Progress Game + +This would be the context of the same game after it has just ended + +```js +{ + /** Number of players in the game */ + numPlayers: 2, + /** List of player IDs in play order */ + playOrder: ["A", "B"], + /** Current position in the play order */ + playOrderPos: 1, + /** + * If there are stages, this is an Object containing all currently active players with their playerIds as fields and the stage they are in as content. + * + * Will be `null` if there are no stages. + * */ + activePlayers: null, + /** playerID of the player that is acting right now */ + currentPlayer: "B", + /** Number of moves that have been taken in the game already */ + numMoves: 6, + /** Turn number of the current turn */ + turn: 6, + /** Active phase of the game or `null` if there are no phases */ + phase: null, + /** + * Contains the return value of the `endIf` function in the `Game` object if the game is over. + * + * Is empty if the game is not over. + */ + gameOver: { + winner: "B", + }, +} +``` + +## Game with Phases and Stages + +This would be the context of a more complex game with phases and stages: + +```js +{ + /** Number of players in the game */ + numPlayers: 4, + /** List of player IDs in play order */ + playOrder: ["A", "B", "C", "D"], + /** Current position in the play order */ + playOrderPos: 1, + /** + * If there are stages, this is an Object containing all currently active players with their playerIds as fields and the stage they are in as content. + * + * Will be `null` if there are no stages. + * */ + activePlayers: { + A: "Choose Opponent", + B: "Choose Opponent", + }, + /** playerID of the player that is acting right now */ + currentPlayer: "B", + /** Number of moves that have been taken in the game already */ + numMoves: 4, + /** Turn number of the current turn */ + turn: 2, + /** Active phase of the game or `null` if there are no phases */ + phase: "Building", +} +``` diff --git a/docs/documentation/events.md b/docs/documentation/events.md index 0cce8b6e5..3b0ac1969 100644 --- a/docs/documentation/events.md +++ b/docs/documentation/events.md @@ -51,9 +51,10 @@ This event ends the game. If you pass an argument to it, then that argument is made available in `ctx.gameover`. After the game is over, further state changes to the game (via a move or event) are not possible. +In order to enable the included Bot/AI logic, you must specify the winner in the `endGame` event. ```js -endGame(); +endGame({ winner: '2' }); ``` #### setStage diff --git a/docs/documentation/multiplayer.md b/docs/documentation/multiplayer.md index 7192be7b2..2466993d5 100644 --- a/docs/documentation/multiplayer.md +++ b/docs/documentation/multiplayer.md @@ -144,6 +144,8 @@ Local({ ## Remote Master +// TODO icorporate our server setup + You can also run the game master on a separate server. Any boardgame.io client can connect to this master (whether it is a browser, an Android app etc.) and it will be kept in sync with other clients in realtime. diff --git a/docs/documentation/phases.md b/docs/documentation/phases.md index ce9b1dc85..aaef3b7a5 100644 --- a/docs/documentation/phases.md +++ b/docs/documentation/phases.md @@ -8,8 +8,8 @@ entering a playing phase, for example. Each phase in [boardgame.io](https://boardgame.io/) defines a set of game configuration options that are applied for the duration of that phase. This includes the ability to define a different -set of moves, use a different turn order etc. Turns happen -inside phases. +set of moves, use a different turn order etc. **Turns happen +inside phases**. ### Card Game @@ -20,19 +20,19 @@ two moves: - play a card from your hand onto the deck. ```js -function DrawCard({ G, playerID }) { +function drawCard({ G, playerID }) { G.deck--; G.hand[playerID]++; } -function PlayCard({ G, playerID }) { +function playCard({ G, playerID }) { G.deck++; G.hand[playerID]--; } const game = { setup: ({ ctx }) => ({ deck: 6, hand: Array(ctx.numPlayers).fill(0) }), - moves: { DrawCard, PlayCard }, + moves: { drawCard, playCard }, turn: { minMoves: 1, maxMoves: 1 }, }; ``` @@ -74,9 +74,7 @@ const game = { ``` !> A phase that doesn't specify any moves just uses moves from -the main `moves` section in the game. However, if it does, -then the `moves` section in the phase overrides the global -one. +the main `moves` section in the game. The game doesn't begin in any of these phases. In order to begin in the "draw" phase, we add a `start: true` to its config. Only @@ -86,7 +84,7 @@ one phase can have `start: true`. phases: { draw: { moves: { DrawCard }, -+ start: true, + start: true, }, play: { @@ -102,8 +100,8 @@ empty. phases: { draw: { moves: { DrawCard }, -+ endIf: ({ G }) => (G.deck <= 0), -+ next: 'play', + endIf: ({ G }) => (G.deck <= 0), + next: 'play', start: true, }, diff --git a/docs/documentation/sidebar.md b/docs/documentation/sidebar.md index b04d2d8fc..74df61a36 100644 --- a/docs/documentation/sidebar.md +++ b/docs/documentation/sidebar.md @@ -20,6 +20,7 @@ - [TypeScript](typescript.md) - **Reference** - [Game](api/Game.md) + - [Context](api/Context.md) - [Client](api/Client.md) - [Server](api/Server.md) - [Lobby](api/Lobby.md) diff --git a/docs/documentation/stages.md b/docs/documentation/stages.md index d86b98bca..714a24402 100644 --- a/docs/documentation/stages.md +++ b/docs/documentation/stages.md @@ -1,6 +1,7 @@ # Stages -A stage is similar to a phase, except that it happens within a turn. +Stages are a way to break a turn into smaller parts. They are useful +when you want to restrict the set of moves that a player can make. A turn can be subdivided into many stages, each allowing a different set of moves during that stage. @@ -40,7 +41,7 @@ const game = { turn: { stages: { discard: { - moves: { DiscardCard }, + moves: { discardCard }, }, }, }, @@ -144,16 +145,16 @@ Let's go back to the example we discussed earlier where we require every other player to discard a card when we play one: ```js -function PlayCard({ events }) { +function playCard({ events }) { events.setActivePlayers({ others: 'discard', minMoves: 1, maxMoves: 1 }); } const game = { - moves: { PlayCard }, + moves: { playCard }, turn: { stages: { discard: { - moves: { Discard }, + moves: { discard }, }, }, }, diff --git a/docs/documentation/tutorial.md b/docs/documentation/tutorial.md index 00b0b24eb..0b8fe25b3 100644 --- a/docs/documentation/tutorial.md +++ b/docs/documentation/tutorial.md @@ -1,11 +1,7 @@ # Tutorial -This tutorial walks through a simple game of Tic-Tac-Toe. - -?> We’re going to be running commands from a terminal and using Node.js/npm. - If you haven’t done that before, you might want to read [an introduction to the command line][cmd] - and follow [the instructions on how to install Node][node]. You’ll also want - a text editor to write code in like [VS Code][vsc] or [Atom][atom]. +The goal of this tutorial is to make a simple TicTacToe game using boardgame.io. +You'll learn the basic concepts of boardgame.io and how to use them and in the end you'll have a working game. [node]: https://nodejs.dev/learn/how-to-install-nodejs [cmd]: https://tutorial.djangogirls.org/en/intro_to_command_line/ @@ -16,143 +12,88 @@ This tutorial walks through a simple game of Tic-Tac-Toe. ## Setup -We’re going to use ES2015 features like module [imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) -and the [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator) -syntax, so we’ll need to use some kind of build system to compile -our code for the browser. - -This tutorial shows two different approaches: one using [React](https://reactjs.org/), -the other using basic browser APIs and compiling our app with -[Parcel](https://parceljs.org/). -You can follow whichever you feel most comfortable with. - - +Clone the boardgame.io template repository from https://github.com/info-hsaka/boardgame-template . +(See here for instructions on how to do that: https://js.oc.is/docs/intro/setup/#34-clone-git-repository) +Like before we need to install some additional programs by running `npm i` in the folder. Take a look here for more detailed (and Windows!) instructions: https://js.oc.is/docs/intro/setup/#35-weitere-programme-installieren -### **Plain JS** +As with the JS tutorial, you should be able to click on the "Run" button to start the game. (See https://js.oc.is/docs/intro/howto/ for a refresher on that) +You should be able to see a website by navigating your browser to http://localhost:3000/ -Let’s create a new Node project from the command line: +Once you have cloned the template repository, this tutorial will work in the src/TicTacToe.js file. You can look at the other files in the src folder, but you don't need to change them for this tutorial. The src/Game.js file includes a game object with a few more functions added to it, so you can get a sense for what will be possible later on. For now we will focus on the TicTacToe object in the TicTacToe.js file. -``` -mkdir bgio-tutorial -cd bgio-tutorial -npm init --yes -``` +## Defining a Game -?> These commands will make a new directory called `bgio-tutorial`, - change to that directory, and initialise a new Node package. - [Read more in the Node Package Manager docs.][pkgjson] +We define a game by creating an object which contains information about your game to +tell boardgame.io how it works. More or less everything +is optional, so we can start simple and gradually add more complexity. +In the template most functions are already defined but not filled out, which means you'll have to fill in the relevant parts. -[pkgjson]: https://docs.npmjs.com/creating-a-package-json-file#creating-a-default-packagejson-file +To start, we’ll fill the `setup` function in the file `src/Game.js`, which will set the +initial value of the game state `G`. -We’re going to add boardgame.io and also Parcel to help us build our app: +?> The game state `G` is a plain JavaScript object that represents the state of the game as we talked about during the in-person session. If, at any point, you have questions about parts of boardgame.io or other concepts that we use here, feel free to ask and also look around at the rest of the documentation here. +```js +export const TicTacToe = { + // Fill the cells of the TicTacToe board with `null` to indicate that they are empty. + // This is the initial state of the game. + // This syntax might be new, but we're just defining a function in a JavaScript object. + // The function will be available in the `setup` field of the `TicTacToe` object and would + // be called like this: `TicTacToe.setup()`. However we don't need to call it ourselves, boardgame.io will do that for us. + setup: function setup() { + return { cells: [null, null, null, null, null, null, null, null, null] } + } +}; ``` -npm install boardgame.io -npm install --save-dev parcel-bundler -``` - - -Now, let’s create the basic structure our project needs: - - -1. A JavaScript file for our web app at `src/App.js`. - - -2. A JavaScript file for our game definition at `src/Game.js`. - - -3. A basic HTML page that will load our app at `index.html`: - - ```html - - - - boardgame.io Tutorial - - - -
- - - - ``` - -Your project directory should now look like this: - - bgio-tutorial/ - ├── index.html - ├── node_modules/ - ├── package-lock.json - ├── package.json - └── src/ - ├── App.js - └── Game.js - -Looking good? OK, let’s get started! 🚀 -?> You can check out the complete code for this tutorial -and play around with it on CodeSandbox:

-[![Edit bgio-plain-js-tutorial](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/bgio-plain-js-tutorial-ewyyt?fontsize=14&hidenavigation=1&module=%2Fsrc%2FApp.js&theme=dark) +Next up is the `moves` object. -### **React** +A move is a function that takes some input (like the cell a player clicked on) and updates `G` to the desired new state. +"Moves" represent things a player can do in the game. In TicTacToe it's pretty simple: a player can click on a cell to place their mark there. -We’ll use the [create-react-app](https://create-react-app.dev/) -command line tool to initialize our React app and then add boardgame.io to it. +The `moves` object is a collection of all possible moves with their names as a normal JavaScript object: -``` -npx create-react-app bgio-tutorial -cd bgio-tutorial -npm install boardgame.io -``` - -While we’re here, let’s also create an empty JavaScript file for our game code: +```js +export const TicTacToe = { + // I won't repeat everything in the object, just new parts that were added + // ... + moves: { + clickCell: () => { + console.log("A clickCell move was made!") + }, + }, +} ``` -touch src/Game.js -``` - -?> You can check out the complete code for this tutorial -and play around with it on CodeSandbox:

-[![Edit boardgame.io](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/boardgameio-wlvi2) - - +Move functions take an argument that contains a few fields, most importantly `G` and `playerID`. `G` is the game state object that we set up with the `setup` function and `playerID` is the ID (short for identifier, in our case 0 or 1) of the player who made the move. The second argument can be anything else we need to pass to make a valid move. We will see later how to use that. In this case, we need to know in which cell the player would like to place their X/O, so we add a second argument `cellIndex` to the `clickCell` function. +```js +export const TicTacToe = { + // ... -## Defining a Game - -We define a game by creating an object whose contents -tell boardgame.io how your game works. More or less everything -is optional, so we can start simple and gradually add complexity. -To start, we’ll add a `setup` function, which will set the -initial value of the game state `G`, and a `moves` object -containing the moves that make up the game. - -A move is a function that updates `G` to the desired new state. -It receives an object containing various fields -as its first argument. This object includes the game state `G` and -`ctx` — an object managed by boardgame.io that contains game metadata. -It also includes `playerID`, which identifies the player making the move. -After the object containing `G` and `ctx`, moves can receive arbitrary arguments -that you pass in when making the move. - -In Tic-Tac-Toe, we only have one type of move and we will -name it `clickCell`. It will take the ID of the cell that was clicked -and update that cell with the ID of the player who clicked it. + moves: { + clickCell: function clickCell(move, cellIndex) { + console.log("Player number " + move.playerID + " wants to place their mark in cell " + cellIndex) + }, + }, +} +``` -Let’s put this together in our `src/Game.js` file to start -defining our game: +Now to update the game state, we simply update G in our move function: ```js export const TicTacToe = { - setup: () => ({ cells: Array(9).fill(null) }), + // ... moves: { - clickCell: ({ G, playerID }, id) => { - G.cells[id] = playerID; + clickCell: function clickCell(move, cellIndex) { + // cells is the array we setup in the setup function and we simply assign the playerID to the cellIndex + // to indicate which player has placed their mark there. + move.G.cells[cellIndex] = move.playerID; }, }, -}; +} ``` ?> The `setup` function also receives an object as its first argument @@ -160,99 +101,41 @@ like moves. This is useful if you need to customize the initial state based on some field in `ctx` — the number of players, for example — but we don't need that for Tic-Tac-Toe. - - -## Creating a Client - - - -### **Plain JS** - -We’ll start by creating a class to manage our web app’s logic in `src/App.js`. - -In the class’s constructor we’ll create a boardgame.io client -and call its `start` method to run it. +At this point your TicTacToe.js file should look like this: ```js -import { Client } from 'boardgame.io/client'; -import { TicTacToe } from './Game'; - -class TicTacToeClient { - constructor() { - this.client = Client({ game: TicTacToe }); - this.client.start(); - } -} - -const app = new TicTacToeClient(); -``` - -Let’s also add a script to `package.json` to make serving the web app simpler -and a [browserslist string](https://github.com/browserslist/browserslist) to -indicate the browsers we want to support: +export const TicTacToe = { + setup: function setup() { + return { cells: [null, null, null, null, null, null, null, null, null] } + }, -```json -{ - "scripts": { - "start": "parcel index.html --open" + moves: { + clickCell: function clickCell(move, cellIndex) { + // cells is the array we setup in the setup function and we simply assign the playerID to the cellIndex + // to indicate which player has placed their mark there. + move.G.cells[cellIndex] = move.playerID; + }, }, - "browserslist": "defaults and supports async-functions" } ``` -?> By dropping support for browsers that don’t support async functions, we don’t - need to worry about including the `regenerator-runtime` polyfill. If you need to - support older browsers, you can skip adding `browserslist`, but may need to - include the polyfill manually. - -You can now serve the app from the command line by running: - -``` -npm start -``` - -### **React** - -Replace the contents of `src/App.js` with - -```js -import { Client } from 'boardgame.io/react'; -import { TicTacToe } from './Game'; - -const App = Client({ game: TicTacToe }); - -export default App; -``` -You can now serve the app from the command line by running: +You can now click the "Run" button to see the game in action. Go to http://localhost:3000/ to see the game. -``` -npm start -``` - - - -Although we haven’t built any UI yet, boardgame.io renders a Debug Panel. +At this point you should see an empty TicTacToe board and the boardgame.io Debug Panel. This panel means we can already play our Tic-Tac-Toe game! You can make a move by clicking on `clickCell` on the -Debug Panel, entering a number between `0` and `8`, and pressing +Debug Panel, entering a number between `0` and `8` in the (), and pressing **Enter**. The current player will make a move on the chosen cell. The number you enter is the `id` passed to the `clickCell` function as -the first argument after `G` and `ctx`. Notice how the +the first argument after `move`. Notice how the `cells` array on the Debug Panel updates as you make moves. You can end the turn by clicking `endTurn` and pressing **Enter**. The next call to `clickCell` will result in a “1” in the chosen cell instead of a “0”. -```react - -``` - - ?> You can turn off the Debug Panel by passing `debug: false` in the `Client` config. - - ## Game Improvements ### Validating Moves @@ -271,11 +154,19 @@ import { INVALID_MOVE } from 'boardgame.io/core'; Now we can return `INVALID_MOVE` from `clickCell`: ```js -clickCell: ({ G, playerID }, id) => { - if (G.cells[id] !== null) { - return INVALID_MOVE; - } - G.cells[id] = playerID; +import { INVALID_MOVE } from 'boardgame.io/core'; + +export const TicTacToe = { + // ... + + moves: { + clickCell: function clickCell(move, cellIndex) { + if (move.G.cells[cellIndex] !== null) { + return INVALID_MOVE; + } + move.G.cells[cellIndex] = move.playerID; + }, + }, } ``` @@ -295,17 +186,18 @@ move has been made, as well as the `minMoves` option, so players ```js export const TicTacToe = { - setup: () => { /* ... */ }, + setup: // ... + moves: { /* ... */ }, turn: { minMoves: 1, maxMoves: 1, }, - - moves: { /* ... */ }, } ``` +Try playing around with the game in the debug panel again. You should see that you can't make a move in a cell that is already filled and that the turn automatically ends after a move is made. + ?> You can learn more in the [Turn Order](turn-order.md) and [Events](events.md) guides. @@ -314,32 +206,37 @@ export const TicTacToe = { The Tic-Tac-Toe game we have so far doesn't really ever end. Let's keep track of a winner in case one player wins the game. -First, let’s declare two helper functions in `src/Game.js` -to test the `cells` array with: +In order to do that, first we need to know if a player won and if so, which one. + +TicTacToe will often end in a draw, so we need to handle that as well. We can add a helper function to check if the game is over, i.e. all cells are filled. ```js -// Return true if `cells` is in a winning configuration. -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] - ]; - - const isRowComplete = row => { - const symbols = row.map(i => cells[i]); - return symbols.every(i => i !== null && i === symbols[0]); - }; - - return positions.map(isRowComplete).some(i => i === true); +function isDraw(cells) { + // Return `true` if all cells are filled and `false` otherwise } +``` + +Write the code for this function as described and we will see later how to use it. + +The other problem we have is finding out if someone has won the game. In Tic-Tac-Toe, a player wins if they have three of their marks in a row, either horizontally, vertically, or diagonally. We should write a helper function to check if a player has won: -// Return true if all `cells` are occupied. -function IsDraw(cells) { - return cells.filter(c => c === null).length === 0; +```js +function isVictory(cells) { + // Return the playerID of the winner if there is one, otherwise `null` } ``` -Now, we add an `endIf` method to our game. +Hints: cells is an array with exactly 9 elements and each element is either `null`, `0`, or `1`. Imagine the elements of the arary being laid out like this: + +``` +0 | 1 | 2 +3 | 4 | 5 +6 | 7 | 8 +``` + +And then write a function that checks for a win in each row, column, and diagonal. + +Now that we have these functions, we add an `endIf` method to our game. This method will be called each time our state updates to check if the game is over. @@ -347,11 +244,15 @@ check if the game is over. export const TicTacToe = { // setup, moves, etc. - endIf: ({ G, ctx }) => { - if (IsVictory(G.cells)) { - return { winner: ctx.currentPlayer }; + endIf: function endIf(endIf) { + const winner = isVictory(endIf.G.cells); + if (winner != null) { + // our isVictory function returned a playerID so the game is over and we have a winner + return { winner: winner }; } - if (IsDraw(G.cells)) { + // if there is no winner, check if the game is a draw + if (isDraw(endIf.G.cells)) { + // the game is a draw, so we tell boardgame.io that result return { draw: true }; } }, @@ -360,248 +261,7 @@ export const TicTacToe = { ?> `endIf` takes a function that determines if the game is over. If it returns anything at all, the game ends and -the return value is available at `ctx.gameover`. - - - -## Building a Board - - - -### **Plain JS** - -You can build your game board with your preferred UI tools. -This example will use basic JavaScript, but you should be able -to adapt this approach to many other frameworks. - -To start with, let’s add a `createBoard` method to our -`TicTacToeClient` and call it in the constructor. This will inject -the required DOM structure for our board into the web page. -To know where to insert our board UI, we’ll pass in an -element when instantiating the class. - -We’ll also add an `attachListeners` method. This will -set up our board cells so that they trigger the `clickCell` -move when they are clicked. - -```js -class TicTacToeClient { - constructor(rootElement) { - this.client = Client({ game: TicTacToe }); - this.client.start(); - this.rootElement = rootElement; - this.createBoard(); - this.attachListeners(); - } - - createBoard() { - // Create cells in rows for the Tic-Tac-Toe board. - const rows = []; - for (let i = 0; i < 3; i++) { - const cells = []; - for (let j = 0; j < 3; j++) { - const id = 3 * i + j; - cells.push(``); - } - rows.push(`${cells.join('')}`); - } - - // Add the HTML to our app
. - // We’ll use the empty

to display the game winner later. - this.rootElement.innerHTML = ` - ${rows.join('')}
-

- `; - } - - attachListeners() { - // This event handler will read the cell id from a cell’s - // `data-id` attribute and make the `clickCell` move. - const handleCellClick = event => { - const id = parseInt(event.target.dataset.id); - this.client.moves.clickCell(id); - }; - // Attach the event listener to each of the board cells. - const cells = this.rootElement.querySelectorAll('.cell'); - cells.forEach(cell => { - cell.onclick = handleCellClick; - }); - } -} - -const appElement = document.getElementById('app'); -const app = new TicTacToeClient(appElement); -``` - -You probably won’t see anything just yet, because all the cells are empty. -Let’s fix that by adding a style for the cells to `index.html`: - -```html - -``` - -Now you should see an empty Tic-Tac-Toe board! -But there’s still one thing missing. If you click -on the board cells, you should see `G.cells` update -in the Debug Panel, but the board itself doesn’t change. -We need to add a way to refresh the board every time -boardgame.io’s state changes. - -Let’s do that by writing an `update` method for our `TicTacToeClient` -class and subscribing to the boardgame.io state: - -```js -class TicTacToeClient { - constructor() { - // As before, but we also subscribe to the client: - this.client.subscribe(state => this.update(state)); - } - - createBoard() { /* ... */ } - - attachListeners() { /* ... */ } - - update(state) { - // Get all the board cells. - const cells = this.rootElement.querySelectorAll('.cell'); - // Update cells to display the values in game state. - cells.forEach(cell => { - const cellId = parseInt(cell.dataset.id); - const cellValue = state.G.cells[cellId]; - cell.textContent = cellValue !== null ? cellValue : ''; - }); - // Get the gameover message element. - const messageEl = this.rootElement.querySelector('.winner'); - // Update the element to show a winner if any. - if (state.ctx.gameover) { - messageEl.textContent = - state.ctx.gameover.winner !== undefined - ? 'Winner: ' + state.ctx.gameover.winner - : 'Draw!'; - } else { - messageEl.textContent = ''; - } - } -} -``` - -Here are the key things to remember: - -- You can trigger the moves defined in your game definition - by calling `client.moves['moveName']`. - - -- You can register callbacks for every state change using `client.subscribe`. - -### **React** - -React can be a good fit for board games because -it provides a declarative API to translate objects -to UI elements. To create a board we need to translate -the game state `G` into actual cells that are clickable. - -Let’s create a new file at `src/Board.js`: - -```js -import React from 'react'; - -export function TicTacToeBoard({ ctx, G, moves }) { - const onClick = (id) => moves.clickCell(id); - - let winner = ''; - if (ctx.gameover) { - winner = - ctx.gameover.winner !== undefined ? ( -
Winner: {ctx.gameover.winner}
- ) : ( -
Draw!
- ); - } - - const cellStyle = { - border: '1px solid #555', - width: '50px', - height: '50px', - lineHeight: '50px', - textAlign: 'center', - }; - - 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( - - {G.cells[id] ? ( -
{G.cells[id]}
- ) : ( -