├── .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 [![components](https://img.shields.io/bit/collection/total-components/joshk/tic-tac-toe-game.svg)](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 |
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 |
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 | } --------------------------------------------------------------------------------