Skip to content

Commit 320a5f1

Browse files
committed
new(apps): ttt
1 parent 6adef5d commit 320a5f1

File tree

5 files changed

+244
-0
lines changed

5 files changed

+244
-0
lines changed

public/apps/apps.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@
8181
"title": "Rock Paper Scissors",
8282
"description": "Play the classic game of Rock Paper Scissors against the computer.",
8383
"icon": "HandshakeIcon"
84+
},
85+
{
86+
"slug": "tic-tac-toe",
87+
"to": "/apps/tic-tac-toe",
88+
"title": "Tic Tac Toe",
89+
"description": "Play the classic game of Tic Tac Toe against another player or AI.",
90+
"icon": "XCircleIcon"
8491
}
8592
]
8693
},

src/components/AnimatedRoutes.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import CardGamePage from '../pages/apps/CardGamePage';
4848
import SoccerPongPage from '../pages/apps/SoccerPongPage';
4949
import MemoryGamePage from '../pages/apps/MemoryGamePage'; // Import MemoryGamePage
5050
import RockPaperScissorsPage from '../pages/apps/RockPaperScissorsPage'; // Import RockPaperScissorsPage
51+
import TicTacToePage from '../pages/apps/TicTacToePage'; // Import TicTacToePage
5152
import SettingsPage from '../pages/SettingsPage';
5253

5354
import UsefulLinksPage from '../pages/UsefulLinksPage';
@@ -407,6 +408,7 @@ function AnimatedRoutes() {
407408
<Route path="/apps::sp" element={<Navigate to="/apps/soccer-pong" replace />} />
408409
<Route path="/apps::mg" element={<Navigate to="/apps/memory-game" replace />} />
409410
<Route path="/apps::rps" element={<Navigate to="/apps/rock-paper-scissors" replace />} />
411+
<Route path="/apps::ttt" element={<Navigate to="/apps/tic-tac-toe" replace />} />
410412
{/* End of hardcoded redirects */}
411413
<Route
412414
path="/apps/ip"
@@ -422,6 +424,20 @@ function AnimatedRoutes() {
422424
</motion.div>
423425
}
424426
/>
427+
<Route
428+
path="/apps/tic-tac-toe"
429+
element={
430+
<motion.div
431+
initial="initial"
432+
animate="in"
433+
exit="out"
434+
variants={pageVariants}
435+
transition={pageTransition}
436+
>
437+
<TicTacToePage />
438+
</motion.div>
439+
}
440+
/>
425441
<Route
426442
path="/apps/rock-paper-scissors"
427443
element={

src/pages/apps/TicTacToePage.js

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import { ArrowLeft, X, Circle } from '@phosphor-icons/react';
4+
import { useToast } from '../../hooks/useToast';
5+
import useSeo from "../../hooks/useSeo";
6+
import colors from '../../config/colors';
7+
8+
const TicTacToePage = () => {
9+
useSeo({
10+
title: 'Tic Tac Toe | Fezcodex',
11+
description: 'Play the classic game of Tic Tac Toe against another player or AI.',
12+
keywords: ['Fezcodex', 'tic tac toe', 'game', 'fun app'],
13+
ogTitle: 'Tic Tac Toe | Fezcodex',
14+
ogDescription: 'Play the classic game of Tic Tac Toe against another player or AI.',
15+
ogImage: 'https://fezcode.github.io/logo512.png',
16+
twitterCard: 'summary_large_image',
17+
twitterTitle: 'Tic Tac Toe | Fezcodex',
18+
twitterDescription: 'Play the classic game of Tic Tac Toe against another player or AI.',
19+
twitterImage: 'https://fezcode.github.io/logo512.png'
20+
});
21+
22+
const [board, setBoard] = useState(Array(9).fill(null));
23+
const [xIsNext, setXIsNext] = useState(true);
24+
const [winner, setWinner] = useState(null);
25+
const { addToast } = useToast();
26+
27+
useEffect(() => {
28+
if (!xIsNext && !winner) {
29+
// AI's turn (simple AI for now)
30+
const timer = setTimeout(() => {
31+
makeAiMove();
32+
}, 500);
33+
return () => clearTimeout(timer);
34+
}
35+
}, [xIsNext, board, winner]);
36+
37+
useEffect(() => {
38+
const calculatedWinner = calculateWinner(board);
39+
if (calculatedWinner) {
40+
setWinner(calculatedWinner);
41+
addToast({ title: 'Game Over', message: `${calculatedWinner} wins!`, duration: 3000 });
42+
} else if (board.every(Boolean)) {
43+
setWinner('Draw');
44+
addToast({ title: 'Game Over', message: 'It\'s a draw!', duration: 3000 });
45+
}
46+
}, [board, addToast]);
47+
48+
const makeAiMove = () => {
49+
const bestMove = findBestMove(board);
50+
if (bestMove !== null) {
51+
handleClick(bestMove);
52+
}
53+
};
54+
55+
const handleClick = (i) => {
56+
if (winner || board[i]) {
57+
return;
58+
}
59+
const newBoard = board.slice();
60+
newBoard[i] = xIsNext ? 'X' : 'O';
61+
setBoard(newBoard);
62+
setXIsNext(!xIsNext);
63+
};
64+
65+
// Minimax algorithm functions
66+
const minimax = (currentBoard, depth, isMaximizingPlayer) => {
67+
const result = calculateWinner(currentBoard);
68+
69+
if (result === 'X') return -10 + depth; // Player X (human) wins
70+
if (result === 'O') return 10 - depth; // Player O (AI) wins
71+
if (currentBoard.every(Boolean)) return 0; // It's a draw
72+
73+
if (isMaximizingPlayer) {
74+
let bestScore = -Infinity;
75+
for (let i = 0; i < currentBoard.length; i++) {
76+
if (currentBoard[i] === null) {
77+
currentBoard[i] = 'O';
78+
let score = minimax(currentBoard, depth + 1, false);
79+
currentBoard[i] = null;
80+
bestScore = Math.max(score, bestScore);
81+
}
82+
}
83+
return bestScore;
84+
} else {
85+
let bestScore = Infinity;
86+
for (let i = 0; i < currentBoard.length; i++) {
87+
if (currentBoard[i] === null) {
88+
currentBoard[i] = 'X';
89+
let score = minimax(currentBoard, depth + 1, true);
90+
currentBoard[i] = null;
91+
bestScore = Math.min(score, bestScore);
92+
}
93+
}
94+
return bestScore;
95+
}
96+
};
97+
98+
const findBestMove = (currentBoard) => {
99+
let bestScore = -Infinity;
100+
let move = null;
101+
for (let i = 0; i < currentBoard.length; i++) {
102+
if (currentBoard[i] === null) {
103+
currentBoard[i] = 'O';
104+
let score = minimax(currentBoard, 0, false);
105+
currentBoard[i] = null;
106+
if (score > bestScore) {
107+
bestScore = score;
108+
move = i;
109+
}
110+
}
111+
}
112+
return move;
113+
};
114+
115+
const renderSquare = (i) => (
116+
<button
117+
className="w-24 h-24 bg-gray-800 border border-gray-600 text-5xl flex items-center justify-center"
118+
onClick={() => handleClick(i)}
119+
disabled={winner || board[i]}
120+
>
121+
{board[i] === 'X' && <X size={48} color={colors.red} />}
122+
{board[i] === 'O' && <Circle size={40} color={colors.blue} weight="thin" />}
123+
</button>
124+
);
125+
126+
const resetGame = () => {
127+
setBoard(Array(9).fill(null));
128+
setXIsNext(true);
129+
setWinner(null);
130+
};
131+
132+
const status = winner
133+
? winner === 'Draw' ? 'Draw!' : `Winner: ${winner}`
134+
: `Next player: ${xIsNext ? 'X' : 'O'}`;
135+
136+
const cardStyle = {
137+
backgroundColor: colors['app-alpha-10'],
138+
borderColor: colors['app-alpha-50'],
139+
color: colors.app,
140+
};
141+
142+
return (
143+
<div className="py-16 sm:py-24">
144+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-gray-300">
145+
<Link
146+
to="/apps"
147+
className="text-article hover:underline flex items-center justify-center gap-2 text-lg mb-4"
148+
>
149+
<ArrowLeft size={24} /> Back to Apps
150+
</Link>
151+
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl mb-4 flex items-center">
152+
<span className="codex-color">fc</span>
153+
<span className="separator-color">::</span>
154+
<span className="apps-color">apps</span>
155+
<span className="separator-color">::</span>
156+
<span className="single-app-color">ttt</span>
157+
</h1>
158+
<hr className="border-gray-700" />
159+
<div className="flex justify-center items-center mt-16">
160+
<div
161+
className="group bg-transparent border rounded-lg shadow-2xl p-6 flex flex-col justify-between relative transform transition-all duration-300 ease-in-out scale-105 overflow-hidden h-full w-full max-w-4xl"
162+
style={cardStyle}
163+
>
164+
<div
165+
className="absolute top-0 left-0 w-full h-full opacity-10"
166+
style={{
167+
backgroundImage:
168+
'radial-gradient(circle, white 1px, transparent 1px)',
169+
backgroundSize: '10px 10px',
170+
}}
171+
></div>
172+
<div className="relative z-10 p-1">
173+
<h1 className="text-3xl font-arvo font-normal mb-4 text-app"> Tic Tac Toe </h1>
174+
<hr className="border-gray-700 mb-4" />
175+
<div className="flex flex-col items-center gap-8">
176+
<div className="text-2xl font-medium mb-4">{status}</div>
177+
<div className="grid grid-cols-3 gap-1">
178+
{Array(9).fill(null).map((_, i) => renderSquare(i))}
179+
</div>
180+
<button
181+
onClick={resetGame}
182+
className="flex items-center gap-2 text-lg font-arvo font-normal px-4 py-2 rounded-md border transition-colors duration-300 ease-in-out bg-app/50 text-white hover:bg-app/70"
183+
style={{ borderColor: colors['app-alpha-50'] }}
184+
>
185+
Reset Game
186+
</button>
187+
</div>
188+
</div>
189+
</div>
190+
</div>
191+
</div>
192+
</div>
193+
);
194+
};
195+
196+
function calculateWinner(squares) {
197+
const lines = [
198+
[0, 1, 2],
199+
[3, 4, 5],
200+
[6, 7, 8],
201+
[0, 3, 6],
202+
[1, 4, 7],
203+
[2, 5, 8],
204+
[0, 4, 8],
205+
[2, 4, 6],
206+
];
207+
for (let i = 0; i < lines.length; i++) {
208+
const [a, b, c] = lines[i];
209+
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
210+
return squares[a];
211+
}
212+
}
213+
return null;
214+
}
215+
216+
export default TicTacToePage;

src/styles/TicTacToePage.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* No custom CSS needed for TicTacToePage yet, as it primarily uses Tailwind CSS. */

src/utils/appIcons.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {
2727
AtomIcon,
2828
SoccerBallIcon,
2929
BrainIcon,
30+
HandshakeIcon,
31+
XCircleIcon,
3032
} from '@phosphor-icons/react';
3133

3234
export const appIcons = {
@@ -58,4 +60,6 @@ export const appIcons = {
5860
AtomIcon,
5961
SoccerBallIcon,
6062
BrainIcon,
63+
HandshakeIcon,
64+
XCircleIcon,
6165
};

0 commit comments

Comments
 (0)