zeroed-some/bashamole / 5b10e6b

Browse files

backend leaderboard things

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
5b10e6b52205b88342c49f10f8d081d45267b2cb
Parents
3bc2376
Tree
4913b7a

2 changed files

StatusFile+-
M backend/apps/trees/models.py 13 2
M backend/apps/trees/views.py 103 11
backend/apps/trees/models.pymodified
@@ -45,6 +45,15 @@ class FileSystemTree(models.Model):
4545
     def __str__(self):
4646
         return f"{self.name} - {'Completed' if self.is_completed else 'Active'}"
4747
     
48
+    def complete_game(self):
49
+        """Mark the game as completed and record completion time"""
50
+        if not self.is_completed:
51
+            self.is_completed = True
52
+            self.completed_at = timezone.now()
53
+            self.save()
54
+            return True
55
+        return False
56
+    
4857
     def generate_tree(self, max_depth=5, directories_per_level=3):
4958
         """Generate a procedural Unix filesystem tree"""
5059
         if self.seed == 0:
@@ -483,6 +492,7 @@ class FileSystemTree(models.Model):
483492
     
484493
     def resolve_path(self, path):
485494
         """Resolve a path that may contain ~ or be relative"""
495
+        # Strip trailing slashes first (except for root)
486496
         if path.endswith('/') and path != '/':
487497
             path = path.rstrip('/')
488498
         
@@ -504,6 +514,7 @@ class FileSystemTree(models.Model):
504514
     
505515
     def normalize_path(self, path):
506516
         """Normalize a path by resolving .. and . components"""
517
+        # Strip trailing slashes first (except for root)
507518
         if path.endswith('/') and path != '/':
508519
             path = path.rstrip('/')
509520
         
backend/apps/trees/views.pymodified
@@ -1,5 +1,6 @@
11
 # apps/trees/views.py
22
 from django.http import HttpResponse
3
+from django.db import models
34
 from rest_framework import viewsets, status
45
 from rest_framework.decorators import action
56
 from rest_framework.response import Response
@@ -99,6 +100,11 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
99100
                     "command": "score",
100101
                     "description": "Show current score and moles killed",
101102
                     "examples": ["score"]
103
+                },
104
+                {
105
+                    "command": "exit",
106
+                    "description": "End the game and save your score",
107
+                    "examples": ["exit"]
102108
                 }
103109
             ],
104110
             "special_paths": [
@@ -239,7 +245,7 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
239245
                 response_data.update({
240246
                     'mole_escaped': True,
241247
                     'escape_data': escape_data,
242
-                    'mole_direction': mole_direction,  # Add this line
248
+                    'mole_direction': mole_direction,
243249
                     'message': f"The mole escaped from {escape_data['old_location']}! A new mole appeared!"
244250
                 })
245251
 
@@ -253,12 +259,6 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
253259
                     except GameSession.DoesNotExist:
254260
                         pass
255261
         
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
-        
262262
         return Response(response_data)
263263
     
264264
     @action(detail=True, methods=['get'])
@@ -632,6 +632,34 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
632632
                 response_data['output'] = "No active session to score."
633633
             response_data['success'] = True
634634
         
635
+        elif cmd == 'exit':
636
+            # Complete the game
637
+            tree.complete_game()
638
+            
639
+            # Complete the session if it exists
640
+            if session:
641
+                session.completed_at = timezone.now()
642
+                session.time_taken = session.completed_at - session.started_at
643
+                session.save()
644
+                
645
+                response_data['output'] = f"Game Over! Final score: {session.calculate_score()} | Moles killed: {session.moles_killed}"
646
+                response_data['score'] = session.calculate_score()
647
+                response_data['game_completed'] = True
648
+                response_data['session_completed'] = True
649
+                response_data['final_stats'] = {
650
+                    'score': session.calculate_score(),
651
+                    'moles_killed': session.moles_killed,
652
+                    'moles_escaped': session.moles_escaped,
653
+                    'commands_used': session.commands_used,
654
+                    'time_taken': str(session.time_taken),
655
+                    'directories_visited': session.directories_visited
656
+                }
657
+            else:
658
+                response_data['output'] = "Game ended. No session to record."
659
+                response_data['game_completed'] = True
660
+            
661
+            response_data['success'] = True
662
+        
635663
         elif cmd == 'help':
636664
             response_data['output'] = """Available commands:
637665
 cd <directory>    - Change directory (supports ~, -, and ..)
@@ -645,6 +673,7 @@ echo <text> - Display text (supports $HOME, $PWD, $OLDPWD)
645673
 tree [-L depth]   - Display directory tree (use -L to limit depth)
646674
 killall moles     - Eliminate moles (when in the same directory)
647675
 score             - Show current score and moles killed
676
+exit              - End the game and save your score
648677
 help              - Show this help message
649678
 
650679
 Special paths:
@@ -715,12 +744,75 @@ class GameSessionViewSet(viewsets.ModelViewSet):
715744
     queryset = GameSession.objects.all()
716745
     serializer_class = GameSessionSerializer
717746
     
747
+    @action(detail=False, methods=['post'])
748
+    def save_player_name(self, request):
749
+        """Save player name for a completed session"""
750
+        session_id = request.data.get('session_id')
751
+        player_name = request.data.get('player_name', 'Anonymous')
752
+        
753
+        if not session_id:
754
+            return Response(
755
+                {'error': 'No session ID provided'}, 
756
+                status=status.HTTP_400_BAD_REQUEST
757
+            )
758
+        
759
+        try:
760
+            session = GameSession.objects.get(id=session_id)
761
+            session.player_name = player_name
762
+            session.save()
763
+            
764
+            # Get leaderboard position
765
+            better_scores = GameSession.objects.filter(
766
+                completed_at__isnull=False
767
+            ).exclude(id=session_id).annotate(
768
+                score=models.F('moles_killed') * 1000  # Simplified score calculation
769
+            ).filter(score__gt=session.calculate_score()).count()
770
+            
771
+            return Response({
772
+                'success': True,
773
+                'player_name': player_name,
774
+                'score': session.calculate_score(),
775
+                'leaderboard_position': better_scores + 1
776
+            })
777
+        except GameSession.DoesNotExist:
778
+            return Response(
779
+                {'error': 'Session not found'}, 
780
+                status=status.HTTP_404_NOT_FOUND
781
+            )
782
+    
718783
     @action(detail=False, methods=['get'])
719784
     def leaderboard(self, request):
720
-        """Get the leaderboard of fastest completions"""
785
+        """Get the leaderboard of top scores"""
786
+        # Get top 10 completed sessions
721787
         completed_sessions = GameSession.objects.filter(
722788
             completed_at__isnull=False
723
-        ).order_by('time_taken', 'commands_used')[:20]
789
+        )[:20]  # Get more than 10 to handle sorting by score
790
+        
791
+        leaderboard_data = []
792
+        for session in completed_sessions:
793
+            score = session.calculate_score()
794
+            leaderboard_data.append({
795
+                'rank': 0,  # Will be set after sorting
796
+                'player_name': session.player_name,
797
+                'score': score,
798
+                'moles_killed': session.moles_killed,
799
+                'moles_escaped': session.moles_escaped,
800
+                'commands_used': session.commands_used,
801
+                'time_taken': str(session.time_taken) if session.time_taken else 'N/A',
802
+                'completed_at': session.completed_at
803
+            })
724804
         
725
-        serializer = self.get_serializer(completed_sessions, many=True)
726
-        return Response(serializer.data)
805
+        # Sort by score (highest first)
806
+        leaderboard_data.sort(key=lambda x: x['score'], reverse=True)
807
+        
808
+        # Keep only top 10
809
+        leaderboard_data = leaderboard_data[:10]
810
+        
811
+        # Update ranks after sorting
812
+        for i, entry in enumerate(leaderboard_data, 1):
813
+            entry['rank'] = i
814
+        
815
+        return Response({
816
+            'leaderboard': leaderboard_data,
817
+            'total_games': GameSession.objects.filter(completed_at__isnull=False).count()
818
+        })