├── 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 |
9 |
10 |
11 | One Player
12 |
13 |
14 |
15 |
Two Players
16 |
17 |
18 |
19 |
27 | >
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 |
16 | {value}
17 |
18 | );
19 | }
20 |
21 | function Reload({ onReloadClick }) {
22 | return (
23 |
24 |
25 | refresh
26 |
27 |
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 |
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 |
155 |
156 | Your browser does not support the audio tag.
157 |
158 | {showWinnerMessage && (
159 |
160 |
Winner!
161 |
{winner}
162 |
163 | )}
164 | {showDrawMessage && (
165 |
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 |
16 | {value}
17 |
18 | );
19 | }
20 |
21 | function Reload({ onReloadClick }) {
22 | return (
23 |
24 |
25 | refresh
26 |
27 |
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 |
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 |
240 |
241 | Your browser does not support the audio tag.
242 |
243 | {showWinnerMessage && (
244 |
245 |
Winner!
246 |
{winner}
247 |
248 | )}
249 | {showDrawMessage && (
250 |
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 |
--------------------------------------------------------------------------------