zeroed-some/bashamole / 5de3bd7

Browse files

refactor, cusotm hooks, stable

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
5de3bd7b6c5cf5d4e6133ab3f4b90d9bf879a1bc
Parents
fff0d7a
Tree
e2adf2a

6 changed files

StatusFile+-
M frontend/src/components/Game.tsx 182 349
A frontend/src/hooks/useCommandExecution.ts 113 0
A frontend/src/hooks/useGameState.ts 126 0
A frontend/src/hooks/useHelpModals.ts 67 0
A frontend/src/hooks/useTerminal.ts 23 0
A frontend/src/hooks/useTreeUtils.ts 32 0
frontend/src/components/Game.tsxmodified
@@ -1,372 +1,209 @@
11
 "use client";
22
 
3
-import React, { useState, useEffect } from 'react';
3
+import React, { useEffect, useCallback } from 'react';
44
 import TreeVisualizer from './TreeVisualizer';
55
 import Terminal from './Terminal';
66
 import HelpModals from './HelpModals';
77
 import GameStatus from './GameStatus';
8
-import { gameApi, FileSystemTree, FHSDirectory, CommandReferenceResponse, MoleDirection, TreeNode } from '@/lib/api';
8
+import { gameApi, CommandResponse } from '@/lib/api';
99
 
10
-interface CommandHistoryEntry {
11
-  command: string;
12
-  output: string;
13
-  success: boolean;
14
-}
10
+// Import our custom hooks
11
+import { useGameState } from '@/hooks/useGameState';
12
+import { useCommandExecution } from '@/hooks/useCommandExecution';
13
+import { useTerminal } from '@/hooks/useTerminal';
14
+import { useHelpModals } from '@/hooks/useHelpModals';
15
+import { useTreeUtils } from '@/hooks/useTreeUtils';
1516
 
1617
 const Game: React.FC = () => {
17
-  const [gameState, setGameState] = useState<{
18
-    tree: FileSystemTree | null;
19
-    sessionId: number | null;
20
-    loading: boolean;
21
-    error: string | null;
22
-  }>({
23
-    tree: null,
24
-    sessionId: null,
25
-    loading: false,
26
-    error: null,
27
-  });
28
-
29
-  const [command, setCommand] = useState('');
30
-  const [commandHistory, setCommandHistory] = useState<CommandHistoryEntry[]>([]);
31
-  const [executing, setExecuting] = useState(false);
32
-  const [showHints, setShowHints] = useState(false);
33
-  const [hints, setHints] = useState<string[]>([]);
34
-  const [showFHS, setShowFHS] = useState(false);
35
-  const [fhsDirs, setFhsDirs] = useState<FHSDirectory[]>([]);
36
-  const [showCommands, setShowCommands] = useState(false);
37
-  const [commandRef, setCommandRef] = useState<CommandReferenceResponse | null>(null);
38
-  const [terminalMinimized, setTerminalMinimized] = useState(true);
39
-  const [hasPlayedIntro, setHasPlayedIntro] = useState(false);
40
-  const [isDarkMode] = useState(true); // Always dark mode, but keep as state to avoid re-renders
41
-  const [moleKilled, setMoleKilled] = useState(false);
42
-  const [moleDirection, setMoleDirection] = useState<MoleDirection | null>(null);
43
-  const [score, setScore] = useState(0);
44
-  const [molesKilled, setMolesKilled] = useState(0);
45
-
4618
   // Canvas background color - always dark mode
4719
   const canvasBackground = 'bg-gray-900';
20
+  const isDarkMode = true;
21
+
22
+  // Use our custom hooks
23
+  const {
24
+    gameState,
25
+    gameStats,
26
+    hasPlayedIntro,
27
+    startNewGame,
28
+    updatePlayerLocation,
29
+    updateTreeData,
30
+    setMoleDirection,
31
+    setMoleKilled,
32
+    updateScore,
33
+    setHasPlayedIntro,
34
+  } = useGameState();
35
+
36
+  const {
37
+    command,
38
+    setCommand,
39
+    clearCommand,
40
+    terminalMinimized,
41
+    setTerminalMinimized,
42
+  } = useTerminal();
43
+
44
+  const helpModals = useHelpModals(gameState.tree?.id || null);
45
+
46
+  const { updateTreeDataToShowMole, removeMoleFromTree } = useTreeUtils();
47
+
48
+  // Handle mole kill animation and updates
49
+  const handleMoleKilled = useCallback((response: CommandResponse) => {
50
+    if (!gameState.tree) return;
4851
 
49
-  // Start a new game
50
-  const startNewGame = async () => {
51
-    try {
52
-      setGameState({ ...gameState, loading: true, error: null });
53
-      const response = await gameApi.createGame('Player1');
54
-      setGameState({
55
-        tree: response.tree,
56
-        sessionId: response.session_id,
57
-        loading: false,
58
-        error: null,
59
-      });
60
-      
61
-      // Reset game state
62
-      setScore(0);
63
-      setMolesKilled(0);
64
-      setMoleDirection(null);
65
-      
66
-      // Create a more dynamic starting message based on random location
67
-      const startLocation = response.tree.player_location;
68
-      const homeDir = response.home_directory || '/home';
69
-      let locationContext = '';
52
+    // First, show the killed mole briefly
53
+    updateTreeData((tree) => 
54
+      updateTreeDataToShowMole(tree, gameState.tree!.player_location)
55
+    );
56
+    
57
+    // Trigger falling animation
58
+    setMoleKilled(true);
59
+    
60
+    // After animation, update tree with new mole location
61
+    setTimeout(() => {
62
+      setMoleKilled(false);
7063
       
71
-      if (startLocation.startsWith('/home')) {
72
-        locationContext = "You've been dropped in someone's home directory. ";
73
-      } else if (startLocation.startsWith('/usr')) {
74
-        locationContext = "You're in the system's usr hierarchy. ";
75
-      } else if (startLocation.startsWith('/var')) {
76
-        locationContext = "You're somewhere in the variable data area. ";
77
-      } else if (startLocation.startsWith('/opt')) {
78
-        locationContext = "You're in the optional packages directory. ";
79
-      } else {
80
-        locationContext = "You've been placed somewhere in the filesystem. ";
64
+      // Update tree to show new mole location
65
+      if (response.new_mole_location) {
66
+        updateTreeData((tree) => {
67
+          const cleanTree = removeMoleFromTree(tree);
68
+          return updateTreeDataToShowMole(cleanTree, response.new_mole_location!);
69
+        });
8170
       }
8271
       
83
-      // Add timer info to starting message
84
-      let timerInfo = '';
85
-      if (response.initial_timer && response.timer_reason) {
86
-        timerInfo = `\nTimer: ${response.initial_timer}s (mole is ${response.timer_reason})`;
72
+      // Set new mole direction
73
+      if (response.mole_direction) {
74
+        setMoleDirection(response.mole_direction);
8775
       }
88
-      
89
-      setCommandHistory([{
90
-        command: 'Hunt started!',
91
-        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.`,
92
-        success: true,
93
-      }]);
94
-      setHints([]);
95
-      setShowHints(false);
96
-      setTerminalMinimized(true); // Keep terminal minimized on new game
97
-      setHasPlayedIntro(false); // Reset intro for new game
98
-      setMoleKilled(false); // Reset mole killed state
99
-    } catch {
100
-      setGameState({
101
-        ...gameState,
102
-        loading: false,
103
-        error: 'Failed to start game. Is the backend running on http://localhost:8000?',
104
-      });
105
-    }
106
-  };
107
-
108
-  // Get hints
109
-  const getHints = async () => {
110
-    if (!gameState.tree) return;
76
+    }, 1500); // Wait for falling animation
11177
     
112
-    try {
113
-      const response = await gameApi.getHint(gameState.tree.id);
114
-      setHints(response.hints);
115
-      setShowHints(true);
116
-    } catch {
117
-      console.error('Failed to get hints');
118
-    }
119
-  };
120
-
121
-  // Get FHS reference
122
-  const getFHSReference = async () => {
123
-    try {
124
-      const response = await gameApi.getFHSReference();
125
-      setFhsDirs(response.directories);
126
-      setShowFHS(true);
127
-    } catch {
128
-      console.error('Failed to get FHS reference');
78
+    // Update score and moles killed
79
+    if (response.score !== undefined && response.moles_killed !== undefined) {
80
+      updateScore(response.score, response.moles_killed);
12981
     }
130
-  };
82
+  }, [gameState.tree, updateTreeData, setMoleKilled, setMoleDirection, updateScore, updateTreeDataToShowMole, removeMoleFromTree]);
83
+
84
+  const {
85
+    executing,
86
+    commandHistory,
87
+    executeCommand: executeCommandBase,
88
+    addToHistory,
89
+    clearHistory,
90
+  } = useCommandExecution(
91
+    gameState.tree?.id || null,
92
+    gameState.sessionId,
93
+    updatePlayerLocation,
94
+    handleMoleKilled,
95
+    updateTreeData
96
+  );
13197
 
132
-  // Get command reference
133
-  const getCommandReference = async () => {
134
-    try {
135
-      if (!commandRef) {
136
-        const response = await gameApi.getCommandReference();
137
-        setCommandRef(response);
138
-      }
139
-      setShowCommands(true);
140
-    } catch {
141
-      console.error('Failed to get command reference');
142
-    }
143
-  };
98
+  // Wrap executeCommand to clear the command input
99
+  const executeCommand = useCallback(async (cmd: string) => {
100
+    await executeCommandBase(cmd);
101
+    clearCommand();
102
+  }, [executeCommandBase, clearCommand]);
144103
 
145
-  // Execute a command
146
-  const executeCommand = async (cmd: string) => {
147
-    if (!gameState.tree || !cmd.trim() || executing) return;
104
+  // Initialize game with welcome message
105
+  const initializeGame = useCallback(async () => {
106
+    const response = await startNewGame();
107
+    if (!response) return;
148108
 
149
-    setExecuting(true);
150
-    try {
151
-      const response = await gameApi.executeCommand(
152
-        gameState.tree.id,
153
-        cmd,
154
-        gameState.sessionId || undefined
155
-      );
109
+    // Clear history for new game
110
+    clearHistory();
156111
 
157
-      // Build output with timer warnings
158
-      let fullOutput = response.output;
159
-      
160
-      // Add timer warnings if present
161
-      if (response.timer_warnings && response.timer_warnings.length > 0) {
162
-        const warnings = response.timer_warnings.map(w => 
163
-          `⚠️ ${w.level}: ${w.message}`
164
-        ).join('\n');
165
-        fullOutput = warnings + (fullOutput ? '\n' + fullOutput : '');
166
-      }
167
-
168
-      // Update command history
169
-      setCommandHistory(prev => [...prev, {
170
-        command: cmd,
171
-        output: fullOutput,
172
-        success: response.success,
173
-      }]);
112
+    // Create a dynamic starting message
113
+    const startLocation = response.tree.player_location;
114
+    const homeDir = response.home_directory || '/home';
115
+    let locationContext = '';
116
+    
117
+    if (startLocation.startsWith('/home')) {
118
+      locationContext = "You've been dropped in someone's home directory. ";
119
+    } else if (startLocation.startsWith('/usr')) {
120
+      locationContext = "You're in the system's usr hierarchy. ";
121
+    } else if (startLocation.startsWith('/var')) {
122
+      locationContext = "You're somewhere in the variable data area. ";
123
+    } else if (startLocation.startsWith('/opt')) {
124
+      locationContext = "You're in the optional packages directory. ";
125
+    } else {
126
+      locationContext = "You've been placed somewhere in the filesystem. ";
127
+    }
128
+    
129
+    // Add timer info to starting message
130
+    let timerInfo = '';
131
+    if (response.initial_timer && response.timer_reason) {
132
+      timerInfo = `\nTimer: ${response.initial_timer}s (mole is ${response.timer_reason})`;
133
+    }
134
+    
135
+    addToHistory({
136
+      command: 'Hunt started!',
137
+      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.`,
138
+      success: true,
139
+    });
140
+  }, [startNewGame, addToHistory, clearHistory]);
174141
 
175
-      // Update player location if moved
176
-      if (response.current_path !== gameState.tree.player_location) {
177
-        setGameState(prev => ({
178
-          ...prev,
179
-          tree: prev.tree ? {
180
-            ...prev.tree,
181
-            player_location: response.current_path,
182
-          } : null,
183
-        }));
184
-      }
142
+  // Handle timer expiration
143
+  const handleTimerExpire = useCallback(async () => {
144
+    if (!gameState.tree) return;
185145
 
186
-      // Check if a new mole was spawned
187
-      if (response.mole_spawned) {
188
-        // First, show the killed mole briefly
189
-        setGameState(prev => ({
190
-          ...prev,
191
-          tree: prev.tree ? {
192
-            ...prev.tree,
193
-            tree_data: updateTreeDataToShowMole(prev.tree!.tree_data, prev.tree!.player_location),
194
-          } : null,
195
-        }));
146
+    try {
147
+      const response = await gameApi.checkTimer(gameState.tree.id, gameState.sessionId || undefined);
148
+      if (response.mole_escaped) {
149
+        // Build the escape message
150
+        let escapeMessage = response.message || 'The mole escaped!';
196151
         
197
-        // Trigger falling animation
198
-        setMoleKilled(true);
152
+        // Add distance info for new mole if available
153
+        if (response.escape_data?.timer_reason) {
154
+          escapeMessage += `\nNew mole detected ${response.escape_data.timer_reason}!`;
155
+        }
199156
         
200
-        // After animation, update tree with new mole location
201
-        setTimeout(() => {
202
-          setMoleKilled(false);
203
-          
157
+        // Update command history with escape message
158
+        addToHistory({
159
+          command: 'Mole escaped!',
160
+          output: escapeMessage,
161
+          success: false,
162
+        });
163
+        
164
+        // Update mole direction if provided
165
+        if (response.escape_data?.new_location) {
204166
           // Update tree to show new mole location
205
-          if (response.new_mole_location && gameState.tree) {
206
-            const treeWithNewMole = updateTreeDataToShowMole(
207
-              removeMoleFromTree(gameState.tree.tree_data),
208
-              response.new_mole_location
209
-            );
210
-            
211
-            setGameState(prev => ({
212
-              ...prev,
213
-              tree: prev.tree ? {
214
-                ...prev.tree,
215
-                tree_data: treeWithNewMole,
216
-              } : null,
217
-            }));
218
-          }
167
+          updateTreeData((tree) => {
168
+            const cleanTree = removeMoleFromTree(tree);
169
+            return updateTreeDataToShowMole(cleanTree, response.escape_data!.new_location);
170
+          });
219171
           
220
-          // Set new mole direction
221
-          if (response.mole_direction) {
222
-            setMoleDirection(response.mole_direction);
223
-            // Hide direction indicator after 5 seconds
224
-            setTimeout(() => {
225
-              setMoleDirection(null);
226
-            }, 5000);
172
+          // Show mole direction indicator if provided
173
+          if (response.escape_data?.mole_direction) {
174
+            setMoleDirection(response.escape_data.mole_direction);
227175
           }
228
-        }, 1500); // Wait for falling animation
229
-        
230
-        // Update score and moles killed
231
-        if (response.score !== undefined) setScore(response.score);
232
-        if (response.moles_killed !== undefined) setMolesKilled(response.moles_killed);
233
-        
234
-        // Format the output to include timer info on new line
235
-        if (response.timer_reason && !response.output.includes('New mole detected')) {
236
-          response.output += `\nNew mole detected ${response.timer_reason}!`;
237176
         }
238177
       }
239
-
240
-      // Legacy: Check if game won (for old backend compatibility)
241
-      if (response.game_won && !response.mole_spawned) {
242
-        setGameState(prev => ({
243
-          ...prev,
244
-          tree: prev.tree ? {
245
-            ...prev.tree,
246
-            is_completed: true,
247
-            tree_data: updateTreeDataToShowMole(prev.tree!.tree_data, prev.tree!.player_location),
248
-          } : null,
249
-        }));
250
-        
251
-        setTimeout(() => {
252
-          setMoleKilled(true);
253
-        }, 200);
254
-      }
255
-
256
-      setCommand('');
257
-    } catch {
258
-      setCommandHistory(prev => [...prev, {
259
-        command: cmd,
260
-        output: 'Error: Failed to execute command. Check your connection.',
261
-        success: false,
262
-      }]);
263
-    } finally {
264
-      setExecuting(false);
178
+    } catch (error) {
179
+      console.error('Failed to check timer:', error);
265180
     }
266
-  };
267
-
268
-  // Update tree data to show mole when found
269
-  const updateTreeDataToShowMole = (treeData: TreeNode, molePath: string): TreeNode => {
270
-    if (treeData.path === molePath) {
271
-      return { ...treeData, has_mole: true };
272
-    }
273
-    if (treeData.children) {
274
-      return {
275
-        ...treeData,
276
-        children: treeData.children.map((child) => 
277
-          updateTreeDataToShowMole(child, molePath)
278
-        ),
279
-      };
280
-    }
281
-    return treeData;
282
-  };
283
-
284
-  // Remove mole from tree data
285
-  const removeMoleFromTree = (treeData: TreeNode): TreeNode => {
286
-    return {
287
-      ...treeData,
288
-      has_mole: false,
289
-      children: treeData.children ? treeData.children.map((child) => removeMoleFromTree(child)) : []
290
-    };
291
-  };
181
+  }, [gameState.tree, gameState.sessionId, addToHistory, updateTreeData, setMoleDirection, removeMoleFromTree, updateTreeDataToShowMole]);
292182
 
293183
   // Handle node click in visualizer
294
-  const handleNodeClick = (path: string) => {
184
+  const handleNodeClick = useCallback((path: string) => {
295185
     executeCommand(`cd ${path}`);
296
-  };
297
-
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
-  };
186
+  }, [executeCommand]);
350187
 
351188
   // Start game on mount
352189
   useEffect(() => {
353
-    startNewGame();
354
-  // eslint-disable-next-line react-hooks/exhaustive-deps
355
-  }, []);
190
+    initializeGame();
191
+  }, []); // eslint-disable-line react-hooks/exhaustive-deps
356192
 
357
-  // Mark intro as played after first render
193
+  // Mark intro as played after delay
358194
   useEffect(() => {
359195
     if (gameState.tree && !hasPlayedIntro) {
360196
       const timer = setTimeout(() => {
361197
         setHasPlayedIntro(true);
362
-      }, 10000); // Add a buffer to ensure animation completes (9.3s + buffer)
198
+      }, 10000); // Add a buffer to ensure animation completes
363199
       return () => clearTimeout(timer);
364200
     }
365
-  }, [gameState.tree, hasPlayedIntro]);
201
+  }, [gameState.tree, hasPlayedIntro, setHasPlayedIntro]);
366202
 
203
+  // Loading state
367204
   if (gameState.loading) {
368205
     return (
369
-      <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-gray-900 dark:text-white`}>
206
+      <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-white`}>
370207
         <div className="text-center">
371208
           <div className="text-2xl mb-4">Loading Bashamole...</div>
372209
           <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-current mx-auto"></div>
@@ -375,13 +212,14 @@ const Game: React.FC = () => {
375212
     );
376213
   }
377214
 
215
+  // Error state
378216
   if (gameState.error) {
379217
     return (
380
-      <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-gray-900 dark:text-white`}>
218
+      <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-white`}>
381219
         <div className="text-center">
382
-          <div className="text-red-600 dark:text-red-400 mb-4">{gameState.error}</div>
220
+          <div className="text-red-400 mb-4">{gameState.error}</div>
383221
           <button
384
-            onClick={startNewGame}
222
+            onClick={initializeGame}
385223
             className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
386224
           >
387225
             Try Again
@@ -391,14 +229,15 @@ const Game: React.FC = () => {
391229
     );
392230
   }
393231
 
232
+  // Initial state
394233
   if (!gameState.tree) {
395234
     return (
396
-      <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-gray-900 dark:text-white`}>
235
+      <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-white`}>
397236
         <div className="text-center">
398237
           <h1 className="text-4xl font-bold mb-4">Bashamole</h1>
399
-          <p className="text-gray-600 dark:text-gray-400 mb-8">Hunt the mole in the Unix filesystem!</p>
238
+          <p className="text-gray-400 mb-8">Hunt the mole in the Unix filesystem!</p>
400239
           <button
401
-            onClick={startNewGame}
240
+            onClick={initializeGame}
402241
             className="px-8 py-4 bg-green-600 text-white rounded-lg hover:bg-green-700 text-xl transition transform hover:scale-105"
403242
           >
404243
             Start New Game
@@ -408,6 +247,7 @@ const Game: React.FC = () => {
408247
     );
409248
   }
410249
 
250
+  // Main game UI
411251
   return (
412252
     <div className={`relative min-h-screen ${canvasBackground} overflow-hidden`}>
413253
       {/* Tree Canvas - Full Screen Background */}
@@ -418,18 +258,18 @@ const Game: React.FC = () => {
418258
           onNodeClick={handleNodeClick}
419259
           playIntro={!hasPlayedIntro}
420260
           isDarkMode={isDarkMode}
421
-          moleKilled={moleKilled}
261
+          moleKilled={gameStats.moleKilled}
422262
         />
423263
       </div>
424264
 
425265
       {/* Game Status (Timer, Score, Direction Indicator) */}
426266
       <GameStatus
427
-        gameTreeId={gameState.tree?.id || null}
267
+        gameTreeId={gameState.tree.id}
428268
         sessionId={gameState.sessionId}
429269
         onTimerExpire={handleTimerExpire}
430
-        score={score}
431
-        molesKilled={molesKilled}
432
-        moleDirection={moleDirection}
270
+        score={gameStats.score}
271
+        molesKilled={gameStats.molesKilled}
272
+        moleDirection={gameStats.moleDirection}
433273
       />
434274
 
435275
       {/* Terminal */}
@@ -439,32 +279,25 @@ const Game: React.FC = () => {
439279
         setCommand={setCommand}
440280
         executeCommand={executeCommand}
441281
         executing={executing}
442
-        currentPath={gameState.tree?.player_location || '~'}
282
+        currentPath={gameState.tree.player_location}
443283
         terminalMinimized={terminalMinimized}
444284
         setTerminalMinimized={setTerminalMinimized}
445
-        onGetCommandReference={getCommandReference}
446
-        onGetHints={getHints}
447
-        onGetFHSReference={getFHSReference}
285
+        onGetCommandReference={helpModals.getCommandReference}
286
+        onGetHints={helpModals.getHints}
287
+        onGetFHSReference={helpModals.getFHSReference}
448288
       />
449289
 
450290
       {/* 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
-      />
291
+      <HelpModals {...helpModals} />
462292
 
463293
       {/* Bottom Game Bar */}
464294
       <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">
465295
         <div className="max-w-7xl mx-auto flex justify-between items-center px-4">
466296
           <div className="flex items-center gap-4">
467
-            <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>
297
+            <h1 className="text-2xl font-bold text-white">
298
+              <span className='font-terminal bg-gray-500 text-red-400 px-0.5 py-0 rounded'>bash</span>
299
+              amole
300
+            </h1>
468301
           </div>
469302
           
470303
           <div className="flex items-center gap-3">
@@ -472,7 +305,7 @@ const Game: React.FC = () => {
472305
               click adjacent nodes or use the terminal
473306
             </div>
474307
             <button
475
-              onClick={startNewGame}
308
+              onClick={initializeGame}
476309
               className="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white text-sm rounded transition"
477310
             >
478311
               New Game
frontend/src/hooks/useCommandExecution.tsadded
@@ -0,0 +1,113 @@
1
+import { useState, useCallback } from 'react';
2
+import { gameApi, CommandResponse, TreeNode } from '@/lib/api';
3
+
4
+interface CommandExecutionState {
5
+  executing: boolean;
6
+  commandHistory: CommandHistoryEntry[];
7
+}
8
+
9
+interface CommandHistoryEntry {
10
+  command: string;
11
+  output: string;
12
+  success: boolean;
13
+}
14
+
15
+export const useCommandExecution = (
16
+  gameTreeId: number | null,
17
+  sessionId: number | null,
18
+  onLocationChange?: (newPath: string) => void,
19
+  onMoleKilled?: (response: CommandResponse) => void,
20
+  onTreeUpdate?: (updater: (tree: TreeNode) => TreeNode) => void
21
+) => {
22
+  const [state, setState] = useState<CommandExecutionState>({
23
+    executing: false,
24
+    commandHistory: [],
25
+  });
26
+
27
+  const addToHistory = useCallback((entry: CommandHistoryEntry) => {
28
+    setState(prev => ({
29
+      ...prev,
30
+      commandHistory: [...prev.commandHistory, entry],
31
+    }));
32
+  }, []);
33
+
34
+  const clearHistory = useCallback(() => {
35
+    setState(prev => ({
36
+      ...prev,
37
+      commandHistory: [],
38
+    }));
39
+  }, []);
40
+
41
+  const executeCommand = useCallback(async (cmd: string) => {
42
+    if (!gameTreeId || !cmd.trim() || state.executing) return;
43
+
44
+    setState(prev => ({ ...prev, executing: true }));
45
+    
46
+    try {
47
+      const response = await gameApi.executeCommand(
48
+        gameTreeId,
49
+        cmd,
50
+        sessionId || undefined
51
+      );
52
+
53
+      // Build output with timer warnings
54
+      let fullOutput = response.output;
55
+      
56
+      // Add timer warnings if present
57
+      if (response.timer_warnings && response.timer_warnings.length > 0) {
58
+        const warnings = response.timer_warnings.map(w => 
59
+          `⚠️ ${w.level}: ${w.message}`
60
+        ).join('\n');
61
+        fullOutput = warnings + (fullOutput ? '\n' + fullOutput : '');
62
+      }
63
+
64
+      // Add to command history
65
+      addToHistory({
66
+        command: cmd,
67
+        output: fullOutput,
68
+        success: response.success,
69
+      });
70
+
71
+      // Handle location change
72
+      if (response.current_path && onLocationChange) {
73
+        onLocationChange(response.current_path);
74
+      }
75
+
76
+      // Handle mole spawning
77
+      if (response.mole_spawned && onMoleKilled) {
78
+        // Format the output to include timer info on new line
79
+        if (response.timer_reason && !response.output.includes('New mole detected')) {
80
+          response.output += `\nNew mole detected ${response.timer_reason}!`;
81
+        }
82
+        onMoleKilled(response);
83
+      }
84
+
85
+      // Legacy: Handle game won
86
+      if (response.game_won && !response.mole_spawned && onTreeUpdate) {
87
+        onTreeUpdate((tree) => ({
88
+          ...tree,
89
+          has_mole: true,
90
+        }));
91
+      }
92
+
93
+      return response;
94
+    } catch (error) {
95
+      addToHistory({
96
+        command: cmd,
97
+        output: 'Error: Failed to execute command. Check your connection.',
98
+        success: false,
99
+      });
100
+      return null;
101
+    } finally {
102
+      setState(prev => ({ ...prev, executing: false }));
103
+    }
104
+  }, [gameTreeId, sessionId, state.executing, addToHistory, onLocationChange, onMoleKilled, onTreeUpdate]);
105
+
106
+  return {
107
+    executing: state.executing,
108
+    commandHistory: state.commandHistory,
109
+    executeCommand,
110
+    addToHistory,
111
+    clearHistory,
112
+  };
113
+};
frontend/src/hooks/useGameState.tsadded
@@ -0,0 +1,126 @@
1
+import { useState, useCallback } from 'react';
2
+import { gameApi, FileSystemTree, TreeNode, MoleDirection } from '@/lib/api';
3
+
4
+interface GameState {
5
+  tree: FileSystemTree | null;
6
+  sessionId: number | null;
7
+  loading: boolean;
8
+  error: string | null;
9
+}
10
+
11
+interface GameStats {
12
+  score: number;
13
+  molesKilled: number;
14
+  moleDirection: MoleDirection | null;
15
+  moleKilled: boolean;
16
+}
17
+
18
+export const useGameState = () => {
19
+  const [gameState, setGameState] = useState<GameState>({
20
+    tree: null,
21
+    sessionId: null,
22
+    loading: false,
23
+    error: null,
24
+  });
25
+
26
+  const [gameStats, setGameStats] = useState<GameStats>({
27
+    score: 0,
28
+    molesKilled: 0,
29
+    moleDirection: null,
30
+    moleKilled: false,
31
+  });
32
+
33
+  const [hasPlayedIntro, setHasPlayedIntro] = useState(false);
34
+
35
+  const updatePlayerLocation = useCallback((newPath: string) => {
36
+    setGameState(prev => ({
37
+      ...prev,
38
+      tree: prev.tree ? {
39
+        ...prev.tree,
40
+        player_location: newPath,
41
+      } : null,
42
+    }));
43
+  }, []);
44
+
45
+  const updateTreeData = useCallback((updater: (tree: TreeNode) => TreeNode) => {
46
+    setGameState(prev => ({
47
+      ...prev,
48
+      tree: prev.tree ? {
49
+        ...prev.tree,
50
+        tree_data: updater(prev.tree.tree_data),
51
+      } : null,
52
+    }));
53
+  }, []);
54
+
55
+  const setMoleDirection = useCallback((direction: MoleDirection | null) => {
56
+    setGameStats(prev => ({ ...prev, moleDirection: direction }));
57
+    
58
+    // Auto-hide after 5 seconds
59
+    if (direction) {
60
+      setTimeout(() => {
61
+        setGameStats(prev => ({ ...prev, moleDirection: null }));
62
+      }, 5000);
63
+    }
64
+  }, []);
65
+
66
+  const setMoleKilled = useCallback((killed: boolean) => {
67
+    setGameStats(prev => ({ ...prev, moleKilled: killed }));
68
+  }, []);
69
+
70
+  const updateScore = useCallback((score: number, molesKilled: number) => {
71
+    setGameStats(prev => ({
72
+      ...prev,
73
+      score,
74
+      molesKilled,
75
+    }));
76
+  }, []);
77
+
78
+  const startNewGame = useCallback(async () => {
79
+    try {
80
+      setGameState(prev => ({ ...prev, loading: true, error: null }));
81
+      const response = await gameApi.createGame('Player1');
82
+      
83
+      setGameState({
84
+        tree: response.tree,
85
+        sessionId: response.session_id,
86
+        loading: false,
87
+        error: null,
88
+      });
89
+      
90
+      // Reset game stats
91
+      setGameStats({
92
+        score: 0,
93
+        molesKilled: 0,
94
+        moleDirection: null,
95
+        moleKilled: false,
96
+      });
97
+      
98
+      setHasPlayedIntro(false);
99
+      
100
+      return response;
101
+    } catch (error) {
102
+      setGameState(prev => ({
103
+        ...prev,
104
+        loading: false,
105
+        error: 'Failed to start game. Is the backend running on http://localhost:8000?',
106
+      }));
107
+      return null;
108
+    }
109
+  }, []);
110
+
111
+  return {
112
+    // State
113
+    gameState,
114
+    gameStats,
115
+    hasPlayedIntro,
116
+    
117
+    // Actions
118
+    startNewGame,
119
+    updatePlayerLocation,
120
+    updateTreeData,
121
+    setMoleDirection,
122
+    setMoleKilled,
123
+    updateScore,
124
+    setHasPlayedIntro,
125
+  };
126
+};
frontend/src/hooks/useHelpModals.tsadded
@@ -0,0 +1,67 @@
1
+import { useState, useCallback } from 'react';
2
+import { gameApi, FHSDirectory, CommandReferenceResponse } from '@/lib/api';
3
+
4
+export const useHelpModals = (gameTreeId: number | null) => {
5
+  const [showHints, setShowHints] = useState(false);
6
+  const [hints, setHints] = useState<string[]>([]);
7
+  
8
+  const [showFHS, setShowFHS] = useState(false);
9
+  const [fhsDirs, setFhsDirs] = useState<FHSDirectory[]>([]);
10
+  
11
+  const [showCommands, setShowCommands] = useState(false);
12
+  const [commandRef, setCommandRef] = useState<CommandReferenceResponse | null>(null);
13
+
14
+  const getHints = useCallback(async () => {
15
+    if (!gameTreeId) return;
16
+    
17
+    try {
18
+      const response = await gameApi.getHint(gameTreeId);
19
+      setHints(response.hints);
20
+      setShowHints(true);
21
+    } catch (error) {
22
+      console.error('Failed to get hints:', error);
23
+    }
24
+  }, [gameTreeId]);
25
+
26
+  const getFHSReference = useCallback(async () => {
27
+    try {
28
+      const response = await gameApi.getFHSReference();
29
+      setFhsDirs(response.directories);
30
+      setShowFHS(true);
31
+    } catch (error) {
32
+      console.error('Failed to get FHS reference:', error);
33
+    }
34
+  }, []);
35
+
36
+  const getCommandReference = useCallback(async () => {
37
+    try {
38
+      if (!commandRef) {
39
+        const response = await gameApi.getCommandReference();
40
+        setCommandRef(response);
41
+      }
42
+      setShowCommands(true);
43
+    } catch (error) {
44
+      console.error('Failed to get command reference:', error);
45
+    }
46
+  }, [commandRef]);
47
+
48
+  return {
49
+    // Hints
50
+    showHints,
51
+    setShowHints,
52
+    hints,
53
+    getHints,
54
+    
55
+    // FHS
56
+    showFHS,
57
+    setShowFHS,
58
+    fhsDirs,
59
+    getFHSReference,
60
+    
61
+    // Commands
62
+    showCommands,
63
+    setShowCommands,
64
+    commandRef,
65
+    getCommandReference,
66
+  };
67
+};
frontend/src/hooks/useTerminal.tsadded
@@ -0,0 +1,23 @@
1
+import { useState, useCallback } from 'react';
2
+
3
+export const useTerminal = () => {
4
+  const [command, setCommand] = useState('');
5
+  const [terminalMinimized, setTerminalMinimized] = useState(true);
6
+
7
+  const clearCommand = useCallback(() => {
8
+    setCommand('');
9
+  }, []);
10
+
11
+  const toggleTerminal = useCallback(() => {
12
+    setTerminalMinimized(prev => !prev);
13
+  }, []);
14
+
15
+  return {
16
+    command,
17
+    setCommand,
18
+    clearCommand,
19
+    terminalMinimized,
20
+    setTerminalMinimized,
21
+    toggleTerminal,
22
+  };
23
+};
frontend/src/hooks/useTreeUtils.tsadded
@@ -0,0 +1,32 @@
1
+import { useCallback } from 'react';
2
+import { TreeNode } from '@/lib/api';
3
+
4
+export const useTreeUtils = () => {
5
+  const updateTreeDataToShowMole = useCallback((treeData: TreeNode, molePath: string): TreeNode => {
6
+    if (treeData.path === molePath) {
7
+      return { ...treeData, has_mole: true };
8
+    }
9
+    if (treeData.children) {
10
+      return {
11
+        ...treeData,
12
+        children: treeData.children.map((child) => 
13
+          updateTreeDataToShowMole(child, molePath)
14
+        ),
15
+      };
16
+    }
17
+    return treeData;
18
+  }, []);
19
+
20
+  const removeMoleFromTree = useCallback((treeData: TreeNode): TreeNode => {
21
+    return {
22
+      ...treeData,
23
+      has_mole: false,
24
+      children: treeData.children ? treeData.children.map((child) => removeMoleFromTree(child)) : [],
25
+    };
26
+  }, []);
27
+
28
+  return {
29
+    updateTreeDataToShowMole,
30
+    removeMoleFromTree,
31
+  };
32
+};