├── 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 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Game Stats
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Reset Board
41 | Restart Game
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 |
--------------------------------------------------------------------------------