Skip to content

Commit bc23969

Browse files
committed
feat: new game.
1 parent d39064b commit bc23969

File tree

5 files changed

+309
-0
lines changed

5 files changed

+309
-0
lines changed

public/apps/apps.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@
123123
"title": "Nonogram",
124124
"description": "Solve picture logic puzzles by filling cells according to numerical clues.",
125125
"icon": "GridFourIcon"
126+
},
127+
{
128+
"slug": "whack-a-bug",
129+
"to": "/apps/whack-a-bug",
130+
"title": "Whack-a-Bug",
131+
"description": "Test your reflexes by fixing bugs as fast as you can!",
132+
"icon": "BugIcon"
126133
}
127134
]
128135
},

src/components/AnimatedRoutes.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import MastermindPage from '../pages/apps/MastermindPage';
5858
import WordLadderPage from '../pages/apps/WordLadderPage'; // Import WordLadderPage
5959
import LightsOutPage from '../pages/apps/LightsOutPage'; // Import LightsOutPage
6060
import NonogramPage from '../pages/apps/NonogramPage'; // Import NonogramPage
61+
import WhackABugPage from '../pages/apps/WhackABugPage';
6162
import SettingsPage from '../pages/SettingsPage';
6263

6364
import UsefulLinksPage from '../pages/UsefulLinksPage';
@@ -559,6 +560,10 @@ function AnimatedRoutes() {
559560
path="/apps::ng"
560561
element={<Navigate to="/apps/nonogram" replace />}
561562
/>
563+
<Route
564+
path="/apps::wab"
565+
element={<Navigate to="/apps/whack-a-bug" replace />}
566+
/>
562567
{/* End of hardcoded redirects */}
563568
<Route
564569
path="/apps/ip"
@@ -1121,6 +1126,20 @@ function AnimatedRoutes() {
11211126
</motion.div>
11221127
}
11231128
/>
1129+
<Route
1130+
path="/apps/whack-a-bug"
1131+
element={
1132+
<motion.div
1133+
initial="initial"
1134+
animate="in"
1135+
exit="out"
1136+
variants={pageVariants}
1137+
transition={pageTransition}
1138+
>
1139+
<WhackABugPage />
1140+
</motion.div>
1141+
}
1142+
/>
11241143
{/* D&D specific 404 page */}
11251144
<Route
11261145
path="/stories/*"

src/pages/apps/WhackABugPage.js

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import React, {useState, useEffect, useRef} from 'react';
2+
import {Link} from 'react-router-dom';
3+
import {ArrowLeftIcon, BugIcon, GavelIcon} from '@phosphor-icons/react';
4+
import colors from '../../config/colors';
5+
import useSeo from '../../hooks/useSeo';
6+
import '../../styles/WhackABugPage.css';
7+
8+
const HOLE_COUNT = 9;
9+
const GAME_DURATION = 30;
10+
const INITIAL_SPEED = 800;
11+
12+
const WhackABugPage = () => {
13+
useSeo({
14+
title: 'Whack-a-Bug | Fezcodex',
15+
description: 'Test your reflexes by fixing bugs as fast as you can!',
16+
keywords: ['Fezcodex', 'whack a bug', 'game', 'reflexes', 'fun'],
17+
ogTitle: 'Whack-a-Bug | Fezcodex',
18+
ogDescription: 'Test your reflexes by fixing bugs as fast as you can!',
19+
ogImage: 'https://fezcode.github.io/logo512.png',
20+
twitterCard: 'summary_large_image',
21+
twitterTitle: 'Whack-a-Bug | Fezcodex',
22+
twitterDescription: 'Test your reflexes by fixing bugs as fast as you can!',
23+
twitterImage: 'https://fezcode.github.io/logo512.png',
24+
});
25+
26+
const [score, setScore] = useState(0);
27+
const [timeLeft, setTimeLeft] = useState(GAME_DURATION);
28+
const [activeHole, setActiveHole] = useState(null);
29+
const [gameActive, setGameActive] = useState(false);
30+
const [lastHole, setLastHole] = useState(null);
31+
const timerRef = useRef(null);
32+
const bugTimerRef = useRef(null);
33+
34+
const startGame = () => {
35+
setScore(0);
36+
setTimeLeft(GAME_DURATION);
37+
setGameActive(true);
38+
setActiveHole(null);
39+
setLastHole(null); // Start game timer
40+
timerRef.current = setInterval(() => {
41+
setTimeLeft((prev) => {
42+
if (prev <= 1) {
43+
endGame();
44+
return 0;
45+
}
46+
return prev - 1;
47+
});
48+
}, 1000);
49+
50+
// Start bug movement
51+
moveBug();
52+
};
53+
54+
const endGame = () => {
55+
setGameActive(false);
56+
clearInterval(timerRef.current);
57+
clearTimeout(bugTimerRef.current);
58+
setActiveHole(null);
59+
};
60+
61+
const moveBug = () => {
62+
if (!gameActive && timeLeft === GAME_DURATION && score === 0) {
63+
// Initial move called from startGame
64+
} else if (!gameActive) {
65+
return;
66+
}
67+
68+
const randomTime = random(500, 1000);
69+
const holeIndex = randomHole(lastHole);
70+
71+
setActiveHole(holeIndex);
72+
setLastHole(holeIndex);
73+
74+
bugTimerRef.current = setTimeout(() => {
75+
if (gameActive) {
76+
moveBug();
77+
}
78+
}, randomTime);
79+
};
80+
81+
// Need a separate effect to keep the bug moving loop correctly responsive to gameActive state change if handled inside moveBug,
82+
// but refs are better for intervals.
83+
// Actually, simpler approach:
84+
useEffect(() => {
85+
if (gameActive) {
86+
const jump = () => {
87+
const time = random(600, 1000);
88+
const idx = Math.floor(Math.random() * HOLE_COUNT);
89+
let newHole = idx;
90+
if (newHole === lastHole) {
91+
newHole = (newHole + 1) % HOLE_COUNT;
92+
}
93+
setActiveHole(newHole);
94+
setLastHole(newHole);
95+
bugTimerRef.current = setTimeout(jump, time);
96+
};
97+
jump();
98+
}
99+
return () => clearTimeout(bugTimerRef.current);
100+
}, [gameActive]);
101+
102+
const random = (min, max) => Math.floor(Math.random() * (max - min) + min);
103+
const randomHole = (previous) => {
104+
const idx = Math.floor(Math.random() * HOLE_COUNT);
105+
if (idx === previous) {
106+
return (idx + 1) % HOLE_COUNT;
107+
}
108+
return idx;
109+
};
110+
111+
const handleWhack = (index) => {
112+
if (!gameActive) return;
113+
if (index === activeHole) {
114+
setScore((prev) => prev + 1);
115+
setActiveHole(null); // Hide immediately
116+
// Optional: Trigger immediate move?
117+
// For now let the timer handle it, or it might be too easy.
118+
}
119+
};
120+
121+
useEffect(() => {
122+
return () => {
123+
clearInterval(timerRef.current);
124+
clearTimeout(bugTimerRef.current);
125+
};
126+
}, []);
127+
128+
const cardStyle = {
129+
backgroundColor: colors['app-alpha-10'],
130+
borderColor: colors['app-alpha-50'],
131+
color: colors.app,
132+
};
133+
134+
return (
135+
<div className="py-16 sm:py-24">
136+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-gray-300">
137+
<Link
138+
to="/apps"
139+
className="text-article hover:underline flex items-center justify-center gap-2 text-lg mb-4"
140+
>
141+
<ArrowLeftIcon size={24}/> Back to Apps
142+
</Link>
143+
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl mb-4 flex items-center">
144+
<span className="codex-color">fc</span>
145+
<span className="separator-color">::</span>
146+
<span className="apps-color">apps</span>
147+
<span className="separator-color">::</span>
148+
<span className="single-app-color">wab</span>
149+
</h1>
150+
<hr className="border-gray-700"/>
151+
<div className="flex justify-center items-center mt-16">
152+
<div
153+
className="group bg-transparent border rounded-lg shadow-2xl p-6 flex flex-col justify-between relative transform overflow-hidden h-full w-full max-w-4xl"
154+
style={cardStyle}
155+
>
156+
<div
157+
className="absolute top-0 left-0 w-full h-full opacity-10"
158+
style={{
159+
backgroundImage:
160+
'radial-gradient(circle, white 1px, transparent 1px)',
161+
backgroundSize: '10px 10px',
162+
}}
163+
></div>
164+
<div className="relative z-10 p-1">
165+
<h1 className="text-3xl font-arvo font-normal mb-4 text-app flex items-center gap-2">
166+
<GavelIcon size={32}/> Whack-a-Bug
167+
</h1>
168+
<hr className="border-gray-700 mb-4"/>
169+
170+
<div className="flex justify-between items-center mb-8 text-xl font-bold">
171+
<div>Score: <span className="text-green-400">{score}</span></div>
172+
<div>Time: <span className={`${timeLeft <= 5 ? 'text-red-500' : ''}`}>{timeLeft}s</span></div>
173+
</div>
174+
175+
{!gameActive && timeLeft === 0 ? (
176+
<div className="text-center mb-8">
177+
<h2 className="text-3xl font-bold mb-2">Game Over!</h2>
178+
<p className="text-xl mb-4">Final Score: {score}</p>
179+
<button
180+
onClick={startGame}
181+
className="px-6 py-2 rounded-md text-lg font-arvo font-normal border transition-colors duration-300 hover:bg-white/10"
182+
style={{borderColor: cardStyle.color, color: cardStyle.color}}
183+
>
184+
Play Again
185+
</button>
186+
</div>
187+
) : !gameActive ? (
188+
<div className="text-center mb-8">
189+
<button
190+
onClick={startGame}
191+
className="px-6 py-2 rounded-md text-lg font-arvo font-normal border transition-colors duration-300 hover:bg-white/10"
192+
style={{borderColor: cardStyle.color, color: cardStyle.color}}
193+
>
194+
Start Game
195+
</button>
196+
</div>
197+
) : null}
198+
199+
<div className="whack-a-bug-grid">
200+
{Array.from({length: HOLE_COUNT}).map((_, index) => (
201+
<div
202+
key={index}
203+
className="bug-hole flex items-center justify-center"
204+
onClick={() => handleWhack(index)}
205+
>
206+
{activeHole === index && (
207+
<div className="bug text-red-400">
208+
<BugIcon size={48} weight="fill"/>
209+
</div>
210+
)}
211+
</div>
212+
))}
213+
</div>
214+
215+
<p className="text-center mt-8 text-sm opacity-70">
216+
Click the bugs as they appear! Don't let them escape.
217+
</p>
218+
</div>
219+
</div>
220+
</div>
221+
</div>
222+
</div>
223+
);
224+
};
225+
226+
export default WhackABugPage;

src/styles/WhackABugPage.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
.whack-a-bug-grid {
2+
display: grid;
3+
grid-template-columns: repeat(3, 1fr);
4+
gap: 20px;
5+
max-width: 400px;
6+
margin: 0 auto;
7+
}
8+
9+
.bug-hole {
10+
width: 100px;
11+
height: 100px;
12+
background-color: rgba(255, 255, 255, 0.1);
13+
border-radius: 50%;
14+
position: relative;
15+
cursor: pointer;
16+
border: 2px solid rgba(255, 255, 255, 0.2);
17+
transition: border-color 0.3s;
18+
overflow: hidden;
19+
}
20+
21+
.bug-hole:hover {
22+
border-color: rgba(255, 255, 255, 0.5);
23+
}
24+
25+
.bug {
26+
position: absolute;
27+
top: 50%;
28+
left: 50%;
29+
transform: translate(-50%, -50%);
30+
font-size: 3rem;
31+
animation: pop-up 0.3s ease-out;
32+
}
33+
34+
@keyframes pop-up {
35+
0% {
36+
transform: translate(-50%, 50%) scale(0);
37+
}
38+
80% {
39+
transform: translate(-50%, -60%) scale(1.1);
40+
}
41+
100% {
42+
transform: translate(-50%, -50%) scale(1);
43+
}
44+
}
45+
46+
.whacked {
47+
animation: whack-anim 0.2s ease-in;
48+
color: #4ade80; /* green-400 */
49+
}
50+
51+
@keyframes whack-anim {
52+
0% { transform: translate(-50%, -50%) scale(1); }
53+
50% { transform: translate(-50%, -50%) scale(0.8); }
54+
100% { transform: translate(-50%, -50%) scale(0); opacity: 0; }
55+
}

src/utils/appIcons.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
LadderSimpleIcon,
3737
LightbulbFilamentIcon,
3838
GridFourIcon,
39+
BugIcon,
3940
} from '@phosphor-icons/react';
4041

4142
export const appIcons = {
@@ -76,4 +77,5 @@ export const appIcons = {
7677
LadderSimpleIcon,
7778
LightbulbFilamentIcon,
7879
GridFourIcon,
80+
BugIcon,
7981
};

0 commit comments

Comments
 (0)