├── README.md ├── index.html ├── script.js └── styles.css /README.md: -------------------------------------------------------------------------------- 1 | # Mod-1 Main Project - Tic Tac Toe 2 | 3 | This is a creation of the classic 'Tic-Tac-Toe' game. 4 | 5 | The game was created using: 6 | - HTML 7 | - CSS (incl. Flexbox) 8 | - JavaScript 9 | 10 | This is a project to complete the first module 11 | of my Software Engineering certification [@Per Scholas](https://www.perscholas.org) 12 | 13 | The purpose is to demonstrate my knowledge of: 14 | - Semantic HTML and application structure 15 | - Styling and appearance using CSS 16 | - Dynamic events and interactions utilizing JavaScript 17 | - Classes and Object Oriented Programming 18 | - DOM manipulation -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | button 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Tic Tac Toe 12 | 13 | 14 | 15 |
16 | 17 | 22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 |
30 |

Game Stats

31 |

32 | 33 | 34 |

35 |

36 | 37 | 38 |

39 |
40 | 41 | 42 |
43 |
44 |
45 | 46 | 47 |
48 |

49 | 50 | , your turn. 51 |

52 |
53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | //* Main Game Class 2 | class Game { 3 | constructor() { 4 | this.numOfPlayers = 0; 5 | } 6 | 7 | createGame() { 8 | // Accessing the DOM elements 9 | const gameArea = document.querySelector('#game-area'); 10 | const p1Name = document.querySelector('#p1-player'); 11 | const p2Name = document.querySelector('#p2-player'); 12 | const p1Score = document.querySelector('#p1-score'); 13 | const p2Score = document.querySelector('#p2-score'); 14 | const nextPlayer = document.querySelector('#next-player'); 15 | const resetButton = document.querySelector('#reset-btn'); 16 | const restartButton = document.querySelector('#restart-btn'); 17 | 18 | // Winnig Combo of indexes to determine winner 19 | const winningCombos = [ 20 | [0, 1, 2], 21 | [3, 4, 5], 22 | [6, 7, 8], 23 | [0, 3, 6], 24 | [1, 4, 7], 25 | [2, 5, 8], 26 | [0, 4, 8], 27 | [2, 4, 6], 28 | ]; 29 | 30 | // Instantiate the players 31 | const player1 = new Player( 32 | prompt('Please enter a name for Player #1'), 33 | 'X' 34 | ); 35 | alert(`Player #1 (${player1.name}) has been created.`); 36 | 37 | let player2; 38 | 39 | if (this.numOfPlayers === '2') { 40 | player2 = new Player( 41 | prompt('Please enter a name for Player #2'), 42 | 'O' 43 | ); 44 | alert(`Player #2 (${player2.name}) has been created.`); 45 | } else { 46 | player2 = new Player('Mr. Computer', 'O'); 47 | } 48 | 49 | alert( 50 | `Game on! \n"${player1.name} ( '${player1.symbol}' )" vs "${player2.name} ( '${player2.symbol}' )"` 51 | ); 52 | 53 | let currentPlayer = player1; 54 | let currentClass = 'symb-x'; 55 | gameArea.classList.add(currentClass); 56 | 57 | p1Name.textContent = `${player1.name}: `; 58 | p1Score.textContent = `${player1.won}`; 59 | 60 | p2Name.textContent = `${player2.name}: `; 61 | p2Score.textContent = `${player2.won}`; 62 | 63 | // Status Message 64 | message.textContent = `${currentPlayer.name} ( '${currentPlayer.symbol}' ), your turn.`; 65 | 66 | //* Handling of the block's click evenet 67 | const handleBlockClick = e => { 68 | let block; 69 | 70 | if (e.target) { 71 | e.preventDefault(); 72 | 73 | block = e.target; 74 | } else { 75 | block = e; 76 | } 77 | 78 | // Checks for the current player and places the appropriate symbol 79 | currentPlayer === player1 80 | ? (currentClass = 'symb-x') 81 | : (currentClass = 'symb-o'); 82 | 83 | block.classList.add(currentClass); 84 | console.log(block); 85 | 86 | // Call to check for winner 87 | if (checkIfWon(currentClass)) { 88 | message.textContent = `${currentPlayer.name} Wins!`; 89 | 90 | currentPlayer.won++; 91 | 92 | // Disable further block clicking 93 | const allBlocks = document.querySelectorAll('.square'); 94 | allBlocks.forEach(block => { 95 | block.removeEventListener('click', handleBlockClick); 96 | block.style.cursor = 'not-allowed'; 97 | }); 98 | 99 | switch (currentPlayer) { 100 | case player1: 101 | p1Score.textContent = player1.won; 102 | break; 103 | case player2: 104 | p2Score.textContent = player2.won; 105 | break; 106 | default: 107 | return; 108 | } 109 | } else { 110 | console.log('Winner?: ', checkIfWon(currentClass)); 111 | // If no winner, switch players 112 | switchPlayers(player1, player2); 113 | } 114 | }; 115 | 116 | // 117 | //* Handling of the reset button's click event *// 118 | const handleResetBtn = e => { 119 | e.preventDefault(); 120 | 121 | const allBlocks = document.querySelectorAll('.square'); 122 | 123 | allBlocks.forEach(block => { 124 | gameArea.removeChild(block); 125 | }); 126 | 127 | const allXs = document.querySelectorAll('.symb-x'); 128 | const allOs = document.querySelectorAll('.symb-o'); 129 | 130 | allXs.forEach(x => { 131 | x.classList.remove('symb-x'); 132 | }); 133 | 134 | allOs.forEach(o => { 135 | o.classList.remove('symb-o'); 136 | }); 137 | 138 | console.clear(); 139 | currentPlayer = player1; 140 | gameArea.classList.add('symb-x'); 141 | message.textContent = `${currentPlayer.name}, your turn.`; 142 | createBoard(); 143 | }; 144 | 145 | //* Handling the restart button's click event 146 | const handleRestartBtn = e => { 147 | e.preventDefault(); 148 | 149 | const allBlocks = document.querySelectorAll('.square'); 150 | 151 | allBlocks.forEach(block => { 152 | gameArea.removeChild(block); 153 | }); 154 | 155 | // Create a new Game 156 | const game = new Game(); 157 | game.start(); 158 | }; 159 | 160 | //* The 'Player 2' logic for the computer, if it's a one-player game *// 161 | function useComputerLogic() { 162 | // House the index of the blocks with no class of 'x' or 'o' 163 | const remainingBlocks = []; 164 | 165 | // Cache all of the blocks 166 | const allBlocks = document.querySelectorAll('.square'); 167 | 168 | // Detemine what blocks do not have an associated class, and push to 'remainingBlocks' 169 | allBlocks.forEach(block => { 170 | if ( 171 | !block.classList.contains('symb-x') && 172 | !block.classList.contains('symb-o') 173 | ) { 174 | // Push the index ('id' value - 1) to remainingBlocks 175 | setTimeout(() => block.classList.add('cmp-symb-0'), 1500); 176 | remainingBlocks.push(block.id - 1); 177 | } 178 | }); 179 | 180 | console.log('Remaining Blocks: ', remainingBlocks); 181 | // Random index selctor or the 'remainingBlocks' 182 | const decision = Math.floor(Math.random() * remainingBlocks.length); 183 | console.log('Decision: ', decision); 184 | 185 | // The random index value of 'remainingBlocks' to check 186 | const idx = remainingBlocks[decision]; 187 | console.log('IDX: ', idx); 188 | 189 | try { 190 | !allBlocks[idx].classList.contains('symb-x') && 191 | !allBlocks[idx].classList.contains('symb-o') 192 | ? handleBlockClick(allBlocks[idx]) 193 | : console.log('Duplicate block picked: ', allBlocks[idx]); 194 | } catch (err) { 195 | message.textContent = "It's a draw!!!"; 196 | } 197 | } 198 | 199 | //* Check for winner *// 200 | function checkIfWon(currentClass) { 201 | // Collects all block elements 202 | const allBlocks = document.querySelectorAll('.square'); 203 | 204 | // 'Cycles through every 'combo' within the 'winningCombos' array 205 | // returning any ('.some') winning 'combo' array (which holds the values of 'allBlocks' indexes) 206 | return winningCombos.some(combo => { 207 | // Returns to 'winningCombos', the 'combo' array if '.every' value 208 | // is the index ('idx') of 'allBlocks' containing the class of 'currentClass' 209 | return combo.every(idx => { 210 | return allBlocks[idx].classList.contains(currentClass); 211 | }); 212 | }); 213 | } 214 | 215 | // Switch the current player on turn exchange 216 | function switchPlayers(player1, player2) { 217 | // Remove the current class associated with the current player 218 | gameArea.classList.remove(currentClass); 219 | 220 | // Update the current player and current class 221 | if (currentPlayer === player1) { 222 | currentClass = 'symb-o'; 223 | currentPlayer = player2; 224 | } else { 225 | currentClass = 'symb-x'; 226 | currentPlayer = player1; 227 | } 228 | 229 | // Update class reference of the updated current user to the game 230 | gameArea.classList.add(currentClass); 231 | 232 | // Update Next Up message 233 | message.textContent = `${currentPlayer.name} ( '${currentPlayer.symbol}' ), your turn.`; 234 | 235 | if (currentPlayer.name === 'Mr. Computer') useComputerLogic(); 236 | } 237 | 238 | //* Initialize and create the game board 239 | function createBoard() { 240 | for (let idx = 0; idx < 9; idx++) { 241 | const block = document.createElement('div'); 242 | 243 | block.classList.add('square'); 244 | block.id = idx + 1; 245 | 246 | if (idx === 0 || idx === 1 || idx === 2) 247 | block.style.borderTop = 'none'; 248 | if (idx === 0 || idx === 3 || idx === 6) 249 | block.style.borderLeft = 'none'; 250 | 251 | if (idx === 2 || idx === 5 || idx === 8) 252 | block.style.borderRight = 'none'; 253 | 254 | if (idx === 6 || idx === 7 || idx === 8) 255 | block.style.borderBottom = 'none'; 256 | 257 | // Add click event listener 258 | block.addEventListener('click', handleBlockClick, { 259 | once: true, 260 | }); 261 | 262 | gameArea.append(block); 263 | } 264 | 265 | resetButton.addEventListener('click', handleResetBtn); 266 | 267 | restartButton.addEventListener('click', handleRestartBtn); 268 | 269 | nextPlayer.textContent = `${currentPlayer.name}`; 270 | 271 | //onsole.log(message); 272 | } 273 | 274 | //* Call to create the game board 275 | createBoard(); 276 | } 277 | 278 | //* Initialize and start the the game 279 | start(currentPlayer = this.player1) { 280 | this.numOfPlayers = prompt( 281 | `Number of Players: '1' (vs. Computer) or '2' (Head to Head)?` 282 | ); 283 | 284 | switch (this.numOfPlayers) { 285 | case '1': 286 | case '2': 287 | game.createGame(); 288 | break; 289 | case '': 290 | this.start(); 291 | break; 292 | case null: 293 | window.close(); 294 | break; 295 | default: 296 | alert( 297 | `Please enter a '1' or '2' for number of players. To quit, hit 'Cancel' or 'Esc'.` 298 | ); 299 | this.start(); 300 | break; 301 | } 302 | } 303 | } 304 | 305 | //* Player 306 | class Player { 307 | constructor(name = 'Computer', symbol) { 308 | this.name = name; 309 | this.symbol = symbol; 310 | this.won = 0; 311 | } 312 | } 313 | 314 | //* Create and start the game 315 | const game = new Game(); 316 | game.start(); 317 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | :root { 10 | --block-size: 200px; 11 | 12 | /* The X/O size - 90% of the block size */ 13 | --symb-size: calc(var(--block-size) * .9); 14 | 15 | /* The X/O block selector color */ 16 | --block-selector: rgb(218, 218, 218); 17 | } 18 | 19 | body { 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: center; 23 | align-items: center; 24 | height: 100vh; 25 | } 26 | 27 | p, span, h1, h2 { 28 | margin-bottom: 0.25em; 29 | } 30 | 31 | #main-container { 32 | width: 70%; 33 | height: 100%; 34 | } 35 | 36 | #header { 37 | text-align: center; 38 | padding: 1em 0; 39 | margin-bottom: 4em; 40 | } 41 | 42 | /* The entire game area and info area */ 43 | #game-board { 44 | display: flex; 45 | justify-content: space-around; 46 | align-items: center; 47 | width: 100%; 48 | height: fit-content; 49 | /* background-color: grey */ 50 | } 51 | 52 | #game-area { 53 | display: flex; 54 | flex-wrap: wrap; 55 | justify-content: center; 56 | align-items: center; 57 | width: 600px; 58 | height: 600px; 59 | } 60 | 61 | .square { 62 | width: var(--block-size); 63 | height: var(--block-size); 64 | border: 1px solid black; 65 | display: flex; 66 | justify-content: center; 67 | align-items: center; 68 | cursor: pointer; 69 | } 70 | 71 | /******* 'X' and 'O' symbols ********/ 72 | .square.symb-x, 73 | .square.symb-o { 74 | cursor: not-allowed; 75 | } 76 | 77 | 78 | /***** 'X' symbol ******/ 79 | .square.symb-x::before, 80 | .square.symb-x::after, 81 | #game-area.symb-x .square:not(.symb-x):not(.symb-o):hover::before, 82 | #game-area.symb-x .square:not(.symb-x):not(.symb-o):hover::after { 83 | content: ''; 84 | position: absolute; 85 | width: calc(var(--symb-size) * .15); 86 | height: var(--symb-size); 87 | background-color: red; 88 | } 89 | 90 | 91 | /* Rotation to form 'X' */ 92 | .square.symb-x::before, 93 | #game-area.symb-x .square:hover::before { 94 | transform: rotate(45deg); 95 | } 96 | 97 | 98 | /* Rotation to form 'X' */ 99 | .square.symb-x::after, 100 | #game-area.symb-x .square:hover::after { 101 | transform: rotate(-45deg); 102 | } 103 | /***************************/ 104 | 105 | 106 | /***** 'O' symbol ******/ 107 | .square.symb-o::before, 108 | .square.symb-o::after, 109 | #game-area.symb-o .square:not(.symb-o):not(.symb-x):hover::before, 110 | #game-area.symb-o .square:not(.symb-o):not(.symb-x):hover::after { 111 | content: ''; 112 | position: absolute; 113 | width: calc(var(--symb-size)); 114 | height: var(--symb-size); 115 | background-color: black; 116 | border-radius: 50%; 117 | } 118 | 119 | .square.symb-o::after, 120 | #game-area.symb-o .square:not(.symb-o):not(.symb-x):hover::after { 121 | scale: .7; 122 | background-color: white; 123 | } 124 | /***********************/ 125 | 126 | /***** Computer 'O' symbol highlight ******/ 127 | .square.cmp-symb-o::before, 128 | .square.cmp-symb-o::after, 129 | #game-area.cmp-symb-o .square:not(.symb-o):not(.symb-x):hover::before, 130 | #game-area.com-symb-o .square:not(.symb-o):not(.symb-x):hover::after { 131 | content: ''; 132 | position: absolute; 133 | width: calc(var(--symb-size)); 134 | height: var(--symb-size); 135 | background-color: black; 136 | border-radius: 50%; 137 | } 138 | 139 | .square.symb-o::after, 140 | #game-area.symb-o .square:not(.symb-o):not(.symb-x):hover::after { 141 | scale: .7; 142 | background-color: white; 143 | } 144 | /***********************/ 145 | 146 | /* X/O symb-ol block selector highlight */ 147 | #game-area.symb-x .square:not(.symb-x):not(.symb-o):hover::before, 148 | #game-area.symb-x .square:not(.symb-x):not(.symb-o):hover::after, 149 | #game-area.symb-o .square:not(.symb-o):not(.symb-x):hover::before { 150 | background-color: var(--block-selector); 151 | } 152 | /***********************/ 153 | /**/ 154 | 155 | #game-stats { 156 | display: flex; 157 | flex-direction: column; 158 | width: 25%; 159 | height: fit-content; 160 | padding: 1em; 161 | border-left: 1px solid; 162 | } 163 | 164 | #buttons { 165 | display: flex; 166 | flex-direction: column; 167 | } 168 | 169 | #reset-btn, #restart-btn { 170 | padding: .5em; 171 | cursor: pointer; 172 | margin-top: 2em; 173 | } 174 | 175 | #game-message { 176 | text-align: center; 177 | margin-top: 4em; 178 | } 179 | 180 | 181 | 182 | --------------------------------------------------------------------------------