zeroed-some/bashamole / 0c04181

Browse files

refactor stable

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
0c04181d951bcbf38e81b48396ff15c13f690a90
Parents
f52610e
Tree
d096d43

5 changed files

StatusFile+-
M frontend/src/app/layout.tsx 1 1
M frontend/src/components/Game.tsx 96 447
A frontend/src/components/GameStatus.tsx 111 0
A frontend/src/components/HelpModals.tsx 194 0
A frontend/src/components/Terminal.tsx 195 0
frontend/src/app/layout.tsxmodified
@@ -11,7 +11,7 @@ const spaceMono = Anonymous_Pro({
1111
 });
1212
 
1313
 export const metadata: Metadata = {
14
-  title: "Bashamole",
14
+  title: "bashamole",
1515
   description: "A game to practice Unix navigation by hunting moles in the filesystem",
1616
   icons: {
1717
     icon: [
frontend/src/components/Game.tsxmodified
@@ -1,9 +1,10 @@
11
 "use client";
22
 
3
-import React, { useState, useEffect, useRef } from 'react';
4
-import Image from 'next/image';
3
+import React, { useState, useEffect } from 'react';
54
 import TreeVisualizer from './TreeVisualizer';
6
-import TimerDisplay from './TimerDisplay';
5
+import Terminal from './Terminal';
6
+import HelpModals from './HelpModals';
7
+import GameStatus from './GameStatus';
78
 import { gameApi, FileSystemTree, FHSDirectory, CommandReferenceResponse, MoleDirection, TreeNode } from '@/lib/api';
89
 
910
 interface CommandHistoryEntry {
@@ -42,22 +43,8 @@ const Game: React.FC = () => {
4243
   const [score, setScore] = useState(0);
4344
   const [molesKilled, setMolesKilled] = useState(0);
4445
 
45
-  const terminalRef = useRef<HTMLDivElement>(null);
46
-  const inputRef = useRef<HTMLInputElement>(null);
47
-
48
-  // Auto-scroll terminal to bottom
49
-  useEffect(() => {
50
-    if (terminalRef.current) {
51
-      terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
52
-    }
53
-  }, [commandHistory]);
54
-
55
-  // Auto-focus input after command execution
56
-  useEffect(() => {
57
-    if (!executing && inputRef.current && !terminalMinimized) {
58
-      inputRef.current.focus();
59
-    }
60
-  }, [executing, terminalMinimized]);
46
+  // Canvas background color - always dark mode
47
+  const canvasBackground = 'bg-gray-900';
6148
 
6249
   // Start a new game
6350
   const startNewGame = async () => {
@@ -308,6 +295,59 @@ const Game: React.FC = () => {
308295
     executeCommand(`cd ${path}`);
309296
   };
310297
 
298
+  // Handle timer expiration
299
+  const handleTimerExpire = async () => {
300
+    if (gameState.tree) {
301
+      try {
302
+        const response = await gameApi.checkTimer(gameState.tree.id, gameState.sessionId || undefined);
303
+        if (response.mole_escaped) {
304
+          // Build the escape message
305
+          let escapeMessage = response.message || 'The mole escaped!';
306
+          
307
+          // Add distance info for new mole if available
308
+          if (response.escape_data?.timer_reason) {
309
+            escapeMessage += `\nNew mole detected ${response.escape_data.timer_reason}!`;
310
+          }
311
+          
312
+          // Update command history with escape message
313
+          setCommandHistory(prev => [...prev, {
314
+            command: 'Mole escaped!',
315
+            output: escapeMessage,
316
+            success: false,
317
+          }]);
318
+          
319
+          // Update mole direction if provided
320
+          if (response.escape_data?.new_location) {
321
+            // Update tree to show new mole location
322
+            const treeWithNewMole = updateTreeDataToShowMole(
323
+              removeMoleFromTree(gameState.tree.tree_data),
324
+              response.escape_data.new_location
325
+            );
326
+            
327
+            setGameState(prev => ({
328
+              ...prev,
329
+              tree: prev.tree ? {
330
+                ...prev.tree,
331
+                tree_data: treeWithNewMole,
332
+              } : null,
333
+            }));
334
+            
335
+            // Show mole direction indicator if provided
336
+            if (response.escape_data?.mole_direction) {
337
+              setMoleDirection(response.escape_data.mole_direction);
338
+              // Hide direction indicator after 5 seconds
339
+              setTimeout(() => {
340
+                setMoleDirection(null);
341
+              }, 5000);
342
+            }
343
+          }
344
+        }
345
+      } catch (error) {
346
+        console.error('Failed to check timer:', error);
347
+      }
348
+    }
349
+  };
350
+
311351
   // Start game on mount
312352
   useEffect(() => {
313353
     startNewGame();
@@ -324,38 +364,6 @@ const Game: React.FC = () => {
324364
     }
325365
   }, [gameState.tree, hasPlayedIntro]);
326366
 
327
-  // Get position for mole direction indicator
328
-  const getMoleIndicatorPosition = (direction: string) => {
329
-    const positions: Record<string, string> = {
330
-      'up': 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-full -mt-8',
331
-      'down': 'bottom-20 left-1/2 -translate-x-1/2',
332
-      'left': 'top-1/2 left-8 -translate-y-1/2',
333
-      'right': 'top-1/2 right-8 -translate-y-1/2',
334
-      'up-left': 'top-20 left-8',
335
-      'up-right': 'top-20 right-8',
336
-      'down-left': 'bottom-20 left-8',
337
-      'down-right': 'bottom-20 right-8',
338
-    };
339
-    return positions[direction] || positions['up'];
340
-  };
341
-
342
-  // Get rotation for arrow based on angle
343
-  const getArrowRotation = (angle: number) => {
344
-    return `rotate(${angle}deg)`;
345
-  };
346
-
347
-  // Terminal color scheme - always dark mode
348
-  const terminalColors = {
349
-    frame: 'bg-stone-200 border-stone-300',
350
-    header: 'bg-stone-300 border-stone-400',
351
-    headerText: 'text-stone-900',
352
-    content: 'bg-black',
353
-    closeButton: 'text-stone-700 hover:text-stone-900'
354
-  };
355
-
356
-  // Canvas background color - always dark mode
357
-  const canvasBackground = 'bg-gray-900';
358
-
359367
   if (gameState.loading) {
360368
     return (
361369
       <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-gray-900 dark:text-white`}>
@@ -414,390 +422,43 @@ const Game: React.FC = () => {
414422
         />
415423
       </div>
416424
 
417
-      {/* Mole Direction Indicator */}
418
-      {moleDirection && (
419
-        <div 
420
-          className={`absolute ${getMoleIndicatorPosition(moleDirection.direction)} z-40 animate-pulse`}
421
-          style={{
422
-            animation: 'pulse 2s ease-in-out infinite, fadeIn 0.5s ease-out'
423
-          }}
424
-        >
425
-          <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">
426
-            <Image 
427
-              src="/mole.svg" 
428
-              alt="Mole" 
429
-              width={32}
430
-              height={32}
431
-              className="w-8 h-8"
432
-            />
433
-            <div 
434
-              className="text-white text-2xl"
435
-              style={{ transform: getArrowRotation(moleDirection.angle) }}
436
-            >
437
-              →
438
-            </div>
439
-          </div>
440
-        </div>
441
-      )}
442
-
443
-      {/* Score and Timer Display - Top Right */}
444
-      <div className="absolute top-4 right-4 flex flex-col gap-3 z-30">
445
-        {/* Timer */}
446
-        <TimerDisplay 
425
+      {/* Game Status (Timer, Score, Direction Indicator) */}
426
+      <GameStatus
447427
         gameTreeId={gameState.tree?.id || null}
448428
         sessionId={gameState.sessionId}
449
-          onTimerExpire={async () => {
450
-            // Handle timer expiration - check for mole escape
451
-            if (gameState.tree) {
452
-              try {
453
-                const response = await gameApi.checkTimer(gameState.tree.id, gameState.sessionId || undefined);
454
-                if (response.mole_escaped) {
455
-                  // Build the escape message
456
-                  let escapeMessage = response.message || 'The mole escaped!';
457
-                  
458
-                  // Add distance info for new mole if available
459
-                  if (response.escape_data?.timer_reason) {
460
-                    escapeMessage += `\nNew mole detected ${response.escape_data.timer_reason}!`;
461
-                  }
462
-                  
463
-                  // Update command history with escape message
464
-                  setCommandHistory(prev => [...prev, {
465
-                    command: 'Mole escaped!',
466
-                    output: escapeMessage,
467
-                    success: false,
468
-                  }]);
469
-                  
470
-                  // Update mole direction if provided
471
-                  if (response.escape_data?.new_location) {
472
-                    // Update tree to show new mole location
473
-                    const treeWithNewMole = updateTreeDataToShowMole(
474
-                      removeMoleFromTree(gameState.tree.tree_data),
475
-                      response.escape_data.new_location
476
-                    );
477
-                    
478
-                    setGameState(prev => ({
479
-                      ...prev,
480
-                      tree: prev.tree ? {
481
-                        ...prev.tree,
482
-                        tree_data: treeWithNewMole,
483
-                      } : null,
484
-                    }));
485
-                    
486
-                    // Show mole direction indicator if provided
487
-                    if (response.escape_data?.mole_direction) {
488
-                      setMoleDirection(response.escape_data.mole_direction);
489
-                      // Hide direction indicator after 5 seconds
490
-                      setTimeout(() => {
491
-                        setMoleDirection(null);
492
-                      }, 5000);
493
-                    }
494
-                  }
495
-                }
496
-              } catch (error) {
497
-                console.error('Failed to check timer:', error);
498
-              }
499
-            }
500
-          }}
429
+        onTimerExpire={handleTimerExpire}
430
+        score={score}
431
+        molesKilled={molesKilled}
432
+        moleDirection={moleDirection}
501433
       />
502434
 
503
-        {/* Score */}
504
-        {molesKilled > 0 && (
505
-          <div className="bg-black/80 backdrop-blur-sm border border-green-500 rounded-lg p-3 shadow-2xl">
506
-            <div className="text-green-400 font-terminal text-sm">
507
-              <div>Score: {score}</div>
508
-              <div>Moles: {molesKilled}</div>
509
-            </div>
510
-          </div>
511
-        )}
512
-      </div>
513
-
514
-      {/* Floating Terminal - Top Left */}
515
-      <div className={`absolute top-4 left-4 ${terminalColors.frame} rounded-lg shadow-2xl border transition-all duration-300 z-30 ${
516
-        terminalMinimized ? 'w-80' : 'w-[700px]'
517
-      }`}>
518
-        {/* Terminal Header */}
519
-        <div className={`flex items-center justify-between ${terminalColors.header} px-4 py-2 rounded-t-lg border-b`}>
520
-          <div className="flex items-center gap-2">
521
-            <div className="flex gap-1.5">
522
-              <button
523
-                onClick={getCommandReference}
524
-                className="w-3.5 h-3.5 bg-red-500 hover:bg-red-400 rounded-full flex items-center justify-center transition-colors relative"
525
-                title="Command Reference"
526
-              >
527
-                <span className="text-[8px] font-bold text-gray-900 absolute">×</span>
528
-              </button>
529
-              <button
530
-                onClick={getHints}
531
-                className="w-3.5 h-3.5 bg-yellow-500 hover:bg-yellow-400 rounded-full flex items-center justify-center transition-colors relative"
532
-                title="Get Hint"
533
-              >
534
-                <span className="text-[9px] font-bold text-gray-900 absolute">?</span>
535
-              </button>
536
-              <button
537
-                onClick={getFHSReference}
538
-                className="w-3.5 h-3.5 bg-green-500 hover:bg-green-400 rounded-full flex items-center justify-center transition-colors relative"
539
-                title="FHS Directory Reference"
540
-              >
541
-                <span className="text-[9px] font-bold text-gray-900 absolute">/</span>
542
-              </button>
543
-            </div>
544
-            <h3 className={`text-sm font-medium ${terminalColors.headerText} ml-2`}>bash</h3>
545
-          </div>
546
-          <button
547
-            onClick={() => setTerminalMinimized(!terminalMinimized)}
548
-            className={`${terminalColors.closeButton} transition`}
549
-          >
550
-            {terminalMinimized ? '▼' : '▲'}
551
-          </button>
552
-        </div>
553
-
554
-        {/* Terminal Content */}
555
-        {!terminalMinimized && (
556
-          <div 
557
-            ref={terminalRef}
558
-            className={`${terminalColors.content} p-4 font-terminal text-base h-[300px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-700`}
559
-            onClick={() => inputRef.current?.focus()}
560
-          >
561
-            {commandHistory.map((entry, index) => (
562
-              <div key={index} className="mb-1">
563
-                <div className="flex items-start font-terminal">
564
-                  <span className="text-green-400">groundskeeper@molehill</span>
565
-                  <span className="text-gray-400 mx-1">::</span>
566
-                  <span className="text-blue-400">{entry.command.startsWith('Hunt started!') ? '~' : gameState.tree?.player_location || '~'}</span>
567
-                  <span className="text-gray-400 ml-1">$</span>
568
-                  <span className={`ml-2 ${entry.command.startsWith('Hunt started!') ? 'text-yellow-400' : 'text-gray-300'}`}>
569
-                    {entry.command.startsWith('Hunt started!') ? '' : entry.command}
570
-                  </span>
571
-                </div>
572
-                {entry.output && (
573
-                  <div className={`${entry.success ? 'text-gray-300' : 'text-red-400'} ml-0 mt-1 font-terminal whitespace-pre-wrap`}>
574
-                    {entry.output.split('\n').map((line, i) => {
575
-                      // Special coloring for mole detection messages
576
-                      let lineClass = '';
577
-                      if (line.includes('New mole detected')) {
578
-                        lineClass = 'text-yellow-400';
579
-                      } else if (line.includes('⚠️')) {
580
-                        // Timer warnings
581
-                        if (line.includes('CRITICAL')) {
582
-                          lineClass = 'text-red-500';
583
-                        } else if (line.includes('ALERT')) {
584
-                          lineClass = 'text-orange-400';
585
-                        } else if (line.includes('WARNING')) {
586
-                          lineClass = 'text-yellow-400';
587
-                        }
588
-                      }
589
-                      
590
-                      return (
591
-                        <div key={i} className={lineClass || ''}>
592
-                          {line}
593
-                        </div>
594
-                      );
595
-                    })}
596
-                  </div>
597
-                )}
598
-              </div>
599
-            ))}
600
-            
601
-            {/* Current input line */}
602
-            <div className="flex items-start font-terminal">
603
-              <span className="text-green-400">groundskeeper@molehill</span>
604
-              <span className="text-gray-400 mx-1">::</span>
605
-              <span className="text-blue-400">{gameState.tree?.player_location || '~'}</span>
606
-              <span className="text-gray-400 ml-1">$</span>
607
-              <div className="flex-1 ml-2">
608
-                <div className="relative inline-block">
609
-                  <span className="text-gray-300 font-terminal">{command}</span>
610
-                  <span 
611
-                    className="text-gray-300 font-terminal"
612
-                    style={{ 
613
-                      animation: 'blink 1s step-end infinite'
614
-                    }}
615
-                  >
616
-                    _
617
-                  </span>
618
-                  <input
619
-                    ref={inputRef}
620
-                    type="text"
621
-                    value={command}
622
-                    onChange={(e) => setCommand(e.target.value)}
623
-                    onKeyDown={(e) => {
624
-                      if (e.key === 'Enter') {
625
-                        e.preventDefault();
626
-                        executeCommand(command);
627
-                      }
628
-                    }}
629
-                    disabled={executing}
630
-                    className="absolute inset-0 w-full bg-transparent text-transparent outline-none caret-transparent font-terminal"
631
-                    placeholder=""
632
-                    autoFocus
633
-                    spellCheck={false}
634
-                    autoComplete="off"
635
-                    autoCorrect="off"
636
-                    autoCapitalize="off"
435
+      {/* Terminal */}
436
+      <Terminal
437
+        commandHistory={commandHistory}
438
+        command={command}
439
+        setCommand={setCommand}
440
+        executeCommand={executeCommand}
441
+        executing={executing}
442
+        currentPath={gameState.tree?.player_location || '~'}
443
+        terminalMinimized={terminalMinimized}
444
+        setTerminalMinimized={setTerminalMinimized}
445
+        onGetCommandReference={getCommandReference}
446
+        onGetHints={getHints}
447
+        onGetFHSReference={getFHSReference}
637448
       />
638
-                </div>
639
-              </div>
640
-            </div>
641
-          </div>
642
-        )}
643
-      </div>
644
-
645
-      {/* Hints Popup */}
646
-      {showHints && hints.length > 0 && (
647
-        <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">
648
-          <button
649
-            onClick={() => setShowHints(false)}
650
-            className="absolute top-2 right-2 text-yellow-400 hover:text-yellow-300"
651
-          >
652
-            ✕
653
-          </button>
654
-          <h3 className="text-yellow-400 font-bold mb-2">Hints:</h3>
655
-          {hints.map((hint, index) => (
656
-            <p key={index} className="text-yellow-200 text-sm">{hint}</p>
657
-          ))}
658
-        </div>
659
-      )}
660
-
661
-      {/* FHS Reference Modal */}
662
-      {showFHS && (
663
-        <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">
664
-          <button
665
-            onClick={() => setShowFHS(false)}
666
-            className="absolute top-3 right-3 text-green-400 hover:text-green-300"
667
-          >
668
-            ✕
669
-          </button>
670
-          <h3 className="text-green-400 font-bold mb-4 text-lg">Filesystem Hierarchy Standard (FHS)</h3>
671
-          <div className="grid grid-cols-1 gap-2 max-h-96 overflow-y-auto">
672
-            {fhsDirs.map((dir, index) => (
673
-              <div key={index} className="flex items-start gap-3">
674
-                <code className="text-green-300 font-terminal text-sm font-bold min-w-[80px]">{dir.path}</code>
675
-                <span className="text-green-200 text-sm">{dir.desc}</span>
676
-              </div>
677
-            ))}
678
-          </div>
679
-        </div>
680
-      )}
681449
 
682
-      {/* Command Reference Modal */}
683
-      {showCommands && commandRef && (
684
-        <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">
685
-          <button
686
-            onClick={() => setShowCommands(false)}
687
-            className="absolute top-3 right-3 text-red-400 hover:text-red-300"
688
-          >
689
-            ✕
690
-          </button>
691
-          <h3 className="text-red-400 font-bold mb-4 text-lg">Command Reference</h3>
692
-          
693
-          <div className="space-y-6">
694
-            {/* Navigation Commands */}
695
-            <div>
696
-              <h4 className="text-red-300 font-semibold mb-3">Navigation Commands</h4>
697
-              <div className="space-y-3">
698
-                {commandRef.navigation.map((cmd, index) => (
699
-                  <div key={index} className="border-l-2 border-red-700 pl-3">
700
-                    <div className="flex items-start gap-3">
701
-                      <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
702
-                      <span className="text-red-100 text-sm">- {cmd.description}</span>
703
-                    </div>
704
-                    {cmd.examples && (
705
-                      <div className="mt-1">
706
-                        <span className="text-red-300 text-xs">Examples: </span>
707
-                        <code className="text-red-200 text-xs">{cmd.examples.join(', ')}</code>
708
-                      </div>
709
-                    )}
710
-                  </div>
711
-                ))}
712
-              </div>
713
-            </div>
714
-
715
-            {/* Exploration Commands */}
716
-            <div>
717
-              <h4 className="text-red-300 font-semibold mb-3">Exploration Commands</h4>
718
-              <div className="space-y-3">
719
-                {commandRef.exploration.map((cmd, index) => (
720
-                  <div key={index} className="border-l-2 border-red-700 pl-3">
721
-                    <div className="flex items-start gap-3">
722
-                      <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
723
-                      <span className="text-red-100 text-sm">- {cmd.description}</span>
724
-                    </div>
725
-                    {cmd.options && (
726
-                      <div className="mt-1 ml-4">
727
-                        {Object.entries(cmd.options).map(([opt, desc]) => (
728
-                          <div key={opt} className="text-xs">
729
-                            <code className="text-red-300">{opt}</code>
730
-                            <span className="text-red-200 ml-2">{desc}</span>
731
-                          </div>
732
-                        ))}
733
-                      </div>
734
-                    )}
735
-                  </div>
736
-                ))}
737
-              </div>
738
-            </div>
739
-
740
-            {/* Utility Commands */}
741
-            <div>
742
-              <h4 className="text-red-300 font-semibold mb-3">Utility Commands</h4>
743
-              <div className="space-y-3">
744
-                {commandRef.utility.map((cmd, index) => (
745
-                  <div key={index} className="border-l-2 border-red-700 pl-3">
746
-                    <div className="flex items-start gap-3">
747
-                      <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
748
-                      <span className="text-red-100 text-sm">- {cmd.description}</span>
749
-                    </div>
750
-                    {cmd.variables && (
751
-                      <div className="mt-1 ml-4">
752
-                        {Object.entries(cmd.variables).map(([variable, desc]) => (
753
-                          <div key={variable} className="text-xs">
754
-                            <code className="text-red-300">{variable}</code>
755
-                            <span className="text-red-200 ml-2">{desc}</span>
756
-                          </div>
757
-                        ))}
758
-                      </div>
759
-                    )}
760
-                  </div>
761
-                ))}
762
-              </div>
763
-            </div>
764
-
765
-            {/* Game Commands */}
766
-            <div>
767
-              <h4 className="text-red-300 font-semibold mb-3">Game Commands</h4>
768
-              <div className="space-y-3">
769
-                {commandRef.game.map((cmd, index) => (
770
-                  <div key={index} className="border-l-2 border-red-700 pl-3">
771
-                    <div className="flex items-start gap-3">
772
-                      <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
773
-                      <span className="text-red-100 text-sm">- {cmd.description}</span>
774
-                    </div>
775
-                  </div>
776
-                ))}
777
-              </div>
778
-            </div>
779
-
780
-            {/* Special Paths */}
781
-            <div>
782
-              <h4 className="text-red-300 font-semibold mb-3">Special Paths</h4>
783
-              <div className="space-y-3">
784
-                {commandRef.special_paths.map((path, index) => (
785
-                  <div key={index} className="border-l-2 border-red-700 pl-3">
786
-                    <div className="flex items-start gap-3">
787
-                      <code className="text-red-200 font-terminal text-sm">{path.path}</code>
788
-                      <span className="text-red-100 text-sm">- {path.description}</span>
789
-                    </div>
790
-                    <div className="mt-1">
791
-                      <span className="text-red-300 text-xs">Examples: </span>
792
-                      <code className="text-red-200 text-xs">{path.examples.join(', ')}</code>
793
-                    </div>
794
-                  </div>
795
-                ))}
796
-              </div>
797
-            </div>
798
-          </div>
799
-        </div>
800
-      )}
450
+      {/* Help Modals */}
451
+      <HelpModals
452
+        showHints={showHints}
453
+        setShowHints={setShowHints}
454
+        hints={hints}
455
+        showFHS={showFHS}
456
+        setShowFHS={setShowFHS}
457
+        fhsDirs={fhsDirs}
458
+        showCommands={showCommands}
459
+        setShowCommands={setShowCommands}
460
+        commandRef={commandRef}
461
+      />
801462
 
802463
       {/* Bottom Game Bar */}
803464
       <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">
@@ -819,18 +480,6 @@ const Game: React.FC = () => {
819480
           </div>
820481
         </div>
821482
       </div>
822
-
823
-      {/* Custom styles for animations */}
824
-      <style jsx>{`
825
-        @keyframes fadeIn {
826
-          from { opacity: 0; transform: scale(0.8); }
827
-          to { opacity: 1; transform: scale(1); }
828
-        }
829
-        @keyframes pulse {
830
-          0%, 100% { opacity: 0.9; transform: scale(1); }
831
-          50% { opacity: 1; transform: scale(1.05); }
832
-        }
833
-      `}</style>
834483
     </div>
835484
   );
836485
 };
frontend/src/components/GameStatus.tsxadded
@@ -0,0 +1,111 @@
1
+import React from 'react';
2
+import Image from 'next/image';
3
+import TimerDisplay from './TimerDisplay';
4
+import { MoleDirection } from '@/lib/api';
5
+
6
+interface GameStatusProps {
7
+  // Timer props
8
+  gameTreeId: number | null;
9
+  sessionId: number | null;
10
+  onTimerExpire: () => Promise<void>;
11
+  
12
+  // Score props
13
+  score: number;
14
+  molesKilled: number;
15
+  
16
+  // Direction indicator props
17
+  moleDirection: MoleDirection | null;
18
+}
19
+
20
+const GameStatus: React.FC<GameStatusProps> = ({
21
+  gameTreeId,
22
+  sessionId,
23
+  onTimerExpire,
24
+  score,
25
+  molesKilled,
26
+  moleDirection,
27
+}) => {
28
+  // Get position for mole direction indicator
29
+  const getMoleIndicatorPosition = (direction: string) => {
30
+    const positions: Record<string, string> = {
31
+      'up': 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-full -mt-8',
32
+      'down': 'bottom-20 left-1/2 -translate-x-1/2',
33
+      'left': 'top-1/2 left-8 -translate-y-1/2',
34
+      'right': 'top-1/2 right-8 -translate-y-1/2',
35
+      'up-left': 'top-20 left-8',
36
+      'up-right': 'top-20 right-8',
37
+      'down-left': 'bottom-20 left-8',
38
+      'down-right': 'bottom-20 right-8',
39
+    };
40
+    return positions[direction] || positions['up'];
41
+  };
42
+
43
+  // Get rotation for arrow based on angle
44
+  const getArrowRotation = (angle: number) => {
45
+    return `rotate(${angle}deg)`;
46
+  };
47
+
48
+  return (
49
+    <>
50
+      {/* Mole Direction Indicator */}
51
+      {moleDirection && (
52
+        <div 
53
+          className={`absolute ${getMoleIndicatorPosition(moleDirection.direction)} z-40 animate-pulse`}
54
+          style={{
55
+            animation: 'pulse 2s ease-in-out infinite, fadeIn 0.5s ease-out'
56
+          }}
57
+        >
58
+          <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">
59
+            <Image 
60
+              src="/mole.svg" 
61
+              alt="Mole" 
62
+              width={32}
63
+              height={32}
64
+              className="w-8 h-8"
65
+            />
66
+            <div 
67
+              className="text-white text-2xl"
68
+              style={{ transform: getArrowRotation(moleDirection.angle) }}
69
+            >
70
+              →
71
+            </div>
72
+          </div>
73
+        </div>
74
+      )}
75
+
76
+      {/* Score and Timer Display - Top Right */}
77
+      <div className="absolute top-4 right-4 flex flex-col gap-3 z-30">
78
+        {/* Timer */}
79
+        <TimerDisplay 
80
+          gameTreeId={gameTreeId}
81
+          sessionId={sessionId}
82
+          onTimerExpire={onTimerExpire}
83
+        />
84
+        
85
+        {/* Score */}
86
+        {molesKilled > 0 && (
87
+          <div className="bg-black/80 backdrop-blur-sm border border-green-500 rounded-lg p-3 shadow-2xl">
88
+            <div className="text-green-400 font-terminal text-sm">
89
+              <div>Score: {score}</div>
90
+              <div>Moles: {molesKilled}</div>
91
+            </div>
92
+          </div>
93
+        )}
94
+      </div>
95
+
96
+      {/* Custom styles for animations */}
97
+      <style jsx>{`
98
+        @keyframes fadeIn {
99
+          from { opacity: 0; transform: scale(0.8); }
100
+          to { opacity: 1; transform: scale(1); }
101
+        }
102
+        @keyframes pulse {
103
+          0%, 100% { opacity: 0.9; transform: scale(1); }
104
+          50% { opacity: 1; transform: scale(1.05); }
105
+        }
106
+      `}</style>
107
+    </>
108
+  );
109
+};
110
+
111
+export default GameStatus;
frontend/src/components/HelpModals.tsxadded
@@ -0,0 +1,194 @@
1
+import React from 'react';
2
+import { FHSDirectory, CommandReferenceResponse } from '@/lib/api';
3
+
4
+interface HelpModalsProps {
5
+  // Hints Modal
6
+  showHints: boolean;
7
+  setShowHints: (val: boolean) => void;
8
+  hints: string[];
9
+  
10
+  // FHS Modal
11
+  showFHS: boolean;
12
+  setShowFHS: (val: boolean) => void;
13
+  fhsDirs: FHSDirectory[];
14
+  
15
+  // Commands Modal
16
+  showCommands: boolean;
17
+  setShowCommands: (val: boolean) => void;
18
+  commandRef: CommandReferenceResponse | null;
19
+}
20
+
21
+const HelpModals: React.FC<HelpModalsProps> = ({
22
+  showHints,
23
+  setShowHints,
24
+  hints,
25
+  showFHS,
26
+  setShowFHS,
27
+  fhsDirs,
28
+  showCommands,
29
+  setShowCommands,
30
+  commandRef,
31
+}) => {
32
+  return (
33
+    <>
34
+      {/* Hints Popup */}
35
+      {showHints && hints.length > 0 && (
36
+        <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">
37
+          <button
38
+            onClick={() => setShowHints(false)}
39
+            className="absolute top-2 right-2 text-yellow-400 hover:text-yellow-300"
40
+          >
41
+            ✕
42
+          </button>
43
+          <h3 className="text-yellow-400 font-bold mb-2">Hints:</h3>
44
+          {hints.map((hint, index) => (
45
+            <p key={index} className="text-yellow-200 text-sm">{hint}</p>
46
+          ))}
47
+        </div>
48
+      )}
49
+
50
+      {/* FHS Reference Modal */}
51
+      {showFHS && (
52
+        <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">
53
+          <button
54
+            onClick={() => setShowFHS(false)}
55
+            className="absolute top-3 right-3 text-green-400 hover:text-green-300"
56
+          >
57
+            ✕
58
+          </button>
59
+          <h3 className="text-green-400 font-bold mb-4 text-lg">Filesystem Hierarchy Standard (FHS)</h3>
60
+          <div className="grid grid-cols-1 gap-2 max-h-96 overflow-y-auto">
61
+            {fhsDirs.map((dir, index) => (
62
+              <div key={index} className="flex items-start gap-3">
63
+                <code className="text-green-300 font-terminal text-sm font-bold min-w-[80px]">{dir.path}</code>
64
+                <span className="text-green-200 text-sm">{dir.desc}</span>
65
+              </div>
66
+            ))}
67
+          </div>
68
+        </div>
69
+      )}
70
+
71
+      {/* Command Reference Modal */}
72
+      {showCommands && commandRef && (
73
+        <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">
74
+          <button
75
+            onClick={() => setShowCommands(false)}
76
+            className="absolute top-3 right-3 text-red-400 hover:text-red-300"
77
+          >
78
+            ✕
79
+          </button>
80
+          <h3 className="text-red-400 font-bold mb-4 text-lg">Command Reference</h3>
81
+          
82
+          <div className="space-y-6">
83
+            {/* Navigation Commands */}
84
+            <div>
85
+              <h4 className="text-red-300 font-semibold mb-3">Navigation Commands</h4>
86
+              <div className="space-y-3">
87
+                {commandRef.navigation.map((cmd, index) => (
88
+                  <div key={index} className="border-l-2 border-red-700 pl-3">
89
+                    <div className="flex items-start gap-3">
90
+                      <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
91
+                      <span className="text-red-100 text-sm">- {cmd.description}</span>
92
+                    </div>
93
+                    {cmd.examples && (
94
+                      <div className="mt-1">
95
+                        <span className="text-red-300 text-xs">Examples: </span>
96
+                        <code className="text-red-200 text-xs">{cmd.examples.join(', ')}</code>
97
+                      </div>
98
+                    )}
99
+                  </div>
100
+                ))}
101
+              </div>
102
+            </div>
103
+
104
+            {/* Exploration Commands */}
105
+            <div>
106
+              <h4 className="text-red-300 font-semibold mb-3">Exploration Commands</h4>
107
+              <div className="space-y-3">
108
+                {commandRef.exploration.map((cmd, index) => (
109
+                  <div key={index} className="border-l-2 border-red-700 pl-3">
110
+                    <div className="flex items-start gap-3">
111
+                      <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
112
+                      <span className="text-red-100 text-sm">- {cmd.description}</span>
113
+                    </div>
114
+                    {cmd.options && (
115
+                      <div className="mt-1 ml-4">
116
+                        {Object.entries(cmd.options).map(([opt, desc]) => (
117
+                          <div key={opt} className="text-xs">
118
+                            <code className="text-red-300">{opt}</code>
119
+                            <span className="text-red-200 ml-2">{desc}</span>
120
+                          </div>
121
+                        ))}
122
+                      </div>
123
+                    )}
124
+                  </div>
125
+                ))}
126
+              </div>
127
+            </div>
128
+
129
+            {/* Utility Commands */}
130
+            <div>
131
+              <h4 className="text-red-300 font-semibold mb-3">Utility Commands</h4>
132
+              <div className="space-y-3">
133
+                {commandRef.utility.map((cmd, index) => (
134
+                  <div key={index} className="border-l-2 border-red-700 pl-3">
135
+                    <div className="flex items-start gap-3">
136
+                      <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
137
+                      <span className="text-red-100 text-sm">- {cmd.description}</span>
138
+                    </div>
139
+                    {cmd.variables && (
140
+                      <div className="mt-1 ml-4">
141
+                        {Object.entries(cmd.variables).map(([variable, desc]) => (
142
+                          <div key={variable} className="text-xs">
143
+                            <code className="text-red-300">{variable}</code>
144
+                            <span className="text-red-200 ml-2">{desc}</span>
145
+                          </div>
146
+                        ))}
147
+                      </div>
148
+                    )}
149
+                  </div>
150
+                ))}
151
+              </div>
152
+            </div>
153
+
154
+            {/* Game Commands */}
155
+            <div>
156
+              <h4 className="text-red-300 font-semibold mb-3">Game Commands</h4>
157
+              <div className="space-y-3">
158
+                {commandRef.game.map((cmd, index) => (
159
+                  <div key={index} className="border-l-2 border-red-700 pl-3">
160
+                    <div className="flex items-start gap-3">
161
+                      <code className="text-red-200 font-terminal text-sm">{cmd.command}</code>
162
+                      <span className="text-red-100 text-sm">- {cmd.description}</span>
163
+                    </div>
164
+                  </div>
165
+                ))}
166
+              </div>
167
+            </div>
168
+
169
+            {/* Special Paths */}
170
+            <div>
171
+              <h4 className="text-red-300 font-semibold mb-3">Special Paths</h4>
172
+              <div className="space-y-3">
173
+                {commandRef.special_paths.map((path, index) => (
174
+                  <div key={index} className="border-l-2 border-red-700 pl-3">
175
+                    <div className="flex items-start gap-3">
176
+                      <code className="text-red-200 font-terminal text-sm">{path.path}</code>
177
+                      <span className="text-red-100 text-sm">- {path.description}</span>
178
+                    </div>
179
+                    <div className="mt-1">
180
+                      <span className="text-red-300 text-xs">Examples: </span>
181
+                      <code className="text-red-200 text-xs">{path.examples.join(', ')}</code>
182
+                    </div>
183
+                  </div>
184
+                ))}
185
+              </div>
186
+            </div>
187
+          </div>
188
+        </div>
189
+      )}
190
+    </>
191
+  );
192
+};
193
+
194
+export default HelpModals;
frontend/src/components/Terminal.tsxadded
@@ -0,0 +1,195 @@
1
+import React, { useRef, useEffect } from 'react';
2
+
3
+interface CommandHistoryEntry {
4
+  command: string;
5
+  output: string;
6
+  success: boolean;
7
+}
8
+
9
+interface TerminalProps {
10
+  commandHistory: CommandHistoryEntry[];
11
+  command: string;
12
+  setCommand: (cmd: string) => void;
13
+  executeCommand: (cmd: string) => void;
14
+  executing: boolean;
15
+  currentPath: string;
16
+  terminalMinimized: boolean;
17
+  setTerminalMinimized: (val: boolean) => void;
18
+  onGetCommandReference: () => void;
19
+  onGetHints: () => void;
20
+  onGetFHSReference: () => void;
21
+}
22
+
23
+const Terminal: React.FC<TerminalProps> = ({
24
+  commandHistory,
25
+  command,
26
+  setCommand,
27
+  executeCommand,
28
+  executing,
29
+  currentPath,
30
+  terminalMinimized,
31
+  setTerminalMinimized,
32
+  onGetCommandReference,
33
+  onGetHints,
34
+  onGetFHSReference,
35
+}) => {
36
+  const terminalRef = useRef<HTMLDivElement>(null);
37
+  const inputRef = useRef<HTMLInputElement>(null);
38
+
39
+  // Auto-scroll terminal to bottom
40
+  useEffect(() => {
41
+    if (terminalRef.current) {
42
+      terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
43
+    }
44
+  }, [commandHistory]);
45
+
46
+  // Auto-focus input after command execution
47
+  useEffect(() => {
48
+    if (!executing && inputRef.current && !terminalMinimized) {
49
+      inputRef.current.focus();
50
+    }
51
+  }, [executing, terminalMinimized]);
52
+
53
+  // Terminal color scheme - always dark mode
54
+  const terminalColors = {
55
+    frame: 'bg-stone-200 border-stone-300',
56
+    header: 'bg-stone-300 border-stone-400',
57
+    headerText: 'text-stone-900',
58
+    content: 'bg-black',
59
+    closeButton: 'text-stone-700 hover:text-stone-900'
60
+  };
61
+
62
+  return (
63
+    <div className={`absolute top-4 left-4 ${terminalColors.frame} rounded-lg shadow-2xl border transition-all duration-300 z-30 ${
64
+      terminalMinimized ? 'w-80' : 'w-[700px]'
65
+    }`}>
66
+      {/* Terminal Header */}
67
+      <div className={`flex items-center justify-between ${terminalColors.header} px-4 py-2 rounded-t-lg border-b`}>
68
+        <div className="flex items-center gap-2">
69
+          <div className="flex gap-1.5">
70
+            <button
71
+              onClick={onGetCommandReference}
72
+              className="w-3.5 h-3.5 bg-red-500 hover:bg-red-400 rounded-full flex items-center justify-center transition-colors relative"
73
+              title="Command Reference"
74
+            >
75
+              <span className="text-[8px] font-bold text-gray-900 absolute">×</span>
76
+            </button>
77
+            <button
78
+              onClick={onGetHints}
79
+              className="w-3.5 h-3.5 bg-yellow-500 hover:bg-yellow-400 rounded-full flex items-center justify-center transition-colors relative"
80
+              title="Get Hint"
81
+            >
82
+              <span className="text-[9px] font-bold text-gray-900 absolute">?</span>
83
+            </button>
84
+            <button
85
+              onClick={onGetFHSReference}
86
+              className="w-3.5 h-3.5 bg-green-500 hover:bg-green-400 rounded-full flex items-center justify-center transition-colors relative"
87
+              title="FHS Directory Reference"
88
+            >
89
+              <span className="text-[9px] font-bold text-gray-900 absolute">/</span>
90
+            </button>
91
+          </div>
92
+          <h3 className={`text-sm font-medium ${terminalColors.headerText} ml-2`}>bash</h3>
93
+        </div>
94
+        <button
95
+          onClick={() => setTerminalMinimized(!terminalMinimized)}
96
+          className={`${terminalColors.closeButton} transition`}
97
+        >
98
+          {terminalMinimized ? '▼' : '▲'}
99
+        </button>
100
+      </div>
101
+
102
+      {/* Terminal Content */}
103
+      {!terminalMinimized && (
104
+        <div 
105
+          ref={terminalRef}
106
+          className={`${terminalColors.content} p-4 font-terminal text-base h-[300px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-700`}
107
+          onClick={() => inputRef.current?.focus()}
108
+        >
109
+          {commandHistory.map((entry, index) => (
110
+            <div key={index} className="mb-1">
111
+              <div className="flex items-start font-terminal">
112
+                <span className="text-green-400">groundskeeper@molehill</span>
113
+                <span className="text-gray-400 mx-1">::</span>
114
+                <span className="text-blue-400">{entry.command.startsWith('Hunt started!') ? '~' : currentPath}</span>
115
+                <span className="text-gray-400 ml-1">$</span>
116
+                <span className={`ml-2 ${entry.command.startsWith('Hunt started!') ? 'text-yellow-400' : 'text-gray-300'}`}>
117
+                  {entry.command.startsWith('Hunt started!') ? '' : entry.command}
118
+                </span>
119
+              </div>
120
+              {entry.output && (
121
+                <div className={`${entry.success ? 'text-gray-300' : 'text-red-400'} ml-0 mt-1 font-terminal whitespace-pre-wrap`}>
122
+                  {entry.output.split('\n').map((line, i) => {
123
+                    // Special coloring for mole detection messages
124
+                    let lineClass = '';
125
+                    if (line.includes('New mole detected')) {
126
+                      lineClass = 'text-yellow-400';
127
+                    } else if (line.includes('⚠️')) {
128
+                      // Timer warnings
129
+                      if (line.includes('CRITICAL')) {
130
+                        lineClass = 'text-red-500';
131
+                      } else if (line.includes('ALERT')) {
132
+                        lineClass = 'text-orange-400';
133
+                      } else if (line.includes('WARNING')) {
134
+                        lineClass = 'text-yellow-400';
135
+                      }
136
+                    }
137
+                    
138
+                    return (
139
+                      <div key={i} className={lineClass || ''}>
140
+                        {line}
141
+                      </div>
142
+                    );
143
+                  })}
144
+                </div>
145
+              )}
146
+            </div>
147
+          ))}
148
+          
149
+          {/* Current input line */}
150
+          <div className="flex items-start font-terminal">
151
+            <span className="text-green-400">groundskeeper@molehill</span>
152
+            <span className="text-gray-400 mx-1">::</span>
153
+            <span className="text-blue-400">{currentPath}</span>
154
+            <span className="text-gray-400 ml-1">$</span>
155
+            <div className="flex-1 ml-2">
156
+              <div className="relative inline-block">
157
+                <span className="text-gray-300 font-terminal">{command}</span>
158
+                <span 
159
+                  className="text-gray-300 font-terminal"
160
+                  style={{ 
161
+                    animation: 'blink 1s step-end infinite'
162
+                  }}
163
+                >
164
+                  _
165
+                </span>
166
+                <input
167
+                  ref={inputRef}
168
+                  type="text"
169
+                  value={command}
170
+                  onChange={(e) => setCommand(e.target.value)}
171
+                  onKeyDown={(e) => {
172
+                    if (e.key === 'Enter') {
173
+                      e.preventDefault();
174
+                      executeCommand(command);
175
+                    }
176
+                  }}
177
+                  disabled={executing}
178
+                  className="absolute inset-0 w-full bg-transparent text-transparent outline-none caret-transparent font-terminal"
179
+                  placeholder=""
180
+                  autoFocus
181
+                  spellCheck={false}
182
+                  autoComplete="off"
183
+                  autoCorrect="off"
184
+                  autoCapitalize="off"
185
+                />
186
+              </div>
187
+            </div>
188
+          </div>
189
+        </div>
190
+      )}
191
+    </div>
192
+  );
193
+};
194
+
195
+export default Terminal;