├── .bitmap
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.scss
├── App.test.tsx
├── App.tsx
├── components
│ ├── Board
│ │ ├── Board.tsx
│ │ ├── index.ts
│ │ └── style.scss
│ ├── Game
│ │ ├── Game.tsx
│ │ ├── index.ts
│ │ └── style.scss
│ ├── PrimereactStyle
│ │ ├── Style.tsx
│ │ └── index.ts
│ ├── Square
│ │ ├── Square.tsx
│ │ ├── index.ts
│ │ └── style.scss
│ └── utils
│ │ ├── HaveEmptyCell
│ │ ├── have-empty-cell.spec.ts
│ │ ├── have-empty-cell.ts
│ │ └── index.ts
│ │ └── WinnerCalc
│ │ ├── index.ts
│ │ ├── winner-calc.spec.ts
│ │ └── winner-calc.ts
├── index.css
├── index.tsx
├── react-app-env.d.ts
└── serviceWorker.ts
├── tsconfig.json
├── tsconfig.spec.json
└── yarn.lock
/.bitmap:
--------------------------------------------------------------------------------
1 | /* THIS IS A BIT-AUTO-GENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. */
2 |
3 | {
4 | "joshk.tic-tac-toe-game/internal/primereact-style@0.1.2": {
5 | "files": [
6 | {
7 | "relativePath": "src/components/PrimereactStyle/Style.tsx",
8 | "test": false,
9 | "name": "Style.tsx"
10 | },
11 | {
12 | "relativePath": "src/components/PrimereactStyle/index.ts",
13 | "test": false,
14 | "name": "index.ts"
15 | }
16 | ],
17 | "mainFile": "src/components/PrimereactStyle/index.ts",
18 | "trackDir": "src/components/PrimereactStyle",
19 | "origin": "AUTHORED",
20 | "exported": true
21 | },
22 | "joshk.tic-tac-toe-game/utils/have-empty-cell@1.0.0": {
23 | "files": [
24 | {
25 | "relativePath": "src/components/utils/HaveEmptyCell/have-empty-cell.spec.ts",
26 | "test": true,
27 | "name": "have-empty-cell.spec.ts"
28 | },
29 | {
30 | "relativePath": "src/components/utils/HaveEmptyCell/have-empty-cell.ts",
31 | "test": false,
32 | "name": "have-empty-cell.ts"
33 | },
34 | {
35 | "relativePath": "src/components/utils/HaveEmptyCell/index.ts",
36 | "test": false,
37 | "name": "index.ts"
38 | }
39 | ],
40 | "mainFile": "src/components/utils/HaveEmptyCell/index.ts",
41 | "trackDir": "src/components/utils/HaveEmptyCell",
42 | "origin": "AUTHORED",
43 | "exported": true
44 | },
45 | "joshk.tic-tac-toe-game/square@1.0.0": {
46 | "files": [
47 | {
48 | "relativePath": "src/components/Square/Square.tsx",
49 | "test": false,
50 | "name": "Square.tsx"
51 | },
52 | {
53 | "relativePath": "src/components/Square/index.ts",
54 | "test": false,
55 | "name": "index.ts"
56 | },
57 | {
58 | "relativePath": "src/components/Square/style.scss",
59 | "test": false,
60 | "name": "style.scss"
61 | }
62 | ],
63 | "mainFile": "src/components/Square/index.ts",
64 | "trackDir": "src/components/Square",
65 | "origin": "AUTHORED",
66 | "exported": true
67 | },
68 | "joshk.tic-tac-toe-game/utils/winner-calc@1.0.1": {
69 | "files": [
70 | {
71 | "relativePath": "src/components/utils/WinnerCalc/index.ts",
72 | "test": false,
73 | "name": "index.ts"
74 | },
75 | {
76 | "relativePath": "src/components/utils/WinnerCalc/winner-calc.spec.ts",
77 | "test": true,
78 | "name": "winner-calc.spec.ts"
79 | },
80 | {
81 | "relativePath": "src/components/utils/WinnerCalc/winner-calc.ts",
82 | "test": false,
83 | "name": "winner-calc.ts"
84 | }
85 | ],
86 | "mainFile": "src/components/utils/WinnerCalc/index.ts",
87 | "trackDir": "src/components/utils/WinnerCalc",
88 | "origin": "AUTHORED",
89 | "exported": true
90 | },
91 | "joshk.tic-tac-toe-game/board@1.0.1": {
92 | "files": [
93 | {
94 | "relativePath": "src/components/Board/Board.tsx",
95 | "test": false,
96 | "name": "Board.tsx"
97 | },
98 | {
99 | "relativePath": "src/components/Board/index.ts",
100 | "test": false,
101 | "name": "index.ts"
102 | },
103 | {
104 | "relativePath": "src/components/Board/style.scss",
105 | "test": false,
106 | "name": "style.scss"
107 | }
108 | ],
109 | "mainFile": "src/components/Board/index.ts",
110 | "trackDir": "src/components/Board",
111 | "origin": "AUTHORED",
112 | "exported": true
113 | },
114 | "joshk.tic-tac-toe-game/game@1.0.1": {
115 | "files": [
116 | {
117 | "relativePath": "src/components/Game/Game.tsx",
118 | "test": false,
119 | "name": "Game.tsx"
120 | },
121 | {
122 | "relativePath": "src/components/Game/index.ts",
123 | "test": false,
124 | "name": "index.ts"
125 | },
126 | {
127 | "relativePath": "src/components/Game/style.scss",
128 | "test": false,
129 | "name": "style.scss"
130 | }
131 | ],
132 | "mainFile": "src/components/Game/index.ts",
133 | "trackDir": "src/components/Game",
134 | "origin": "AUTHORED",
135 | "exported": true
136 | },
137 | "version": "14.1.0"
138 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 | .vscode
11 |
12 | # production
13 | /build
14 | /dist
15 |
16 | # misc
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Josh Kuttler
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Modular Tic Tac Toe Game built with TypeScript and tested with Mocha [](https://bit.dev/joshk/tic-tac-toe-game)
2 |
3 | A simple Tic Tac Toe game build with TypeScript components and test with Mocha tester then shared them to [bit](https://bit.dev/joshk/tic-tac-toe-game) for testing in the live PlayGround and see the result of tests runnig in bit.
4 | Allow users to consume the entire game or just a part of the game components using NPM and Yarn or using bit to consume and modify the component directly inside the project.
5 |
6 | The game has multiple options to modify the game rules, like the dynamic dimension of the table, and the number of matching value to win the game.
7 |
8 | Try the game in live PlayGround in the project [collection](https://bit.dev/joshk/tic-tac-toe-game)
9 |
10 |
11 |
12 |
13 |
14 | ## Tutorial
15 |
16 | See the full tutorial- build your own modular application with React TypeScript components.
17 |
18 | **[Build a Tic Tac Toe App with TypeScript, React and Mocha](https://blog.bitsrc.io/build-a-tic-tac-toe-game-with-typescript-react-and-mocha-ce6f1e74c996)**.
19 |
20 | ## Import and use the entire game component in few seconds with NPM, Yarn and bit
21 | To install components from this project, first configure [bit.dev](https://bit.dev) as a scoped registry (one-time action).
22 | ```
23 | npm config set '@bit:registry' https://node.bit.dev
24 |
25 | npm i @bit/joshk.tic-tac-toe-game.game
26 | yarn add @bit/joshk.tic-tac-toe-game.game
27 | bit import joshk.tic-tac-toe-game/game
28 | ```
29 |
30 | Then import the component in your app:
31 | ```
32 | import React from 'react';
33 | import Game from '@bit/joshk.tic-tac-toe-game.game';
34 |
35 | export default
36 | ```
37 |
38 | That's it!
39 |
40 | ## Button and input text
41 |
42 | These components is used from [bit](https://bit.dev) to work less, and not create from scratch.
43 |
44 | ### PrimeReact
45 | [primereact input text component](https://bit.dev/primefaces/primereact/inputtext)
46 | [primereact button component](https://bit.dev/primefaces/primereact/button)
47 |
48 | ### to install them in your project
49 |
50 | ```bash
51 | yarn add @bit/primefaces.primereact.inputtext
52 | yarn add @bit/primefaces.primereact.button
53 | ```
54 |
55 | ## Contributing
56 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
57 |
58 | Please make sure to update tests as appropriate.
59 |
60 | ## License
61 | [MIT](https://choosealicense.com/licenses/mit/)
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tic-tac-toe-game-using-bit",
3 | "version": "1.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "@bit/primefaces.primereact.button": "^3.1.0",
7 | "@bit/primefaces.primereact.inputtext": "^3.1.0",
8 | "node-sass": "^4.12.0",
9 | "react-dom": "^16.8.6",
10 | "react-scripts": "3.0.1",
11 | "yarn": "^1.22.0"
12 | },
13 | "peerDependencies": {
14 | "react": "^16.8.6"
15 | },
16 | "devDependencies": {
17 | "@types/chai": "^4.1.7",
18 | "@types/mocha": "^5.2.6",
19 | "@types/node": "12.0.0",
20 | "@types/react": "16.8.17",
21 | "@types/react-dom": "16.8.4",
22 | "chai": "^4.2.0",
23 | "mocha": "^6.1.4",
24 | "ts-mocha": "^6.0.0",
25 | "ts-node": "^8.1.0",
26 | "typescript": "3.4.5"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "ts-mocha -p tsconfig.spec.json src/components/**/*.spec.ts",
32 | "eject": "react-scripts eject"
33 | },
34 | "eslintConfig": {
35 | "extends": "react-app"
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | },
49 | "bit": {
50 | "env": {
51 | "compiler": "bit.envs/compilers/react-typescript@3.0.1",
52 | "tester": "bit.envs/testers/mocha@5.0.2"
53 | },
54 | "componentsDefaultDirectory": "components/{name}",
55 | "packageManager": "npm"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JoshK2/tic-tac-toe-game-using-bit/aba609ac89708bb2c1db1bbdb0323746668ae0c5/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
1 | .App {
2 | width: 100%;
3 | height: 100%;
4 | background-color: #ffffff;
5 | }
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.scss';
3 | import Game from './components/Game';
4 |
5 | const App: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/src/components/Board/Board.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Game from '../Game'
3 | import Square from '../Square'
4 | import winnerCalc from '../utils/WinnerCalc'
5 | import './style.scss'
6 | import PrimereactStyle from '../PrimereactStyle'
7 | import { Button } from '@bit/primefaces.primereact.button'
8 |
9 | type State = { matrix: Array>, turn: string, winner: string, restart: boolean }
10 | type Props = {
11 | /** number of rows in 2d array */
12 | rows: number,
13 | /** number of columns in 2d array */
14 | cols: number,
15 | /** number of matching value to win */
16 | numToWin: number
17 | }
18 |
19 | /**
20 | * @description
21 | * Board generate dynamic 2d array by props, and manage player turn and check fo winner.
22 | * @example
23 | * import React from 'react';
24 | * import Board from '@bit/joshk.tic-tac-toe-game.board';
25 | *
26 | * export default (
27 | *
28 | * )
29 | */
30 | class Board extends Component {
31 | state = {
32 | matrix: new Array(this.props.rows).fill(null).map(item => (new Array(this.props.cols).fill(null))),
33 | turn: 'X',
34 | winner: '',
35 | restart: false
36 | }
37 |
38 | createBoard = () => {
39 | let board = [];
40 | let matrix = this.state.matrix;
41 | for (let r = 0; r < this.props.rows; r++) {
42 | let row = [];
43 | for (let c = 0; c < this.props.cols; c++) {
44 | row.push();
45 | }
46 | board.push({row}
);
47 | }
48 | return {board}
49 | }
50 |
51 | handleSetValue = (lastRow: number, lastCol: number) => {
52 | let { matrix, turn } = this.state;
53 | matrix[lastRow][lastCol] = turn;
54 | const { rows, cols, numToWin } = this.props;
55 | const winner: string = winnerCalc(matrix, rows, cols, numToWin, lastRow, lastCol);
56 | console.log(`the winner is: ${winner}`);
57 | this.setState({ matrix: matrix, turn: turn === 'X' ? 'O' : 'X', winner: winner });
58 | }
59 |
60 | restartGame = () => {
61 | this.setState({ restart: true });
62 | }
63 |
64 | render() {
65 | console.log(this.state.matrix);
66 | const { turn, winner, restart } = this.state;
67 | if (restart)
68 | return
69 | let status: any = `Next player: ${turn}`;
70 | if (winner !== '') {
71 | if (winner === '-1') {
72 | status = `Draw!`;
73 | } else if (winner === 'X' || winner === 'O') {
74 | status = `The winner is ${winner}`;
75 | }
76 | }
77 | return (
78 |
79 |
{status}
80 | {this.createBoard()}
81 |
82 |
83 |
84 | )
85 | }
86 | }
87 |
88 | export default Board
--------------------------------------------------------------------------------
/src/components/Board/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Board'
--------------------------------------------------------------------------------
/src/components/Board/style.scss:
--------------------------------------------------------------------------------
1 | .board {
2 | display: grid;
3 | margin: auto;
4 | margin-top: 20px;
5 | .status {
6 | text-align: center;
7 | font-weight: bold;
8 | font-size: 28px;
9 | margin-bottom: 10px;
10 | }
11 | .rows-holder {
12 | margin: auto;
13 | }
14 | .row {
15 | float: left;
16 | width: auto;
17 | }
18 | .restart {
19 | width: 80px;
20 | margin: auto;
21 | margin-top: 20px;
22 | }
23 | }
--------------------------------------------------------------------------------
/src/components/Game/Game.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Board from '../Board'
3 | import './style.scss'
4 | import PrimereactStyle from '../PrimereactStyle'
5 | import { InputText } from '@bit/primefaces.primereact.inputtext'
6 | import { Button } from '@bit/primefaces.primereact.button'
7 |
8 | type State = { rows: number, columns: number, minToWin: number, goodValues: boolean }
9 | type Props = {}
10 |
11 | /**
12 | * @description
13 | * Game component is the main component of the game with configuration options like rows, columns, match number to win.
14 | * After config set, click Play and enjoy.
15 | * @example
16 | * import React from 'react';
17 | * import Game from '@bit/joshk.tic-tac-toe-game.game';
18 | *
19 | * export default (
20 | *
21 | * )
22 | */
23 | class Game extends Component {
24 | state = {
25 | rows: 0,
26 | columns: 0,
27 | minToWin: 0,
28 | goodValues: false
29 | }
30 |
31 | StartGame() {
32 | return <>>
33 | }
34 |
35 | ConfigGame() {
36 | return (
37 |
38 |
39 |
Set rows and columns number to start playing
40 |
41 |
42 | this.SetValue('rows', (e.target as HTMLTextAreaElement).value)}
49 | />
50 |
51 |
52 |
53 | this.SetValue('columns', (e.target as HTMLTextAreaElement).value)}
60 | />
61 |
62 |
63 |
64 | this.SetValue('minToWin', (e.target as HTMLTextAreaElement).value)}
71 | />
72 |
73 |
74 |
79 |
80 |
81 |
82 |
83 | )
84 | }
85 |
86 | PlayBtnClick = () => {
87 | const { rows, columns, minToWin } = this.state;
88 | const max = rows > columns ? rows : columns;
89 | if (rows > 0 && columns > 0 && minToWin > 0 && minToWin <= max)
90 | this.setState({ goodValues: true });
91 | }
92 |
93 | SetValue = (key: string, value: string) => {
94 | this.setState({ [key]: value && parseInt(value[0]) !== 0 ? parseInt(value) : '' } as any);
95 | }
96 |
97 | render() {
98 | if (!this.state.goodValues) {
99 | return this.ConfigGame();
100 | } else {
101 | return this.StartGame();
102 | }
103 | }
104 | }
105 |
106 | export default Game
--------------------------------------------------------------------------------
/src/components/Game/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Game'
--------------------------------------------------------------------------------
/src/components/Game/style.scss:
--------------------------------------------------------------------------------
1 | .config {
2 | width: 650px;
3 | margin: auto;
4 | h3 {
5 | width: 100%;
6 | float: left;
7 | }
8 | .input-value {
9 | width: 100%;
10 | margin-top: 20px;
11 | float: left;
12 | .p-float-label, .p-button-raised {
13 | float: left;
14 | margin-left: 10px;
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/components/PrimereactStyle/Style.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class Style extends Component {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | )
12 | }
13 | }
14 |
15 | export default Style
--------------------------------------------------------------------------------
/src/components/PrimereactStyle/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Style'
--------------------------------------------------------------------------------
/src/components/Square/Square.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import './style.scss'
3 |
4 | type State = { clicked: boolean }
5 | type Props = {
6 | /** row value in 2d array to be returned when is clicked */
7 | row: number,
8 | /** column value in 2d array to be returned when is clicked */
9 | col: number,
10 | /** function that called to send row and column values to update value in 2d array */
11 | setValue: Function,
12 | /** value to set (X/O) */
13 | value: string,
14 | /** disable the square */
15 | disable: boolean,
16 | /** optional hex color to override X and O color */
17 | color?: string
18 | }
19 |
20 | /**
21 | * @description
22 | * Square is a cell in the main 2d array, and can receive any value and optional color.
23 | * @example
24 | * import React from 'react';
25 | * import Square from '@bit/joshk.tic-tac-toe-game.square';
26 | *
27 | * export default (
28 | *
29 | * )
30 | */
31 | class Square extends Component {
32 | state = {
33 | clicked: false
34 | }
35 |
36 | componentWillReceiveProps(nextProps: any) {
37 | if (nextProps.disable === true) {
38 | this.setState({ clicked: true });
39 | }
40 | }
41 |
42 | handleClick = () => {
43 | if (!this.state.clicked) {
44 | this.setState({ clicked: true });
45 | this.props.setValue(this.props.row, this.props.col);
46 | }
47 | }
48 |
49 | render() {
50 | const { value, color } = this.props;
51 | return (
52 | {value}
53 | )
54 | }
55 | }
56 |
57 | export default Square
--------------------------------------------------------------------------------
/src/components/Square/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Square'
--------------------------------------------------------------------------------
/src/components/Square/style.scss:
--------------------------------------------------------------------------------
1 | .square {
2 | width: 50px;
3 | height: 50px;
4 | margin: 3px;
5 | border: 1px #ccc solid;
6 | border-radius: 3px;
7 | float: left;
8 | cursor: pointer;
9 | text-align: center;
10 | span {
11 | font-size: 40px;
12 | text-align: center;
13 | line-height: 1.2;
14 | }
15 | &.O span {
16 | color: #0808c1;
17 | }
18 | &.X span {
19 | color: #e00707;
20 | }
21 | }
--------------------------------------------------------------------------------
/src/components/utils/HaveEmptyCell/have-empty-cell.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import haveEmptyCell from './have-empty-cell'
3 |
4 | describe('haveEmptyCell cell function', () => {
5 | it('should return false', () => {
6 | const matrix = [
7 | ['X', 'O', 'X'],
8 | ['O', 'X', 'O'],
9 | ['O', 'X', 'O']
10 | ];
11 | const result = haveEmptyCell(matrix, 3, 3);
12 | expect(result).to.equal(false);
13 | });
14 | it('should return true', () => {
15 | const matrix = [
16 | ['O', 'O', 'X'],
17 | ['X', 'X', ''],
18 | ['', '', '']
19 | ];
20 | const result = haveEmptyCell(matrix, 3, 3);
21 | expect(result).to.equal(true);
22 | });
23 | });
--------------------------------------------------------------------------------
/src/components/utils/HaveEmptyCell/have-empty-cell.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description
3 | * check if 2d array have an empty cell
4 | * @param {{Array.}} matrix 2d array
5 | * @param {number} rowsNum number of rows
6 | * @param {number} colsNum number of columns
7 | * @returns {boolean} return true if empty cell was found, and false if not.
8 | * @example
9 | * import haveEmptyCell from '@bit/joshk.tic-tac-toe-game.utils.have-empty-cell';
10 | *
11 | * const matrix = [
12 | * ['X', 'O', 'X'],
13 | * ['O', 'X', 'O'],
14 | * ['O', 'X', 'O']
15 | * ];
16 | * const result = haveEmptyCell(matrix, 3, 3);
17 | *
18 | * export default result
19 | * @example
20 | * import haveEmptyCell from '@bit/joshk.tic-tac-toe-game.utils.have-empty-cell';
21 | *
22 | * const matrix = [
23 | * ['X', 'O', 'X'],
24 | * ['O', '', 'O'],
25 | * ['O', 'X', 'O']
26 | * ];
27 | * const result = haveEmptyCell(matrix, 3, 3);
28 | *
29 | * export default result
30 | * @example
31 | * import haveEmptyCell from '@bit/joshk.tic-tac-toe-game.utils.have-empty-cell';
32 | *
33 | * const matrix = [
34 | * ['X', 'O', 'X'],
35 | * ['O', , 'O'],
36 | * ['O', 'X', 'O']
37 | * ];
38 | * const result = haveEmptyCell(matrix, 3, 3);
39 | *
40 | * export default result
41 | * @example
42 | * import haveEmptyCell from '@bit/joshk.tic-tac-toe-game.utils.have-empty-cell';
43 | *
44 | * const matrix = [
45 | * ['X', 'O', 'X'],
46 | * ['O', null, 'O'],
47 | * ['O', 'X', 'O']
48 | * ];
49 | * const result = haveEmptyCell(matrix, 3, 3);
50 | *
51 | * export default result
52 | */
53 |
54 | function haveEmptyCell(matrix: Array>, rowsNum: number, colsNum: number): boolean {
55 | let empty: boolean = false;
56 | for (let x = 0; x < rowsNum; x++) {
57 | for (let y = 0; y < colsNum; y++) {
58 | const element: any = matrix[x][y];
59 | if (!element) {
60 | empty = true;
61 | break;
62 | }
63 | }
64 | if (empty)
65 | break;
66 | }
67 | return empty;
68 | }
69 |
70 | export default haveEmptyCell
--------------------------------------------------------------------------------
/src/components/utils/HaveEmptyCell/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './have-empty-cell'
--------------------------------------------------------------------------------
/src/components/utils/WinnerCalc/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './winner-calc'
--------------------------------------------------------------------------------
/src/components/utils/WinnerCalc/winner-calc.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import winnerCalc from './winner-calc'
3 |
4 | describe('Winner check function', () => {
5 | it('should return draw', () => {
6 | const matrix = [
7 | ['X', 'O', 'X'],
8 | ['O', 'X', 'O'],
9 | ['O', 'X', 'O']
10 | ];
11 | const result = winnerCalc(matrix, 3, 3, 3, 0, 0);
12 | expect(result).to.equal('-1');
13 | });
14 | it('should return no winner', () => {
15 | const matrix = [
16 | ['O', 'O', 'X'],
17 | ['X', 'X', ''],
18 | ['', '', '']
19 | ];
20 | const result = winnerCalc(matrix, 3, 3, 3, 0, 0);
21 | expect(result).to.equal('');
22 | });
23 | it('should return winner in horizontal first row', () => {
24 | const matrix = [
25 | ['O', 'O', 'O'],
26 | ['X', 'X', ''],
27 | ['X', '', '']
28 | ];
29 | const result = winnerCalc(matrix, 3, 3, 3, 0, 0);
30 | expect(result).to.equal('O');
31 | });
32 | it('should return winner in horizontal second row', () => {
33 | const matrix = [
34 | ['O', 'X', 'X'],
35 | ['O', 'O', 'O'],
36 | ['X', '', 'X']
37 | ];
38 | const result = winnerCalc(matrix, 3, 3, 3, 1, 2);
39 | expect(result).to.equal('O');
40 | });
41 | it('should return winner in horizontal third row', () => {
42 | const matrix = [
43 | ['O', 'X', 'X'],
44 | ['X', 'X', 'O'],
45 | ['O', 'O', 'O']
46 | ];
47 | const result = winnerCalc(matrix, 3, 3, 3, 2, 2);
48 | expect(result).to.equal('O');
49 | });
50 | it('should return winner in vertical fisrt row', () => {
51 | const matrix = [
52 | ['O', 'X', 'X'],
53 | ['O', 'X', 'O'],
54 | ['O', 'O', 'X']
55 | ];
56 | const result = winnerCalc(matrix, 3, 3, 3, 0, 0);
57 | expect(result).to.equal('O');
58 | });
59 | it('should return winner in vertical second row', () => {
60 | const matrix = [
61 | ['X', 'O', 'X'],
62 | ['X', 'O', 'O'],
63 | ['O', 'O', 'X']
64 | ];
65 | const result = winnerCalc(matrix, 3, 3, 3, 0, 1);
66 | expect(result).to.equal('O');
67 | });
68 | it('should return winner in vertical third row', () => {
69 | const matrix = [
70 | ['X', 'O', 'O'],
71 | ['X', 'X', 'O'],
72 | ['O', 'X', 'O']
73 | ];
74 | const result = winnerCalc(matrix, 3, 3, 3, 0, 2);
75 | expect(result).to.equal('O');
76 | });
77 | it('should return winner in diagonal bottom right - top left', () => {
78 | const matrix = [
79 | ['O', 'X', 'X'],
80 | ['X', 'O', 'O'],
81 | ['O', 'X', 'O']
82 | ];
83 | const result = winnerCalc(matrix, 3, 3, 3, 0, 0);
84 | expect(result).to.equal('O');
85 | });
86 | it('should return winner in diagonal bottom left - top right', () => {
87 | const matrix = [
88 | ['X', 'O', 'O'],
89 | ['X', 'O', 'X'],
90 | ['O', 'X', 'O']
91 | ];
92 | const result = winnerCalc(matrix, 3, 3, 3, 2, 0);
93 | expect(result).to.equal('O');
94 | });
95 | it('should return winner in diagonal', () => {
96 | const matrix = [
97 | ['', '', '', 'X', 'O', '', '', ''],
98 | ['', '', 'X', 'O', '', '', '', ''],
99 | ['', 'X', 'O', 'O', '', '', '', ''],
100 | ['X', '', '', '', '', '', '', ''],
101 | ['', '', '', '', '', '', '', ''],
102 | ['', '', '', '', '', '', '', ''],
103 | ['', '', '', '', '', '', '', ''],
104 | ['', '', '', '', '', '', '', '']
105 | ];
106 | const result = winnerCalc(matrix, 8, 8, 4, 3, 0);
107 | expect(result).to.equal('X');
108 | });
109 | });
--------------------------------------------------------------------------------
/src/components/utils/WinnerCalc/winner-calc.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description
3 | * check winner horizontal, vertical and diagonal
4 | * @param {Array.} matrix 2d array with X and O
5 | * @param {number} rowsNum number of rows
6 | * @param {number} colsNum number of columns
7 | * @param {number} numToWin the number of matching to win
8 | * @param {number} lastRow the row number of the square player click
9 | * @param {number} lastCol the column number of the square player click
10 | * @returns {string} return the winner, X or O or '' if no one win.
11 | * @example
12 | * import winnerCalc from '@bit/joshk.tic-tac-toe-game.utils.winner-calc';
13 | *
14 | * const matrix = [
15 | * ['O', 'O', 'X'],
16 | * ['O', 'X', ''],
17 | * ['X', '', '']
18 | * ];
19 | * const result = winnerCalc(matrix, 3, 3, 3, 0, 2);
20 | *
21 | * export default result
22 | */
23 |
24 | import haveEmptyCell from '../HaveEmptyCell'
25 |
26 | function winnerCalc(matrix: Array>, rowsNum: number, colsNum: number, numToWin: number, lastRow: number, lastCol: number): string {
27 | let winner: string = '';
28 | let match: number = 0;
29 | const lastValue: string = matrix[lastRow][lastCol];
30 |
31 | //check Horizontal
32 | for (let c = 0; c < colsNum; c++) {
33 | let currentValue = matrix[lastRow][c];
34 | if (currentValue === lastValue)
35 | match++;
36 | else match = 0;
37 | if (match === numToWin) {
38 | winner = lastValue;
39 | break;
40 | }
41 | }
42 | if (winner !== '')
43 | return winner;
44 |
45 | match = 0;
46 | //check Vertical
47 | for (let r = 0; r < rowsNum; r++) {
48 | let currentValue = matrix[r][lastCol];
49 | if (currentValue === lastValue)
50 | match++;
51 | else match = 0;
52 | if (match === numToWin) {
53 | winner = lastValue;
54 | break;
55 | }
56 | }
57 | if (winner !== '')
58 | return winner;
59 |
60 | //check diagonal top-left to bottom-right - include middle
61 | match = 0;
62 | for (let r = 0; r <= rowsNum - numToWin; r++)
63 | {
64 | let rowPosition = r;
65 | for (let column = 0; column < colsNum && rowPosition < rowsNum; column++)
66 | {
67 | const currentValue = matrix[rowPosition][column];
68 | if (currentValue === lastValue)
69 | match++;
70 | else match = 0;
71 | if (match === numToWin)
72 | {
73 | winner = lastValue;
74 | break;
75 | }
76 | rowPosition++;
77 | }
78 | if (winner !== '') break;
79 | }
80 | if (winner !== '')
81 | return winner;
82 |
83 | //check diagonal top-left to bottom-right - after middle
84 | match = 0;
85 | for (let c = 1; c <= colsNum - numToWin; c++)
86 | {
87 | let columnPosition = c;
88 | for (let row = 0; row < rowsNum && columnPosition < colsNum; row++)
89 | {
90 | let currentValue = matrix[row][columnPosition];
91 | if (currentValue === lastValue)
92 | match++;
93 | else match = 0;
94 | if (match === numToWin)
95 | {
96 | winner = lastValue;
97 | break;
98 | }
99 | columnPosition++;
100 | }
101 | if (winner !== '') break;
102 | }
103 | if (winner !== '')
104 | return winner;
105 |
106 | //check diagonal bottom-left to top-right - include middle
107 | match = 0;
108 | for (let r = rowsNum - 1; r >= rowsNum - numToWin - 1; r--)
109 | {
110 | let rowPosition = r;
111 | for (let column = 0; column < colsNum && rowPosition < rowsNum && rowPosition >= 0; column++)
112 | {
113 | let currentValue = matrix[rowPosition][column];
114 | if (currentValue === lastValue)
115 | match++;
116 | else match = 0;
117 | if (match === numToWin)
118 | {
119 | winner = lastValue;
120 | break;
121 | }
122 | rowPosition--;
123 | }
124 | if (winner !== '') break;
125 | }
126 | if (winner !== '')
127 | return winner;
128 |
129 | //check diagonal bottom-left to top-right - after middle
130 | match = 0;
131 | for (let c = 1; c < colsNum; c++)
132 | {
133 | let columnPosition = c;
134 | for (let row = rowsNum - 1; row < rowsNum && row >= 0 && columnPosition < colsNum && columnPosition >= 1; row--)
135 | {
136 | console.log(`[${row}][${columnPosition}]`);
137 | let currentValue = matrix[row][columnPosition];
138 | if (currentValue === lastValue)
139 | match++;
140 | else match = 0;
141 | if (match === numToWin)
142 | {
143 | winner = lastValue;
144 | break;
145 | }
146 | columnPosition++;
147 | }
148 | if (winner !== '') break;
149 | }
150 | if (winner !== '')
151 | return winner;
152 |
153 | if(haveEmptyCell(matrix, rowsNum, colsNum) === false) {
154 | winner = '-1';
155 | }
156 |
157 | return winner;
158 | }
159 |
160 | export default winnerCalc
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then(response => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type');
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then(registration => {
121 | registration.unregister().then(() => {
122 | window.location.reload();
123 | });
124 | });
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config);
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.'
133 | );
134 | });
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then(registration => {
140 | registration.unregister();
141 | });
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "preserve"
21 | },
22 | "include": [
23 | "src"
24 | ],
25 | "exclude": [
26 | "node_modules"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------