An open API service indexing awesome lists of open source software.

https://github.com/lesnitsky/tic-tac-toe

An example tutorial built with git-tutor https://github.com/lesnitsky/git-tutor
https://github.com/lesnitsky/tic-tac-toe

canvas2d game tutorial

Last synced: 10 months ago
JSON representation

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

Awesome Lists containing this project

README

          

# Tic Tac Toe

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

> Built with [Git Tutor](https://github.com/R1ZZU/git-tutor)

## Project setup

Before 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

📄 .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
```html



Tic Tac Toe

```
`index.js` will be a js main file

📄 src/index.js
```js
console.log('Hello world');

```
Now we need to add `script` to `index.html`

📄 index.html
```diff
Tic Tac Toe


-
+


-
+


+

```
and reset default margins

📄 index.html
```diff
html, body {
height: 100%;
}
+
+ body {
+ margin: 0;
+ }


```
Setup canvas size

📄 src/index.js
```diff
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
```diff
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
```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
```diff

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
```diff
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
```js
/**
* @typedef GameState
* @property {Number} currentPlayer
* @property {Array} 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
```diff
* @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
```diff
*/
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
```diff
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`

```sh
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
```diff
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
```diff
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
```diff
body {
margin: 0;
}
+
+ canvas {
+ border: 5px solid black;
+ }


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

📄 index.html
```diff

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
```diff
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
```diff
* @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
```diff
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
```diff
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
```diff

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
```diff
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
```diff
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
```diff
}

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
```diff

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
```diff
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
```diff
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
```diff
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
```diff

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
```diff

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
```diff
- 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](http://www.wtfpl.net/)