├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── assets │ └── images │ │ ├── friends.webp │ │ └── project-preview.png ├── components │ ├── square │ │ └── Square.js │ ├── moves │ │ └── Moves.js │ ├── game-info │ │ └── GameInfo.js │ └── board │ │ └── Board.js ├── setupTests.js ├── reportWebVitals.js ├── helpers │ └── calculateWinner.js ├── index.js ├── logo.svg └── index.css ├── .gitignore ├── LICENSE.md ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherineisonline/tic-tac-toe/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherineisonline/tic-tac-toe/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherineisonline/tic-tac-toe/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/images/friends.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherineisonline/tic-tac-toe/HEAD/src/assets/images/friends.webp -------------------------------------------------------------------------------- /src/assets/images/project-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherineisonline/tic-tac-toe/HEAD/src/assets/images/project-preview.png -------------------------------------------------------------------------------- /src/components/square/Square.js: -------------------------------------------------------------------------------- 1 | const Square = ({ value, onClick }) => { 2 | return ( 3 | 6 | ) 7 | } 8 | 9 | export default Square 10 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /.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 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/components/moves/Moves.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Moves = ({ history, jumpTo }) => { 4 | const moves = history.map((step, move) => { 5 | const desc = move ? 'Go to move #' + move : 'Go to game start' 6 | return ( 7 |
  • 8 | 9 |
  • 10 | ) 11 | }) 12 | return {moves} 13 | } 14 | 15 | export default Moves 16 | -------------------------------------------------------------------------------- /src/helpers/calculateWinner.js: -------------------------------------------------------------------------------- 1 | function calculateWinner(squares) { 2 | const lines = [ 3 | [0, 1, 2], 4 | [3, 4, 5], 5 | [6, 7, 8], 6 | [0, 3, 6], 7 | [1, 4, 7], 8 | [2, 5, 8], 9 | [0, 4, 8], 10 | [2, 4, 6], 11 | ]; 12 | for (let i = 0; i < lines.length; i++) { 13 | const [a, b, c] = lines[i]; 14 | if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { 15 | return squares[a]; 16 | } 17 | } 18 | return null; 19 | } 20 | 21 | export default calculateWinner; -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/game-info/GameInfo.js: -------------------------------------------------------------------------------- 1 | import Friends from '../../assets/images/friends.webp' 2 | 3 | const GameInfo = ({ status, winner, xIsNext }) => { 4 | return ( 5 |
    6 | {xIsNext && !winner ? ( 7 |

    It's your turn, player X

    8 | ) : !xIsNext && !winner ? ( 9 |

    Now you, player O!

    10 | ) : winner && status === 'Winner: X' ? ( 11 |

    Nice! I won!

    12 | ) : ( 13 |

    Wohoo! I made it!

    14 | )} 15 | Player X and Player O 16 |
    17 | ) 18 | } 19 | 20 | export default GameInfo 21 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Tic Tac Toe 17 | 18 | 19 | 20 |
    21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/board/Board.js: -------------------------------------------------------------------------------- 1 | import Square from '../square/Square' 2 | 3 | const Board = ({ squares, onClick, jumpTo }) => { 4 | const renderSquare = (i) => { 5 | return onClick(i)} /> 6 | } 7 | return ( 8 |
    9 |
    10 |
    11 | {renderSquare(0)} 12 | {renderSquare(1)} 13 | {renderSquare(2)} 14 |
    15 |
    16 | {renderSquare(3)} 17 | {renderSquare(4)} 18 | {renderSquare(5)} 19 |
    20 |
    21 | {renderSquare(6)} 22 | {renderSquare(7)} 23 | {renderSquare(8)} 24 |
    25 |
    26 | 29 |
    30 | ) 31 | } 32 | 33 | export default Board 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ekaterine (Catherine) Mitagvaria 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tictactoe", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://catherineisonline.github.io/tic-tac-toe", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.3.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-router-dom": "^6.22.2", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "predeploy": "npm run build", 18 | "deploy": "gh-pages -d build", 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "gh-pages": "^6.1.1" 44 | } 45 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Tic Tac Toe](https://github.com/catherineisonline/tic-tac-toe/blob/main/src/assets/images/project-preview.png?raw=true) 3 | # Tic Tac Toe 4 | 5 | 6 | [Tic Tac Toe](https://catherineisonline.github.io/tic-tac-toe/) game, a classic game for two players where each player takes turns marking a grid of 3x3 squares with their X or O. The player who succeeds in placing three of their marks in a horizontal, vertical, or diagonal row wins the game. It is also known as Noughts and Crosses or Xs and Os. The game is implemented using React and CSS 7 | 8 | ## Game rules 9 | 10 | 1. The game is played on a grid that is 3 squares by 3 squares 11 | 2. You are X, your friend is O. Players take turns putting their marks in empty squares 12 | 3. The first player to get 3 of their marks in a row (up, down, across, or diagonally) is the winner 13 | 4. When all 9 squares are full, the game is over 14 | 15 | ## Getting Started with Create React App 16 | 17 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 18 | 19 | ## Available Scripts 20 | 21 | In the project directory, you can run: 22 | 23 | ### `npm start` 24 | 25 | Runs the app in the development mode.\ 26 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 27 | 28 | The page will reload when you make changes.\ 29 | You may also see any lint errors in the console. 30 | 31 | ### `npm test` 32 | 33 | Launches the test runner in the interactive watch mode.\ 34 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 35 | 36 | ### `npm run build` 37 | 38 | Builds the app for production to the `build` folder.\ 39 | It correctly bundles React in production mode and optimizes the build for the best performance. 40 | 41 | The build is minified and the filenames include the hashes.\ 42 | Your app is ready to be deployed! 43 | 44 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import './index.css' 4 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 5 | import calculateWinner from './helpers/calculateWinner' 6 | import Board from './components/board/Board' 7 | import GameInfo from './components/game-info/GameInfo' 8 | 9 | class Game extends React.Component { 10 | constructor(props) { 11 | super(props) 12 | this.state = { 13 | history: [ 14 | { 15 | squares: Array(9).fill(null), 16 | }, 17 | ], 18 | stepNumber: 0, 19 | xIsNext: true, 20 | } 21 | } 22 | 23 | handleClick(i) { 24 | const history = this.state.history.slice(0, this.state.stepNumber + 1) 25 | const current = history[history.length - 1] 26 | const squares = current.squares.slice() 27 | if (calculateWinner(squares) || squares[i]) { 28 | return 29 | } 30 | squares[i] = this.state.xIsNext ? 'X' : 'O' 31 | this.setState({ 32 | history: history.concat([ 33 | { 34 | squares: squares, 35 | }, 36 | ]), 37 | stepNumber: history.length, 38 | xIsNext: !this.state.xIsNext, 39 | }) 40 | } 41 | 42 | jumpTo(step) { 43 | console.log(step) 44 | this.setState({ 45 | stepNumber: step, 46 | xIsNext: step % 2 === 0, 47 | }) 48 | } 49 | 50 | render() { 51 | const history = this.state.history 52 | const current = history[this.state.stepNumber] 53 | const winner = calculateWinner(current.squares) 54 | let status 55 | if (winner) { 56 | status = 'Winner: ' + winner 57 | } else { 58 | status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O') 59 | } 60 | return ( 61 | 62 |

    Tic Tac Toe

    63 |
    64 | 69 | this.handleClick(i)} 72 | jumpTo={(i) => this.jumpTo(i)} 73 | /> 74 |
    75 |
    76 | ) 77 | } 78 | } 79 | 80 | const root = ReactDOM.createRoot(document.getElementById('root')) 81 | root.render( 82 | 83 | } /> 84 | 85 | ) 86 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Seymour+One&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Sofia+Sans:wght@100;400&display=swap"); 3 | 4 | :root { 5 | --game-font: "Seymour One", sans-serif; 6 | --speech-font: "Sofia Sans", sans-serif; 7 | --dark-purple: rgb(18, 22, 75); 8 | --dark-blue: rgb(42, 118, 180); 9 | --light-blue: rgba(121, 185, 255, 255); 10 | --white: rgb(255, 255, 255); 11 | --light-grey: rgb(221, 221, 221); 12 | } 13 | 14 | *, 15 | *::before, 16 | *::after { 17 | box-sizing: border-box; 18 | } 19 | 20 | * { 21 | margin: 0; 22 | padding: 0; 23 | } 24 | html { 25 | min-height: 100%; 26 | } 27 | body { 28 | -ms-overflow-style: none; /* for Internet Explorer, Edge */ 29 | scrollbar-width: none; /* for Firefox */ 30 | overflow-y: scroll; 31 | line-height: 1.5; 32 | -webkit-font-smoothing: antialiased; 33 | height: 100vh; 34 | } 35 | body::-webkit-scrollbar { 36 | display: none; /* for Chrome, Safari, and Opera */ 37 | } 38 | 39 | img, 40 | picture, 41 | video, 42 | canvas, 43 | svg { 44 | display: block; 45 | max-width: 100%; 46 | } 47 | img { 48 | display: block; 49 | } 50 | 51 | ul, 52 | ol, 53 | li { 54 | padding: 0; 55 | margin: 0; 56 | } 57 | input, 58 | button, 59 | textarea, 60 | select { 61 | font: inherit; 62 | } 63 | p, 64 | h1, 65 | h2, 66 | h3, 67 | h4, 68 | h5, 69 | h6 { 70 | overflow-wrap: break-word; 71 | } 72 | 73 | #root, 74 | #__next { 75 | isolation: isolate; 76 | } 77 | 78 | main { 79 | position: relative; 80 | min-height: 100%; 81 | background-color: var(--light-blue); 82 | display: flex; 83 | flex-direction: column; 84 | align-items: center; 85 | justify-content: center; 86 | } 87 | h1 { 88 | font-family: var(--game-font); 89 | color: var(--dark-purple); 90 | font-size: 6vw; 91 | letter-spacing: 1px; 92 | padding: 1rem 1rem 6rem 1rem; 93 | } 94 | .game-board { 95 | position: relative; 96 | display: flex; 97 | flex-direction: column; 98 | align-items: center; 99 | padding: 25px; 100 | font-size: 30px; 101 | font-weight: bold; 102 | color: var(--white); 103 | transition: 104 | margin-top 0.3s ease, 105 | margin-left 0.3s ease, 106 | box-shadow 0.3s ease; 107 | background: var(--light-blue); 108 | } 109 | .game-section { 110 | display: flex; 111 | flex-direction: column; 112 | gap: 1rem; 113 | align-items: center; 114 | } 115 | .restart { 116 | background-color: transparent; 117 | padding: 0.5rem 4rem; 118 | cursor: pointer; 119 | border: 1px solid var(--dark-purple); 120 | background-color: var(--dark-purple); 121 | color: var(--white); 122 | font-family: var(--speech-font); 123 | font-weight: 400; 124 | border-radius: 5px; 125 | transition: all ease-in-out 0.3s; 126 | } 127 | .status { 128 | margin-bottom: 10px; 129 | } 130 | 131 | .square { 132 | background: var(--white); 133 | border: 10px solid var(--dark-purple); 134 | cursor: pointer; 135 | float: left; 136 | font-size: 6vw; 137 | font-weight: bold; 138 | line-height: 34px; 139 | height: 8rem; 140 | margin-right: -1px; 141 | margin-top: -1px; 142 | padding: 0; 143 | text-align: center; 144 | width: 8rem; 145 | color: var(--dark-purple); 146 | font-family: var(--game-font); 147 | } 148 | 149 | .square:focus { 150 | outline: none; 151 | } 152 | 153 | .game { 154 | display: grid; 155 | grid-template-columns: repeat(2, 1fr); 156 | max-width: 1200px; 157 | width: 100%; 158 | align-items: center; 159 | justify-content: space-between; 160 | } 161 | 162 | .game img { 163 | max-width: 35rem; 164 | width: 100%; 165 | position: relative; 166 | object-fit: contain; 167 | min-width: 20rem; 168 | margin: 0 auto; 169 | } 170 | 171 | .game-info { 172 | position: absolute; 173 | } 174 | 175 | .player-x { 176 | margin-left: 3rem; 177 | } 178 | 179 | .player-o { 180 | margin-left: 23rem; 181 | margin-top: -3rem; 182 | } 183 | 184 | .player-x, 185 | .player-o { 186 | position: absolute; 187 | z-index: 999; 188 | transform: translatey(0px); 189 | -webkit-animation: float 5s ease-in-out infinite; 190 | animation: float 5s ease-in-out infinite; 191 | mix-blend-mode: multiply; 192 | text-align: center; 193 | letter-spacing: 1px; 194 | color: var(--dark-purple); 195 | padding: 1rem 3rem; 196 | border-radius: 11px; 197 | width: max-content; 198 | box-shadow: 20px 20px var(--dark-blue); 199 | font-size: 2.5vw; 200 | background-color: var(--light-grey); 201 | font-family: var(--speech-font); 202 | } 203 | 204 | .player-x:after, 205 | .player-o:after { 206 | z-index: 999; 207 | transform: translatey(0px); 208 | -webkit-animation: float2 5s ease-in-out infinite; 209 | animation: float2 5s ease-in-out infinite; 210 | content: "."; 211 | text-align: left; 212 | font-size: 8vw; 213 | width: 55px; 214 | height: 11px; 215 | line-height: 30px; 216 | border-radius: 11px; 217 | background-color: var(--light-grey); 218 | position: absolute; 219 | display: block; 220 | bottom: -30px; 221 | left: 0; 222 | box-shadow: 22px 22px var(--dark-blue); 223 | z-index: -2; 224 | } 225 | 226 | @-webkit-keyframes float { 227 | 0% { 228 | transform: translatey(0px); 229 | } 230 | 50% { 231 | transform: translatey(-20px); 232 | } 233 | 100% { 234 | transform: translatey(0px); 235 | } 236 | } 237 | 238 | @keyframes float { 239 | 0% { 240 | transform: translatey(0px); 241 | } 242 | 50% { 243 | transform: translatey(-20px); 244 | } 245 | 100% { 246 | transform: translatey(0px); 247 | } 248 | } 249 | @-webkit-keyframes float2 { 250 | 0% { 251 | line-height: 30px; 252 | transform: translatey(0px); 253 | } 254 | 55% { 255 | transform: translatey(-20px); 256 | } 257 | 60% { 258 | line-height: 10px; 259 | } 260 | 100% { 261 | line-height: 30px; 262 | transform: translatey(0px); 263 | } 264 | } 265 | @keyframes float2 { 266 | 0% { 267 | line-height: 30px; 268 | transform: translatey(0px); 269 | } 270 | 55% { 271 | transform: translatey(-20px); 272 | } 273 | 60% { 274 | line-height: 10px; 275 | } 276 | 100% { 277 | line-height: 30px; 278 | transform: translatey(0px); 279 | } 280 | } 281 | 282 | @media (hover: hover) { 283 | .restart:hover { 284 | transition: all ease-in-out 0.3s; 285 | background-color: var(--dark-blue); 286 | border-color: var(--dark-blue); 287 | } 288 | } 289 | 290 | @media screen and (max-width: 900px) { 291 | .game { 292 | grid-template-columns: 1fr; 293 | margin-bottom: 3rem; 294 | } 295 | } 296 | --------------------------------------------------------------------------------