zeroed-some/bashamole / da833f2

Browse files

endless mode

Authored by espadonne
SHA
da833f28e14d40ff4133b4a4cae620292f8d77b8
Parents
bff1dc9
Tree
9434518

8 changed files

StatusFile+-
A backend/apps/trees/migrations/0003_filesystemtree_active_moles_and_more.py 63 0
A backend/apps/trees/migrations/0004_remove_filesystemtree_active_moles_and_more.py 53 0
A backend/apps/trees/migrations/0005_filesystemtree_moles_killed_and_more.py 38 0
M backend/apps/trees/models.py 156 1
M backend/apps/trees/serializers.py 11 4
M backend/apps/trees/views.py 61 9
M frontend/src/components/Game.tsx 151 34
M frontend/src/lib/api.ts 13 0
backend/apps/trees/migrations/0003_filesystemtree_active_moles_and_more.pyadded
@@ -0,0 +1,63 @@
1
+# Generated by Django 5.2.3 on 2025-06-15 05:16
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ("trees", "0002_filesystemtree_directory_stack_and_more"),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.AddField(
14
+            model_name="filesystemtree",
15
+            name="active_moles",
16
+            field=models.JSONField(default=list),
17
+        ),
18
+        migrations.AddField(
19
+            model_name="filesystemtree",
20
+            name="difficulty_level",
21
+            field=models.IntegerField(default=1),
22
+        ),
23
+        migrations.AddField(
24
+            model_name="filesystemtree",
25
+            name="game_mode",
26
+            field=models.CharField(default="single", max_length=20),
27
+        ),
28
+        migrations.AddField(
29
+            model_name="filesystemtree",
30
+            name="max_moles_percentage",
31
+            field=models.FloatField(default=0.15),
32
+        ),
33
+        migrations.AddField(
34
+            model_name="filesystemtree",
35
+            name="mole_spawn_interval",
36
+            field=models.FloatField(default=30.0),
37
+        ),
38
+        migrations.AddField(
39
+            model_name="filesystemtree",
40
+            name="mole_type_weights",
41
+            field=models.JSONField(default=dict),
42
+        ),
43
+        migrations.AddField(
44
+            model_name="filesystemtree",
45
+            name="next_mole_spawn",
46
+            field=models.DateTimeField(blank=True, null=True),
47
+        ),
48
+        migrations.AddField(
49
+            model_name="gamesession",
50
+            name="moles_caught",
51
+            field=models.IntegerField(default=0),
52
+        ),
53
+        migrations.AddField(
54
+            model_name="gamesession",
55
+            name="moles_escaped",
56
+            field=models.IntegerField(default=0),
57
+        ),
58
+        migrations.AddField(
59
+            model_name="gamesession",
60
+            name="score",
61
+            field=models.IntegerField(default=0),
62
+        ),
63
+    ]
backend/apps/trees/migrations/0004_remove_filesystemtree_active_moles_and_more.pyadded
@@ -0,0 +1,53 @@
1
+# Generated by Django 5.2.3 on 2025-06-15 05:51
2
+
3
+from django.db import migrations
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ("trees", "0003_filesystemtree_active_moles_and_more"),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.RemoveField(
14
+            model_name="filesystemtree",
15
+            name="active_moles",
16
+        ),
17
+        migrations.RemoveField(
18
+            model_name="filesystemtree",
19
+            name="difficulty_level",
20
+        ),
21
+        migrations.RemoveField(
22
+            model_name="filesystemtree",
23
+            name="game_mode",
24
+        ),
25
+        migrations.RemoveField(
26
+            model_name="filesystemtree",
27
+            name="max_moles_percentage",
28
+        ),
29
+        migrations.RemoveField(
30
+            model_name="filesystemtree",
31
+            name="mole_spawn_interval",
32
+        ),
33
+        migrations.RemoveField(
34
+            model_name="filesystemtree",
35
+            name="mole_type_weights",
36
+        ),
37
+        migrations.RemoveField(
38
+            model_name="filesystemtree",
39
+            name="next_mole_spawn",
40
+        ),
41
+        migrations.RemoveField(
42
+            model_name="gamesession",
43
+            name="moles_caught",
44
+        ),
45
+        migrations.RemoveField(
46
+            model_name="gamesession",
47
+            name="moles_escaped",
48
+        ),
49
+        migrations.RemoveField(
50
+            model_name="gamesession",
51
+            name="score",
52
+        ),
53
+    ]
backend/apps/trees/migrations/0005_filesystemtree_moles_killed_and_more.pyadded
@@ -0,0 +1,38 @@
1
+# Generated by Django 5.2.3 on 2025-06-15 18:15
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ("trees", "0004_remove_filesystemtree_active_moles_and_more"),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.AddField(
14
+            model_name="filesystemtree",
15
+            name="moles_killed",
16
+            field=models.IntegerField(default=0),
17
+        ),
18
+        migrations.AddField(
19
+            model_name="filesystemtree",
20
+            name="total_commands",
21
+            field=models.IntegerField(default=0),
22
+        ),
23
+        migrations.AddField(
24
+            model_name="filesystemtree",
25
+            name="total_directories_visited",
26
+            field=models.IntegerField(default=0),
27
+        ),
28
+        migrations.AddField(
29
+            model_name="gamesession",
30
+            name="mole_stats",
31
+            field=models.JSONField(default=list),
32
+        ),
33
+        migrations.AddField(
34
+            model_name="gamesession",
35
+            name="moles_killed",
36
+            field=models.IntegerField(default=0),
37
+        ),
38
+    ]
backend/apps/trees/models.pymodified
@@ -4,6 +4,7 @@ from django.utils import timezone
44
 import json
55
 import random
66
 import string
7
+import math
78
 
89
 class FileSystemTree(models.Model):
910
     """A complete filesystem tree for a game session"""
@@ -22,6 +23,11 @@ class FileSystemTree(models.Model):
2223
     directory_stack = models.JSONField(default=list)  # For pushd/popd
2324
     home_directory = models.CharField(max_length=500, default="/home")  # Player's home
2425
     
26
+    # Game statistics
27
+    moles_killed = models.IntegerField(default=0)
28
+    total_commands = models.IntegerField(default=0)
29
+    total_directories_visited = models.IntegerField(default=0)
30
+    
2531
     # Cached tree structure
2632
     tree_data = models.JSONField(null=True, blank=True)
2733
     
@@ -202,6 +208,112 @@ class FileSystemTree(models.Model):
202208
             mole_dir = random.choice(candidates)
203209
             self.mole_location = mole_dir.path
204210
     
211
+    def spawn_new_mole(self):
212
+        """Spawn a new mole after the current one is killed"""
213
+        # Get all possible spawn locations (any directory)
214
+        all_directories = DirectoryNode.objects.filter(tree=self).exclude(path="/")
215
+        
216
+        if all_directories.exists():
217
+            # Randomly select a new location
218
+            new_mole_dir = random.choice(all_directories)
219
+            self.mole_location = new_mole_dir.path
220
+            
221
+            # Update the cached tree data
222
+            self.cache_tree()
223
+            self.save()
224
+            
225
+            return True
226
+        return False
227
+    
228
+    def get_mole_direction(self):
229
+        """Get the relative direction from player to mole in the tree structure"""
230
+        if not self.mole_location or not self.player_location:
231
+            return None
232
+        
233
+        # Build paths to compare
234
+        player_parts = self.player_location.split('/')
235
+        mole_parts = self.mole_location.split('/')
236
+        
237
+        # Remove empty strings from split
238
+        player_parts = [p for p in player_parts if p]
239
+        mole_parts = [p for p in mole_parts if p]
240
+        
241
+        # If player is at root, special handling
242
+        if not player_parts:
243
+            player_parts = ['']
244
+        
245
+        # Find common ancestor
246
+        common_depth = 0
247
+        for i in range(min(len(player_parts), len(mole_parts))):
248
+            if player_parts[i] == mole_parts[i]:
249
+                common_depth += 1
250
+            else:
251
+                break
252
+        
253
+        # Determine relative position
254
+        player_depth = len(player_parts)
255
+        mole_depth = len(mole_parts)
256
+        
257
+        # Calculate tree-based direction
258
+        if self.mole_location == self.player_location:
259
+            return {"direction": "here", "angle": 0}
260
+        
261
+        # Mole is in a parent directory (need to go up)
262
+        if common_depth == mole_depth and mole_depth < player_depth:
263
+            return {"direction": "up", "angle": 270}  # Up in tree
264
+        
265
+        # Mole is in a child directory (need to go down) 
266
+        if common_depth == player_depth and mole_depth > player_depth:
267
+            # It's directly below us
268
+            return {"direction": "down", "angle": 90}  # Down in tree
269
+        
270
+        # Mole is in a sibling or cousin branch
271
+        if common_depth < player_depth:
272
+            # Need to go up first, then sideways
273
+            # Determine left or right based on alphabetical order of diverging paths
274
+            if common_depth < len(mole_parts) and common_depth < len(player_parts):
275
+                if mole_parts[common_depth] < player_parts[common_depth]:
276
+                    return {"direction": "up-left", "angle": 225}
277
+                else:
278
+                    return {"direction": "up-right", "angle": 315}
279
+            return {"direction": "up", "angle": 270}
280
+        else:
281
+            # At same level or need to go down and sideways
282
+            if common_depth < len(mole_parts):
283
+                # Determine left or right based on tree structure
284
+                # This is a simplification - in reality we'd need to check the actual tree layout
285
+                if mole_parts[common_depth] < (player_parts[common_depth] if common_depth < len(player_parts) else 'z'):
286
+                    return {"direction": "left", "angle": 180}
287
+                else:
288
+                    return {"direction": "right", "angle": 0}
289
+            return {"direction": "down", "angle": 90}
290
+    
291
+    def calculate_path_distance(self, from_path, to_path):
292
+        """Calculate the minimum number of cd commands needed to go from one path to another"""
293
+        if from_path == to_path:
294
+            return 0
295
+        
296
+        from_parts = from_path.split('/')
297
+        to_parts = to_path.split('/')
298
+        
299
+        # Remove empty strings
300
+        from_parts = [p for p in from_parts if p]
301
+        to_parts = [p for p in to_parts if p]
302
+        
303
+        # Find common ancestor depth
304
+        common_depth = 0
305
+        for i in range(min(len(from_parts), len(to_parts))):
306
+            if from_parts[i] == to_parts[i]:
307
+                common_depth += 1
308
+            else:
309
+                break
310
+        
311
+        # Calculate moves needed
312
+        moves_up = len(from_parts) - common_depth
313
+        moves_down = len(to_parts) - common_depth
314
+        
315
+        return moves_up + moves_down
316
+    
205317
     def _set_random_start_position(self):
206318
         """Set a random starting position for the player"""
207319
         # Get all directories that could be valid starting positions
@@ -329,6 +441,7 @@ class FileSystemTree(models.Model):
329441
             # Save previous location before moving
330442
             self.previous_location = self.player_location
331443
             self.player_location = new_path
444
+            self.total_directories_visited += 1
332445
             self.save()
333446
             return True, f"Moved to {self.player_location}"
334447
         else:
@@ -367,6 +480,7 @@ class FileSystemTree(models.Model):
367480
         # Save current location as previous (for cd -)
368481
         self.previous_location = self.player_location
369482
         self.player_location = target_dir
483
+        self.total_directories_visited += 1
370484
         self.save()
371485
         
372486
         return True, f"Popped and moved to {self.player_location}"
@@ -420,6 +534,10 @@ class GameSession(models.Model):
420534
     commands_used = models.IntegerField(default=0)
421535
     directories_visited = models.IntegerField(default=0)
422536
     time_taken = models.DurationField(null=True, blank=True)
537
+    moles_killed = models.IntegerField(default=0)
538
+    
539
+    # Per-mole tracking
540
+    mole_stats = models.JSONField(default=list)  # List of {mole_number, location, commands, time, distance}
423541
     
424542
     # Command history
425543
     command_history = models.JSONField(default=list)
@@ -434,4 +552,41 @@ class GameSession(models.Model):
434552
             'timestamp': str(timezone.now())
435553
         })
436554
         self.commands_used += 1
437
-        self.save()
555
+        self.save()
556
+    
557
+    def record_mole_kill(self, mole_location, commands_for_mole, time_for_mole, distance_traveled):
558
+        """Record statistics for a mole kill"""
559
+        self.moles_killed += 1
560
+        self.mole_stats.append({
561
+            'mole_number': self.moles_killed,
562
+            'location': mole_location,
563
+            'commands': commands_for_mole,
564
+            'time': str(time_for_mole),
565
+            'distance': distance_traveled
566
+        })
567
+        self.save()
568
+    
569
+    def calculate_score(self):
570
+        """Calculate a score based on performance"""
571
+        if self.moles_killed == 0:
572
+            return 0
573
+        
574
+        # Base score per mole
575
+        base_score = 1000
576
+        
577
+        # Calculate average performance
578
+        total_commands = sum(stat['commands'] for stat in self.mole_stats)
579
+        avg_commands = total_commands / self.moles_killed if self.moles_killed > 0 else 0
580
+        
581
+        # Score formula (rough draft)
582
+        # More moles = better
583
+        # Fewer commands = better
584
+        # Less time = better (when we implement timing)
585
+        score = self.moles_killed * base_score
586
+        
587
+        # Efficiency bonus (fewer commands is better)
588
+        if avg_commands > 0:
589
+            efficiency_multiplier = max(0.5, min(2.0, 10 / avg_commands))
590
+            score *= efficiency_multiplier
591
+        
592
+        return int(score)
backend/apps/trees/serializers.pymodified
@@ -18,9 +18,10 @@ class FileSystemTreeSerializer(serializers.ModelSerializer):
1818
         fields = [
1919
             'id', 'name', 'created_at', 'seed', 
2020
             'player_location', 'is_completed', 'completed_at',
21
-            'tree_data', 'total_directories'
21
+            'tree_data', 'total_directories', 'moles_killed',
22
+            'total_commands', 'total_directories_visited'
2223
         ]
23
-        read_only_fields = ['created_at', 'tree_data', 'total_directories']
24
+        read_only_fields = ['created_at', 'tree_data', 'total_directories', 'moles_killed']
2425
     
2526
     def get_total_directories(self, obj):
2627
         return obj.nodes.count()
@@ -28,17 +29,23 @@ class FileSystemTreeSerializer(serializers.ModelSerializer):
2829
 
2930
 class GameSessionSerializer(serializers.ModelSerializer):
3031
     tree_name = serializers.CharField(source='tree.name', read_only=True)
32
+    score = serializers.SerializerMethodField()
3133
     
3234
     class Meta:
3335
         model = GameSession
3436
         fields = [
3537
             'id', 'tree', 'tree_name', 'player_name',
3638
             'started_at', 'completed_at', 'commands_used',
37
-            'directories_visited', 'time_taken', 'command_history'
39
+            'directories_visited', 'time_taken', 'command_history',
40
+            'moles_killed', 'mole_stats', 'score'
3841
         ]
3942
         read_only_fields = [
40
-            'started_at', 'completed_at', 'time_taken', 'command_history'
43
+            'started_at', 'completed_at', 'time_taken', 'command_history',
44
+            'mole_stats', 'score'
4145
         ]
46
+    
47
+    def get_score(self, obj):
48
+        return obj.calculate_score()
4249
 
4350
 
4451
 class GameCommandSerializer(serializers.Serializer):
backend/apps/trees/views.pymodified
@@ -94,6 +94,11 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
9494
                     "command": "killall moles",
9595
                     "description": "Eliminate moles when in the same directory",
9696
                     "examples": ["killall moles"]
97
+                },
98
+                {
99
+                    "command": "score",
100
+                    "description": "Show current score and moles killed",
101
+                    "examples": ["score"]
97102
                 }
98103
             ],
99104
             "special_paths": [
@@ -203,12 +208,19 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
203208
                 status=status.HTTP_400_BAD_REQUEST
204209
             )
205210
         
211
+        # Track commands for tree
212
+        tree.total_commands += 1
213
+        tree.save()
214
+        
206215
         # Get session if provided
207216
         session = None
217
+        commands_before_mole = tree.total_commands  # Track for scoring
218
+        
208219
         if session_id:
209220
             try:
210221
                 session = GameSession.objects.get(id=session_id, tree=tree)
211222
                 session.add_command(command)
223
+                commands_before_mole = session.commands_used
212224
             except GameSession.DoesNotExist:
213225
                 pass
214226
         
@@ -220,7 +232,10 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
220232
             'command': command,
221233
             'success': False,
222234
             'output': '',
223
-            'current_path': tree.player_location
235
+            'current_path': tree.player_location,
236
+            'mole_spawned': False,
237
+            'mole_direction': None,
238
+            'score': 0
224239
         }
225240
         
226241
         if cmd == 'cd':
@@ -425,22 +440,58 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
425440
         
426441
         elif cmd == 'killall' and len(parts) > 1 and parts[1] == 'moles':
427442
             if tree.check_win_condition():
428
-                tree.is_completed = True
429
-                tree.completed_at = timezone.now()
443
+                # Track old mole location for stats
444
+                old_mole_location = tree.mole_location
445
+                
446
+                # Update mole kill count
447
+                tree.moles_killed += 1
430448
                 tree.save()
431449
                 
450
+                # Record stats for session
432451
                 if session:
433
-                    session.completed_at = timezone.now()
434
-                    session.time_taken = session.completed_at - session.started_at
435
-                    session.save()
452
+                    # Calculate commands used for this mole
453
+                    commands_for_mole = session.commands_used - commands_before_mole + 1  # +1 for killall
454
+                    
455
+                    # Calculate time (simplified for now - would need to track per-mole start time)
456
+                    time_for_mole = timezone.now() - session.started_at
457
+                    
458
+                    # Calculate distance traveled (simplified - just the path distance)
459
+                    distance = tree.calculate_path_distance(tree.home_directory, old_mole_location)
460
+                    
461
+                    session.record_mole_kill(
462
+                        old_mole_location,
463
+                        commands_for_mole,
464
+                        time_for_mole,
465
+                        distance
466
+                    )
467
+                    response_data['score'] = session.calculate_score()
436468
                 
437
-                response_data['output'] = "🎉 Congratulations! You found and eliminated the mole!"
438
-                response_data['success'] = True
439
-                response_data['game_won'] = True
469
+                # Spawn new mole
470
+                if tree.spawn_new_mole():
471
+                    mole_direction = tree.get_mole_direction()
472
+                    
473
+                    response_data['output'] = f"🎉 You eliminated the mole! (Total moles killed: {tree.moles_killed})\n🐭 A new mole has appeared somewhere in the filesystem!"
474
+                    response_data['success'] = True
475
+                    response_data['mole_spawned'] = True
476
+                    response_data['mole_direction'] = mole_direction
477
+                    response_data['moles_killed'] = tree.moles_killed
478
+                    response_data['new_mole_location'] = tree.mole_location  # Add this!
479
+                else:
480
+                    response_data['output'] = "🎉 You eliminated the mole! Unable to spawn new mole."
481
+                    response_data['success'] = True
440482
             else:
441483
                 response_data['output'] = "No moles found in this directory."
442484
                 response_data['success'] = True
443485
         
486
+        elif cmd == 'score':
487
+            # New command to check current score
488
+            if session:
489
+                response_data['output'] = f"Score: {session.calculate_score()} | Moles killed: {session.moles_killed}"
490
+                response_data['score'] = session.calculate_score()
491
+            else:
492
+                response_data['output'] = "No active session to score."
493
+            response_data['success'] = True
494
+        
444495
         elif cmd == 'help':
445496
             response_data['output'] = """Available commands:
446497
 cd <directory>    - Change directory (supports ~, -, and ..)
@@ -453,6 +504,7 @@ pwd - Print working directory
453504
 echo <text>       - Display text (supports $HOME, $PWD, $OLDPWD)
454505
 tree [-L depth]   - Display directory tree (use -L to limit depth)
455506
 killall moles     - Eliminate moles (when in the same directory)
507
+score             - Show current score and moles killed
456508
 help              - Show this help message
457509
 
458510
 Special paths:
frontend/src/components/Game.tsxmodified
@@ -1,9 +1,8 @@
1
-'use client';
1
+"use client";
22
 
3
-// src/components/Game.tsx
43
 import React, { useState, useEffect, useRef } from 'react';
54
 import TreeVisualizer from './TreeVisualizer';
6
-import { gameApi, FileSystemTree, FHSDirectory, CommandReferenceResponse } from '@/lib/api';
5
+import { gameApi, FileSystemTree, FHSDirectory, CommandReferenceResponse, MoleDirection } from '@/lib/api';
76
 
87
 interface CommandHistoryEntry {
98
   command: string;
@@ -37,6 +36,9 @@ const Game: React.FC = () => {
3736
   const [hasPlayedIntro, setHasPlayedIntro] = useState(false);
3837
   const [isDarkMode, setIsDarkMode] = useState(true);
3938
   const [moleKilled, setMoleKilled] = useState(false);
39
+  const [moleDirection, setMoleDirection] = useState<MoleDirection | null>(null);
40
+  const [score, setScore] = useState(0);
41
+  const [molesKilled, setMolesKilled] = useState(0);
4042
   
4143
   const terminalRef = useRef<HTMLDivElement>(null);
4244
   const inputRef = useRef<HTMLInputElement>(null);
@@ -80,6 +82,11 @@ const Game: React.FC = () => {
8082
         error: null,
8183
       });
8284
       
85
+      // Reset game state
86
+      setScore(0);
87
+      setMolesKilled(0);
88
+      setMoleDirection(null);
89
+      
8390
       // Create a more dynamic starting message based on random location
8491
       const startLocation = response.tree.player_location;
8592
       const homeDir = response.home_directory || '/home';
@@ -183,9 +190,57 @@ const Game: React.FC = () => {
183190
         }));
184191
       }
185192
 
186
-      // Check if game won
187
-      if (response.game_won) {
188
-        // First, show the mole
193
+      // Check if a new mole was spawned
194
+      if (response.mole_spawned) {
195
+        // First, show the killed mole briefly
196
+        setGameState(prev => ({
197
+          ...prev,
198
+          tree: prev.tree ? {
199
+            ...prev.tree,
200
+            tree_data: updateTreeDataToShowMole(prev.tree!.tree_data, prev.tree!.player_location),
201
+          } : null,
202
+        }));
203
+        
204
+        // Trigger falling animation
205
+        setMoleKilled(true);
206
+        
207
+        // After animation, update tree with new mole location
208
+        setTimeout(() => {
209
+          setMoleKilled(false);
210
+          
211
+          // Update tree to show new mole location
212
+          if (response.new_mole_location && gameState.tree) {
213
+            const treeWithNewMole = updateTreeDataToShowMole(
214
+              removeMoleFromTree(gameState.tree.tree_data),
215
+              response.new_mole_location
216
+            );
217
+            
218
+            setGameState(prev => ({
219
+              ...prev,
220
+              tree: prev.tree ? {
221
+                ...prev.tree,
222
+                tree_data: treeWithNewMole,
223
+              } : null,
224
+            }));
225
+          }
226
+          
227
+          // Set new mole direction
228
+          if (response.mole_direction) {
229
+            setMoleDirection(response.mole_direction);
230
+            // Hide direction indicator after 5 seconds
231
+            setTimeout(() => {
232
+              setMoleDirection(null);
233
+            }, 5000);
234
+          }
235
+        }, 1500); // Wait for falling animation
236
+        
237
+        // Update score and moles killed
238
+        if (response.score !== undefined) setScore(response.score);
239
+        if (response.moles_killed !== undefined) setMolesKilled(response.moles_killed);
240
+      }
241
+
242
+      // Legacy: Check if game won (for old backend compatibility)
243
+      if (response.game_won && !response.mole_spawned) {
189244
         setGameState(prev => ({
190245
           ...prev,
191246
           tree: prev.tree ? {
@@ -195,7 +250,6 @@ const Game: React.FC = () => {
195250
           } : null,
196251
         }));
197252
         
198
-        // Then trigger the falling animation after a short delay
199253
         setTimeout(() => {
200254
           setMoleKilled(true);
201255
         }, 200);
@@ -210,11 +264,10 @@ const Game: React.FC = () => {
210264
       }]);
211265
     } finally {
212266
       setExecuting(false);
213
-      // Focus will be restored by useEffect
214267
     }
215268
   };
216269
 
217
-  // Update tree data to show mole when game is won
270
+  // Update tree data to show mole when found
218271
   const updateTreeDataToShowMole = (treeData: any, molePath: string): any => {
219272
     if (treeData.path === molePath) {
220273
       return { ...treeData, has_mole: true };
@@ -230,6 +283,15 @@ const Game: React.FC = () => {
230283
     return treeData;
231284
   };
232285
 
286
+  // Remove mole from tree data
287
+  const removeMoleFromTree = (treeData: any): any => {
288
+    return {
289
+      ...treeData,
290
+      has_mole: false,
291
+      children: treeData.children ? treeData.children.map((child: any) => removeMoleFromTree(child)) : []
292
+    };
293
+  };
294
+
233295
   // Handle node click in visualizer
234296
   const handleNodeClick = (path: string) => {
235297
     executeCommand(`cd ${path}`);
@@ -243,14 +305,33 @@ const Game: React.FC = () => {
243305
   // Mark intro as played after first render
244306
   useEffect(() => {
245307
     if (gameState.tree && !hasPlayedIntro) {
246
-      // Set timeout to mark intro as played after animation completes
247308
       const timer = setTimeout(() => {
248309
         setHasPlayedIntro(true);
249
-      }, 6500); // Total intro duration
310
+      }, 10000); // Add a buffer to ensure animation completes (9.3s + buffer)
250311
       return () => clearTimeout(timer);
251312
     }
252313
   }, [gameState.tree, hasPlayedIntro]);
253314
 
315
+  // Get position for mole direction indicator
316
+  const getMoleIndicatorPosition = (direction: string) => {
317
+    const positions: Record<string, string> = {
318
+      'up': 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-full -mt-8',
319
+      'down': 'bottom-20 left-1/2 -translate-x-1/2',
320
+      'left': 'top-1/2 left-8 -translate-y-1/2',
321
+      'right': 'top-1/2 right-8 -translate-y-1/2',
322
+      'up-left': 'top-20 left-8',
323
+      'up-right': 'top-20 right-8',
324
+      'down-left': 'bottom-20 left-8',
325
+      'down-right': 'bottom-20 right-8',
326
+    };
327
+    return positions[direction] || positions['up'];
328
+  };
329
+
330
+  // Get rotation for arrow based on angle
331
+  const getArrowRotation = (angle: number) => {
332
+    return `rotate(${angle}deg)`;
333
+  };
334
+
254335
   // Terminal color scheme based on dark/light mode
255336
   const terminalColors = isDarkMode ? {
256337
     frame: 'bg-stone-200 border-stone-300',
@@ -327,6 +408,40 @@ const Game: React.FC = () => {
327408
         />
328409
       </div>
329410
 
411
+      {/* Mole Direction Indicator */}
412
+      {moleDirection && (
413
+        <div 
414
+          className={`absolute ${getMoleIndicatorPosition(moleDirection.direction)} z-40 animate-pulse`}
415
+          style={{
416
+            animation: 'pulse 2s ease-in-out infinite, fadeIn 0.5s ease-out'
417
+          }}
418
+        >
419
+          <div className="bg-red-600/90 backdrop-blur-sm border-2 border-red-400 rounded-lg p-3 shadow-2xl flex items-center gap-2">
420
+            <img 
421
+              src="/mole.svg" 
422
+              alt="Mole" 
423
+              className="w-8 h-8"
424
+            />
425
+            <div 
426
+              className="text-white text-2xl"
427
+              style={{ transform: getArrowRotation(moleDirection.angle) }}
428
+            >
429
+              →
430
+            </div>
431
+          </div>
432
+        </div>
433
+      )}
434
+
435
+      {/* Score Display - Top Right */}
436
+      {molesKilled > 0 && (
437
+        <div className="absolute top-4 right-4 bg-black/80 backdrop-blur-sm border border-green-500 rounded-lg p-3 shadow-2xl z-30">
438
+          <div className="text-green-400 font-terminal text-sm">
439
+            <div>Score: {score}</div>
440
+            <div>Moles: {molesKilled}</div>
441
+          </div>
442
+        </div>
443
+      )}
444
+
330445
       {/* Floating Terminal - Top Left */}
331446
       <div className={`absolute top-4 left-4 ${terminalColors.frame} rounded-lg shadow-2xl border transition-all duration-300 z-30 ${
332447
         terminalMinimized ? 'w-80' : 'w-[700px]'
@@ -342,17 +457,13 @@ const Game: React.FC = () => {
342457
               >
343458
                 <span className="text-[8px] font-bold text-gray-900 absolute">×</span>
344459
               </button>
345
-              {gameState.tree && !gameState.tree.is_completed ? (
346
-                <button
347
-                  onClick={getHints}
348
-                  className="w-3.5 h-3.5 bg-yellow-500 hover:bg-yellow-400 rounded-full flex items-center justify-center transition-colors relative"
349
-                  title="Get Hint"
350
-                >
351
-                  <span className="text-[9px] font-bold text-gray-900 absolute">?</span>
352
-                </button>
353
-              ) : (
354
-                <div className="w-3.5 h-3.5 bg-yellow-500 rounded-full"></div>
355
-              )}
460
+              <button
461
+                onClick={getHints}
462
+                className="w-3.5 h-3.5 bg-yellow-500 hover:bg-yellow-400 rounded-full flex items-center justify-center transition-colors relative"
463
+                title="Get Hint"
464
+              >
465
+                <span className="text-[9px] font-bold text-gray-900 absolute">?</span>
466
+              </button>
356467
               <button
357468
                 onClick={getFHSReference}
358469
                 className="w-3.5 h-3.5 bg-green-500 hover:bg-green-400 rounded-full flex items-center justify-center transition-colors relative"
@@ -427,7 +538,7 @@ const Game: React.FC = () => {
427538
                         executeCommand(command);
428539
                       }
429540
                     }}
430
-                    disabled={executing || gameState.tree?.is_completed}
541
+                    disabled={executing}
431542
                     className="absolute inset-0 w-full bg-transparent text-transparent outline-none caret-transparent font-terminal"
432543
                     placeholder=""
433544
                     autoFocus
@@ -443,7 +554,7 @@ const Game: React.FC = () => {
443554
         )}
444555
       </div>
445556
 
446
-      {/* Hints Popup - Now positioned relative to terminal */}
557
+      {/* Hints Popup */}
447558
       {showHints && hints.length > 0 && (
448559
         <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">
449560
           <button
@@ -600,7 +711,7 @@ const Game: React.FC = () => {
600711
         </div>
601712
       )}
602713
 
603
-      {/* Bottom Game Bar - Updated with blue shade */}
714
+      {/* Bottom Game Bar */}
604715
       <div className={`absolute bottom-0 left-0 right-0 ${isDarkMode ? 'bg-slate-800/90' : 'bg-blue-50/90'} backdrop-blur-sm border-t ${isDarkMode ? 'border-slate-700' : 'border-blue-200'} p-3 z-20`}>
605716
         <div className="max-w-7xl mx-auto flex justify-between items-center px-4">
606717
           <div className="flex items-center gap-4">
@@ -608,15 +719,9 @@ const Game: React.FC = () => {
608719
           </div>
609720
           
610721
           <div className="flex items-center gap-3">
611
-            {gameState.tree.is_completed ? (
612
-              <div className="text-green-600 dark:text-green-400 font-bold animate-pulse">
613
-                You found a mole!
614
-              </div>
615
-            ) : (
616
-              <div className={`text-xs ${isDarkMode ? 'text-slate-400' : 'text-blue-700'}`}>
617
-                click adjacent nodes or use the terminal
618
-              </div>
619
-            )}
722
+            <div className={`text-xs ${isDarkMode ? 'text-slate-400' : 'text-blue-700'}`}>
723
+              click adjacent nodes or use the terminal
724
+            </div>
620725
             <button
621726
               onClick={startNewGame}
622727
               className={`px-3 py-1.5 ${isDarkMode ? 'bg-slate-700 hover:bg-slate-600' : 'bg-blue-200 hover:bg-blue-300'} ${isDarkMode ? 'text-white' : 'text-blue-900'} text-sm rounded transition`}
@@ -626,6 +731,18 @@ const Game: React.FC = () => {
626731
           </div>
627732
         </div>
628733
       </div>
734
+
735
+      {/* Custom styles for animations */}
736
+      <style jsx>{`
737
+        @keyframes fadeIn {
738
+          from { opacity: 0; transform: scale(0.8); }
739
+          to { opacity: 1; transform: scale(1); }
740
+        }
741
+        @keyframes pulse {
742
+          0%, 100% { opacity: 0.9; transform: scale(1); }
743
+          50% { opacity: 1; transform: scale(1.05); }
744
+        }
745
+      `}</style>
629746
     </div>
630747
   );
631748
 };
frontend/src/lib/api.tsmodified
@@ -29,6 +29,14 @@ export interface FileSystemTree {
2929
   completed_at: string | null;
3030
   tree_data: TreeNode;
3131
   total_directories: number;
32
+  moles_killed?: number;
33
+  total_commands?: number;
34
+  total_directories_visited?: number;
35
+}
36
+
37
+export interface MoleDirection {
38
+  direction: string;
39
+  angle: number;
3240
 }
3341
 
3442
 export interface CommandResponse {
@@ -37,6 +45,11 @@ export interface CommandResponse {
3745
   output: string;
3846
   current_path: string;
3947
   game_won?: boolean;
48
+  mole_spawned?: boolean;
49
+  mole_direction?: MoleDirection | null;
50
+  score?: number;
51
+  moles_killed?: number;
52
+  new_mole_location?: string;
4053
 }
4154
 
4255
 export interface GameCreationResponse {