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