TypeScript · 27772 bytes Raw Blame History
1 "use client";
2
3 import React, { useState, useEffect, useRef } from 'react';
4 import Image from 'next/image';
5 import TreeVisualizer from './TreeVisualizer';
6 import { gameApi, FileSystemTree, FHSDirectory, CommandReferenceResponse, MoleDirection, TreeNode } from '@/lib/api';
7
8 interface CommandHistoryEntry {
9 command: string;
10 output: string;
11 success: boolean;
12 }
13
14 const Game: React.FC = () => {
15 const [gameState, setGameState] = useState<{
16 tree: FileSystemTree | null;
17 sessionId: number | null;
18 loading: boolean;
19 error: string | null;
20 }>({
21 tree: null,
22 sessionId: null,
23 loading: false,
24 error: null,
25 });
26
27 const [command, setCommand] = useState('');
28 const [commandHistory, setCommandHistory] = useState<CommandHistoryEntry[]>([]);
29 const [executing, setExecuting] = useState(false);
30 const [showHints, setShowHints] = useState(false);
31 const [hints, setHints] = useState<string[]>([]);
32 const [showFHS, setShowFHS] = useState(false);
33 const [fhsDirs, setFhsDirs] = useState<FHSDirectory[]>([]);
34 const [showCommands, setShowCommands] = useState(false);
35 const [commandRef, setCommandRef] = useState<CommandReferenceResponse | null>(null);
36 const [terminalMinimized, setTerminalMinimized] = useState(true);
37 const [hasPlayedIntro, setHasPlayedIntro] = useState(false);
38 const [isDarkMode, setIsDarkMode] = useState(true);
39 const [moleKilled, setMoleKilled] = useState(false);
40 const [moleDirection, setMoleDirection] = useState<MoleDirection | null>(null);
41 const [score, setScore] = useState(0);
42 const [molesKilled, setMolesKilled] = useState(0);
43
44 const terminalRef = useRef<HTMLDivElement>(null);
45 const inputRef = useRef<HTMLInputElement>(null);
46
47 // Auto-scroll terminal to bottom
48 useEffect(() => {
49 if (terminalRef.current) {
50 terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
51 }
52 }, [commandHistory]);
53
54 // Auto-focus input after command execution
55 useEffect(() => {
56 if (!executing && inputRef.current && !terminalMinimized) {
57 inputRef.current.focus();
58 }
59 }, [executing, terminalMinimized]);
60
61 // Start a new game
62 const startNewGame = async () => {
63 try {
64 setGameState({ ...gameState, loading: true, error: null });
65 const response = await gameApi.createGame('Player1');
66 setGameState({
67 tree: response.tree,
68 sessionId: response.session_id,
69 loading: false,
70 error: null,
71 });
72
73 // Reset game state
74 setScore(0);
75 setMolesKilled(0);
76 setMoleDirection(null);
77
78 // Create a more dynamic starting message based on random location
79 const startLocation = response.tree.player_location;
80 const homeDir = response.home_directory || '/home';
81 let locationContext = '';
82
83 if (startLocation.startsWith('/home')) {
84 locationContext = "You've been dropped in someone's home directory. ";
85 } else if (startLocation.startsWith('/usr')) {
86 locationContext = "You're in the system's usr hierarchy. ";
87 } else if (startLocation.startsWith('/var')) {
88 locationContext = "You're somewhere in the variable data area. ";
89 } else if (startLocation.startsWith('/opt')) {
90 locationContext = "You're in the optional packages directory. ";
91 } else {
92 locationContext = "You've been placed somewhere in the filesystem. ";
93 }
94
95 setCommandHistory([{
96 command: 'Hunt started!',
97 output: `${response.mole_hint}\n${locationContext}Your home directory is ${homeDir}.\nUse 'pwd' to see where you are, 'cd ~' to go home.\nType "help" for available commands.`,
98 success: true,
99 }]);
100 setHints([]);
101 setShowHints(false);
102 setTerminalMinimized(true); // Keep terminal minimized on new game
103 setHasPlayedIntro(false); // Reset intro for new game
104 setMoleKilled(false); // Reset mole killed state
105 } catch {
106 setGameState({
107 ...gameState,
108 loading: false,
109 error: 'Failed to start game. Is the backend running on http://localhost:8000?',
110 });
111 }
112 };
113
114 // Get hints
115 const getHints = async () => {
116 if (!gameState.tree) return;
117
118 try {
119 const response = await gameApi.getHint(gameState.tree.id);
120 setHints(response.hints);
121 setShowHints(true);
122 } catch {
123 console.error('Failed to get hints');
124 }
125 };
126
127 // Get FHS reference
128 const getFHSReference = async () => {
129 try {
130 const response = await gameApi.getFHSReference();
131 setFhsDirs(response.directories);
132 setShowFHS(true);
133 } catch {
134 console.error('Failed to get FHS reference');
135 }
136 };
137
138 // Get command reference
139 const getCommandReference = async () => {
140 try {
141 if (!commandRef) {
142 const response = await gameApi.getCommandReference();
143 setCommandRef(response);
144 }
145 setShowCommands(true);
146 } catch {
147 console.error('Failed to get command reference');
148 }
149 };
150
151 // Execute a command
152 const executeCommand = async (cmd: string) => {
153 if (!gameState.tree || !cmd.trim() || executing) return;
154
155 setExecuting(true);
156 try {
157 const response = await gameApi.executeCommand(
158 gameState.tree.id,
159 cmd,
160 gameState.sessionId || undefined
161 );
162
163 // Update command history
164 setCommandHistory(prev => [...prev, {
165 command: cmd,
166 output: response.output,
167 success: response.success,
168 }]);
169
170 // Update player location if moved
171 if (response.current_path !== gameState.tree.player_location) {
172 setGameState(prev => ({
173 ...prev,
174 tree: prev.tree ? {
175 ...prev.tree,
176 player_location: response.current_path,
177 } : null,
178 }));
179 }
180
181 // Check if a new mole was spawned
182 if (response.mole_spawned) {
183 // First, show the killed mole briefly
184 setGameState(prev => ({
185 ...prev,
186 tree: prev.tree ? {
187 ...prev.tree,
188 tree_data: updateTreeDataToShowMole(prev.tree!.tree_data, prev.tree!.player_location),
189 } : null,
190 }));
191
192 // Trigger falling animation
193 setMoleKilled(true);
194
195 // After animation, update tree with new mole location
196 setTimeout(() => {
197 setMoleKilled(false);
198
199 // Update tree to show new mole location
200 if (response.new_mole_location && gameState.tree) {
201 const treeWithNewMole = updateTreeDataToShowMole(
202 removeMoleFromTree(gameState.tree.tree_data),
203 response.new_mole_location
204 );
205
206 setGameState(prev => ({
207 ...prev,
208 tree: prev.tree ? {
209 ...prev.tree,
210 tree_data: treeWithNewMole,
211 } : null,
212 }));
213 }
214
215 // Set new mole direction
216 if (response.mole_direction) {
217 setMoleDirection(response.mole_direction);
218 // Hide direction indicator after 5 seconds
219 setTimeout(() => {
220 setMoleDirection(null);
221 }, 5000);
222 }
223 }, 1500); // Wait for falling animation
224
225 // Update score and moles killed
226 if (response.score !== undefined) setScore(response.score);
227 if (response.moles_killed !== undefined) setMolesKilled(response.moles_killed);
228 }
229
230 // Legacy: Check if game won (for old backend compatibility)
231 if (response.game_won && !response.mole_spawned) {
232 setGameState(prev => ({
233 ...prev,
234 tree: prev.tree ? {
235 ...prev.tree,
236 is_completed: true,
237 tree_data: updateTreeDataToShowMole(prev.tree!.tree_data, prev.tree!.player_location),
238 } : null,
239 }));
240
241 setTimeout(() => {
242 setMoleKilled(true);
243 }, 200);
244 }
245
246 setCommand('');
247 } catch {
248 setCommandHistory(prev => [...prev, {
249 command: cmd,
250 output: 'Error: Failed to execute command. Check your connection.',
251 success: false,
252 }]);
253 } finally {
254 setExecuting(false);
255 }
256 };
257
258 // Update tree data to show mole when found
259 const updateTreeDataToShowMole = (treeData: TreeNode, molePath: string): TreeNode => {
260 if (treeData.path === molePath) {
261 return { ...treeData, has_mole: true };
262 }
263 if (treeData.children) {
264 return {
265 ...treeData,
266 children: treeData.children.map((child) =>
267 updateTreeDataToShowMole(child, molePath)
268 ),
269 };
270 }
271 return treeData;
272 };
273
274 // Remove mole from tree data
275 const removeMoleFromTree = (treeData: TreeNode): TreeNode => {
276 return {
277 ...treeData,
278 has_mole: false,
279 children: treeData.children ? treeData.children.map((child) => removeMoleFromTree(child)) : []
280 };
281 };
282
283 // Handle node click in visualizer
284 const handleNodeClick = (path: string) => {
285 executeCommand(`cd ${path}`);
286 };
287
288 // Start game on mount
289 useEffect(() => {
290 startNewGame();
291 // eslint-disable-next-line react-hooks/exhaustive-deps
292 }, []);
293
294 // Mark intro as played after first render
295 useEffect(() => {
296 if (gameState.tree && !hasPlayedIntro) {
297 const timer = setTimeout(() => {
298 setHasPlayedIntro(true);
299 }, 10000); // Add a buffer to ensure animation completes (9.3s + buffer)
300 return () => clearTimeout(timer);
301 }
302 }, [gameState.tree, hasPlayedIntro]);
303
304 // Get position for mole direction indicator
305 const getMoleIndicatorPosition = (direction: string) => {
306 const positions: Record<string, string> = {
307 'up': 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-full -mt-8',
308 'down': 'bottom-20 left-1/2 -translate-x-1/2',
309 'left': 'top-1/2 left-8 -translate-y-1/2',
310 'right': 'top-1/2 right-8 -translate-y-1/2',
311 'up-left': 'top-20 left-8',
312 'up-right': 'top-20 right-8',
313 'down-left': 'bottom-20 left-8',
314 'down-right': 'bottom-20 right-8',
315 };
316 return positions[direction] || positions['up'];
317 };
318
319 // Get rotation for arrow based on angle
320 const getArrowRotation = (angle: number) => {
321 return `rotate(${angle}deg)`;
322 };
323
324 // Terminal color scheme - always dark mode
325 const terminalColors = {
326 frame: 'bg-stone-200 border-stone-300',
327 header: 'bg-stone-300 border-stone-400',
328 headerText: 'text-stone-900',
329 content: 'bg-black',
330 closeButton: 'text-stone-700 hover:text-stone-900'
331 };
332
333 // Canvas background color - always dark mode
334 const canvasBackground = 'bg-gray-900';
335
336 if (gameState.loading) {
337 return (
338 <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-gray-900 dark:text-white`}>
339 <div className="text-center">
340 <div className="text-2xl mb-4">Loading Bashamole...</div>
341 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-current mx-auto"></div>
342 </div>
343 </div>
344 );
345 }
346
347 if (gameState.error) {
348 return (
349 <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-gray-900 dark:text-white`}>
350 <div className="text-center">
351 <div className="text-red-600 dark:text-red-400 mb-4">{gameState.error}</div>
352 <button
353 onClick={startNewGame}
354 className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
355 >
356 Try Again
357 </button>
358 </div>
359 </div>
360 );
361 }
362
363 if (!gameState.tree) {
364 return (
365 <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-gray-900 dark:text-white`}>
366 <div className="text-center">
367 <h1 className="text-4xl font-bold mb-4">Bashamole</h1>
368 <p className="text-gray-600 dark:text-gray-400 mb-8">Hunt the mole in the Unix filesystem!</p>
369 <button
370 onClick={startNewGame}
371 className="px-8 py-4 bg-green-600 text-white rounded-lg hover:bg-green-700 text-xl transition transform hover:scale-105"
372 >
373 Start New Game
374 </button>
375 </div>
376 </div>
377 );
378 }
379
380 return (
381 <div className={`relative min-h-screen ${canvasBackground} overflow-hidden`}>
382 {/* Tree Canvas - Full Screen Background */}
383 <div className={`absolute inset-0 ${canvasBackground}`}>
384 <TreeVisualizer
385 treeData={gameState.tree.tree_data}
386 playerLocation={gameState.tree.player_location}
387 onNodeClick={handleNodeClick}
388 playIntro={!hasPlayedIntro}
389 isDarkMode={isDarkMode}
390 moleKilled={moleKilled}
391 />
392 </div>
393
394 {/* Mole Direction Indicator */}
395 {moleDirection && (
396 <div
397 className={`absolute ${getMoleIndicatorPosition(moleDirection.direction)} z-40 animate-pulse`}
398 style={{
399 animation: 'pulse 2s ease-in-out infinite, fadeIn 0.5s ease-out'
400 }}
401 >
402 <div className="bg-red-600/90 backdrop-blur-sm border-2 border-red-400 rounded-lg p-3 shadow-2xl flex items-center gap-2">
403 <Image
404 src="/mole.svg"
405 alt="Mole"
406 width={32}
407 height={32}
408 className="w-8 h-8"
409 />
410 <div
411 className="text-white text-2xl"
412 style={{ transform: getArrowRotation(moleDirection.angle) }}
413 >
414
415 </div>
416 </div>
417 </div>
418 )}
419
420 {/* Score Display - Top Right */}
421 {molesKilled > 0 && (
422 <div className="absolute top-4 right-4 bg-black/80 backdrop-blur-sm border border-green-500 rounded-lg p-3 shadow-2xl z-30">
423 <div className="text-green-400 font-terminal text-sm">
424 <div>Score: {score}</div>
425 <div>Moles: {molesKilled}</div>
426 </div>
427 </div>
428 )}
429
430 {/* Floating Terminal - Top Left */}
431 <div className={`absolute top-4 left-4 ${terminalColors.frame} rounded-lg shadow-2xl border transition-all duration-300 z-30 ${
432 terminalMinimized ? 'w-80' : 'w-[700px]'
433 }`}>
434 {/* Terminal Header */}
435 <div className={`flex items-center justify-between ${terminalColors.header} px-4 py-2 rounded-t-lg border-b`}>
436 <div className="flex items-center gap-2">
437 <div className="flex gap-1.5">
438 <button
439 onClick={getCommandReference}
440 className="w-3.5 h-3.5 bg-red-500 hover:bg-red-400 rounded-full flex items-center justify-center transition-colors relative"
441 title="Command Reference"
442 >
443 <span className="text-[8px] font-bold text-gray-900 absolute">×</span>
444 </button>
445 <button
446 onClick={getHints}
447 className="w-3.5 h-3.5 bg-yellow-500 hover:bg-yellow-400 rounded-full flex items-center justify-center transition-colors relative"
448 title="Get Hint"
449 >
450 <span className="text-[9px] font-bold text-gray-900 absolute">?</span>
451 </button>
452 <button
453 onClick={getFHSReference}
454 className="w-3.5 h-3.5 bg-green-500 hover:bg-green-400 rounded-full flex items-center justify-center transition-colors relative"
455 title="FHS Directory Reference"
456 >
457 <span className="text-[9px] font-bold text-gray-900 absolute">/</span>
458 </button>
459 </div>
460 <h3 className={`text-sm font-medium ${terminalColors.headerText} ml-2`}>bash</h3>
461 </div>
462 <button
463 onClick={() => setTerminalMinimized(!terminalMinimized)}
464 className={`${terminalColors.closeButton} transition`}
465 >
466 {terminalMinimized ? '▼' : '▲'}
467 </button>
468 </div>
469
470 {/* Terminal Content */}
471 {!terminalMinimized && (
472 <div
473 ref={terminalRef}
474 className={`${terminalColors.content} p-4 font-terminal text-base h-[300px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-700`}
475 onClick={() => inputRef.current?.focus()}
476 >
477 {commandHistory.map((entry, index) => (
478 <div key={index} className="mb-1">
479 <div className="flex items-start font-terminal">
480 <span className="text-green-400">groundskeeper@molehill</span>
481 <span className="text-gray-400 mx-1">::</span>
482 <span className="text-blue-400">{entry.command.startsWith('Hunt started!') ? '~' : gameState.tree?.player_location || '~'}</span>
483 <span className="text-gray-400 ml-1">$</span>
484 <span className={`ml-2 ${entry.command.startsWith('Hunt started!') ? 'text-yellow-400' : 'text-gray-300'}`}>
485 {entry.command.startsWith('Hunt started!') ? '' : entry.command}
486 </span>
487 </div>
488 {entry.output && (
489 <div className={`${entry.success ? 'text-gray-300' : 'text-red-400'} ml-0 mt-1 font-terminal whitespace-pre-wrap`}>
490 {entry.output.split('\n').map((line, i) => (
491 <div key={i}>{line}</div>
492 ))}
493 </div>
494 )}
495 </div>
496 ))}
497
498 {/* Current input line */}
499 <div className="flex items-start font-terminal">
500 <span className="text-green-400">groundskeeper@molehill</span>
501 <span className="text-gray-400 mx-1">::</span>
502 <span className="text-blue-400">{gameState.tree?.player_location || '~'}</span>
503 <span className="text-gray-400 ml-1">$</span>
504 <div className="flex-1 ml-2">
505 <div className="relative inline-block">
506 <span className="text-gray-300 font-terminal">{command}</span>
507 <span
508 className="text-gray-300 font-terminal"
509 style={{
510 animation: 'blink 1s step-end infinite'
511 }}
512 >
513 _
514 </span>
515 <input
516 ref={inputRef}
517 type="text"
518 value={command}
519 onChange={(e) => setCommand(e.target.value)}
520 onKeyDown={(e) => {
521 if (e.key === 'Enter') {
522 e.preventDefault();
523 executeCommand(command);
524 }
525 }}
526 disabled={executing}
527 className="absolute inset-0 w-full bg-transparent text-transparent outline-none caret-transparent font-terminal"
528 placeholder=""
529 autoFocus
530 spellCheck={false}
531 autoComplete="off"
532 autoCorrect="off"
533 autoCapitalize="off"
534 />
535 </div>
536 </div>
537 </div>
538 </div>
539 )}
540 </div>
541
542 {/* Hints Popup */}
543 {showHints && hints.length > 0 && (
544 <div className="absolute top-16 left-4 bg-yellow-900/95 backdrop-blur-sm border border-yellow-600 rounded-lg p-4 max-w-md z-40 shadow-2xl">
545 <button
546 onClick={() => setShowHints(false)}
547 className="absolute top-2 right-2 text-yellow-400 hover:text-yellow-300"
548 >
549
550 </button>
551 <h3 className="text-yellow-400 font-bold mb-2">Hints:</h3>
552 {hints.map((hint, index) => (
553 <p key={index} className="text-yellow-200 text-sm">{hint}</p>
554 ))}
555 </div>
556 )}
557
558 {/* FHS Reference Modal */}
559 {showFHS && (
560 <div className="absolute top-16 left-4 bg-green-900/95 backdrop-blur-sm border border-green-600 rounded-lg p-6 max-w-2xl z-40 shadow-2xl">
561 <button
562 onClick={() => setShowFHS(false)}
563 className="absolute top-3 right-3 text-green-400 hover:text-green-300"
564 >
565
566 </button>
567 <h3 className="text-green-400 font-bold mb-4 text-lg">Filesystem Hierarchy Standard (FHS)</h3>
568 <div className="grid grid-cols-1 gap-2 max-h-96 overflow-y-auto">
569 {fhsDirs.map((dir, index) => (
570 <div key={index} className="flex items-start gap-3">
571 <code className="text-green-300 font-terminal text-sm font-bold min-w-[80px]">{dir.path}</code>
572 <span className="text-green-200 text-sm">{dir.desc}</span>
573 </div>
574 ))}
575 </div>
576 </div>
577 )}
578
579 {/* Command Reference Modal */}
580 {showCommands && commandRef && (
581 <div className="absolute top-16 left-4 bg-red-900/95 backdrop-blur-sm border border-red-600 rounded-lg p-6 max-w-2xl max-h-[80vh] overflow-y-auto z-40 shadow-2xl">
582 <button
583 onClick={() => setShowCommands(false)}
584 className="absolute top-3 right-3 text-red-400 hover:text-red-300"
585 >
586
587 </button>
588 <h3 className="text-red-400 font-bold mb-4 text-lg">Command Reference</h3>
589
590 <div className="space-y-6">
591 {/* Navigation Commands */}
592 <div>
593 <h4 className="text-red-300 font-semibold mb-3">Navigation Commands</h4>
594 <div className="space-y-3">
595 {commandRef.navigation.map((cmd, index) => (
596 <div key={index} className="border-l-2 border-red-700 pl-3">
597 <div className="flex items-start gap-3">
598 <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
599 <span className="text-red-100 text-sm">- {cmd.description}</span>
600 </div>
601 {cmd.examples && (
602 <div className="mt-1">
603 <span className="text-red-300 text-xs">Examples: </span>
604 <code className="text-red-200 text-xs">{cmd.examples.join(', ')}</code>
605 </div>
606 )}
607 </div>
608 ))}
609 </div>
610 </div>
611
612 {/* Exploration Commands */}
613 <div>
614 <h4 className="text-red-300 font-semibold mb-3">Exploration Commands</h4>
615 <div className="space-y-3">
616 {commandRef.exploration.map((cmd, index) => (
617 <div key={index} className="border-l-2 border-red-700 pl-3">
618 <div className="flex items-start gap-3">
619 <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
620 <span className="text-red-100 text-sm">- {cmd.description}</span>
621 </div>
622 {cmd.options && (
623 <div className="mt-1 ml-4">
624 {Object.entries(cmd.options).map(([opt, desc]) => (
625 <div key={opt} className="text-xs">
626 <code className="text-red-300">{opt}</code>
627 <span className="text-red-200 ml-2">{desc}</span>
628 </div>
629 ))}
630 </div>
631 )}
632 </div>
633 ))}
634 </div>
635 </div>
636
637 {/* Utility Commands */}
638 <div>
639 <h4 className="text-red-300 font-semibold mb-3">Utility Commands</h4>
640 <div className="space-y-3">
641 {commandRef.utility.map((cmd, index) => (
642 <div key={index} className="border-l-2 border-red-700 pl-3">
643 <div className="flex items-start gap-3">
644 <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
645 <span className="text-red-100 text-sm">- {cmd.description}</span>
646 </div>
647 {cmd.variables && (
648 <div className="mt-1 ml-4">
649 {Object.entries(cmd.variables).map(([variable, desc]) => (
650 <div key={variable} className="text-xs">
651 <code className="text-red-300">{variable}</code>
652 <span className="text-red-200 ml-2">{desc}</span>
653 </div>
654 ))}
655 </div>
656 )}
657 </div>
658 ))}
659 </div>
660 </div>
661
662 {/* Game Commands */}
663 <div>
664 <h4 className="text-red-300 font-semibold mb-3">Game Commands</h4>
665 <div className="space-y-3">
666 {commandRef.game.map((cmd, index) => (
667 <div key={index} className="border-l-2 border-red-700 pl-3">
668 <div className="flex items-start gap-3">
669 <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
670 <span className="text-red-100 text-sm">- {cmd.description}</span>
671 </div>
672 </div>
673 ))}
674 </div>
675 </div>
676
677 {/* Special Paths */}
678 <div>
679 <h4 className="text-red-300 font-semibold mb-3">Special Paths</h4>
680 <div className="space-y-3">
681 {commandRef.special_paths.map((path, index) => (
682 <div key={index} className="border-l-2 border-red-700 pl-3">
683 <div className="flex items-start gap-3">
684 <code className="text-red-200 font-terminal text-sm">{path.path}</code>
685 <span className="text-red-100 text-sm">- {path.description}</span>
686 </div>
687 <div className="mt-1">
688 <span className="text-red-300 text-xs">Examples: </span>
689 <code className="text-red-200 text-xs">{path.examples.join(', ')}</code>
690 </div>
691 </div>
692 ))}
693 </div>
694 </div>
695 </div>
696 </div>
697 )}
698
699 {/* Bottom Game Bar */}
700 <div className="absolute bottom-0 left-0 right-0 bg-slate-800/90 backdrop-blur-sm border-t border-slate-700 p-3 z-20">
701 <div className="max-w-7xl mx-auto flex justify-between items-center px-4">
702 <div className="flex items-center gap-4">
703 <h1 className="text-2xl font-bold text-white"><span className='font-terminal bg-gray-200 dark:bg-gray-500 text-red-900 dark:text-red-400 px-0.5 py-0 rounded'>bash</span> amole</h1>
704 </div>
705
706 <div className="flex items-center gap-3">
707 <div className="text-xs text-slate-400">
708 click adjacent nodes or use the terminal
709 </div>
710 <button
711 onClick={startNewGame}
712 className="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white text-sm rounded transition"
713 >
714 New Game
715 </button>
716 </div>
717 </div>
718 </div>
719
720 {/* Custom styles for animations */}
721 <style jsx>{`
722 @keyframes fadeIn {
723 from { opacity: 0; transform: scale(0.8); }
724 to { opacity: 1; transform: scale(1); }
725 }
726 @keyframes pulse {
727 0%, 100% { opacity: 0.9; transform: scale(1); }
728 50% { opacity: 1; transform: scale(1.05); }
729 }
730 `}</style>
731 </div>
732 );
733 };
734
735 export default Game;