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
- Host: GitHub
- URL: https://github.com/lesnitsky/tic-tac-toe
- Owner: lesnitsky
- License: other
- Created: 2018-08-19T03:19:15.000Z (almost 8 years ago)
- Default Branch: master
- Last Pushed: 2018-08-19T03:57:58.000Z (almost 8 years ago)
- Last Synced: 2024-10-19T02:29:56.401Z (over 1 year ago)
- Topics: canvas2d, game, tutorial
- Language: JavaScript
- Homepage:
- Size: 32.2 KB
- Stars: 20
- Watchers: 4
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
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/)