Skip to content

Commit 802c5b1

Browse files
committed
app: mastermind
1 parent ab27c95 commit 802c5b1

File tree

4 files changed

+193
-0
lines changed

4 files changed

+193
-0
lines changed

public/apps/apps.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@
9595
"title": "Connect Four",
9696
"description": "Play the classic game of Connect Four against another player or AI.",
9797
"icon": "AtomIcon"
98+
},
99+
{
100+
"slug": "mastermind",
101+
"to": "/apps/mastermind",
102+
"title": "Mastermind",
103+
"description": "Play the classic code-breaking game of Mastermind (Bulls and Cows).",
104+
"icon": "PuzzlePieceIcon"
98105
}
99106
]
100107
},

src/components/AnimatedRoutes.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import ImageCompressorPage from '../pages/apps/ImageCompressorPage'; // Import I
5454
import StopwatchAppPage from '../pages/StopwatchAppPage'; // Import StopwatchAppPage
5555
import PomodoroTimerPage from '../pages/apps/PomodoroTimerPage';
5656
import MorseCodeTranslatorPage from '../pages/apps/MorseCodeTranslatorPage';
57+
import MastermindPage from '../pages/apps/MastermindPage';
5758
import SettingsPage from '../pages/SettingsPage';
5859

5960
import UsefulLinksPage from '../pages/UsefulLinksPage';
@@ -539,6 +540,10 @@ function AnimatedRoutes() {
539540
path="/apps::mct"
540541
element={<Navigate to="/apps/morse-code-translator" replace />}
541542
/>
543+
<Route
544+
path="/apps::mm"
545+
element={<Navigate to="/apps/mastermind" replace />}
546+
/>
542547
{/* End of hardcoded redirects */}
543548
<Route
544549
path="/apps/ip"
@@ -1045,6 +1050,20 @@ function AnimatedRoutes() {
10451050
</motion.div>
10461051
}
10471052
/>
1053+
<Route
1054+
path="/apps/mastermind"
1055+
element={
1056+
<motion.div
1057+
initial="initial"
1058+
animate="in"
1059+
exit="out"
1060+
variants={pageVariants}
1061+
transition={pageTransition}
1062+
>
1063+
<MastermindPage />
1064+
</motion.div>
1065+
}
1066+
/>
10481067
{/* D&D specific 404 page */}
10491068
<Route
10501069
path="/stories/*"

src/pages/apps/MastermindPage.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import { ArrowLeftIcon, LightbulbIcon, ArrowCounterClockwiseIcon } from '@phosphor-icons/react';
4+
import useSeo from '../../hooks/useSeo';
5+
import { useToast } from '../../hooks/useToast';
6+
7+
const MastermindPage = () => {
8+
useSeo({
9+
title: 'Mastermind Game | Fezcodex',
10+
description: 'Play the classic code-breaking game of Mastermind (Bulls and Cows).',
11+
keywords: ['Fezcodex', 'mastermind', 'bulls and cows', 'game', 'logic game'],
12+
ogTitle: 'Mastermind Game | Fezcodex',
13+
ogDescription: 'Play the classic code-breaking game of Mastermind (Bulls and Cows).',
14+
ogImage: 'https://fezcode.github.io/logo512.png',
15+
twitterCard: 'summary_large_image',
16+
twitterTitle: 'Mastermind Game | Fezcodex',
17+
twitterDescription: 'Play the classic code-breaking game of Mastermind (Bulls and Cows).',
18+
twitterImage: 'https://fezcode.github.io/logo512.png',
19+
});
20+
21+
const { addToast } = useToast();
22+
const [secretCode, setSecretCode] = useState('');
23+
const [guesses, setGuesses] = useState([]);
24+
const [currentGuess, setCurrentGuess] = useState('');
25+
const [gameOver, setGameOver] = useState(false);
26+
const [message, setMessage] = useState('');
27+
const MAX_GUESSES = 10;
28+
29+
const generateSecretCode = () => {
30+
let digits = '0123456789'.split('');
31+
let code = '';
32+
for (let i = 0; i < 4; i++) {
33+
const randomIndex = Math.random() * digits.length;
34+
code += digits.splice(randomIndex, 1)[0];
35+
}
36+
setSecretCode(code);
37+
};
38+
39+
useEffect(() => {
40+
generateSecretCode();
41+
}, []);
42+
43+
useEffect(() => {
44+
if (gameOver && message) {
45+
addToast({ title: "Mastermind", message: message, duration: 5000 });
46+
}
47+
}, [gameOver, message, addToast]);
48+
49+
const handleGuessSubmit = (e) => {
50+
e.preventDefault();
51+
if (gameOver) return;
52+
53+
if (currentGuess.length !== 4 || !/^\d{4}$/.test(currentGuess) || new Set(currentGuess).size !== 4) {
54+
setMessage('Please enter a 4-digit number with unique digits.');
55+
addToast({ title: "Mastermind Error", message: "Please enter a 4-digit number with unique digits.", duration: 3000 });
56+
return;
57+
}
58+
59+
let bulls = 0;
60+
let cows = 0;
61+
for (let i = 0; i < 4; i++) {
62+
if (currentGuess[i] === secretCode[i]) {
63+
bulls++;
64+
} else if (secretCode.includes(currentGuess[i])) {
65+
cows++;
66+
}
67+
}
68+
69+
const newGuesses = [...guesses, { guess: currentGuess, bulls, cows }];
70+
setGuesses(newGuesses);
71+
72+
if (bulls === 4) {
73+
setGameOver(true);
74+
setMessage(`You won! The code was ${secretCode}.`);
75+
} else if (newGuesses.length >= MAX_GUESSES) {
76+
setGameOver(true);
77+
setMessage(`Game Over! The secret code was ${secretCode}.`);
78+
}
79+
80+
setCurrentGuess('');
81+
};
82+
83+
const handleResetGame = () => {
84+
generateSecretCode();
85+
setGuesses([]);
86+
setCurrentGuess('');
87+
setGameOver(false);
88+
setMessage('');
89+
};
90+
91+
return (
92+
<div className="py-16 sm:py-24">
93+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-gray-300">
94+
<Link to="/apps" className="text-article hover:underline flex items-center justify-center gap-2 text-lg mb-4">
95+
<ArrowLeftIcon size={24} /> Back to Apps
96+
</Link>
97+
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl mb-4 flex items-center">
98+
<span className="codex-color">fc</span>
99+
<span className="separator-color">::</span>
100+
<span className="apps-color">apps</span>
101+
<span className="separator-color">::</span>
102+
<span className="single-app-color">mm</span>
103+
</h1>
104+
<hr className="border-gray-700" />
105+
<div className="flex justify-center items-center mt-16">
106+
<div className="bg-app-alpha-10 border-app-alpha-50 text-app hover:bg-app/15 group 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-lg">
107+
<div className="absolute top-0 left-0 w-full h-full opacity-10" style={{ backgroundImage: 'radial-gradient(circle, white 1px, transparent 1px)', backgroundSize: '10px 10px' }}></div>
108+
<div className="relative z-10 p-4">
109+
<h1 className="text-3xl font-arvo font-normal mb-2 text-app">Mastermind</h1>
110+
<p className="text-center text-app-light mb-4">Guess the secret 4-digit code. Digits are unique.</p>
111+
<hr className="border-gray-700 mb-6" />
112+
{!gameOver && (
113+
<form onSubmit={handleGuessSubmit} className="flex gap-2 justify-center mb-4">
114+
<input
115+
type="text"
116+
maxLength="4"
117+
value={currentGuess}
118+
onChange={(e) => setCurrentGuess(e.target.value)}
119+
className="w-32 text-center text-2xl p-2 bg-gray-900/50 font-mono border rounded-md focus:ring-0 text-app-light border-app-alpha-50"
120+
disabled={gameOver}
121+
/>
122+
<button type="submit" className="px-4 py-2 rounded-md font-semibold bg-tb text-app border-app-alpha-50 hover:bg-app/15 border flex items-center gap-2" disabled={gameOver}>
123+
<LightbulbIcon size={24} /> Guess
124+
</button>
125+
</form>
126+
)}
127+
128+
{message && <p className="text-center text-lg font-semibold my-4">{message}</p>}
129+
<div className="flex justify-center mb-4">
130+
<button onClick={handleResetGame} className="px-4 py-2 rounded-md font-semibold bg-tb text-app border-app-alpha-50 hover:bg-app/15 border flex items-center gap-2">
131+
<ArrowCounterClockwiseIcon size={24} /> New Game
132+
</button>
133+
</div>
134+
<div className="text-center text-lg font-semibold my-4">
135+
Guesses: {guesses.length} / {MAX_GUESSES}
136+
</div>
137+
<div className="h-64 overflow-y-auto border border-app-alpha-50 rounded-md p-2">
138+
<table className="w-full text-left font-mono">
139+
<thead>
140+
<tr className="border-b border-app-alpha-50">
141+
<th className="p-2">Guess</th>
142+
<th className="p-2">Bulls (Correct)</th>
143+
<th className="p-2">Cows (Present)</th>
144+
</tr>
145+
</thead>
146+
<tbody>
147+
{guesses.slice(0).reverse().map((g, index) => (
148+
<tr key={index} className="border-b border-app-alpha-50/50">
149+
<td className="p-2">{g.guess}</td>
150+
<td className="p-2">{g.bulls}</td>
151+
<td className="p-2">{g.cows}</td>
152+
</tr>
153+
))}
154+
</tbody>
155+
</table>
156+
</div>
157+
</div>
158+
</div>
159+
</div>
160+
</div>
161+
</div>
162+
);
163+
};
164+
165+
export default MastermindPage;

src/utils/appIcons.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
ArrowsInLineHorizontalIcon,
3333
TimerIcon,
3434
TranslateIcon,
35+
PuzzlePieceIcon,
3536
} from '@phosphor-icons/react';
3637

3738
export const appIcons = {
@@ -68,4 +69,5 @@ export const appIcons = {
6869
ArrowsInLineHorizontalIcon,
6970
TimerIcon,
7071
TranslateIcon,
72+
PuzzlePieceIcon,
7173
};

0 commit comments

Comments
 (0)