{"id":15476063,"url":"https://github.com/lesnitsky/tic-tac-toe","last_synced_at":"2025-09-09T13:46:31.390Z","repository":{"id":85541816,"uuid":"145269582","full_name":"lesnitsky/tic-tac-toe","owner":"lesnitsky","description":"An example tutorial built with git-tutor https://github.com/lesnitsky/git-tutor","archived":false,"fork":false,"pushed_at":"2018-08-19T03:57:58.000Z","size":33,"stargazers_count":20,"open_issues_count":0,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-10-19T02:29:56.401Z","etag":null,"topics":["canvas2d","game","tutorial"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lesnitsky.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-08-19T03:19:15.000Z","updated_at":"2024-01-04T15:50:25.000Z","dependencies_parsed_at":null,"dependency_job_id":"5f497cbe-8029-4d0e-9f44-cb6a03df91ed","html_url":"https://github.com/lesnitsky/tic-tac-toe","commit_stats":{"total_commits":73,"total_committers":1,"mean_commits":73.0,"dds":0.0,"last_synced_commit":"8aea76175249c7263ffa231024f1ab60f6202898"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lesnitsky%2Ftic-tac-toe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lesnitsky%2Ftic-tac-toe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lesnitsky%2Ftic-tac-toe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lesnitsky%2Ftic-tac-toe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lesnitsky","download_url":"https://codeload.github.com/lesnitsky/tic-tac-toe/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250255700,"owners_count":21400410,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["canvas2d","game","tutorial"],"created_at":"2024-10-02T03:22:17.249Z","updated_at":"2025-04-22T14:10:46.413Z","avatar_url":"https://github.com/lesnitsky.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tic Tac Toe\n\nThis tutorial will walk you through a process of creation of a tic-tac-toe game\n\n\u003e Built with [Git Tutor](https://github.com/R1ZZU/git-tutor)\n\n## Project setup\n\nBefore we actually start writing code, I recommend to install [editorconfig](https://editorconfig.org/) plugin for your ide/text editor. It will keep code consistent in terms of line-endings style, indentation, newlines\n\n📄 .editorconfig\n```\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 4\nend_of_line = lf\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n```\nEvery web-app needs an html entry-point, this ain't exception, so let's add simple html file\n\n📄 index.html\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n    \u003cmeta charset=\"UTF-8\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n    \u003ctitle\u003eTic Tac Toe\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e\n\n```\n`index.js` will be a js main file\n\n📄 src/index.js\n```js\nconsole.log('Hello world');\n\n```\nNow we need to add `script` to `index.html`\n\n📄 index.html\n```diff\n      \u003ctitle\u003eTic Tac Toe\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n-\n+     \u003cscript src=\"./src/index.js\"\u003e\u003c/script\u003e\n  \u003c/body\u003e\n  \u003c/html\u003e\n\n```\nMost 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](https://webpack.js.org/), 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`\n\n📄 index.html\n```diff\n      \u003ctitle\u003eTic Tac Toe\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n-     \u003cscript src=\"./src/index.js\"\u003e\u003c/script\u003e\n+     \u003cscript src=\"./src/index.js\" type=\"module\"\u003e\u003c/script\u003e\n  \u003c/body\u003e\n  \u003c/html\u003e\n\n```\n## Let's get started\n\n### Game state\n\nLet's define a game state variable\n\n📄 src/index.js\n```diff\n- console.log('Hello world');\n+ const GameState = {\n+\n+ }\n\n```\nWe'll need an information about current player to know whether `x` or `o` should be placed on a game field.\n\n📄 src/index.js\n```diff\n  const GameState = {\n-\n+     currentPlayer: 0,\n  }\n\n```\n`0` – `x` should be placed\n\n`1` – `o`\n\n\n`field` property will represent a game state.\nThat's an array of 9 elements (3 columns x 3 rows) with initial value `-1`. Simple `if (fieldValue \u003e 0)` check will work to distinguish empty fields from filled.\n\n📄 src/index.js\n```diff\n  const GameState = {\n      currentPlayer: 0,\n+     field: Array.from({ length: 9 }).fill(-1),\n  }\n\n```\n### Game state modifications\n\nNow we need to implement a function which will switch a current player. Let's do this with `XOR` operator. ([how xor works](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Bitwise_XOR)).\n\n📄 src/index.js\n```diff\n      currentPlayer: 0,\n      field: Array.from({ length: 9 }).fill(-1),\n  }\n+\n+ function changeCurrentPlayer(gameState) {\n+     gameState.currentPlayer = 1 ^ gameState.currentPlayer;\n+ }\n\n```\nTo modify field values in plain array we'll need a function to convert `row` and `col` indices to an array index\n\n📄 src/index.js\n```diff\n  function changeCurrentPlayer(gameState) {\n      gameState.currentPlayer = 1 ^ gameState.currentPlayer;\n  }\n+\n+ function getArrayIndexFromRowAndCol(rowIndex, colIndex) {\n+     return rowIndex * 3 + colIndex;\n+ }\n\n```\n### Game turn logic\n\nNow we'll start handling game turn logic.\nCreate a function placeholder\n\n📄 src/index.js\n```diff\n  function getArrayIndexFromRowAndCol(rowIndex, colIndex) {\n      return rowIndex * 3 + colIndex;\n  }\n+\n+ function turn(gameState, rowIndex, colIndex) {\n+\n+ }\n\n```\nConvert row and col indices to plain array index\n\n📄 src/index.js\n```diff\n  }\n\n  function turn(gameState, rowIndex, colIndex) {\n-\n+     const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);\n  }\n\n```\nIf game field already contains some value, do nothing\n\n📄 src/index.js\n```diff\n\n  function turn(gameState, rowIndex, colIndex) {\n      const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);\n+     const fieldValue = GameState.field[index];\n+\n+     if (fieldValue \u003e= 0) {\n+         return;\n+     }\n  }\n\n```\nPut player identifier to a field\n\n📄 src/index.js\n```diff\n      if (fieldValue \u003e= 0) {\n          return;\n      }\n+\n+     gameState.field[index] = gameState.currentPlayer;\n  }\n\n```\nand change current player\n\n📄 src/index.js\n```diff\n      }\n\n      gameState.field[index] = gameState.currentPlayer;\n+     changeCurrentPlayer(gameState);\n  }\n\n```\n### Win\n\nThe next thing we need to handle is a \"win\" state.\nLets add helper variables which will contain array indices by rows:\n\n📄 src/index.js\n```diff\n      field: Array.from({ length: 9 }).fill(-1),\n  }\n\n+ const Rows = [\n+     [0, 1, 2],\n+     [3, 4, 5],\n+     [6, 7, 8],\n+ ];\n+\n  function changeCurrentPlayer(gameState) {\n      gameState.currentPlayer = 1 ^ gameState.currentPlayer;\n  }\n\n```\ncols:\n\n📄 src/index.js\n```diff\n      [6, 7, 8],\n  ];\n\n+ const Cols = [\n+     [0, 3, 6],\n+     [1, 4, 7],\n+     [6, 7, 8],\n+ ];\n+\n  function changeCurrentPlayer(gameState) {\n      gameState.currentPlayer = 1 ^ gameState.currentPlayer;\n  }\n\n```\nand diagonals\n\n📄 src/index.js\n```diff\n      [6, 7, 8],\n  ];\n\n+ const Diagonals = [\n+     [0, 4, 8],\n+     [2, 4, 6],\n+ ];\n+\n  function changeCurrentPlayer(gameState) {\n      gameState.currentPlayer = 1 ^ gameState.currentPlayer;\n  }\n\n```\nNow let's take a look at some examples of a \"win\" state\n\n```\n  1 -1  0\n  0  1 -1\n -1 -1  1\n```\n\nWinner is `1`. Sum of diagonal values equals 3\n\nWe 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)\n\nBut here's another example\n\n```\n  0 -1  1\n  1  0 -1\n -1 -1  0\n```\n\nA sum of 1st and 2nd row = 0\n\nSum of both diagonals = 0\n\nSum of 1st and 3d cols = 0\n\nThat's not the right way to go... 😞\n\n💡 Easy fix!\nChange initial value of field to `-3` 😎\n\n📄 src/index.js\n```diff\n  const GameState = {\n      currentPlayer: 0,\n-     field: Array.from({ length: 9 }).fill(-1),\n+     field: Array.from({ length: 9 }).fill(-3),\n  }\n\n  const Rows = [\n\n```\nOk, now we are good. So let's create a simple `sum` function\n\n📄 src/index.js\n```diff\n      gameState.field[index] = gameState.currentPlayer;\n      changeCurrentPlayer(gameState);\n  }\n+\n+ function sum(arr) {\n+     return arr.reduce((a, b) =\u003e a + b, 0);\n+ }\n\n```\nand a helper function which maps field indices to values\n\n📄 src/index.js\n```diff\n  function sum(arr) {\n      return arr.reduce((a, b) =\u003e a + b, 0);\n  }\n+\n+ function getValues(gameState, indices) {\n+     return indices.map(index =\u003e gameState.field[index]);\n+ }\n\n```\nfunction `getWinner` should find if some row, col or diagonal sum is 0 or 3. Let's get values of all rows\n\n📄 src/index.js\n```diff\n  function getValues(gameState, indices) {\n      return indices.map(index =\u003e gameState.field[index]);\n  }\n+\n+ function getWinner(gameState) {\n+     const rows = Rows.map((row) =\u003e getValues(gameState, row));\n+ }\n\n```\nand do the same for cols and diagonals\n\n📄 src/index.js\n```diff\n\n  function getWinner(gameState) {\n      const rows = Rows.map((row) =\u003e getValues(gameState, row));\n+     const cols = Cols.map((col) =\u003e getValues(gameState, col));\n+     const diagonals = Diagonals.map((col) =\u003e getValues(gameState, col));\n  }\n\n```\nnow let's create a single array of all values in field\n\n📄 src/index.js\n```diff\n      const rows = Rows.map((row) =\u003e getValues(gameState, row));\n      const cols = Cols.map((col) =\u003e getValues(gameState, col));\n      const diagonals = Diagonals.map((col) =\u003e getValues(gameState, col));\n+\n+     const values = [...rows, ...cols, ...diagonals];\n  }\n\n```\nand find if some chunk sum equals 0 or 3\n\n📄 src/index.js\n```diff\n      const diagonals = Diagonals.map((col) =\u003e getValues(gameState, col));\n\n      const values = [...rows, ...cols, ...diagonals];\n+\n+     let winner = -1;\n+\n+     values.forEach((chunk) =\u003e {\n+         const chunkSum = sum(chunk);\n+\n+         if (chunkSum === 0) {\n+             winner = 0;\n+             return;\n+         }\n+\n+         if (chunkSum === 3) {\n+             winner = 1;\n+             return;\n+         }\n+     });\n+\n+     return winner;\n  }\n\n```\n### Game loop\n\nNow let's describe a game loop.\nWe'll create a generator function to query `row` and `col` for each next turn from outside world.\nIf you are not familliar with generator functions – read [this](https://codeburst.io/what-are-javascript-generators-and-how-to-use-them-c6f2713fd12e) medium post\n\n📄 src/index.js\n```diff\n\n      return winner;\n  }\n+\n+ function* gameLoop(gameState) {\n+\n+ }\n\n```\nGenerator should execute until `getWinner` returns anything but `-1`.\n\n📄 src/index.js\n```diff\n  }\n\n  function* gameLoop(gameState) {\n+     let winner = -1;\n+\n+     while (winner \u003c 0) {\n\n+         winner = getWinner(gameState);\n+     }\n  }\n\n```\nit should also make a turn befor each `getWinner` call\n\n📄 src/index.js\n```diff\n      let winner = -1;\n\n      while (winner \u003c 0) {\n+         const [rowIndex, colIndex] = yield;\n+         turn(gameState, rowIndex, colIndex);\n\n          winner = getWinner(gameState);\n      }\n\n```\nNow let's test our `gameLoop`\n\nCreate a mock scenario of a game:\n\n📄 src/index.js\n```diff\n          winner = getWinner(gameState);\n      }\n  }\n+\n+ const turns = [\n+     [1, 1],\n+     [0, 1],\n+     [0, 0],\n+     [1, 2],\n+     [2, 2],\n+ ];\n\n```\nCreate a game generator object\n\n📄 src/index.js\n```diff\n      [1, 2],\n      [2, 2],\n  ];\n+\n+ const game = gameLoop(GameState);\n+ game.next();\n\n```\nIterate over game turns and pass each turn to generator\n\n📄 src/index.js\n```diff\n\n  const game = gameLoop(GameState);\n  game.next();\n+\n+ turns.forEach(turn =\u003e game.next(turn));\n\n```\nAfter execution of this scenario game generator should finish it execution.\nThis means that leading `.next()` call should return an object `{ value: undefined, done: true }`\n\n📄 src/index.js\n```diff\n  game.next();\n\n  turns.forEach(turn =\u003e game.next(turn));\n+\n+ console.log(game.next());\n\n```\nLet's check it with node.js\n\n```sh\nnode src/index.js\n{ value: undefined, done: true }\n```\n\nYay, it works!\n\n\n### Refactor time\n\nNow as a core of a game is ready let's start refactor our `index.js` and split it in several modules\n\nDrop testing code\n\n📄 src/index.js\n```diff\n      }\n  }\n\n- const turns = [\n-     [1, 1],\n-     [0, 1],\n-     [0, 0],\n-     [1, 2],\n-     [2, 2],\n- ];\n-\n  const game = gameLoop(GameState);\n  game.next();\n-\n- turns.forEach(turn =\u003e game.next(turn));\n-\n- console.log(game.next());\n\n```\nMove everything but `gameLoop` from `index.js` to `game-state.js`.\n\n📄 src/game-state.js\n```js\nconst GameState = {\n    currentPlayer: 0,\n    field: Array.from({ length: 9 }).fill(-3),\n}\n\nconst Rows = [\n    [0, 1, 2],\n    [3, 4, 5],\n    [6, 7, 8],\n];\n\nconst Cols = [\n    [0, 3, 6],\n    [1, 4, 7],\n    [6, 7, 8],\n];\n\nconst Diagonals = [\n    [0, 4, 8],\n    [2, 4, 6],\n];\n\nfunction changeCurrentPlayer(gameState) {\n    gameState.currentPlayer = 1 ^ gameState.currentPlayer;\n}\n\nfunction getArrayIndexFromRowAndCol(rowIndex, colIndex) {\n    return rowIndex * 3 + colIndex;\n}\n\nfunction turn(gameState, rowIndex, colIndex) {\n    const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);\n    const fieldValue = gameState.field[index];\n\n    if (fieldValue \u003e= 0) {\n        return;\n    }\n\n    gameState.field[index] = gameState.currentPlayer;\n    changeCurrentPlayer(gameState);\n}\n\nfunction sum(arr) {\n    return arr.reduce((a, b) =\u003e a + b, 0);\n}\n\nfunction getValues(gameState, indices) {\n    return indices.map(index =\u003e gameState.field[index]);\n}\n\nfunction getWinner(gameState) {\n    const rows = Rows.map((row) =\u003e getValues(gameState, row));\n    const cols = Cols.map((col) =\u003e getValues(gameState, col));\n    const diagonals = Diagonals.map((col) =\u003e getValues(gameState, col));\n\n    const values = [...rows, ...cols, ...diagonals];\n\n    let winner = -1;\n\n    values.forEach((chunk) =\u003e {\n        const chunkSum = sum(chunk);\n\n        if (chunkSum === 0) {\n            winner = 0;\n            return;\n        }\n\n        if (chunkSum === 3) {\n            winner = 1;\n            return;\n        }\n    });\n\n    return winner;\n}\n\n```\n📄 src/index.js\n```diff\n- const GameState = {\n-     currentPlayer: 0,\n-     field: Array.from({ length: 9 }).fill(-3),\n- }\n-\n- const Rows = [\n-     [0, 1, 2],\n-     [3, 4, 5],\n-     [6, 7, 8],\n- ];\n-\n- const Cols = [\n-     [0, 3, 6],\n-     [1, 4, 7],\n-     [6, 7, 8],\n- ];\n-\n- const Diagonals = [\n-     [0, 4, 8],\n-     [2, 4, 6],\n- ];\n-\n- function changeCurrentPlayer(gameState) {\n-     gameState.currentPlayer = 1 ^ gameState.currentPlayer;\n- }\n-\n- function getArrayIndexFromRowAndCol(rowIndex, colIndex) {\n-     return rowIndex * 3 + colIndex;\n- }\n-\n- function turn(gameState, rowIndex, colIndex) {\n-     const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);\n-     const fieldValue = GameState.field[index];\n-\n-     if (fieldValue \u003e= 0) {\n-         return;\n-     }\n-\n-     gameState.field[index] = gameState.currentPlayer;\n-     changeCurrentPlayer(gameState);\n- }\n-\n- function sum(arr) {\n-     return arr.reduce((a, b) =\u003e a + b, 0);\n- }\n-\n- function getValues(gameState, indices) {\n-     return indices.map(index =\u003e gameState.field[index]);\n- }\n-\n- function getWinner(gameState) {\n-     const rows = Rows.map((row) =\u003e getValues(gameState, row));\n-     const cols = Cols.map((col) =\u003e getValues(gameState, col));\n-     const diagonals = Diagonals.map((col) =\u003e getValues(gameState, col));\n-\n-     const values = [...rows, ...cols, ...diagonals];\n-\n-     let winner = -1;\n-\n-     values.forEach((chunk) =\u003e {\n-         const chunkSum = sum(chunk);\n-\n-         if (chunkSum === 0) {\n-             winner = 0;\n-             return;\n-         }\n-\n-         if (chunkSum === 3) {\n-             winner = 1;\n-             return;\n-         }\n-     });\n-\n-     return winner;\n- }\n-\n  function* gameLoop(gameState) {\n      let winner = -1;\n\n\n```\nExport everything `gameLoop` depends on\n\n📄 src/game-state.js\n```diff\n- const GameState = {\n+ export const GameState = {\n      currentPlayer: 0,\n      field: Array.from({ length: 9 }).fill(-3),\n  }\n      return rowIndex * 3 + colIndex;\n  }\n\n- function turn(gameState, rowIndex, colIndex) {\n+ export function turn(gameState, rowIndex, colIndex) {\n      const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);\n      const fieldValue = gameState.field[index];\n\n      return indices.map(index =\u003e gameState.field[index]);\n  }\n\n- function getWinner(gameState) {\n+ export function getWinner(gameState) {\n      const rows = Rows.map((row) =\u003e getValues(gameState, row));\n      const cols = Cols.map((col) =\u003e getValues(gameState, col));\n      const diagonals = Diagonals.map((col) =\u003e getValues(gameState, col));\n\n```\nand import it in `index.js`\n\n📄 src/index.js\n```diff\n+ import { GameState, getWinner, turn } from './game-state.js';\n+\n  function* gameLoop(gameState) {\n      let winner = -1;\n\n\n```\n### Rendering game state on canvas\n\nAdd canvas to `index.html`\n\n📄 index.html\n```diff\n  \u003c/head\u003e\n  \u003cbody\u003e\n      \u003cscript src=\"./src/index.js\" type=\"module\"\u003e\u003c/script\u003e\n+     \u003ccanvas\u003e\u003c/canvas\u003e\n  \u003c/body\u003e\n  \u003c/html\u003e\n\n```\nand get a reference to canvas with `querySelector`\n\n📄 src/index.js\n```diff\n\n  const game = gameLoop(GameState);\n  game.next();\n+\n+ const canvas = document.querySelector('canvas');\n\n```\nLet's make body full-height\n\n📄 index.html\n```diff\n      \u003cmeta charset=\"UTF-8\"\u003e\n      \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n      \u003ctitle\u003eTic Tac Toe\u003c/title\u003e\n+     \u003cstyle\u003e\n+     html, body {\n+         height: 100%;\n+     }\n+     \u003c/style\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n      \u003cscript src=\"./src/index.js\" type=\"module\"\u003e\u003c/script\u003e\n\n```\nand reset default margins\n\n📄 index.html\n```diff\n      html, body {\n          height: 100%;\n      }\n+\n+     body {\n+         margin: 0;\n+     }\n      \u003c/style\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n\n```\nSetup canvas size\n\n📄 src/index.js\n```diff\n  game.next();\n\n  const canvas = document.querySelector('canvas');\n+\n+ const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);\n+ canvas.width = size;\n+ canvas.height = size;\n\n```\nand get a 2d context\n\n📄 src/index.js\n```diff\n  const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);\n  canvas.width = size;\n  canvas.height = size;\n+\n+ const ctx = canvas.getContext('2d');\n\n```\nMove canvas setup code to separate file\n\n📄 src/canvas-setup.js\n```js\nexport function setupCanvas() {\n    const canvas = document.querySelector('canvas');\n\n    const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);\n    canvas.width = size;\n    canvas.height = size;\n\n    const ctx = canvas.getContext('2d');\n\n    return { canvas, ctx };\n}\n\n```\n📄 src/index.js\n```diff\n\n  const game = gameLoop(GameState);\n  game.next();\n-\n- const canvas = document.querySelector('canvas');\n-\n- const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);\n- canvas.width = size;\n- canvas.height = size;\n-\n- const ctx = canvas.getContext('2d');\n\n```\nand import it to `index.js`\n\n📄 src/index.js\n```diff\n  import { GameState, getWinner, turn } from './game-state.js';\n+ import { setupCanvas } from './canvas-setup.js';\n\n  function* gameLoop(gameState) {\n      let winner = -1;\n\n  const game = gameLoop(GameState);\n  game.next();\n+\n+ const { canvas, ctx } = setupCanvas();\n\n```\nNow let's create `render` function which will visualize the game state\n\n📄 src/renderer.js\n```js\n/**\n * @typedef GameState\n * @property {Number} currentPlayer\n * @property {Array\u003cnumber\u003e} field\n *\n * @param {HTMLCanvasElement} canvas\n * @param {CanvasRenderingContext2D} ctx\n * @param {GameState} gameState\n */\nexport function draw(canvas, ctx, gameState) {\n\n}\n\n```\nWe'll need to clear the whole canvas on each `render` call\n\n📄 src/renderer.js\n```diff\n   * @param {GameState} gameState\n   */\n  export function draw(canvas, ctx, gameState) {\n-\n+     ctx.clearRect(0, 0, canvas.width, canvas.height);\n  }\n\n```\nWe'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)\n\n📄 src/renderer.js\n```diff\n   */\n  export function draw(canvas, ctx, gameState) {\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n+\n+     ctx.lineWidth = 10;\n+     const cellSize = canvas.width / 3;\n+\n  }\n\n```\nAnd finally we rendered smth! 🎉\n\n📄 src/renderer.js\n```diff\n      ctx.lineWidth = 10;\n      const cellSize = canvas.width / 3;\n\n+     gameState.field.forEach((_, index) =\u003e {\n+         const top = Math.floor(index / 3) * cellSize;\n+         const left = index % 3 * cellSize;\n+\n+         ctx.strokeRect(top, left, cellSize, cellSize);\n+     });\n  }\n\n```\nTo see the result install `live-server`\n\n```sh\nnpm i -g live-server\nlive-server .\n```\n\n\nWait, what? Nothing rendered 😢\nThat's because we forgot to import and call `draw` function\n\n📄 src/index.js\n```diff\n  import { GameState, getWinner, turn } from './game-state.js';\n  import { setupCanvas } from './canvas-setup.js';\n+ import { draw } from './renderer.js';\n\n  function* gameLoop(gameState) {\n      let winner = -1;\n  game.next();\n\n  const { canvas, ctx } = setupCanvas();\n+ draw(canvas, ctx, GameState);\n\n```\nLet's make canvas a bit smaller to leave some space for other UI\n\n📄 src/canvas-setup.js\n```diff\n  export function setupCanvas() {\n      const canvas = document.querySelector('canvas');\n\n-     const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);\n+     const size = Math.min(document.body.offsetHeight, document.body.offsetWidth) * 0.8;\n      canvas.width = size;\n      canvas.height = size;\n\n\n```\nand add a css border to make all cell edges look the same\n\n📄 index.html\n```diff\n      body {\n          margin: 0;\n      }\n+\n+     canvas {\n+         border: 5px solid black;\n+     }\n      \u003c/style\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n\n```\nIt also looks weird in top-left corner, so align canvas to center with flex-box\n\n📄 index.html\n```diff\n      \u003cstyle\u003e\n      html, body {\n          height: 100%;\n+         display: flex;\n+         align-items: center;\n+         justify-content: center;\n      }\n\n      body {\n\n```\nSo, we've rendered game field cells.\nNow let's render `X` and `O` symbols\n\n📄 src/renderer.js\n```diff\n          ctx.strokeRect(top, left, cellSize, cellSize);\n      });\n  }\n+\n+ /**\n+  * @param {CanvasRenderingContext2D} ctx\n+  */\n+ function drawX(ctx, top, left, size) {\n+\n+ }\n\n```\nWe'll use `path` to render symbol both for `X` and `O`\n\n📄 src/renderer.js\n```diff\n   * @param {CanvasRenderingContext2D} ctx\n   */\n  function drawX(ctx, top, left, size) {\n+     ctx.beginPath();\n+\n+     ctx.closePath();\n+     ctx.stroke();\n\n  }\n\n```\nDraw a line from top-left to bottom-right\n\n📄 src/renderer.js\n```diff\n  function drawX(ctx, top, left, size) {\n      ctx.beginPath();\n\n+     ctx.moveTo(left, top);\n+     ctx.lineTo(left + size, top + size);\n+\n      ctx.closePath();\n      ctx.stroke();\n\n\n```\nDraw a line from top-right to bottom-left\n\n📄 src/renderer.js\n```diff\n      ctx.moveTo(left, top);\n      ctx.lineTo(left + size, top + size);\n\n+     ctx.moveTo(left + size, top);\n+     ctx.lineTo(left, top + size);\n+\n      ctx.closePath();\n      ctx.stroke();\n\n\n```\nRendering `O` is even more simple\n\n📄 src/renderer.js\n```diff\n\n      ctx.closePath();\n      ctx.stroke();\n+ }\n\n+ /**\n+  * @param {CanvasRenderingContext2D} ctx\n+  */\n+ function drawO(ctx, centerX, centerY, radius) {\n+     ctx.beginPath();\n+\n+     ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);\n+     ctx.closePath();\n+\n+     ctx.stroke();\n  }\n\n```\nAnd let's actually render X or O depending on a field value\n\n📄 src/renderer.js\n```diff\n      ctx.lineWidth = 10;\n      const cellSize = canvas.width / 3;\n\n-     gameState.field.forEach((_, index) =\u003e {\n+     gameState.field.forEach((value, index) =\u003e {\n          const top = Math.floor(index / 3) * cellSize;\n          const left = index % 3 * cellSize;\n\n          ctx.strokeRect(top, left, cellSize, cellSize);\n+\n+         if (value \u003c 0) {\n+             return;\n+         }\n+\n+         if (value === 0) {\n+             drawX(ctx, top, left, cellSize);\n+         } else {\n+             drawO(ctx, left + cellSize / 2, top + cellSize / 2, cellSize / 2);\n+         }\n      });\n  }\n\n\n```\nNothing rendered? That's correct, every field value is -2, so let's make some turns\n\n📄 src/index.js\n```diff\n  game.next();\n\n  const { canvas, ctx } = setupCanvas();\n+\n+ turn(GameState, 0, 1);\n+ turn(GameState, 1, 1);\n+ turn(GameState, 2, 0);\n+\n  draw(canvas, ctx, GameState);\n\n```\n📄 src/renderer.js\n```diff\n          }\n\n          if (value === 0) {\n-             drawX(ctx, top, left, cellSize);\n+             const margin = cellSize * 0.2;\n+             const size = cellSize * 0.6;\n+\n+             drawX(ctx, top + margin, left + margin, size);\n          } else {\n-             drawO(ctx, left + cellSize / 2, top + cellSize / 2, cellSize / 2);\n+             const radius = cellSize * 0.3;\n+             drawO(ctx, left + cellSize / 2, top + cellSize / 2, radius);\n          }\n      });\n  }\n\n```\n### Interactions\n\nEverything seems to be done, the only thing left – interactions.\nLet's start with cleanup:\n\n📄 src/index.js\n```diff\n\n  const { canvas, ctx } = setupCanvas();\n\n- turn(GameState, 0, 1);\n- turn(GameState, 1, 1);\n- turn(GameState, 2, 0);\n-\n  draw(canvas, ctx, GameState);\n\n```\nAdd click listener and calculate clicked row and col\n\n📄 src/index.js\n```diff\n  const { canvas, ctx } = setupCanvas();\n\n  draw(canvas, ctx, GameState);\n+\n+ canvas.addEventListener('click', ({ layerX, layerY }) =\u003e {\n+     const row = Math.floor(layerY / canvas.height * 100 / 33);\n+     const col = Math.floor(layerX / canvas.width * 100 / 33);\n+ });\n\n```\nPass row and col indices to game loop generator\n\n📄 src/index.js\n```diff\n  canvas.addEventListener('click', ({ layerX, layerY }) =\u003e {\n      const row = Math.floor(layerY / canvas.height * 100 / 33);\n      const col = Math.floor(layerX / canvas.width * 100 / 33);\n+\n+     game.next([row, col]);\n  });\n\n```\nand reflect game state changes on canvas\n\n📄 src/index.js\n```diff\n      const col = Math.floor(layerX / canvas.width * 100 / 33);\n\n      game.next([row, col]);\n+     draw(canvas, ctx, GameState);\n  });\n\n```\nNow let's congratulate a winner\n\n📄 src/index.js\n```diff\n\n          winner = getWinner(gameState);\n      }\n+\n+     setTimeout(() =\u003e {\n+         alert(`Congratulations, ${['X', 'O'][winner]}! You won!`);\n+     });\n  }\n\n  const game = gameLoop(GameState);\n\n```\nOh, we forgot to handle a draw! No worries. Let's add `isGameFinished` helper:\n\n📄 src/game-state.js\n```diff\n\n      return winner;\n  }\n+\n+ export function isGameFinished(gameState) {\n+     return gameState.field.every(f =\u003e f \u003e= 0);\n+ }\n\n```\nand call it on each iteration of a game loop\n\n📄 src/index.js\n```diff\n- import { GameState, getWinner, turn } from './game-state.js';\n+ import { GameState, getWinner, turn, isGameFinished } from './game-state.js';\n  import { setupCanvas } from './canvas-setup.js';\n  import { draw } from './renderer.js';\n\n  function* gameLoop(gameState) {\n      let winner = -1;\n\n-     while (winner \u003c 0) {\n+     while (winner \u003c 0 \u0026\u0026 !isGameFinished(gameState)) {\n          const [rowIndex, colIndex] = yield;\n          turn(gameState, rowIndex, colIndex);\n\n      }\n\n      setTimeout(() =\u003e {\n-         alert(`Congratulations, ${['X', 'O'][winner]}! You won!`);\n+         if (winner \u003c 0) {\n+             alert(`It's a draw`);\n+         } else {\n+             alert(`Congratulations, ${['X', 'O'][winner]}! You won!`);\n+         }\n      });\n  }\n\n\n```\n\n## LICENSE\n\n[WTFPL](http://www.wtfpl.net/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flesnitsky%2Ftic-tac-toe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flesnitsky%2Ftic-tac-toe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flesnitsky%2Ftic-tac-toe/lists"}