├── tailwind.config.js ├── .dockerignore ├── images ├── demo.png └── title.png ├── src ├── assets │ └── FoamRubber.mp3 ├── index.css ├── pages │ ├── NotFound.jsx │ ├── Home.jsx │ ├── TwoPlayers.jsx │ └── OnePlayer.jsx ├── main.jsx └── App.jsx ├── Dockerfile.dev ├── Dockerfile.stg ├── vite.config.js ├── .gitignore ├── .eslintrc.cjs ├── package.json ├── index.html ├── README.md └── public └── TicTacToe.svg /tailwind.config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log -------------------------------------------------------------------------------- /images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizalfahlevi8/game-TicTacToe/HEAD/images/demo.png -------------------------------------------------------------------------------- /images/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizalfahlevi8/game-TicTacToe/HEAD/images/title.png -------------------------------------------------------------------------------- /src/assets/FoamRubber.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizalfahlevi8/game-TicTacToe/HEAD/src/assets/FoamRubber.mp3 -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .boxborder-me{ 2 | box-shadow: 0 0 0 8px red; 3 | outline: dashed 8px white; 4 | height: 112px; 5 | } -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package* . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /src/pages/NotFound.jsx: -------------------------------------------------------------------------------- 1 | export default function Home(){ 2 | return ( 3 |
4 |

Halaman Tidak Ditemukan

5 |
6 | ); 7 | } -------------------------------------------------------------------------------- /Dockerfile.stg: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package* . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | EXPOSE 3001 14 | 15 | CMD ["npm", "run", "preview"] -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | host: "0.0.0.0", 9 | port: 3000, 10 | }, 11 | preview: { 12 | port: 3001 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | // App.jsx 2 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 3 | import Home from './pages/Home'; 4 | import NotFound from './pages/NotFound'; 5 | import OnePlayer from './pages/OnePlayer'; 6 | import TwoPlayers from './pages/TwoPlayers'; 7 | 8 | export default function App() { 9 | return ( 10 | 11 | 12 | } /> 13 | } /> 14 | } /> 15 | } /> 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project-tictactoe", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-router-dom": "^6.21.1" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.2.43", 19 | "@types/react-dom": "^18.2.17", 20 | "@vitejs/plugin-react": "^4.2.1", 21 | "eslint": "^8.55.0", 22 | "eslint-plugin-react": "^7.33.2", 23 | "eslint-plugin-react-hooks": "^4.6.0", 24 | "eslint-plugin-react-refresh": "^0.4.5", 25 | "vite": "^5.0.8" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 15 | 16 | 17 | TicTacToe - Master the Grid, Own the Game! 18 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import Title from '../assets/Title.svg'; 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 |
8 | Tic Tac Toe 9 | 10 |
11 | One Player 12 |
13 | 14 | 15 |
Two Players 16 |
17 | 18 |
19 |
20 | 26 |
27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Tic-Tac-Toe Title 3 |
4 | 5 | # Tic-Tac-Toe Game 6 | 7 | A simple Tic-Tac-Toe game built using React and styled with TailwindCSS. This project allows users to play in two modes: Single Player (where the player always plays as 'X') and Two Player (local multiplayer). 8 | 9 | ## Features 10 | 11 | - Single Player Mode: The player always plays as 'X', while the 'O' moves are handled by basic game logic (the computer automatically takes turns as 'O'). 12 | - Two Player Mode: Two players can take turns playing on the same device. 13 | - Responsive design using TailwindCSS for smooth gameplay on various devices. 14 | - A simple, intuitive interface with a minimalistic design 15 | 16 | ## Demo 17 | 18 |
19 | Tic-Tac-Toe Title 20 |
21 |
22 | 23 | Check out the live demo [here](https://rizalfahlevi8-tictactoe.vercel.app/) 24 |
25 | 26 | ## Installation 27 | 28 | Follow these steps to run the project on your local machine: 29 | 30 | 1. Clone this repository: 31 | 32 | ``` 33 | git clone https://github.com/rizalfahlevi8/game-TicTacToe.git 34 | ``` 35 | 2. Navigate into the project directory: 36 | 37 | ``` 38 | cd game-TicTacToe 39 | ``` 40 | 3. Install the dependencies: 41 | 42 | ``` 43 | npm install 44 | ``` 45 | 4. Start the development server: 46 | 47 | ``` 48 | npm run dev 49 | ``` 50 | 5. Open the app in your browser with the address that appears in terminal 51 | 52 | ## How to Play 53 | 54 | Once the app is running, you can choose between two modes: 55 | 56 | - **Single Player Mode:** The player always plays as 'X', and the 'O' moves are controlled by basic game logic. 57 | - **Two Player Mode:** Two players take turns playing on the same device. Player 1 plays as 'X', and Player 2 plays as 'O'. 58 | 59 | Players take turns selecting squares on the board to place their symbol. The game ends when one player gets three of their symbols in a row (horizontally, vertically, or diagonally), or if all the squares are filled and there is no winner, the game ends in a draw. 60 | 61 | ## Technologies Used 62 | 63 | - **React:** A JavaScript library for building dynamic user interfaces. 64 | - **TailwindCSS:** A utility-first CSS framework for rapid styling and responsiveness. 65 | - **Vite:** A fast frontend build tool for modern web development. 66 | 67 | ## Contributions 68 | 69 | Contributions are welcome! Feel free to open an issue or submit a pull request. For major changes, please open an issue first to discuss what you would like to change. 70 | 71 | ## Contact 72 | If you have questions or problems, don't hesitate to contact us via the social media listed on [our profile page.](https://github.com/rizalfahlevi8) 73 | -------------------------------------------------------------------------------- /public/TicTacToe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/TwoPlayers.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import { useState, useEffect } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import BackSound from '../assets/FoamRubber.mp3'; 5 | 6 | // Function Square 7 | function Square({ value, onSquareClick }) { 8 | let colorValue = 'bg-gradient-to-t from-[#7B9E2C] to-[#9DC544] bg-clip-text text-transparent'; 9 | 10 | if (value === 'O') { 11 | colorValue = 'bg-gradient-to-tr from-[#E95656] to-[#BFA165] bg-clip-text text-transparent'; 12 | } 13 | 14 | return ( 15 | 18 | ); 19 | } 20 | 21 | function Reload({ onReloadClick }) { 22 | return ( 23 | 28 | ); 29 | } 30 | 31 | function Status({ order }) { 32 | const card = 'flex items-center justify-center shadow-md cursor-pointer w-[187px] h-28 rounded-2xl text-6xl font-fredoka font-bold bg-[#F9E9D0]'; 33 | let borderX = ''; 34 | let borderO = ''; 35 | 36 | if (order) { 37 | borderX = 'boxborder-me rounded-2xl'; 38 | } else { 39 | borderO = 'boxborder-me rounded-2xl'; 40 | } 41 | 42 | return ( 43 |
44 |
45 |
X
46 |
47 |
48 |
O
49 |
50 |
51 | ); 52 | } 53 | 54 | function Score({ value }) { 55 | const [scoreX, setScoreX] = useState(0); 56 | const [scoreO, setScoreO] = useState(0); 57 | 58 | useEffect(() => { 59 | if (value === "X") { 60 | setScoreX(score => score + 1); 61 | } else if (value === "O") { 62 | setScoreO(score => score + 1); 63 | } 64 | }, [value]); 65 | 66 | return ( 67 |
68 |

Score

69 |

70 |

X
71 |
: {scoreX}
72 |

73 |

74 |

O
75 |
: {scoreO}
76 |

77 |
78 | ); 79 | } 80 | 81 | export default function App() { 82 | const [squares, setSquare] = useState(Array(9).fill(null)); 83 | const [xIsNext, setXIsNext] = useState(true); 84 | const [showWinnerMessage, setShowWinnerMessage] = useState(false); 85 | const [showDrawMessage, setShowDrawMessage] = useState(false); 86 | 87 | useEffect(function () { 88 | let handleWinner = function () { 89 | const winner = calculateWinner(squares); 90 | 91 | if (winner) { 92 | setShowWinnerMessage(true); 93 | setTimeout(function () { 94 | setShowWinnerMessage(false); 95 | var nextSquares = Array(9).fill(null); 96 | setSquare(nextSquares); 97 | }, 1500); 98 | } else if (squares.every((square) => square !== null)) { 99 | // Periksa kondisi seri ketika semua kotak terisi dan tidak ada pemenang 100 | setShowDrawMessage(true); 101 | setTimeout(function () { 102 | setShowDrawMessage(false); 103 | var nextSquares = Array(9).fill(null); 104 | setSquare(nextSquares); 105 | }, 1000); 106 | } 107 | }; 108 | 109 | handleWinner(); 110 | }, [squares]); 111 | 112 | function handleClick(i) { 113 | if (squares[i] || calculateWinner(squares)) return; 114 | const nextSquare = squares.slice(); 115 | nextSquare[i] = xIsNext ? 'X' : 'O'; 116 | setSquare(nextSquare); 117 | setXIsNext(!xIsNext); 118 | } 119 | 120 | function handleReload() { 121 | const nextSquares = Array(9).fill(null); 122 | setSquare(nextSquares); 123 | setXIsNext(true); 124 | } 125 | 126 | const winner = calculateWinner(squares); 127 | 128 | return ( 129 |
130 |
131 | 132 |
133 |
134 | {squares.map((value, index) => ( 135 | handleClick(index)} /> 136 | ))} 137 |
138 |
139 | Two Player 140 |
141 |
142 | 143 |
144 |
145 | 146 | 147 | home 148 | 149 | 150 |
151 |
152 | handleReload()} /> 153 |
154 | 158 | {showWinnerMessage && ( 159 |
160 |

Winner!

161 |
{winner}
162 |
163 | )} 164 | {showDrawMessage && ( 165 |
166 |

Draw!

167 |
168 | )} 169 |
170 | ); 171 | } 172 | 173 | //Function calculateWinner 174 | function calculateWinner(squares) { 175 | const lines = [ 176 | [0, 1, 2], 177 | [3, 4, 5], 178 | [6, 7, 8], 179 | [0, 3, 6], 180 | [1, 4, 7], 181 | [2, 5, 8], 182 | [0, 4, 8], 183 | [2, 4, 6] 184 | ]; 185 | 186 | for (let i = 0; i < lines.length; i++) { 187 | // const a = lines[i][0]; //0 188 | // const b = lines[i][1]; //1 189 | // const c = lines[i][2]; //2 190 | // supaya rapi pakai teknik destructuring js 191 | const [a, b, c] = lines[i]; 192 | 193 | //["X", "X", "X", "O", "O", null, null, null, null] 194 | if (squares[a] === squares[b] && squares[a] === squares[c]) { 195 | return squares[a]; 196 | } 197 | } 198 | return false; 199 | } 200 | -------------------------------------------------------------------------------- /src/pages/OnePlayer.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import { useState, useEffect } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import BackSound from '../assets/FoamRubber.mp3'; 5 | 6 | // Function Square 7 | function Square({ value, onSquareClick }) { 8 | let colorValue = 'bg-gradient-to-t from-[#7B9E2C] to-[#9DC544] bg-clip-text text-transparent'; 9 | 10 | if (value === 'O') { 11 | colorValue = 'bg-gradient-to-tr from-[#E95656] to-[#BFA165] bg-clip-text text-transparent'; 12 | } 13 | 14 | return ( 15 | 18 | ); 19 | } 20 | 21 | function Reload({ onReloadClick }) { 22 | return ( 23 | 28 | ); 29 | } 30 | 31 | function Status({ order }) { 32 | const card = 'flex items-center justify-center shadow-md cursor-pointer w-[187px] h-28 rounded-2xl text-6xl font-fredoka font-bold bg-[#F9E9D0]'; 33 | let borderX = ''; 34 | let borderO = ''; 35 | 36 | if (order) { 37 | borderX = 'boxborder-me rounded-2xl'; 38 | } else { 39 | borderO = 'boxborder-me rounded-2xl'; 40 | } 41 | 42 | return ( 43 |
44 |
45 |
X
46 |
47 |
48 |
O
49 |
50 |
51 | ); 52 | } 53 | 54 | function Score({ value }) { 55 | const [scoreX, setScoreX] = useState(0); 56 | const [scoreO, setScoreO] = useState(0); 57 | 58 | useEffect(() => { 59 | if (value === "X") { 60 | setScoreX(score => score + 1); 61 | } else if (value === "O") { 62 | setScoreO(score => score + 1); 63 | } 64 | }, [value]); 65 | 66 | return ( 67 |
68 |

Score

69 |
70 |
X
71 |
: {scoreX}
72 |
73 |
74 |
O
75 |
: {scoreO}
76 |
77 |
78 | 79 | ); 80 | } 81 | 82 | export default function App() { 83 | const [squares, setSquare] = useState(Array(9).fill(null)); 84 | const [xIsNext, setXIsNext] = useState(true); 85 | const [showWinnerMessage, setShowWinnerMessage] = useState(false); 86 | const [showDrawMessage, setShowDrawMessage] = useState(false); 87 | 88 | useEffect(function () { 89 | let handleWinner = function () { 90 | const winner = calculateWinner(squares); 91 | 92 | if (winner) { 93 | setShowWinnerMessage(true); 94 | setTimeout(function () { 95 | setShowWinnerMessage(false); 96 | var nextSquares = Array(9).fill(null); 97 | setSquare(nextSquares); 98 | }, 1500); 99 | } else if (squares.every((square) => square !== null)) { 100 | // Periksa kondisi seri ketika semua kotak terisi dan tidak ada pemenang 101 | setShowDrawMessage(true); 102 | setTimeout(function () { 103 | setShowDrawMessage(false); 104 | var nextSquares = Array(9).fill(null); 105 | setSquare(nextSquares); 106 | }, 1000); 107 | } 108 | }; 109 | 110 | handleWinner(); 111 | }, [squares]); 112 | 113 | function handleClick(i) { 114 | if (squares[i] || calculateWinner(squares)) return; 115 | 116 | const nextSquare = squares.slice(); 117 | nextSquare[i] = 'X'; // Langkah manusia 118 | setSquare(nextSquare); 119 | 120 | if (calculateWinner(squares) || nextSquare.every((square) => square !== null)) return; 121 | setXIsNext(false); 122 | 123 | // Periksa apakah ada baris dengan dua nilai O 124 | for (let row of [ 125 | [0, 1, 2], 126 | [3, 4, 5], 127 | [6, 7, 8], 128 | [0, 3, 6], 129 | [1, 4, 7], 130 | [2, 5, 8], 131 | [0, 4, 8], 132 | [2, 4, 6] 133 | ]) { 134 | const [a, b, c] = row; 135 | const nilaiBaris = [nextSquare[a], nextSquare[b], nextSquare[c]]; 136 | 137 | if (nilaiBaris.filter(nilai => nilai === 'O').length === 2 && nilaiBaris.includes(null)) { 138 | const indeksNull = nilaiBaris.indexOf(null); 139 | const langkahBotIndex = row[indeksNull]; 140 | 141 | // Tambahkan setTimeout sebelum langkah bot diambil 142 | setTimeout(() => { 143 | const updatedSquare = nextSquare.slice(); 144 | updatedSquare[langkahBotIndex] = 'O'; // Langkah bot 145 | if (calculateWinner(squares)) return; 146 | setSquare(updatedSquare); 147 | setXIsNext(true); 148 | }, 500); 149 | 150 | return; 151 | } 152 | } 153 | 154 | 155 | // Periksa apakah ada baris dengan dua nilai X 156 | for (let row of [ 157 | [0, 1, 2], 158 | [3, 4, 5], 159 | [6, 7, 8], 160 | [0, 3, 6], 161 | [1, 4, 7], 162 | [2, 5, 8], 163 | [0, 4, 8], 164 | [2, 4, 6] 165 | ]) { 166 | const [a, b, c] = row; 167 | const nilaiBaris = [nextSquare[a], nextSquare[b], nextSquare[c]]; 168 | 169 | if (nilaiBaris.filter(nilai => nilai === 'X').length === 2 && nilaiBaris.includes(null)) { 170 | const indeksNull = nilaiBaris.indexOf(null); 171 | const langkahBotIndex = row[indeksNull]; 172 | 173 | // Tambahkan setTimeout sebelum langkah bot diambil 174 | setTimeout(() => { 175 | const updatedSquare = nextSquare.slice(); 176 | updatedSquare[langkahBotIndex] = 'O'; // Langkah bot 177 | if (calculateWinner(squares)) return; 178 | setSquare(updatedSquare); 179 | setXIsNext(true); 180 | }, 500); 181 | 182 | return; 183 | } 184 | } 185 | 186 | // Jika tidak ada baris dengan dua nilai X, lakukan langkah bot acak 187 | setTimeout(() => { 188 | let langkahBot; 189 | do { 190 | langkahBot = Math.floor(Math.random() * 9); 191 | } while (nextSquare[langkahBot] !== null); 192 | 193 | if (calculateWinner(squares)) return; 194 | 195 | setSquare((prevSquare) => { 196 | const squareTerbaru = prevSquare.slice(); 197 | squareTerbaru[langkahBot] = 'O'; // Langkah bot 198 | return squareTerbaru; 199 | }); 200 | setXIsNext(true); 201 | }, 500); 202 | } 203 | 204 | 205 | function handleReload() { 206 | const nextSquares = Array(9).fill(null); 207 | setSquare(nextSquares); 208 | setXIsNext(true); 209 | } 210 | 211 | const winner = calculateWinner(squares); 212 | 213 | return ( 214 |
215 |
216 | 217 |
218 |
219 | {squares.map((value, index) => ( 220 | handleClick(index)} /> 221 | ))} 222 |
223 |
224 | One Player 225 |
226 |
227 | 228 |
229 |
230 | 231 | 232 | home 233 | 234 | 235 |
236 |
237 | handleReload()} /> 238 |
239 | 243 | {showWinnerMessage && ( 244 |
245 |

Winner!

246 |
{winner}
247 |
248 | )} 249 | {showDrawMessage && ( 250 |
251 |

Draw!

252 |
253 | )} 254 |
255 | ); 256 | } 257 | 258 | //Function calculateWinner 259 | function calculateWinner(squares) { 260 | const lines = [ 261 | [0, 1, 2], 262 | [3, 4, 5], 263 | [6, 7, 8], 264 | [0, 3, 6], 265 | [1, 4, 7], 266 | [2, 5, 8], 267 | [0, 4, 8], 268 | [2, 4, 6] 269 | ]; 270 | 271 | for (let i = 0; i < lines.length; i++) { 272 | // const a = lines[i][0]; //0 273 | // const b = lines[i][1]; //1 274 | // const c = lines[i][2]; //2 275 | // supaya rapi pakai teknik destructuring js 276 | const [a, b, c] = lines[i]; 277 | 278 | //["X", "X", "X", "O", "O", null, null, null, null] 279 | if (squares[a] === squares[b] && squares[a] === squares[c]) { 280 | return squares[a]; 281 | } 282 | } 283 | return false; 284 | } 285 | --------------------------------------------------------------------------------