All Projects → lesnitsky → Tic Tac Toe

lesnitsky / Tic Tac Toe

Licence: other
An example tutorial built with git-tutor https://github.com/lesnitsky/git-tutor

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to Tic Tac Toe

Expo Three Demo
🍎👩‍🏫 Collection of Demos for THREE.js in Expo!
Stars: ✭ 76 (+245.45%)
Mutual labels:  game, tutorial
Gridgarden
A game for learning CSS grid layout 🥕
Stars: ✭ 2,331 (+10495.45%)
Mutual labels:  game, tutorial
Amongusbot
This is an Among Us Discord bot that auto mutes and unmutes players in certain situations, without anyone in the game needing to mute manually. This will make Among Us way more intense and doesn't break the immersion of the game having to mute all the time.
Stars: ✭ 110 (+400%)
Mutual labels:  game, tutorial
Weixin Minigame Tutorial
Flappy Bird adaptation on Wechat Minigame using PhaserJS + English Wechat Minigames Tutorial
Stars: ✭ 56 (+154.55%)
Mutual labels:  game, tutorial
Pygame tutorials
Code to go along with lessons at http://kidscancode.org/lessons
Stars: ✭ 363 (+1550%)
Mutual labels:  game, tutorial
Bytepath
A replayable arcade shooter with a focus on build theorycrafting made using Lua and LÖVE.
Stars: ✭ 1,119 (+4986.36%)
Mutual labels:  game, tutorial
Epicsurvivalgameseries
Third-person Survival Game for Unreal Engine 4 (Sample Project)
Stars: ✭ 2,389 (+10759.09%)
Mutual labels:  game, tutorial
Teleball
Build your own Arduino based retro handheld game console
Stars: ✭ 21 (-4.55%)
Mutual labels:  game, tutorial
Pillar Valley
👾A cross-platform video game built with Expo, three.js, and Firebase! 🎮🕹
Stars: ✭ 242 (+1000%)
Mutual labels:  game, tutorial
Stealthgameudemy
C++ Stealth Game in Unreal Engine (Udemy Project)
Stars: ✭ 221 (+904.55%)
Mutual labels:  game, tutorial
Sunset Cyberspace
🎮👾Retro-runner Game made in Expo, Three.js, OpenGL, WebGL, Tween. 🕹
Stars: ✭ 54 (+145.45%)
Mutual labels:  game, tutorial
Arshooter
A demo Augmented Reality shooter made with ARKit in Swift (iOS 11)
Stars: ✭ 794 (+3509.09%)
Mutual labels:  game, tutorial
Pacgo
A Pac Man clone written in Go (with emojis!)
Stars: ✭ 961 (+4268.18%)
Mutual labels:  game, tutorial
Snake
🐍🎮 Snake game made with Expo & PIXI.js 👾 iOS, Android, and Web
Stars: ✭ 67 (+204.55%)
Mutual labels:  game, tutorial
Pixi.js
The HTML5 Creation Engine: Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.
Stars: ✭ 34,982 (+158909.09%)
Mutual labels:  game, canvas2d
Classic Pool Game
Classic 8 Ball pool game written in JavaScript
Stars: ✭ 177 (+704.55%)
Mutual labels:  game, canvas2d
Godot Kickstarter 2019
Create your Own Games with Godot, the Free Game Engine: sources from the January Kickstarter project from GDQuest
Stars: ✭ 194 (+781.82%)
Mutual labels:  game, tutorial
Expo Crossy Road
🐥🚙 Crossy Road game clone made in Expo (iOS, Android, web), THREE.js, Tween, React Native. 🐔
Stars: ✭ 701 (+3086.36%)
Mutual labels:  game, tutorial
Syscrack
Virtual Online Crime Simulator (VOCS) written in PHP 7.0
Stars: ✭ 17 (-22.73%)
Mutual labels:  game, tutorial
Blockly Gamepad
A Blockly extension designed to develop games (made with love ❤)
Stars: ✭ 18 (-18.18%)
Mutual labels:  game

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,
  }

0x should be placed

1o

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

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].