backend progression, timer, api
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
5f22d926796ab399b9dc36a58fa8e3ffacb1fc7d- Parents
-
6ea57e5 - Tree
9bd94fd
5f22d92
5f22d926796ab399b9dc36a58fa8e3ffacb1fc7d6ea57e5
9bd94fd| Status | File | + | - |
|---|---|---|---|
| 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): | ||
| 28 | 28 | total_commands = models.IntegerField(default=0) |
| 29 | 29 | total_directories_visited = models.IntegerField(default=0) |
| 30 | 30 | |
| 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 | + | |
| 31 | 42 | # Cached tree structure |
| 32 | 43 | tree_data = models.JSONField(null=True, blank=True) |
| 33 | 44 | |
@@ -208,8 +219,52 @@ class FileSystemTree(models.Model): | ||
| 208 | 219 | mole_dir = random.choice(candidates) |
| 209 | 220 | self.mole_location = mole_dir.path |
| 210 | 221 | |
| 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 | + | |
| 211 | 266 | 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""" | |
| 213 | 268 | # Get all possible spawn locations (any directory) |
| 214 | 269 | all_directories = DirectoryNode.objects.filter(tree=self).exclude(path="/") |
| 215 | 270 | |
@@ -218,12 +273,57 @@ class FileSystemTree(models.Model): | ||
| 218 | 273 | new_mole_dir = random.choice(all_directories) |
| 219 | 274 | self.mole_location = new_mole_dir.path |
| 220 | 275 | |
| 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 | + | |
| 221 | 288 | # Update the cached tree data |
| 222 | 289 | self.cache_tree() |
| 223 | 290 | self.save() |
| 224 | 291 | |
| 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 | |
| 227 | 327 | |
| 228 | 328 | def get_mole_direction(self): |
| 229 | 329 | """Get the relative direction from player to mole in the tree structure""" |
@@ -536,6 +636,11 @@ class GameSession(models.Model): | ||
| 536 | 636 | time_taken = models.DurationField(null=True, blank=True) |
| 537 | 637 | moles_killed = models.IntegerField(default=0) |
| 538 | 638 | |
| 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 | + | |
| 539 | 644 | # Per-mole tracking |
| 540 | 645 | mole_stats = models.JSONField(default=list) # List of {mole_number, location, commands, time, distance} |
| 541 | 646 | |
@@ -555,7 +660,25 @@ class GameSession(models.Model): | ||
| 555 | 660 | self.save() |
| 556 | 661 | |
| 557 | 662 | 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 | + | |
| 559 | 682 | self.moles_killed += 1 |
| 560 | 683 | self.mole_stats.append({ |
| 561 | 684 | 'mole_number': self.moles_killed, |
@@ -567,26 +690,34 @@ class GameSession(models.Model): | ||
| 567 | 690 | self.save() |
| 568 | 691 | |
| 569 | 692 | def calculate_score(self): |
| 570 | - """Calculate a score based on performance""" | |
| 693 | + """Updated scoring with timer bonuses""" | |
| 571 | 694 | if self.moles_killed == 0: |
| 572 | 695 | return 0 |
| 573 | 696 | |
| 574 | - # Base score per mole | |
| 575 | 697 | base_score = 1000 |
| 698 | + score = self.moles_killed * base_score | |
| 576 | 699 | |
| 577 | 700 | # Calculate average performance |
| 578 | 701 | total_commands = sum(stat['commands'] for stat in self.mole_stats) |
| 579 | 702 | avg_commands = total_commands / self.moles_killed if self.moles_killed > 0 else 0 |
| 580 | 703 | |
| 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 | 704 | # Efficiency bonus (fewer commands is better) |
| 588 | 705 | if avg_commands > 0: |
| 589 | 706 | efficiency_multiplier = max(0.5, min(2.0, 10 / avg_commands)) |
| 590 | 707 | score *= efficiency_multiplier |
| 591 | 708 | |
| 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 | + | |
| 592 | 723 | return int(score) |
backend/apps/trees/views.pymodified@@ -157,6 +157,20 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet): | ||
| 157 | 157 | tree = FileSystemTree.objects.create(name=tree_name) |
| 158 | 158 | tree.generate_tree(max_depth=max_depth, directories_per_level=dirs_per_level) |
| 159 | 159 | |
| 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 | + | |
| 160 | 174 | # Create game session |
| 161 | 175 | player_name = request.data.get('player_name', 'Anonymous') |
| 162 | 176 | session = GameSession.objects.create( |
@@ -169,7 +183,10 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet): | ||
| 169 | 183 | 'tree': serializer.data, |
| 170 | 184 | 'session_id': session.id, |
| 171 | 185 | '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 | |
| 173 | 190 | }, status=status.HTTP_201_CREATED) |
| 174 | 191 | |
| 175 | 192 | @action(detail=True, methods=['get']) |
@@ -195,6 +212,80 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet): | ||
| 195 | 212 | status=status.HTTP_404_NOT_FOUND |
| 196 | 213 | ) |
| 197 | 214 | |
| 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 | + | |
| 198 | 289 | @action(detail=True, methods=['post']) |
| 199 | 290 | def execute_command(self, request, pk=None): |
| 200 | 291 | """Execute a shell command in the game""" |
@@ -224,6 +315,9 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet): | ||
| 224 | 315 | except GameSession.DoesNotExist: |
| 225 | 316 | pass |
| 226 | 317 | |
| 318 | + # Check timer before executing command | |
| 319 | + expired, remaining = tree.check_mole_timer() | |
| 320 | + | |
| 227 | 321 | # Parse and execute command |
| 228 | 322 | parts = command.split() |
| 229 | 323 | cmd = parts[0] if parts else "" |
@@ -235,9 +329,32 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet): | ||
| 235 | 329 | 'current_path': tree.player_location, |
| 236 | 330 | 'mole_spawned': False, |
| 237 | 331 | '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 | |
| 239 | 338 | } |
| 240 | 339 | |
| 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 | + | |
| 241 | 358 | if cmd == 'cd': |
| 242 | 359 | if len(parts) < 2: |
| 243 | 360 | # cd with no args goes to home directory |
@@ -440,6 +557,9 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet): | ||
| 440 | 557 | |
| 441 | 558 | elif cmd == 'killall' and len(parts) > 1 and parts[1] == 'moles': |
| 442 | 559 | if tree.check_win_condition(): |
| 560 | + # Check if killed before timer expired | |
| 561 | + expired, remaining = tree.check_mole_timer() | |
| 562 | + | |
| 443 | 563 | # Track old mole location for stats |
| 444 | 564 | old_mole_location = tree.mole_location |
| 445 | 565 | |
@@ -467,15 +587,26 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet): | ||
| 467 | 587 | response_data['score'] = session.calculate_score() |
| 468 | 588 | |
| 469 | 589 | # Spawn new mole |
| 470 | - if tree.spawn_new_mole(): | |
| 590 | + success, new_timer, timer_reason, timer_distance = tree.spawn_new_mole() | |
| 591 | + if success: | |
| 471 | 592 | mole_direction = tree.get_mole_direction() |
| 472 | 593 | |
| 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 | + | |
| 474 | 602 | response_data['success'] = True |
| 475 | 603 | response_data['mole_spawned'] = True |
| 476 | 604 | response_data['mole_direction'] = mole_direction |
| 477 | 605 | 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 | |
| 479 | 610 | else: |
| 480 | 611 | response_data['output'] = "🎉 You eliminated the mole! Unable to spawn new mole." |
| 481 | 612 | response_data['success'] = True |