├── src ├── symbols │ └── symbols.js ├── index.css ├── actions │ └── actions.js ├── components │ ├── BlankSymbol.test.js │ ├── App.test.js │ ├── App.js │ ├── OSymbol.js │ ├── BlankSymbol.js │ ├── XSymbol.js │ ├── Result.test.js │ ├── App.css │ ├── Result.js │ ├── Board.test.js │ └── Board.js ├── index.js ├── reducers │ ├── gameReducer.test.js │ └── gameReducer.js ├── logic │ ├── logic.js │ └── logic.test.js ├── logo.svg └── registerServiceWorker.js ├── public ├── favicon.ico ├── manifest.json └── index.html ├── .gitignore ├── README.md ├── package.json └── .travis.yml /src/symbols/symbols.js: -------------------------------------------------------------------------------- 1 | export const X = 'x'; 2 | export const O = 'o'; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belfz/tic-tac-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | } 10 | -------------------------------------------------------------------------------- /src/actions/actions.js: -------------------------------------------------------------------------------- 1 | export const addSymbol = (row, position, symbol) => ({ 2 | type: 'ADD_SYMBOL', 3 | symbol, 4 | row, 5 | position 6 | }); 7 | 8 | export const startAgain = () => ({ 9 | type: 'START_AGAIN' 10 | }); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Tic-Tac-React", 3 | "name": "Tic-Tac-React", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tic-tac-react 2 | 3 | [![Build Status](https://travis-ci.org/belfz/tic-tac-react.svg?branch=master)](https://travis-ci.org/belfz/tic-tac-react) 4 | 5 | A tic-tac-toe game (for two players) implemented with react and redux. 6 | [Click here to play!](https://belfz.github.io/tic-tac-react/) 7 | 8 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 9 | -------------------------------------------------------------------------------- /src/components/BlankSymbol.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import BlankSymbol from './BlankSymbol'; 4 | 5 | it('Should call a passed addSymbol callback when it is clicked', () => { 6 | const addSymbol = jest.fn(); 7 | const wrapper = shallow(); 8 | wrapper.simulate('click'); 9 | expect(addSymbol.mock.calls.length).toBe(1); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import App from './App'; 4 | import Result from './Result'; 5 | import Board from './Board'; 6 | 7 | it('Should render an App component with Result and Board components', () => { 8 | const wrapper = shallow().dive(); 9 | expect(wrapper.find(Result).length).toBe(1); 10 | expect(wrapper.find(Board).length).toBe(1); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Board from './Board'; 3 | import Result from './Result'; 4 | import styled from 'styled-components'; 5 | import './App.css'; 6 | 7 | const App = ({className}) => { 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | } 15 | 16 | export default styled(App)` 17 | font-family: Courier New, Courier, monospace; 18 | margin: 0 auto; 19 | width: 200px; 20 | `; 21 | -------------------------------------------------------------------------------- /src/components/OSymbol.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Symbol } from './BlankSymbol'; 4 | 5 | const OSymbol = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | OSymbol.propTypes = { 16 | position: PropTypes.number.isRequired 17 | }; 18 | 19 | export default OSymbol; 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {Provider} from 'react-redux'; 4 | import {createStore} from 'redux'; 5 | import { initialState, gameReducer } from './reducers/gameReducer'; 6 | import App from './components/App'; 7 | import registerServiceWorker from './registerServiceWorker'; 8 | import './index.css'; 9 | 10 | const store = createStore(gameReducer, initialState); 11 | 12 | ReactDOM.render( 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | 19 | registerServiceWorker(); 20 | -------------------------------------------------------------------------------- /src/components/BlankSymbol.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | export const Symbol = styled.div` 6 | background-color: white; 7 | border: 1px solid black; 8 | height: 60px; 9 | margin: 1px; 10 | transition: background-color .5s ease; 11 | width: 60px; 12 | `; 13 | 14 | const BlankSymbol = (props) => { 15 | return props.addSymbol(props.turn)}>; 16 | }; 17 | 18 | BlankSymbol.propTypes = { 19 | addSymbol: PropTypes.func.isRequired 20 | }; 21 | 22 | export default BlankSymbol; 23 | -------------------------------------------------------------------------------- /src/components/XSymbol.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Symbol } from './BlankSymbol'; 4 | 5 | const XSymbol = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | XSymbol.propTypes = { 17 | position: PropTypes.number.isRequired 18 | }; 19 | 20 | export default XSymbol; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tic-tac-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "http://belfz.github.io/tic-tac-react", 6 | "devDependencies": { 7 | "enzyme": "^2.7.0", 8 | "gh-pages": "^0.12.0", 9 | "react-addons-test-utils": "^15.4.2", 10 | "react-scripts": "1.0.17" 11 | }, 12 | "dependencies": { 13 | "lodash": "^4.17.4", 14 | "prop-types": "^15.6.0", 15 | "react": "^15.4.2", 16 | "react-dom": "^15.4.2", 17 | "react-redux": "^5.0.1", 18 | "redux": "^3.6.0", 19 | "styled-components": "^2.3.3" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "deploy": "npm run build&&gh-pages -d build", 25 | "test": "react-scripts test --env=jsdom", 26 | "eject": "react-scripts eject" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Result.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { PureResult as Result } from './Result'; 4 | 5 | it('Should render the Result component with message about current turn', () => { 6 | const wrapper = shallow(); 7 | expect(wrapper.find('p').node.props.children).toEqual('It\'s O\'s turn.'); 8 | }); 9 | 10 | it('Should render the Result component with message about winning symbol', () => { 11 | const wrapper = shallow(); 12 | expect(wrapper.find('p').node.props.children).toEqual('Yay! X won!'); 13 | }); 14 | 15 | it('Should render the Result component with message about the draw', () => { 16 | const wrapper = shallow(); 17 | expect(wrapper.find('p').node.props.children).toEqual('We have a draw!'); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/App.css: -------------------------------------------------------------------------------- 1 | .board.won-row0 .row0 .symbol, 2 | .board.won-row1 .row1 .symbol, 3 | .board.won-row2 .row2 .symbol, 4 | .board.won-column0 .symbol.column0, 5 | .board.won-column1 .symbol.column1, 6 | .board.won-column2 .symbol.column2, 7 | .board.won-leftSlant .row0 .symbol.column0, 8 | .board.won-leftSlant .row1 .symbol.column1, 9 | .board.won-leftSlant .row2 .symbol.column2, 10 | .board.won-rightSlant .row0 .symbol.column2, 11 | .board.won-rightSlant .row1 .symbol.column1, 12 | .board.won-rightSlant .row2 .symbol.column0 { 13 | background-color: mediumseagreen; 14 | } 15 | 16 | .board.draw .symbol { 17 | background-color: palevioletred; 18 | } 19 | 20 | .row { 21 | display: flex; 22 | } 23 | 24 | 25 | @keyframes symbol-appear { 26 | from { 27 | transform: scale(1.9); 28 | } 29 | to { 30 | transform: scale(1); 31 | } 32 | } 33 | 34 | svg { 35 | animation-name: symbol-appear; 36 | animation-duration: .15s; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Result.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from 'react-redux'; 4 | 5 | class Result extends Component { 6 | render () { 7 | let result = ''; 8 | if (this.props.turn) { 9 | result = `It's ${this.props.turn.toUpperCase()}'s turn.`; 10 | } 11 | if (this.props.won) { 12 | result = `Yay! ${this.props.won.toUpperCase()} won!` 13 | } else if (this.props.draw) { 14 | result = 'We have a draw!'; 15 | } 16 | return ( 17 |
18 |

19 | {result} 20 |

21 |
22 | ); 23 | } 24 | } 25 | 26 | Result.propTypes = { 27 | won: PropTypes.string, 28 | turn: PropTypes.string.isRequired, 29 | draw: PropTypes.bool.isRequired 30 | }; 31 | 32 | export default connect( 33 | ({won, turn, draw}) => ({ 34 | won, turn, draw 35 | }) 36 | )(Result); 37 | 38 | export {Result as PureResult}; 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8.9.1 4 | before_script: 5 | - git config --global user.email "marcinbar1@gmail.com" 6 | - git config --global user.name "belfz" 7 | - git remote rm origin 8 | - git remote add origin https://user:${GH_TOKEN}@github.com/belfz/tic-tac-react.git 9 | after_success: 10 | - npm run deploy 11 | env: 12 | global: 13 | secure: LEgabKoDNd/LklOyTSwNpnS58uMMLFCF808dpaf1csUmSGsUIBxOXY8OT36ZTVH5dLI3LLTiUT+6rgZY/FqlmVgnL1m6f5GKyh1V6g2AWEOB1OJ0rVyzcpgbsB5G5zsBmfMUXr4LKbdnI05WtPH9i1lgUFk1D54v0+xFAnwGgkVkgufyLwI0ZQg590sKfnW3nOGMOfB6yhDnUALsJ0IdphxJGGGE8FGx/O3C2ecR593PfQYiGjj+DNBwcqC5zmt2JMSzxmVVuuloCF5GhvRoV7wZ/tmRzK3Rzz3cSbQ+TVpS78QOoijWaDp75fEVp2qE7jxqLGGRLAGAjm+urMc+0JJK9TKf1kGsNR94ll1ZqflomZJy5sDP+MvKHlMx8a3ZswfEVqOA5CHXCzrml2Ki+B2yHw97qU3mxMfT+LA+tWFxZzNz2sytDMzhtSoUlAzHCLL/ci5I0tDhkf7f7zMMwF6dmM4ZUBKVd9VtwKuZ/necX1r+F8L3ger7mTPfpzHJUfzUSfELECQwUfOgWHhR8OSTrPRV5z7jt/vvtsBm+/Qj8h9ryumvMDpkkxHpXJrMq0gtLK1z4rxigUCRUkxzE+0QNkaGSPTpYJHInozxNA+9qjJwaUyZU7rrAwNu5GVDaiZVHkHjLjtH5/YuvFwNvs5iFwRpHBrRd8g7euz6x+k= 14 | -------------------------------------------------------------------------------- /src/reducers/gameReducer.test.js: -------------------------------------------------------------------------------- 1 | import { initialState, gameReducer } from './gameReducer'; 2 | import { X, O } from '../symbols/symbols'; 3 | 4 | it('Should add a symbol at given position and change turn', () => { 5 | const state = { 6 | board: { 7 | 0: ['', '', ''], 8 | 1: ['', '', ''], 9 | 2: ['', '', ''] 10 | }, 11 | won: undefined, 12 | wonLine: undefined, 13 | draw: false, 14 | turn: O 15 | }; 16 | const nextState = gameReducer(state, {type: 'ADD_SYMBOL', symbol: O, row: 0, position: 0}); 17 | expect(nextState.board[0]).toEqual([O, '', '']); 18 | expect(nextState.turn).toEqual(X); 19 | }); 20 | 21 | it('Should set "won" symbol when a winning line is set', () => { 22 | const state = { 23 | board: { 24 | 0: [X, O, ''], 25 | 1: ['', X, ''], 26 | 2: [O, '', ''] 27 | }, 28 | won: undefined, 29 | wonLine: undefined, 30 | draw: false, 31 | turn: X 32 | }; 33 | const nextState = gameReducer(state, {type: 'ADD_SYMBOL', symbol: X, row: 2, position: 2}); 34 | expect(nextState.won).toEqual(X); 35 | }); 36 | 37 | it('Should reset the state to initial', () => { 38 | const state = { 39 | board: { 40 | 0: [X, O, ''], 41 | 1: ['', X, ''], 42 | 2: [O, '', ''] 43 | }, 44 | won: undefined, 45 | wonLine: undefined, 46 | draw: false, 47 | turn: X 48 | }; 49 | const nextState = gameReducer(state, {type: 'START_AGAIN'}); 50 | expect(nextState).toEqual(initialState); 51 | }); 52 | -------------------------------------------------------------------------------- /src/reducers/gameReducer.js: -------------------------------------------------------------------------------- 1 | import { X, O } from '../symbols/symbols'; 2 | import { resultForSymbol } from '../logic/logic'; 3 | import * as _ from 'lodash'; 4 | 5 | export const initialState = { 6 | board: { 7 | 0: ['', '', ''], 8 | 1: ['', '', ''], 9 | 2: ['', '', ''] 10 | }, 11 | won: undefined, 12 | wonLine: undefined, 13 | draw: false, 14 | turn: O 15 | }; 16 | 17 | export const gameReducer = (state, action) => { 18 | switch (action.type) { 19 | case 'ADD_SYMBOL': 20 | const {symbol, row, position} = action; 21 | const newState = _.cloneDeep(state); 22 | newState.board[row][position] = symbol; 23 | 24 | const xResult = resultForSymbol(X, newState.board); 25 | const oResult = resultForSymbol(O, newState.board); 26 | 27 | if (xResult.won) { 28 | newState.won = X; 29 | newState.wonLine = xResult.line; 30 | } 31 | 32 | if (oResult.won) { 33 | newState.won = O; 34 | newState.wonLine = oResult.line; 35 | } 36 | 37 | if (!newState.won) { 38 | newState.turn = newState.turn === O ? X : O; 39 | } 40 | 41 | const boardIsFull = [ 42 | ...newState.board[0], 43 | ...newState.board[1], 44 | ...newState.board[2] 45 | ] 46 | .filter(symbol => symbol !== '') 47 | .length === 9; 48 | 49 | if (boardIsFull && !newState.won) { 50 | newState.draw = true; 51 | } 52 | 53 | return newState; 54 | case 'START_AGAIN': 55 | return initialState; 56 | default: 57 | return state; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 |
26 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/logic/logic.js: -------------------------------------------------------------------------------- 1 | const countInRow = (symbol, row) => row.filter(el => el === symbol).length; 2 | const hasWonInRow = (symbol, row) => countInRow(symbol, row) === 3; 3 | export const hasThreatInRow = (symbol, row) => countInRow(symbol, row) === 2; 4 | 5 | const countInColumn = (symbol, colNumber, ...rows) => rows.map(row => row[colNumber]).filter(el => el === symbol).length; 6 | const hasWonInColumn = (symbol, colNumber, ...rows) => countInColumn(symbol, colNumber, ...rows) === 3; 7 | export const hasThreatInColumn = (symbol, colNumber, ...rows) => countInColumn(symbol, colNumber, ...rows) === 2; 8 | 9 | const countInLeftSlant = (symbol, ...rows) => { 10 | const [row0, row1, row2] = rows; 11 | return [row0[0], row1[1], row2[2]].filter(el => el === symbol).length; 12 | }; 13 | const hasWonInLeftSlant = (symbol, ...rows) => countInLeftSlant(symbol, ...rows) === 3; 14 | export const hasThreatInLeftSlant = (symbol, ...rows) => countInLeftSlant(symbol, ...rows) === 2; 15 | 16 | const countInRightSlant = (symbol, ...rows) => { 17 | const [row0, row1, row2] = rows; 18 | return [row0[2], row1[1], row2[0]].filter(el => el === symbol).length; 19 | }; 20 | const hasWonInRightSlant = (symbol, ...rows) => countInRightSlant(symbol, ...rows) === 3; 21 | export const hasThreatInRightSlant = (symbol, ...rows) => countInRightSlant(symbol, ...rows) === 2; 22 | 23 | export const resultForSymbol = (symbol, board) => { 24 | const rows = Object.keys(board).map(row => board[row]); 25 | return [ 26 | {line: 'row0', won: hasWonInRow(symbol, board[0])}, 27 | {line: 'row1', won: hasWonInRow(symbol, board[1])}, 28 | {line: 'row2', won: hasWonInRow(symbol, board[2])}, 29 | {line: 'column0', won: hasWonInColumn(symbol, 0, ...rows)}, 30 | {line: 'column1', won: hasWonInColumn(symbol, 1, ...rows)}, 31 | {line: 'column2', won: hasWonInColumn(symbol, 2, ...rows)}, 32 | {line: 'leftSlant', won: hasWonInLeftSlant(symbol, ...rows)}, 33 | {line: 'rightSlant', won: hasWonInRightSlant(symbol, ...rows)} 34 | ] 35 | .reduce((answer, nextCheck) => { 36 | return nextCheck.won ? nextCheck : answer; 37 | }, {won: false}); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/Board.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { X, O } from '../symbols/symbols'; 4 | import BlankSymbol from './BlankSymbol'; 5 | import XSymbol from './XSymbol'; 6 | import OSymbol from './OSymbol'; 7 | import { PureBoard as Board } from './Board'; 8 | 9 | const board = { 10 | 0: [X, O, ''], 11 | 1: ['', X, O], 12 | 2: [X, X, O] 13 | }; 14 | 15 | it('Should render Board with symbols', () => { 16 | const startAgain = jest.fn(); 17 | const addSymbol = jest.fn(); 18 | const wrapper = shallow(); 19 | expect(wrapper.find(XSymbol).length).toBe(4); 20 | expect(wrapper.find(OSymbol).length).toBe(3); 21 | expect(wrapper.find(BlankSymbol).length).toBe(2); 22 | }); 23 | 24 | it('Should not display a "start again" text when neither won and there was no draw', () => { 25 | const startAgain = jest.fn(); 26 | const addSymbol = jest.fn(); 27 | const wrapper = shallow(); 28 | expect(wrapper.find('p.startAgain').length).toBe(0); 29 | }); 30 | 31 | it('Should display a "start again" text when one symbol won', () => { 32 | const startAgain = jest.fn(); 33 | const addSymbol = jest.fn(); 34 | const wrapper = shallow(); 35 | expect(wrapper.find('p.startAgain').length).toBe(1); 36 | }); 37 | 38 | it('Should display a "start again" text when there was a draw', () => { 39 | const startAgain = jest.fn(); 40 | const addSymbol = jest.fn(); 41 | const wrapper = shallow(); 42 | expect(wrapper.find('p.startAgain').length).toBe(1); 43 | }); 44 | 45 | it('Should call a passed callback when clicked upon the "start again"', () => { 46 | const startAgain = jest.fn(); 47 | const addSymbol = jest.fn(); 48 | const wrapper = shallow(); 49 | wrapper.find('p.startAgain').simulate('click'); 50 | expect(startAgain.mock.calls.length).toBe(1); 51 | }); 52 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/Board.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import BlankSymbol from './BlankSymbol'; 4 | import XSymbol from './XSymbol'; 5 | import OSymbol from './OSymbol'; 6 | import { X, O } from '../symbols/symbols'; 7 | import { addSymbol, startAgain } from '../actions/actions'; 8 | import { connect } from 'react-redux'; 9 | 10 | class Board extends Component { 11 | addSymbol (rowIndex, position, symbol) { 12 | !this.props.won && this.props.addSymbol(rowIndex, position, symbol); 13 | } 14 | 15 | getSymbol(rowIndex, position, symbol) { 16 | if (symbol === X) { 17 | return ; 18 | } 19 | if (symbol === O) { 20 | return ; 21 | } 22 | return ; 23 | } 24 | 25 | render() { 26 | const wonClass = this.props.won ? ` won-${this.props.wonLine}` : ''; 27 | const drawClass = this.props.draw ? ' draw' : ''; 28 | const boardClass = 'board' + wonClass + drawClass; 29 | return ( 30 |
31 | { 32 | Object.keys(this.props.board) 33 | .map(rowIndex => { 34 | return ( 35 |
36 | { 37 | this.props.board[rowIndex].map((symbol, positon) => { 38 | return this.getSymbol(rowIndex, positon, symbol); 39 | }) 40 | } 41 |
42 | ); 43 | }) 44 | } 45 | { 46 | this.props.won || this.props.draw ? 47 |

48 | Click to start again! 49 |

: false 50 | } 51 |
52 | ); 53 | } 54 | } 55 | 56 | Board.propTypes = { 57 | board: PropTypes.object.isRequired, 58 | turn: PropTypes.string.isRequired, 59 | won: PropTypes.string, 60 | draw: PropTypes.bool.isRequired, 61 | wonLine: PropTypes.string, 62 | addSymbol: PropTypes.func.isRequired, 63 | startAgain: PropTypes.func.isRequired 64 | }; 65 | 66 | export default connect( 67 | ({board, turn, won, draw, wonLine}) => ({ 68 | board, turn, won, draw, wonLine 69 | }), 70 | (dispatch) => { 71 | return { 72 | addSymbol (rowIndex, position, symbol) { 73 | dispatch(addSymbol(rowIndex, position, symbol)); 74 | }, 75 | startAgain () { 76 | dispatch(startAgain()); 77 | } 78 | }; 79 | } 80 | )(Board); 81 | 82 | export {Board as PureBoard}; 83 | -------------------------------------------------------------------------------- /src/logic/logic.test.js: -------------------------------------------------------------------------------- 1 | import { resultForSymbol } from './logic'; 2 | import { X, O } from '../symbols/symbols'; 3 | 4 | it('Should indicate no winning result', () => { 5 | const board = { 6 | 0: ['', X, ''], 7 | 1: [O, '', O], 8 | 2: [X, '', ''] 9 | }; 10 | const xResult = resultForSymbol(X, board); 11 | const oResult = resultForSymbol(O, board); 12 | expect(xResult.won).toBe(false); 13 | expect(oResult.won).toBe(false); 14 | }); 15 | 16 | it('Should indicate X as a winner in row0', () => { 17 | const board = { 18 | 0: [X, X, X], 19 | 1: ['', O, ''], 20 | 2: ['', '', O] 21 | }; 22 | const xResult = resultForSymbol(X, board); 23 | const oResult = resultForSymbol(O, board); 24 | expect(xResult.won).toBe(true); 25 | expect(xResult.line).toBe('row0'); 26 | expect(oResult.won).toBe(false); 27 | }); 28 | 29 | it('Should indicate X as a winner in row1', () => { 30 | const board = { 31 | 0: ['', '', O], 32 | 1: [X, X, X], 33 | 2: ['', '', O] 34 | }; 35 | const xResult = resultForSymbol(X, board); 36 | const oResult = resultForSymbol(O, board); 37 | expect(xResult.won).toBe(true); 38 | expect(xResult.line).toBe('row1'); 39 | expect(oResult.won).toBe(false); 40 | }); 41 | 42 | it('Should indicate X as a winner in row2', () => { 43 | const board = { 44 | 0: ['', O, ''], 45 | 1: ['', O, ''], 46 | 2: [X, X, X] 47 | }; 48 | const xResult = resultForSymbol(X, board); 49 | const oResult = resultForSymbol(O, board); 50 | expect(xResult.won).toBe(true); 51 | expect(xResult.line).toBe('row2'); 52 | expect(oResult.won).toBe(false); 53 | }); 54 | 55 | it('Should indicate O as a winner in column0', () => { 56 | const board = { 57 | 0: [O, X, X], 58 | 1: [O, '', ''], 59 | 2: [O, '', X] 60 | }; 61 | const xResult = resultForSymbol(X, board); 62 | const oResult = resultForSymbol(O, board); 63 | expect(xResult.won).toBe(false); 64 | expect(oResult.won).toBe(true); 65 | expect(oResult.line).toBe('column0'); 66 | }); 67 | 68 | it('Should indicate O as a winner in column1', () => { 69 | const board = { 70 | 0: ['', O, X], 71 | 1: ['', O, ''], 72 | 2: [X, O, X] 73 | }; 74 | const xResult = resultForSymbol(X, board); 75 | const oResult = resultForSymbol(O, board); 76 | expect(xResult.won).toBe(false); 77 | expect(oResult.won).toBe(true); 78 | expect(oResult.line).toBe('column1'); 79 | }); 80 | 81 | it('Should indicate O as a winner in column2', () => { 82 | const board = { 83 | 0: ['', X, O], 84 | 1: [X, '', O], 85 | 2: [X, '', O] 86 | }; 87 | const xResult = resultForSymbol(X, board); 88 | const oResult = resultForSymbol(O, board); 89 | expect(xResult.won).toBe(false); 90 | expect(oResult.won).toBe(true); 91 | expect(oResult.line).toBe('column2'); 92 | }); 93 | 94 | it('Should indicate O as a winner in leftSlant', () => { 95 | const board = { 96 | 0: [O, X, X], 97 | 1: ['', O, ''], 98 | 2: [X, '', O] 99 | }; 100 | const xResult = resultForSymbol(X, board); 101 | const oResult = resultForSymbol(O, board); 102 | expect(xResult.won).toBe(false); 103 | expect(oResult.won).toBe(true); 104 | expect(oResult.line).toBe('leftSlant'); 105 | }); 106 | 107 | it('Should indicate X as a winner in rightSlant', () => { 108 | const board = { 109 | 0: ['', O, X], 110 | 1: ['', X, ''], 111 | 2: [X, '', O] 112 | }; 113 | const xResult = resultForSymbol(X, board); 114 | const oResult = resultForSymbol(O, board); 115 | expect(xResult.won).toBe(true); 116 | expect(xResult.line).toBe('rightSlant'); 117 | expect(oResult.won).toBe(false); 118 | }); 119 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | } else { 39 | // Is not local host. Just register service worker 40 | registerValidSW(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | --------------------------------------------------------------------------------