Git Product home page Git Product logo

tic-tac-toe's Introduction

Tic Tac Toe

This tutorial will walk you through a process of creation of a tic-tac-toe game

Built with Git Tutor

Project setup

Before we actually start writing code, I recommend to install editorconfig plugin for your ide/text editor. It will keep code consistent in terms of line-endings style, indentation, newlines

๐Ÿ“„ .editorconfig

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

Every web-app needs an html entry-point, this ain't exception, so let's add simple html file

๐Ÿ“„ index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tic Tac Toe</title>
</head>
<body>

</body>
</html>

index.js will be a js main file

๐Ÿ“„ src/index.js

console.log('Hello world');

Now we need to add script to index.html

๐Ÿ“„ index.html

      <title>Tic Tac Toe</title>
  </head>
  <body>
-
+     <script src="./src/index.js"></script>
  </body>
  </html>

Most likely the codebase will grow, so eventually we'll need some module system. This tutorial is not about setting-up a javascript bundler like webpack, so let's just use es6 modules which are already supported by latest Chrome. To make chrome understand import statement, type attribute should be set to module

๐Ÿ“„ index.html

      <title>Tic Tac Toe</title>
  </head>
  <body>
-     <script src="./src/index.js"></script>
+     <script src="./src/index.js" type="module"></script>
  </body>
  </html>

Let's get started

Game state

Let's define a game state variable

๐Ÿ“„ src/index.js

- console.log('Hello world');
+ const GameState = {
+
+ }

We'll need an information about current player to know whether x or o should be placed on a game field.

๐Ÿ“„ src/index.js

  const GameState = {
-
+     currentPlayer: 0,
  }

0 โ€“ x should be placed

1 โ€“ o

field property will represent a game state. That's an array of 9 elements (3 columns x 3 rows) with initial value -1. Simple if (fieldValue > 0) check will work to distinguish empty fields from filled.

๐Ÿ“„ src/index.js

  const GameState = {
      currentPlayer: 0,
+     field: Array.from({ length: 9 }).fill(-1),
  }

Game state modifications

Now we need to implement a function which will switch a current player. Let's do this with XOR operator. (how xor works).

๐Ÿ“„ src/index.js

      currentPlayer: 0,
      field: Array.from({ length: 9 }).fill(-1),
  }
+
+ function changeCurrentPlayer(gameState) {
+     gameState.currentPlayer = 1 ^ gameState.currentPlayer;
+ }

To modify field values in plain array we'll need a function to convert row and col indices to an array index

๐Ÿ“„ src/index.js

  function changeCurrentPlayer(gameState) {
      gameState.currentPlayer = 1 ^ gameState.currentPlayer;
  }
+
+ function getArrayIndexFromRowAndCol(rowIndex, colIndex) {
+     return rowIndex * 3 + colIndex;
+ }

Game turn logic

Now we'll start handling game turn logic. Create a function placeholder

๐Ÿ“„ src/index.js

  function getArrayIndexFromRowAndCol(rowIndex, colIndex) {
      return rowIndex * 3 + colIndex;
  }
+
+ function turn(gameState, rowIndex, colIndex) {
+
+ }

Convert row and col indices to plain array index

๐Ÿ“„ src/index.js

  }

  function turn(gameState, rowIndex, colIndex) {
-
+     const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);
  }

If game field already contains some value, do nothing

๐Ÿ“„ src/index.js

  function turn(gameState, rowIndex, colIndex) {
      const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);
+     const fieldValue = GameState.field[index];
+
+     if (fieldValue >= 0) {
+         return;
+     }
  }

Put player identifier to a field

๐Ÿ“„ src/index.js

      if (fieldValue >= 0) {
          return;
      }
+
+     gameState.field[index] = gameState.currentPlayer;
  }

and change current player

๐Ÿ“„ src/index.js

      }

      gameState.field[index] = gameState.currentPlayer;
+     changeCurrentPlayer(gameState);
  }

Win

The next thing we need to handle is a "win" state. Lets add helper variables which will contain array indices by rows:

๐Ÿ“„ src/index.js

      field: Array.from({ length: 9 }).fill(-1),
  }

+ const Rows = [
+     [0, 1, 2],
+     [3, 4, 5],
+     [6, 7, 8],
+ ];
+
  function changeCurrentPlayer(gameState) {
      gameState.currentPlayer = 1 ^ gameState.currentPlayer;
  }

cols:

๐Ÿ“„ src/index.js

      [6, 7, 8],
  ];

+ const Cols = [
+     [0, 3, 6],
+     [1, 4, 7],
+     [6, 7, 8],
+ ];
+
  function changeCurrentPlayer(gameState) {
      gameState.currentPlayer = 1 ^ gameState.currentPlayer;
  }

and diagonals

๐Ÿ“„ src/index.js

      [6, 7, 8],
  ];

+ const Diagonals = [
+     [0, 4, 8],
+     [2, 4, 6],
+ ];
+
  function changeCurrentPlayer(gameState) {
      gameState.currentPlayer = 1 ^ gameState.currentPlayer;
  }

Now let's take a look at some examples of a "win" state

  1 -1  0
  0  1 -1
 -1 -1  1

Winner is 1. Sum of diagonal values equals 3

We can assume that we can detect a winner by getting a sum of each row, col and diagonal values and comparing it to a 0 (0 + 0 + 0) or 3 (1 + 1 + 1)

But here's another example

  0 -1  1
  1  0 -1
 -1 -1  0

A sum of 1st and 2nd row = 0

Sum of both diagonals = 0

Sum of 1st and 3d cols = 0

That's not the right way to go... ๐Ÿ˜ž

๐Ÿ’ก Easy fix! Change initial value of field to -3 ๐Ÿ˜Ž

๐Ÿ“„ src/index.js

  const GameState = {
      currentPlayer: 0,
-     field: Array.from({ length: 9 }).fill(-1),
+     field: Array.from({ length: 9 }).fill(-3),
  }

  const Rows = [

Ok, now we are good. So let's create a simple sum function

๐Ÿ“„ src/index.js

      gameState.field[index] = gameState.currentPlayer;
      changeCurrentPlayer(gameState);
  }
+
+ function sum(arr) {
+     return arr.reduce((a, b) => a + b, 0);
+ }

and a helper function which maps field indices to values

๐Ÿ“„ src/index.js

  function sum(arr) {
      return arr.reduce((a, b) => a + b, 0);
  }
+
+ function getValues(gameState, indices) {
+     return indices.map(index => gameState.field[index]);
+ }

function getWinner should find if some row, col or diagonal sum is 0 or 3. Let's get values of all rows

๐Ÿ“„ src/index.js

  function getValues(gameState, indices) {
      return indices.map(index => gameState.field[index]);
  }
+
+ function getWinner(gameState) {
+     const rows = Rows.map((row) => getValues(gameState, row));
+ }

and do the same for cols and diagonals

๐Ÿ“„ src/index.js

  function getWinner(gameState) {
      const rows = Rows.map((row) => getValues(gameState, row));
+     const cols = Cols.map((col) => getValues(gameState, col));
+     const diagonals = Diagonals.map((col) => getValues(gameState, col));
  }

now let's create a single array of all values in field

๐Ÿ“„ src/index.js

      const rows = Rows.map((row) => getValues(gameState, row));
      const cols = Cols.map((col) => getValues(gameState, col));
      const diagonals = Diagonals.map((col) => getValues(gameState, col));
+
+     const values = [...rows, ...cols, ...diagonals];
  }

and find if some chunk sum equals 0 or 3

๐Ÿ“„ src/index.js

      const diagonals = Diagonals.map((col) => getValues(gameState, col));

      const values = [...rows, ...cols, ...diagonals];
+
+     let winner = -1;
+
+     values.forEach((chunk) => {
+         const chunkSum = sum(chunk);
+
+         if (chunkSum === 0) {
+             winner = 0;
+             return;
+         }
+
+         if (chunkSum === 3) {
+             winner = 1;
+             return;
+         }
+     });
+
+     return winner;
  }

Game loop

Now let's describe a game loop. We'll create a generator function to query row and col for each next turn from outside world. If you are not familliar with generator functions โ€“ read this medium post

๐Ÿ“„ src/index.js

      return winner;
  }
+
+ function* gameLoop(gameState) {
+
+ }

Generator should execute until getWinner returns anything but -1.

๐Ÿ“„ src/index.js

  }

  function* gameLoop(gameState) {
+     let winner = -1;
+
+     while (winner < 0) {

+         winner = getWinner(gameState);
+     }
  }

it should also make a turn befor each getWinner call

๐Ÿ“„ src/index.js

      let winner = -1;

      while (winner < 0) {
+         const [rowIndex, colIndex] = yield;
+         turn(gameState, rowIndex, colIndex);

          winner = getWinner(gameState);
      }

Now let's test our gameLoop

Create a mock scenario of a game:

๐Ÿ“„ src/index.js

          winner = getWinner(gameState);
      }
  }
+
+ const turns = [
+     [1, 1],
+     [0, 1],
+     [0, 0],
+     [1, 2],
+     [2, 2],
+ ];

Create a game generator object

๐Ÿ“„ src/index.js

      [1, 2],
      [2, 2],
  ];
+
+ const game = gameLoop(GameState);
+ game.next();

Iterate over game turns and pass each turn to generator

๐Ÿ“„ src/index.js

  const game = gameLoop(GameState);
  game.next();
+
+ turns.forEach(turn => game.next(turn));

After execution of this scenario game generator should finish it execution. This means that leading .next() call should return an object { value: undefined, done: true }

๐Ÿ“„ src/index.js

  game.next();

  turns.forEach(turn => game.next(turn));
+
+ console.log(game.next());

Let's check it with node.js

node src/index.js
{ value: undefined, done: true }

Yay, it works!

Refactor time

Now as a core of a game is ready let's start refactor our index.js and split it in several modules

Drop testing code

๐Ÿ“„ src/index.js

      }
  }

- const turns = [
-     [1, 1],
-     [0, 1],
-     [0, 0],
-     [1, 2],
-     [2, 2],
- ];
-
  const game = gameLoop(GameState);
  game.next();
-
- turns.forEach(turn => game.next(turn));
-
- console.log(game.next());

Move everything but gameLoop from index.js to game-state.js.

๐Ÿ“„ src/game-state.js

const GameState = {
    currentPlayer: 0,
    field: Array.from({ length: 9 }).fill(-3),
}

const Rows = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
];

const Cols = [
    [0, 3, 6],
    [1, 4, 7],
    [6, 7, 8],
];

const Diagonals = [
    [0, 4, 8],
    [2, 4, 6],
];

function changeCurrentPlayer(gameState) {
    gameState.currentPlayer = 1 ^ gameState.currentPlayer;
}

function getArrayIndexFromRowAndCol(rowIndex, colIndex) {
    return rowIndex * 3 + colIndex;
}

function turn(gameState, rowIndex, colIndex) {
    const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);
    const fieldValue = gameState.field[index];

    if (fieldValue >= 0) {
        return;
    }

    gameState.field[index] = gameState.currentPlayer;
    changeCurrentPlayer(gameState);
}

function sum(arr) {
    return arr.reduce((a, b) => a + b, 0);
}

function getValues(gameState, indices) {
    return indices.map(index => gameState.field[index]);
}

function getWinner(gameState) {
    const rows = Rows.map((row) => getValues(gameState, row));
    const cols = Cols.map((col) => getValues(gameState, col));
    const diagonals = Diagonals.map((col) => getValues(gameState, col));

    const values = [...rows, ...cols, ...diagonals];

    let winner = -1;

    values.forEach((chunk) => {
        const chunkSum = sum(chunk);

        if (chunkSum === 0) {
            winner = 0;
            return;
        }

        if (chunkSum === 3) {
            winner = 1;
            return;
        }
    });

    return winner;
}

๐Ÿ“„ src/index.js

- const GameState = {
-     currentPlayer: 0,
-     field: Array.from({ length: 9 }).fill(-3),
- }
-
- const Rows = [
-     [0, 1, 2],
-     [3, 4, 5],
-     [6, 7, 8],
- ];
-
- const Cols = [
-     [0, 3, 6],
-     [1, 4, 7],
-     [6, 7, 8],
- ];
-
- const Diagonals = [
-     [0, 4, 8],
-     [2, 4, 6],
- ];
-
- function changeCurrentPlayer(gameState) {
-     gameState.currentPlayer = 1 ^ gameState.currentPlayer;
- }
-
- function getArrayIndexFromRowAndCol(rowIndex, colIndex) {
-     return rowIndex * 3 + colIndex;
- }
-
- function turn(gameState, rowIndex, colIndex) {
-     const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);
-     const fieldValue = GameState.field[index];
-
-     if (fieldValue >= 0) {
-         return;
-     }
-
-     gameState.field[index] = gameState.currentPlayer;
-     changeCurrentPlayer(gameState);
- }
-
- function sum(arr) {
-     return arr.reduce((a, b) => a + b, 0);
- }
-
- function getValues(gameState, indices) {
-     return indices.map(index => gameState.field[index]);
- }
-
- function getWinner(gameState) {
-     const rows = Rows.map((row) => getValues(gameState, row));
-     const cols = Cols.map((col) => getValues(gameState, col));
-     const diagonals = Diagonals.map((col) => getValues(gameState, col));
-
-     const values = [...rows, ...cols, ...diagonals];
-
-     let winner = -1;
-
-     values.forEach((chunk) => {
-         const chunkSum = sum(chunk);
-
-         if (chunkSum === 0) {
-             winner = 0;
-             return;
-         }
-
-         if (chunkSum === 3) {
-             winner = 1;
-             return;
-         }
-     });
-
-     return winner;
- }
-
  function* gameLoop(gameState) {
      let winner = -1;

Export everything gameLoop depends on

๐Ÿ“„ src/game-state.js

- const GameState = {
+ export const GameState = {
      currentPlayer: 0,
      field: Array.from({ length: 9 }).fill(-3),
  }
      return rowIndex * 3 + colIndex;
  }

- function turn(gameState, rowIndex, colIndex) {
+ export function turn(gameState, rowIndex, colIndex) {
      const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);
      const fieldValue = gameState.field[index];

      return indices.map(index => gameState.field[index]);
  }

- function getWinner(gameState) {
+ export function getWinner(gameState) {
      const rows = Rows.map((row) => getValues(gameState, row));
      const cols = Cols.map((col) => getValues(gameState, col));
      const diagonals = Diagonals.map((col) => getValues(gameState, col));

and import it in index.js

๐Ÿ“„ src/index.js

+ import { GameState, getWinner, turn } from './game-state.js';
+
  function* gameLoop(gameState) {
      let winner = -1;

Rendering game state on canvas

Add canvas to index.html

๐Ÿ“„ index.html

  </head>
  <body>
      <script src="./src/index.js" type="module"></script>
+     <canvas></canvas>
  </body>
  </html>

and get a reference to canvas with querySelector

๐Ÿ“„ src/index.js

  const game = gameLoop(GameState);
  game.next();
+
+ const canvas = document.querySelector('canvas');

Let's make body full-height

๐Ÿ“„ index.html

      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Tic Tac Toe</title>
+     <style>
+     html, body {
+         height: 100%;
+     }
+     </style>
  </head>
  <body>
      <script src="./src/index.js" type="module"></script>

and reset default margins

๐Ÿ“„ index.html

      html, body {
          height: 100%;
      }
+
+     body {
+         margin: 0;
+     }
      </style>
  </head>
  <body>

Setup canvas size

๐Ÿ“„ src/index.js

  game.next();

  const canvas = document.querySelector('canvas');
+
+ const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);
+ canvas.width = size;
+ canvas.height = size;

and get a 2d context

๐Ÿ“„ src/index.js

  const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);
  canvas.width = size;
  canvas.height = size;
+
+ const ctx = canvas.getContext('2d');

Move canvas setup code to separate file

๐Ÿ“„ src/canvas-setup.js

export function setupCanvas() {
    const canvas = document.querySelector('canvas');

    const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);
    canvas.width = size;
    canvas.height = size;

    const ctx = canvas.getContext('2d');

    return { canvas, ctx };
}

๐Ÿ“„ src/index.js

  const game = gameLoop(GameState);
  game.next();
-
- const canvas = document.querySelector('canvas');
-
- const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);
- canvas.width = size;
- canvas.height = size;
-
- const ctx = canvas.getContext('2d');

and import it to index.js

๐Ÿ“„ src/index.js

  import { GameState, getWinner, turn } from './game-state.js';
+ import { setupCanvas } from './canvas-setup.js';

  function* gameLoop(gameState) {
      let winner = -1;

  const game = gameLoop(GameState);
  game.next();
+
+ const { canvas, ctx } = setupCanvas();

Now let's create render function which will visualize the game state

๐Ÿ“„ src/renderer.js

/**
 * @typedef GameState
 * @property {Number} currentPlayer
 * @property {Array<number>} field
 *
 * @param {HTMLCanvasElement} canvas
 * @param {CanvasRenderingContext2D} ctx
 * @param {GameState} gameState
 */
export function draw(canvas, ctx, gameState) {

}

We'll need to clear the whole canvas on each render call

๐Ÿ“„ src/renderer.js

   * @param {GameState} gameState
   */
  export function draw(canvas, ctx, gameState) {
-
+     ctx.clearRect(0, 0, canvas.width, canvas.height);
  }

We'll render each cell with strokeRect, so let's setup cellSize (width and height of each game field cell) and lineWidth (border width of each cell)

๐Ÿ“„ src/renderer.js

   */
  export function draw(canvas, ctx, gameState) {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+     ctx.lineWidth = 10;
+     const cellSize = canvas.width / 3;
+
  }

And finally we rendered smth! ๐ŸŽ‰

๐Ÿ“„ src/renderer.js

      ctx.lineWidth = 10;
      const cellSize = canvas.width / 3;

+     gameState.field.forEach((_, index) => {
+         const top = Math.floor(index / 3) * cellSize;
+         const left = index % 3 * cellSize;
+
+         ctx.strokeRect(top, left, cellSize, cellSize);
+     });
  }

To see the result install live-server

npm i -g live-server
live-server .

Wait, what? Nothing rendered ๐Ÿ˜ข That's because we forgot to import and call draw function

๐Ÿ“„ src/index.js

  import { GameState, getWinner, turn } from './game-state.js';
  import { setupCanvas } from './canvas-setup.js';
+ import { draw } from './renderer.js';

  function* gameLoop(gameState) {
      let winner = -1;
  game.next();

  const { canvas, ctx } = setupCanvas();
+ draw(canvas, ctx, GameState);

Let's make canvas a bit smaller to leave some space for other UI

๐Ÿ“„ src/canvas-setup.js

  export function setupCanvas() {
      const canvas = document.querySelector('canvas');

-     const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);
+     const size = Math.min(document.body.offsetHeight, document.body.offsetWidth) * 0.8;
      canvas.width = size;
      canvas.height = size;

and add a css border to make all cell edges look the same

๐Ÿ“„ index.html

      body {
          margin: 0;
      }
+
+     canvas {
+         border: 5px solid black;
+     }
      </style>
  </head>
  <body>

It also looks weird in top-left corner, so align canvas to center with flex-box

๐Ÿ“„ index.html

      <style>
      html, body {
          height: 100%;
+         display: flex;
+         align-items: center;
+         justify-content: center;
      }

      body {

So, we've rendered game field cells. Now let's render X and O symbols

๐Ÿ“„ src/renderer.js

          ctx.strokeRect(top, left, cellSize, cellSize);
      });
  }
+
+ /**
+  * @param {CanvasRenderingContext2D} ctx
+  */
+ function drawX(ctx, top, left, size) {
+
+ }

We'll use path to render symbol both for X and O

๐Ÿ“„ src/renderer.js

   * @param {CanvasRenderingContext2D} ctx
   */
  function drawX(ctx, top, left, size) {
+     ctx.beginPath();
+
+     ctx.closePath();
+     ctx.stroke();

  }

Draw a line from top-left to bottom-right

๐Ÿ“„ src/renderer.js

  function drawX(ctx, top, left, size) {
      ctx.beginPath();

+     ctx.moveTo(left, top);
+     ctx.lineTo(left + size, top + size);
+
      ctx.closePath();
      ctx.stroke();

Draw a line from top-right to bottom-left

๐Ÿ“„ src/renderer.js

      ctx.moveTo(left, top);
      ctx.lineTo(left + size, top + size);

+     ctx.moveTo(left + size, top);
+     ctx.lineTo(left, top + size);
+
      ctx.closePath();
      ctx.stroke();

Rendering O is even more simple

๐Ÿ“„ src/renderer.js

      ctx.closePath();
      ctx.stroke();
+ }

+ /**
+  * @param {CanvasRenderingContext2D} ctx
+  */
+ function drawO(ctx, centerX, centerY, radius) {
+     ctx.beginPath();
+
+     ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
+     ctx.closePath();
+
+     ctx.stroke();
  }

And let's actually render X or O depending on a field value

๐Ÿ“„ src/renderer.js

      ctx.lineWidth = 10;
      const cellSize = canvas.width / 3;

-     gameState.field.forEach((_, index) => {
+     gameState.field.forEach((value, index) => {
          const top = Math.floor(index / 3) * cellSize;
          const left = index % 3 * cellSize;

          ctx.strokeRect(top, left, cellSize, cellSize);
+
+         if (value < 0) {
+             return;
+         }
+
+         if (value === 0) {
+             drawX(ctx, top, left, cellSize);
+         } else {
+             drawO(ctx, left + cellSize / 2, top + cellSize / 2, cellSize / 2);
+         }
      });
  }

Nothing rendered? That's correct, every field value is -2, so let's make some turns

๐Ÿ“„ src/index.js

  game.next();

  const { canvas, ctx } = setupCanvas();
+
+ turn(GameState, 0, 1);
+ turn(GameState, 1, 1);
+ turn(GameState, 2, 0);
+
  draw(canvas, ctx, GameState);

๐Ÿ“„ src/renderer.js

          }

          if (value === 0) {
-             drawX(ctx, top, left, cellSize);
+             const margin = cellSize * 0.2;
+             const size = cellSize * 0.6;
+
+             drawX(ctx, top + margin, left + margin, size);
          } else {
-             drawO(ctx, left + cellSize / 2, top + cellSize / 2, cellSize / 2);
+             const radius = cellSize * 0.3;
+             drawO(ctx, left + cellSize / 2, top + cellSize / 2, radius);
          }
      });
  }

Interactions

Everything seems to be done, the only thing left โ€“ interactions. Let's start with cleanup:

๐Ÿ“„ src/index.js

  const { canvas, ctx } = setupCanvas();

- turn(GameState, 0, 1);
- turn(GameState, 1, 1);
- turn(GameState, 2, 0);
-
  draw(canvas, ctx, GameState);

Add click listener and calculate clicked row and col

๐Ÿ“„ src/index.js

  const { canvas, ctx } = setupCanvas();

  draw(canvas, ctx, GameState);
+
+ canvas.addEventListener('click', ({ layerX, layerY }) => {
+     const row = Math.floor(layerY / canvas.height * 100 / 33);
+     const col = Math.floor(layerX / canvas.width * 100 / 33);
+ });

Pass row and col indices to game loop generator

๐Ÿ“„ src/index.js

  canvas.addEventListener('click', ({ layerX, layerY }) => {
      const row = Math.floor(layerY / canvas.height * 100 / 33);
      const col = Math.floor(layerX / canvas.width * 100 / 33);
+
+     game.next([row, col]);
  });

and reflect game state changes on canvas

๐Ÿ“„ src/index.js

      const col = Math.floor(layerX / canvas.width * 100 / 33);

      game.next([row, col]);
+     draw(canvas, ctx, GameState);
  });

Now let's congratulate a winner

๐Ÿ“„ src/index.js

          winner = getWinner(gameState);
      }
+
+     setTimeout(() => {
+         alert(`Congratulations, ${['X', 'O'][winner]}! You won!`);
+     });
  }

  const game = gameLoop(GameState);

Oh, we forgot to handle a draw! No worries. Let's add isGameFinished helper:

๐Ÿ“„ src/game-state.js

      return winner;
  }
+
+ export function isGameFinished(gameState) {
+     return gameState.field.every(f => f >= 0);
+ }

and call it on each iteration of a game loop

๐Ÿ“„ src/index.js

- import { GameState, getWinner, turn } from './game-state.js';
+ import { GameState, getWinner, turn, isGameFinished } from './game-state.js';
  import { setupCanvas } from './canvas-setup.js';
  import { draw } from './renderer.js';

  function* gameLoop(gameState) {
      let winner = -1;

-     while (winner < 0) {
+     while (winner < 0 && !isGameFinished(gameState)) {
          const [rowIndex, colIndex] = yield;
          turn(gameState, rowIndex, colIndex);

      }

      setTimeout(() => {
-         alert(`Congratulations, ${['X', 'O'][winner]}! You won!`);
+         if (winner < 0) {
+             alert(`It's a draw`);
+         } else {
+             alert(`Congratulations, ${['X', 'O'][winner]}! You won!`);
+         }
      });
  }

LICENSE

WTFPL

tic-tac-toe's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.