zeroed-some/bashamole / 5f22d92

Browse files

backend progression, timer, api

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
5f22d926796ab399b9dc36a58fa8e3ffacb1fc7d
Parents
6ea57e5
Tree
9bd94fd

3 changed files

StatusFile+-
A backend/apps/trees/migrations/0006_filesystemtree_current_mole_timer_and_more.py 53 0
M backend/apps/trees/models.py 143 12
M backend/apps/trees/views.py 136 5
backend/apps/trees/migrations/0006_filesystemtree_current_mole_timer_and_more.pyadded
@@ -0,0 +1,53 @@
1
+# Generated by Django 5.2.3 on 2025-06-16 02:57
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ("trees", "0005_filesystemtree_moles_killed_and_more"),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.AddField(
14
+            model_name="filesystemtree",
15
+            name="current_mole_timer",
16
+            field=models.IntegerField(default=60),
17
+        ),
18
+        migrations.AddField(
19
+            model_name="filesystemtree",
20
+            name="default_mole_timer",
21
+            field=models.IntegerField(default=60),
22
+        ),
23
+        migrations.AddField(
24
+            model_name="filesystemtree",
25
+            name="mole_spawned_at",
26
+            field=models.DateTimeField(blank=True, null=True),
27
+        ),
28
+        migrations.AddField(
29
+            model_name="filesystemtree",
30
+            name="timer_expired_count",
31
+            field=models.IntegerField(default=0),
32
+        ),
33
+        migrations.AddField(
34
+            model_name="filesystemtree",
35
+            name="timer_paused",
36
+            field=models.BooleanField(default=False),
37
+        ),
38
+        migrations.AddField(
39
+            model_name="gamesession",
40
+            name="average_kill_time",
41
+            field=models.FloatField(blank=True, null=True),
42
+        ),
43
+        migrations.AddField(
44
+            model_name="gamesession",
45
+            name="fastest_kill_time",
46
+            field=models.FloatField(blank=True, null=True),
47
+        ),
48
+        migrations.AddField(
49
+            model_name="gamesession",
50
+            name="moles_escaped",
51
+            field=models.IntegerField(default=0),
52
+        ),
53
+    ]
backend/apps/trees/models.pymodified
@@ -28,6 +28,17 @@ class FileSystemTree(models.Model):
2828
     total_commands = models.IntegerField(default=0)
2929
     total_directories_visited = models.IntegerField(default=0)
3030
     
31
+    # Timer configuration
32
+    default_mole_timer = models.IntegerField(default=60)  # seconds (will be calculated dynamically)
33
+    current_mole_timer = models.IntegerField(default=60)  # seconds remaining
34
+    
35
+    # Mole spawn timestamp
36
+    mole_spawned_at = models.DateTimeField(null=True, blank=True)
37
+    
38
+    # Timer state
39
+    timer_paused = models.BooleanField(default=False)
40
+    timer_expired_count = models.IntegerField(default=0)  # Track escapes
41
+    
3142
     # Cached tree structure
3243
     tree_data = models.JSONField(null=True, blank=True)
3344
     
@@ -208,8 +219,52 @@ class FileSystemTree(models.Model):
208219
             mole_dir = random.choice(candidates)
209220
             self.mole_location = mole_dir.path
210221
             
222
+            # Calculate initial timer based on starting position
223
+            timer, reason, distance = self.calculate_mole_timer(
224
+                self.mole_location,
225
+                self.player_location if self.player_location else "/home"
226
+            )
227
+            
228
+            # Set initial timer
229
+            self.mole_spawned_at = timezone.now()
230
+            self.default_mole_timer = timer
231
+            self.current_mole_timer = timer
232
+    
233
+    def calculate_mole_timer(self, mole_path, player_path):
234
+        """Calculate timer based on distance between player and mole"""
235
+        distance = self.calculate_path_distance(player_path, mole_path)
236
+        
237
+        # Base timer values based on distance - MORE AGGRESSIVE
238
+        if distance <= 1:
239
+            base_timer = 10  # Was 20 - Adjacent mole needs quick action!
240
+            timer_reason = "nearby"
241
+        elif distance <= 3:
242
+            base_timer = 20  # Was 30 - Still close, needs urgency
243
+            timer_reason = "close"
244
+        elif distance <= 5:
245
+            base_timer = 35  # Was 45 - Moderate distance
246
+            timer_reason = "moderate distance"
247
+        else:
248
+            base_timer = 50  # Was 60 - Far away
249
+            timer_reason = "far away"
250
+        
251
+        # Apply difficulty progression (10% reduction per 5 moles, capped at 50% reduction)
252
+        difficulty_multiplier = 1.0
253
+        if self.moles_killed >= 5:
254
+            # Calculate 10% reduction per 5 moles
255
+            reductions = min(self.moles_killed // 5, 5)  # Cap at 5 reductions (50%)
256
+            difficulty_multiplier = 1.0 - (0.1 * reductions)
257
+        
258
+        # Calculate final timer
259
+        final_timer = int(base_timer * difficulty_multiplier)
260
+        
261
+        # Ensure minimum timer of 10 seconds
262
+        final_timer = max(final_timer, 10)
263
+        
264
+        return final_timer, timer_reason, distance
265
+    
211266
     def spawn_new_mole(self):
212
-        """Spawn a new mole after the current one is killed"""
267
+        """Spawn a new mole after the current one is killed or escapes"""
213268
         # Get all possible spawn locations (any directory)
214269
         all_directories = DirectoryNode.objects.filter(tree=self).exclude(path="/")
215270
         
@@ -218,12 +273,57 @@ class FileSystemTree(models.Model):
218273
             new_mole_dir = random.choice(all_directories)
219274
             self.mole_location = new_mole_dir.path
220275
             
276
+            # Calculate smart timer based on distance
277
+            timer, reason, distance = self.calculate_mole_timer(
278
+                self.mole_location, 
279
+                self.player_location
280
+            )
281
+            
282
+            # Set timer for new mole
283
+            self.mole_spawned_at = timezone.now()
284
+            self.default_mole_timer = timer
285
+            self.current_mole_timer = timer
286
+            self.timer_paused = False
287
+            
221288
             # Update the cached tree data
222289
             self.cache_tree()
223290
             self.save()
224291
             
225
-            return True
226
-        return False
292
+            return True, timer, reason, distance
293
+        return False, None, None, None
294
+    
295
+    def check_mole_timer(self):
296
+        """Check if mole timer has expired"""
297
+        if not self.mole_spawned_at or self.timer_paused:
298
+            return False, self.current_mole_timer
299
+            
300
+        elapsed = (timezone.now() - self.mole_spawned_at).total_seconds()
301
+        remaining = max(0, self.default_mole_timer - elapsed)
302
+        
303
+        if remaining <= 0:
304
+            # Mole escaped!
305
+            return True, 0
306
+        
307
+        return False, int(remaining)
308
+    
309
+    def handle_mole_escape(self):
310
+        """Handle when a mole escapes due to timer expiration"""
311
+        self.timer_expired_count += 1
312
+        old_location = self.mole_location
313
+        
314
+        # Spawn new mole
315
+        success, timer, reason, distance = self.spawn_new_mole()
316
+        if success:
317
+            return {
318
+                'escaped': True,
319
+                'old_location': old_location,
320
+                'new_location': self.mole_location,
321
+                'total_escapes': self.timer_expired_count,
322
+                'new_timer': timer,
323
+                'timer_reason': reason,
324
+                'distance': distance
325
+            }
326
+        return None
227327
     
228328
     def get_mole_direction(self):
229329
         """Get the relative direction from player to mole in the tree structure"""
@@ -536,6 +636,11 @@ class GameSession(models.Model):
536636
     time_taken = models.DurationField(null=True, blank=True)
537637
     moles_killed = models.IntegerField(default=0)
538638
     
639
+    # Timer stats
640
+    moles_escaped = models.IntegerField(default=0)
641
+    fastest_kill_time = models.FloatField(null=True, blank=True)  # seconds
642
+    average_kill_time = models.FloatField(null=True, blank=True)  # seconds
643
+    
539644
     # Per-mole tracking
540645
     mole_stats = models.JSONField(default=list)  # List of {mole_number, location, commands, time, distance}
541646
     
@@ -555,7 +660,25 @@ class GameSession(models.Model):
555660
         self.save()
556661
     
557662
     def record_mole_kill(self, mole_location, commands_for_mole, time_for_mole, distance_traveled):
558
-        """Record statistics for a mole kill"""
663
+        """Updated to track timer performance"""
664
+        # Calculate actual time to kill (not total session time)
665
+        if self.tree.mole_spawned_at:
666
+            actual_kill_time = (timezone.now() - self.tree.mole_spawned_at).total_seconds()
667
+            
668
+            # Update fastest time
669
+            if self.fastest_kill_time is None or actual_kill_time < self.fastest_kill_time:
670
+                self.fastest_kill_time = actual_kill_time
671
+            
672
+            # Update average (simple running average)
673
+            if self.average_kill_time is None:
674
+                self.average_kill_time = actual_kill_time
675
+            else:
676
+                total_kills = self.moles_killed + 1
677
+                self.average_kill_time = (
678
+                    (self.average_kill_time * self.moles_killed + actual_kill_time) 
679
+                    / total_kills
680
+                )
681
+        
559682
         self.moles_killed += 1
560683
         self.mole_stats.append({
561684
             'mole_number': self.moles_killed,
@@ -567,26 +690,34 @@ class GameSession(models.Model):
567690
         self.save()
568691
     
569692
     def calculate_score(self):
570
-        """Calculate a score based on performance"""
693
+        """Updated scoring with timer bonuses"""
571694
         if self.moles_killed == 0:
572695
             return 0
573696
         
574
-        # Base score per mole
575697
         base_score = 1000
698
+        score = self.moles_killed * base_score
576699
         
577700
         # Calculate average performance
578701
         total_commands = sum(stat['commands'] for stat in self.mole_stats)
579702
         avg_commands = total_commands / self.moles_killed if self.moles_killed > 0 else 0
580703
         
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
-        
587704
         # Efficiency bonus (fewer commands is better)
588705
         if avg_commands > 0:
589706
             efficiency_multiplier = max(0.5, min(2.0, 10 / avg_commands))
590707
             score *= efficiency_multiplier
591708
         
709
+        # Timer bonus - faster kills = more points
710
+        if self.average_kill_time and self.tree.default_mole_timer:
711
+            # Bonus for beating the timer by a good margin
712
+            time_ratio = self.average_kill_time / self.tree.default_mole_timer
713
+            if time_ratio < 0.5:  # Killed in less than half the time
714
+                score *= 1.5
715
+            elif time_ratio < 0.75:  # Killed in less than 3/4 time
716
+                score *= 1.25
717
+        
718
+        # Penalty for escapes
719
+        if self.moles_escaped > 0:
720
+            escape_penalty = 0.9 ** self.moles_escaped  # 10% penalty per escape
721
+            score *= escape_penalty
722
+        
592723
         return int(score)
backend/apps/trees/views.pymodified
@@ -157,6 +157,20 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
157157
         tree = FileSystemTree.objects.create(name=tree_name)
158158
         tree.generate_tree(max_depth=max_depth, directories_per_level=dirs_per_level)
159159
         
160
+        # Get initial timer info
161
+        initial_timer = tree.default_mole_timer
162
+        timer_distance = tree.calculate_path_distance(tree.player_location, tree.mole_location)
163
+        
164
+        # Determine timer reason
165
+        if timer_distance <= 1:
166
+            timer_reason = "nearby"
167
+        elif timer_distance <= 3:
168
+            timer_reason = "close"
169
+        elif timer_distance <= 5:
170
+            timer_reason = "moderate distance"
171
+        else:
172
+            timer_reason = "far away"
173
+        
160174
         # Create game session
161175
         player_name = request.data.get('player_name', 'Anonymous')
162176
         session = GameSession.objects.create(
@@ -169,7 +183,10 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
169183
             'tree': serializer.data,
170184
             'session_id': session.id,
171185
             'mole_hint': f"The mole is hiding somewhere in the filesystem!",
172
-            'home_directory': tree.home_directory
186
+            'home_directory': tree.home_directory,
187
+            'initial_timer': initial_timer,
188
+            'timer_reason': timer_reason,
189
+            'timer_distance': timer_distance
173190
         }, status=status.HTTP_201_CREATED)
174191
     
175192
     @action(detail=True, methods=['get'])
@@ -195,6 +212,80 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
195212
                 status=status.HTTP_404_NOT_FOUND
196213
             )
197214
     
215
+    @action(detail=True, methods=['get'])
216
+    def check_timer(self, request, pk=None):
217
+        """Check the current mole timer status"""
218
+        tree = self.get_object()
219
+        
220
+        # Check if timer expired
221
+        expired, remaining = tree.check_mole_timer()
222
+        
223
+        response_data = {
224
+            'timer_remaining': remaining,
225
+            'timer_expired': expired,
226
+            'mole_location': tree.mole_location,
227
+            'timer_paused': tree.timer_paused
228
+        }
229
+        
230
+        # Handle escape if timer expired
231
+        if expired:
232
+            escape_data = tree.handle_mole_escape()
233
+            if escape_data:
234
+                
235
+                mole_direction = tree.get_mole_direction()
236
+
237
+                escape_data['mole_direction'] = mole_direction
238
+
239
+                response_data.update({
240
+                    'mole_escaped': True,
241
+                    'escape_data': escape_data,
242
+                    'mole_direction': mole_direction,  # Add this line
243
+                    'message': f"The mole escaped from {escape_data['old_location']}! A new mole appeared!"
244
+                })
245
+
246
+                # Update session stats
247
+                session_id = request.query_params.get('session_id')
248
+                if session_id:
249
+                    try:
250
+                        session = GameSession.objects.get(id=session_id, tree=tree)
251
+                        session.moles_escaped += 1
252
+                        session.save()
253
+                    except GameSession.DoesNotExist:
254
+                        pass
255
+                
256
+                response_data.update({
257
+                    'mole_escaped': True,
258
+                    'escape_data': escape_data,
259
+                    'message': f"The mole escaped from {escape_data['old_location']}! A new mole appeared!"
260
+                })
261
+        
262
+        return Response(response_data)
263
+    
264
+    @action(detail=True, methods=['get'])
265
+    def timer_status(self, request, pk=None):
266
+        """Get detailed timer status for UI updates"""
267
+        tree = self.get_object()
268
+        expired, remaining = tree.check_mole_timer()
269
+        
270
+        # Calculate warning level
271
+        warning_level = None
272
+        if not expired and remaining > 0:
273
+            if remaining <= 5:
274
+                warning_level = 'critical'
275
+            elif remaining <= 15:
276
+                warning_level = 'alert'
277
+            elif remaining <= 30:
278
+                warning_level = 'warning'
279
+        
280
+        return Response({
281
+            'remaining': remaining,
282
+            'total': tree.default_mole_timer,
283
+            'percentage': (remaining / tree.default_mole_timer * 100) if tree.default_mole_timer > 0 else 0,
284
+            'warning_level': warning_level,
285
+            'expired': expired,
286
+            'paused': tree.timer_paused
287
+        })
288
+    
198289
     @action(detail=True, methods=['post'])
199290
     def execute_command(self, request, pk=None):
200291
         """Execute a shell command in the game"""
@@ -224,6 +315,9 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
224315
             except GameSession.DoesNotExist:
225316
                 pass
226317
         
318
+        # Check timer before executing command
319
+        expired, remaining = tree.check_mole_timer()
320
+        
227321
         # Parse and execute command
228322
         parts = command.split()
229323
         cmd = parts[0] if parts else ""
@@ -235,9 +329,32 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
235329
             'current_path': tree.player_location,
236330
             'mole_spawned': False,
237331
             'mole_direction': None,
238
-            'score': 0
332
+            'score': 0,
333
+            'timer_remaining': remaining,
334
+            'timer_warnings': [],
335
+            'new_timer': None,
336
+            'timer_reason': None,
337
+            'timer_distance': None
239338
         }
240339
         
340
+        # Add timer warnings to output
341
+        if not expired and remaining > 0:
342
+            if remaining <= 5:
343
+                response_data['timer_warnings'].append({
344
+                    'level': 'CRITICAL',
345
+                    'message': f'Mole escaping from {tree.mole_location}! ({remaining}s remaining)'
346
+                })
347
+            elif remaining <= 15:
348
+                response_data['timer_warnings'].append({
349
+                    'level': 'ALERT',
350
+                    'message': f'Mole burrowing soon! ({remaining}s remaining)'
351
+                })
352
+            elif remaining <= 30:
353
+                response_data['timer_warnings'].append({
354
+                    'level': 'WARNING',
355
+                    'message': f'Mole detected at {tree.mole_location}! ({remaining}s remaining)'
356
+                })
357
+        
241358
         if cmd == 'cd':
242359
             if len(parts) < 2:
243360
                 # cd with no args goes to home directory
@@ -440,6 +557,9 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
440557
         
441558
         elif cmd == 'killall' and len(parts) > 1 and parts[1] == 'moles':
442559
             if tree.check_win_condition():
560
+                # Check if killed before timer expired
561
+                expired, remaining = tree.check_mole_timer()
562
+                
443563
                 # Track old mole location for stats
444564
                 old_mole_location = tree.mole_location
445565
                 
@@ -467,15 +587,26 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
467587
                     response_data['score'] = session.calculate_score()
468588
                 
469589
                 # Spawn new mole
470
-                if tree.spawn_new_mole():
590
+                success, new_timer, timer_reason, timer_distance = tree.spawn_new_mole()
591
+                if success:
471592
                     mole_direction = tree.get_mole_direction()
472593
                     
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!"
594
+                    # Include timer info in message
595
+                    timer_msg = f"Timer: {new_timer}s (mole is {timer_reason})"
596
+                    
597
+                    if not expired:
598
+                        response_data['output'] = f"🎉 You eliminated the mole with {remaining}s to spare! (Total moles killed: {tree.moles_killed})\n🐭 A new mole has appeared! {timer_msg}"
599
+                    else:
600
+                        response_data['output'] = f"🎉 You eliminated the mole! (Total moles killed: {tree.moles_killed})\n🐭 A new mole has appeared! {timer_msg}"
601
+                    
474602
                     response_data['success'] = True
475603
                     response_data['mole_spawned'] = True
476604
                     response_data['mole_direction'] = mole_direction
477605
                     response_data['moles_killed'] = tree.moles_killed
478
-                    response_data['new_mole_location'] = tree.mole_location  # Add this!
606
+                    response_data['new_mole_location'] = tree.mole_location
607
+                    response_data['new_timer'] = new_timer
608
+                    response_data['timer_reason'] = timer_reason
609
+                    response_data['timer_distance'] = timer_distance
479610
                 else:
480611
                     response_data['output'] = "🎉 You eliminated the mole! Unable to spawn new mole."
481612
                     response_data['success'] = True