zeroed-some/bashamole / 6ea57e5

Browse files

frontend progression, timer, api

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
6ea57e5667056293c39adfcaf64e9976d497c5a9
Parents
49d9fb6
Tree
d7f0f4a

3 changed files

StatusFile+-
M frontend/src/components/Game.tsx 116 13
A frontend/src/components/TimerDisplay.tsx 98 0
M frontend/src/lib/api.ts 75 0
frontend/src/components/Game.tsxmodified
@@ -3,6 +3,7 @@
33
 import React, { useState, useEffect, useRef } from 'react';
44
 import Image from 'next/image';
55
 import TreeVisualizer from './TreeVisualizer';
6
+import TimerDisplay from './TimerDisplay';
67
 import { gameApi, FileSystemTree, FHSDirectory, CommandReferenceResponse, MoleDirection, TreeNode } from '@/lib/api';
78
 
89
 interface CommandHistoryEntry {
@@ -92,9 +93,15 @@ const Game: React.FC = () => {
9293
         locationContext = "You've been placed somewhere in the filesystem. ";
9394
       }
9495
       
96
+      // Add timer info to starting message
97
+      let timerInfo = '';
98
+      if (response.initial_timer && response.timer_reason) {
99
+        timerInfo = `\nTimer: ${response.initial_timer}s (mole is ${response.timer_reason})`;
100
+      }
101
+      
95102
       setCommandHistory([{
96103
         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.`,
104
+        output: `${response.mole_hint}\n${locationContext}Your home directory is ${homeDir}.\nUse 'pwd' to see where you are, 'cd ~' to go home.${timerInfo}\nType "help" for available commands.`,
98105
         success: true,
99106
       }]);
100107
       setHints([]);
@@ -160,10 +167,21 @@ const Game: React.FC = () => {
160167
         gameState.sessionId || undefined
161168
       );
162169
 
170
+      // Build output with timer warnings
171
+      let fullOutput = response.output;
172
+      
173
+      // Add timer warnings if present
174
+      if (response.timer_warnings && response.timer_warnings.length > 0) {
175
+        const warnings = response.timer_warnings.map(w => 
176
+          `⚠️ ${w.level}: ${w.message}`
177
+        ).join('\n');
178
+        fullOutput = warnings + (fullOutput ? '\n' + fullOutput : '');
179
+      }
180
+
163181
       // Update command history
164182
       setCommandHistory(prev => [...prev, {
165183
         command: cmd,
166
-        output: response.output,
184
+        output: fullOutput,
167185
         success: response.success,
168186
       }]);
169187
 
@@ -225,6 +243,11 @@ const Game: React.FC = () => {
225243
         // Update score and moles killed
226244
         if (response.score !== undefined) setScore(response.score);
227245
         if (response.moles_killed !== undefined) setMolesKilled(response.moles_killed);
246
+        
247
+        // Format the output to include timer info on new line
248
+        if (response.timer_reason && !response.output.includes('New mole detected')) {
249
+          response.output += `\nNew mole detected ${response.timer_reason}!`;
250
+        }
228251
       }
229252
 
230253
       // Legacy: Check if game won (for old backend compatibility)
@@ -417,15 +440,76 @@ const Game: React.FC = () => {
417440
         </div>
418441
       )}
419442
 
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>
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 
447
+          gameTreeId={gameState.tree?.id || null}
448
+          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
+          }}
501
+        />
502
+        
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>
426510
           </div>
427
-        </div>
428
-      )}
511
+        )}
512
+      </div>
429513
 
430514
       {/* Floating Terminal - Top Left */}
431515
       <div className={`absolute top-4 left-4 ${terminalColors.frame} rounded-lg shadow-2xl border transition-all duration-300 z-30 ${
@@ -487,9 +571,28 @@ const Game: React.FC = () => {
487571
                 </div>
488572
                 {entry.output && (
489573
                   <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
-                    ))}
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
+                    })}
493596
                   </div>
494597
                 )}
495598
               </div>
frontend/src/components/TimerDisplay.tsxadded
@@ -0,0 +1,98 @@
1
+import React, { useEffect, useState } from 'react';
2
+
3
+interface TimerDisplayProps {
4
+  gameTreeId: number | null;
5
+  sessionId: number | null;
6
+  onTimerExpire?: () => void;
7
+}
8
+
9
+const TimerDisplay: React.FC<TimerDisplayProps> = ({ 
10
+  gameTreeId,
11
+  sessionId,
12
+  onTimerExpire
13
+}) => {
14
+  const [timerData, setTimerData] = useState({
15
+    remaining: 60,
16
+    warningLevel: null as string | null,
17
+    expired: false,
18
+    paused: false
19
+  });
20
+
21
+  useEffect(() => {
22
+    if (!gameTreeId) return;
23
+
24
+    const checkTimer = async () => {
25
+      try {
26
+        const response = await fetch(
27
+          `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'}/trees/filesystem-trees/${gameTreeId}/timer_status/`
28
+        );
29
+        
30
+        if (response.ok) {
31
+          const data = await response.json();
32
+          setTimerData({
33
+            remaining: data.remaining,
34
+            warningLevel: data.warning_level,
35
+            expired: data.expired,
36
+            paused: data.paused
37
+          });
38
+
39
+          // Notify parent if timer expired
40
+          if (data.expired && onTimerExpire) {
41
+            onTimerExpire();
42
+          }
43
+        }
44
+      } catch (error) {
45
+        console.error('Failed to check timer:', error);
46
+      }
47
+    };
48
+
49
+    // Check immediately
50
+    checkTimer();
51
+
52
+    // Then check every second
53
+    const interval = setInterval(checkTimer, 1000);
54
+
55
+    return () => clearInterval(interval);
56
+  }, [gameTreeId, sessionId, onTimerExpire]);
57
+
58
+  // Determine display based on warning level
59
+  let borderColor = 'border-green-500';
60
+  let textColor = 'text-green-400';
61
+  let statusMessage = null;
62
+  let pulseAnimation = '';
63
+  
64
+  if (timerData.expired || timerData.remaining <= 0) {
65
+    borderColor = 'border-red-600';
66
+    textColor = 'text-red-600';
67
+    statusMessage = 'Escaped!';
68
+  } else if (timerData.warningLevel === 'critical') {
69
+    borderColor = 'border-red-500';
70
+    textColor = 'text-red-400';
71
+    statusMessage = 'Escaping!';
72
+    pulseAnimation = 'animate-pulse';
73
+  } else if (timerData.warningLevel === 'alert') {
74
+    borderColor = 'border-orange-500';
75
+    textColor = 'text-orange-400';
76
+    statusMessage = 'Burrowing!';
77
+  } else if (timerData.warningLevel === 'warning') {
78
+    borderColor = 'border-yellow-500';
79
+    textColor = 'text-yellow-400';
80
+    statusMessage = 'Alert!';
81
+  }
82
+  
83
+  return (
84
+    <div className={`bg-black/80 backdrop-blur-sm border ${borderColor} rounded-lg p-3 shadow-2xl ${pulseAnimation}`}>
85
+      <div className={`${textColor} font-terminal text-sm`}>
86
+        <div className="flex items-center justify-between">
87
+          <span>Time:</span>
88
+          <span className="font-bold">{Math.max(0, timerData.remaining)}s</span>
89
+        </div>
90
+        {statusMessage && (
91
+          <div className="text-center text-xs mt-1">{statusMessage}</div>
92
+        )}
93
+      </div>
94
+    </div>
95
+  );
96
+};
97
+
98
+export default TimerDisplay;
frontend/src/lib/api.tsmodified
@@ -95,6 +95,69 @@ export interface CommandReferenceResponse {
9595
   special_paths: SpecialPath[];
9696
 }
9797
 
98
+export interface TimerWarning {
99
+  level: string;
100
+  message: string;
101
+}
102
+
103
+export interface CommandResponse {
104
+  command: string;
105
+  success: boolean;
106
+  output: string;
107
+  current_path: string;
108
+  game_won?: boolean;
109
+  mole_spawned?: boolean;
110
+  mole_direction?: MoleDirection | null;
111
+  score?: number;
112
+  moles_killed?: number;
113
+  new_mole_location?: string;
114
+  timer_remaining?: number;
115
+  timer_warnings?: TimerWarning[];
116
+  new_timer?: number;
117
+  timer_reason?: string;
118
+  timer_distance?: number;
119
+}
120
+
121
+export interface TimerStatusResponse {
122
+  remaining: number;
123
+  total: number;
124
+  percentage: number;
125
+  warning_level: string | null;
126
+  expired: boolean;
127
+  paused: boolean;
128
+}
129
+
130
+export interface EscapeData {
131
+  escaped: boolean;
132
+  old_location: string;
133
+  new_location: string;
134
+  total_escapes: number;
135
+  new_timer: number;
136
+  timer_reason: string;
137
+  distance: number;
138
+  mole_direction?: MoleDirection | null;
139
+}
140
+
141
+export interface CheckTimerResponse {
142
+  timer_remaining: number;
143
+  timer_expired: boolean;
144
+  mole_location: string;
145
+  timer_paused: boolean;
146
+  mole_escaped?: boolean;
147
+  escape_data?: EscapeData;
148
+  message?: string;
149
+}
150
+
151
+export interface GameCreationResponse {
152
+  tree: FileSystemTree;
153
+  session_id: number;
154
+  mole_hint: string;
155
+  home_directory: string;
156
+  initial_timer?: number;
157
+  timer_reason?: string;
158
+  timer_distance?: number;
159
+}
160
+
98161
 export const gameApi = {
99162
   createGame: async (playerName: string = 'Anonymous'): Promise<GameCreationResponse> => {
100163
     const response = await api.post('/trees/filesystem-trees/create_game/', {
@@ -136,4 +199,16 @@ export const gameApi = {
136199
     const response = await api.get('/trees/filesystem-trees/command_reference/');
137200
     return response.data;
138201
   },
202
+
203
+    getTimerStatus: async (treeId: number): Promise<TimerStatusResponse> => {
204
+    const response = await api.get(`/trees/filesystem-trees/${treeId}/timer_status/`);
205
+    return response.data;
206
+  },
207
+
208
+  checkTimer: async (treeId: number, sessionId?: number): Promise<CheckTimerResponse> => {
209
+    const url = `/trees/filesystem-trees/${treeId}/check_timer/`;
210
+    const params = sessionId ? `?session_id=${sessionId}` : '';
211
+    const response = await api.get(url + params);
212
+    return response.data;
213
+  },
139214
 };