zeroed-some/bashamole / 703cd03

Browse files

expand command functionality

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
703cd03f47782768e981ef46f2c774c51105bf79
Parents
9bb5d95
Tree
4945a6a

5 changed files

StatusFile+-
A backend/apps/trees/migrations/0002_filesystemtree_directory_stack_and_more.py 28 0
M backend/apps/trees/models.py 116 17
M backend/apps/trees/views.py 108 9
M frontend/src/components/Game.tsx 2 1
M frontend/src/lib/api.ts 1 0
backend/apps/trees/migrations/0002_filesystemtree_directory_stack_and_more.pyadded
@@ -0,0 +1,28 @@
1
+# Generated by Django 5.2.3 on 2025-06-15 01:27
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ("trees", "0001_initial"),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.AddField(
14
+            model_name="filesystemtree",
15
+            name="directory_stack",
16
+            field=models.JSONField(default=list),
17
+        ),
18
+        migrations.AddField(
19
+            model_name="filesystemtree",
20
+            name="home_directory",
21
+            field=models.CharField(default="/home", max_length=500),
22
+        ),
23
+        migrations.AddField(
24
+            model_name="filesystemtree",
25
+            name="previous_location",
26
+            field=models.CharField(default="", max_length=500),
27
+        ),
28
+    ]
backend/apps/trees/models.pymodified
@@ -17,6 +17,11 @@ class FileSystemTree(models.Model):
1717
     is_completed = models.BooleanField(default=False)
1818
     completed_at = models.DateTimeField(null=True, blank=True)
1919
     
20
+    # Navigation state
21
+    previous_location = models.CharField(max_length=500, default="")  # For cd -
22
+    directory_stack = models.JSONField(default=list)  # For pushd/popd
23
+    home_directory = models.CharField(max_length=500, default="/home")  # Player's home
24
+    
2025
     # Cached tree structure
2126
     tree_data = models.JSONField(null=True, blank=True)
2227
     
@@ -231,9 +236,22 @@ class FileSystemTree(models.Model):
231236
             # Select random starting position with weights
232237
             start_node = random.choices(valid_starts, weights=weights, k=1)[0]
233238
             self.player_location = start_node.path
239
+            
240
+            # Set home directory based on starting location
241
+            if start_node.path.startswith('/home/'):
242
+                # Extract the user's home directory
243
+                parts = start_node.path.split('/')
244
+                if len(parts) >= 3:
245
+                    self.home_directory = f"/home/{parts[2]}"
246
+                else:
247
+                    self.home_directory = "/home"
248
+            else:
249
+                # Default home directory
250
+                self.home_directory = "/home"
234251
         else:
235252
             # Fallback to /home if no valid candidates
236253
             self.player_location = "/home"
254
+            self.home_directory = "/home"
237255
     
238256
     def cache_tree(self):
239257
         """Cache the tree structure for efficient retrieval"""
@@ -251,32 +269,113 @@ class FileSystemTree(models.Model):
251269
         root = DirectoryNode.objects.get(tree=self, path="/")
252270
         self.tree_data = build_tree_dict(root)
253271
     
272
+    def resolve_path(self, path):
273
+        """Resolve a path that may contain ~ or be relative"""
274
+        if path == "~":
275
+            return self.home_directory
276
+        elif path.startswith("~/"):
277
+            return self.home_directory + path[1:]
278
+        elif path == "-":
279
+            return self.previous_location if self.previous_location else self.player_location
280
+        elif not path.startswith('/'):
281
+            # Relative path
282
+            if self.player_location == "/":
283
+                return "/" + path
284
+            else:
285
+                return self.player_location + "/" + path
286
+        else:
287
+            # Absolute path
288
+            return path
289
+    
290
+    def normalize_path(self, path):
291
+        """Normalize a path by resolving .. and . components"""
292
+        parts = path.split('/')
293
+        resolved = []
294
+        
295
+        for part in parts:
296
+            if part == '' and len(resolved) == 0:
297
+                # Leading slash
298
+                resolved.append('')
299
+            elif part == '..':
300
+                if len(resolved) > 1:
301
+                    resolved.pop()
302
+            elif part != '.' and part != '':
303
+                resolved.append(part)
304
+        
305
+        if len(resolved) == 1 and resolved[0] == '':
306
+            return '/'
307
+        return '/'.join(resolved)
308
+    
254309
     def move_player(self, target_path):
255310
         """Move player to a new location if valid"""
256
-        # Normalize path
257
-        if not target_path.startswith('/'):
258
-            # Relative path
259
-            if target_path == "..":
260
-                # Go up one directory
311
+        # Resolve special path symbols
312
+        resolved_path = self.resolve_path(target_path)
313
+        
314
+        # Handle ".." in the path
315
+        if ".." in resolved_path or resolved_path == "..":
316
+            if resolved_path == "..":
317
+                # Go up one directory from current location
261318
                 if self.player_location == "/":
262319
                     return False, "Already at root directory"
263
-                self.player_location = "/".join(self.player_location.split("/")[:-1]) or "/"
320
+                new_path = "/".join(self.player_location.split("/")[:-1]) or "/"
264321
             else:
265
-                # Go to subdirectory
266
-                new_path = f"{self.player_location}/{target_path}" if self.player_location != "/" else f"/{target_path}"
267
-                if DirectoryNode.objects.filter(tree=self, path=new_path).exists():
268
-                    self.player_location = new_path
269
-                else:
270
-                    return False, f"Directory not found: {target_path}"
322
+                # Normalize the path to handle .. components
323
+                new_path = self.normalize_path(resolved_path)
271324
         else:
272
-            # Absolute path
273
-            if DirectoryNode.objects.filter(tree=self, path=target_path).exists():
274
-                self.player_location = target_path
325
+            new_path = resolved_path
326
+        
327
+        # Check if the directory exists
328
+        if DirectoryNode.objects.filter(tree=self, path=new_path).exists():
329
+            # Save previous location before moving
330
+            self.previous_location = self.player_location
331
+            self.player_location = new_path
332
+            self.save()
333
+            return True, f"Moved to {self.player_location}"
334
+        else:
335
+            return False, f"Directory not found: {target_path}"
336
+    
337
+    def push_directory(self, target_path=None):
338
+        """Push current directory onto stack and optionally change to new directory"""
339
+        # Add current directory to stack
340
+        if not isinstance(self.directory_stack, list):
341
+            self.directory_stack = []
342
+        
343
+        self.directory_stack.append(self.player_location)
344
+        
345
+        # If target path provided, change to it
346
+        if target_path:
347
+            success, message = self.move_player(target_path)
348
+            if success:
349
+                self.save()
350
+                return True, f"Pushed {self.directory_stack[-1]} and moved to {self.player_location}"
275351
             else:
276
-                return False, f"Directory not found: {target_path}"
352
+                # Remove from stack if move failed
353
+                self.directory_stack.pop()
354
+                return False, message
355
+        else:
356
+            self.save()
357
+            return True, f"Pushed {self.player_location} onto directory stack"
358
+    
359
+    def pop_directory(self):
360
+        """Pop directory from stack and change to it"""
361
+        if not self.directory_stack:
362
+            return False, "Directory stack is empty"
277363
         
364
+        # Get directory from stack
365
+        target_dir = self.directory_stack.pop()
366
+        
367
+        # Save current location as previous (for cd -)
368
+        self.previous_location = self.player_location
369
+        self.player_location = target_dir
278370
         self.save()
279
-        return True, f"Moved to {self.player_location}"
371
+        
372
+        return True, f"Popped and moved to {self.player_location}"
373
+    
374
+    def get_directory_stack(self):
375
+        """Get the current directory stack for display"""
376
+        if not self.directory_stack:
377
+            return []
378
+        return list(self.directory_stack) + [self.player_location]
280379
     
281380
     def check_win_condition(self):
282381
         """Check if player is in the same directory as the mole"""
backend/apps/trees/views.pymodified
@@ -39,7 +39,8 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
3939
         return Response({
4040
             'tree': serializer.data,
4141
             'session_id': session.id,
42
-            'mole_hint': f"The mole is hiding somewhere in the filesystem!"
42
+            'mole_hint': f"The mole is hiding somewhere in the filesystem!",
43
+            'home_directory': tree.home_directory
4344
         }, status=status.HTTP_201_CREATED)
4445
     
4546
     @action(detail=True, methods=['get'])
@@ -100,7 +101,11 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
100101
         
101102
         if cmd == 'cd':
102103
             if len(parts) < 2:
103
-                response_data['output'] = "cd: missing operand"
104
+                # cd with no args goes to home directory
105
+                success, message = tree.move_player("~")
106
+                response_data['success'] = success
107
+                response_data['output'] = message if not success else ""
108
+                response_data['current_path'] = tree.player_location
104109
             else:
105110
                 target = parts[1]
106111
                 success, message = tree.move_player(target)
@@ -112,17 +117,84 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
112117
                     session.directories_visited += 1
113118
                     session.save()
114119
         
120
+        elif cmd == 'pushd':
121
+            if len(parts) < 2:
122
+                # pushd with no args swaps top two directories on stack
123
+                if tree.directory_stack:
124
+                    success, message = tree.push_directory()
125
+                    response_data['success'] = success
126
+                    response_data['output'] = message
127
+                else:
128
+                    response_data['output'] = "pushd: no other directory"
129
+            else:
130
+                target = parts[1]
131
+                success, message = tree.push_directory(target)
132
+                response_data['success'] = success
133
+                response_data['output'] = message
134
+                response_data['current_path'] = tree.player_location
135
+                
136
+                if success and session:
137
+                    session.directories_visited += 1
138
+                    session.save()
139
+        
140
+        elif cmd == 'popd':
141
+            success, message = tree.pop_directory()
142
+            response_data['success'] = success
143
+            response_data['output'] = message
144
+            response_data['current_path'] = tree.player_location
145
+            
146
+            if success and session:
147
+                session.directories_visited += 1
148
+                session.save()
149
+        
150
+        elif cmd == 'dirs':
151
+            # Show directory stack
152
+            stack = tree.get_directory_stack()
153
+            if stack:
154
+                response_data['output'] = ' '.join(stack)
155
+            else:
156
+                response_data['output'] = tree.player_location
157
+            response_data['success'] = True
158
+        
115159
         elif cmd == 'ls':
160
+            # Handle ls with options
161
+            show_all = '-a' in parts or '-la' in parts or '-al' in parts
162
+            long_format = '-l' in parts or '-la' in parts or '-al' in parts
163
+            
116164
             try:
117165
                 current_dir = DirectoryNode.objects.get(
118166
                     tree=tree, 
119167
                     path=tree.player_location
120168
                 )
121169
                 contents = current_dir.get_contents()
170
+                
171
+                output_lines = []
172
+                
173
+                if show_all:
174
+                    # Add . and .. entries
175
+                    if long_format:
176
+                        output_lines.append("drwxr-xr-x  .  " + current_dir.description)
177
+                        if current_dir.parent:
178
+                            output_lines.append("drwxr-xr-x  ..  " + current_dir.parent.description)
179
+                    else:
180
+                        output_lines.extend(['.', '..'])
181
+                
122182
                 if contents:
123
-                    response_data['output'] = '\n'.join([d.name for d in contents])
183
+                    for d in contents:
184
+                        if long_format:
185
+                            output_lines.append(f"drwxr-xr-x  {d.name}  {d.description}")
186
+                        else:
187
+                            output_lines.append(d.name)
188
+                
189
+                if long_format:
190
+                    response_data['output'] = '\n'.join(output_lines)
124191
                 else:
125
-                    response_data['output'] = ''
192
+                    # Format in columns for regular ls
193
+                    if output_lines:
194
+                        response_data['output'] = '  '.join(output_lines)
195
+                    else:
196
+                        response_data['output'] = ''
197
+                
126198
                 response_data['success'] = True
127199
             except DirectoryNode.DoesNotExist:
128200
                 response_data['output'] = "ls: cannot access directory"
@@ -131,6 +203,23 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
131203
             response_data['output'] = tree.player_location
132204
             response_data['success'] = True
133205
         
206
+        elif cmd == 'echo':
207
+            # Simple echo implementation
208
+            if len(parts) > 1:
209
+                # Handle special variables
210
+                echo_text = ' '.join(parts[1:])
211
+                if echo_text == '$HOME':
212
+                    response_data['output'] = tree.home_directory
213
+                elif echo_text == '$PWD':
214
+                    response_data['output'] = tree.player_location
215
+                elif echo_text == '$OLDPWD':
216
+                    response_data['output'] = tree.previous_location or ''
217
+                else:
218
+                    response_data['output'] = echo_text
219
+            else:
220
+                response_data['output'] = ''
221
+            response_data['success'] = True
222
+        
134223
         elif cmd == 'killall' and len(parts) > 1 and parts[1] == 'moles':
135224
             if tree.check_win_condition():
136225
                 tree.is_completed = True
@@ -151,11 +240,21 @@ class FileSystemTreeViewSet(viewsets.ModelViewSet):
151240
         
152241
         elif cmd == 'help':
153242
             response_data['output'] = """Available commands:
154
-cd <directory>  - Change directory
155
-ls              - List directory contents
156
-pwd             - Print working directory
157
-killall moles   - Eliminate moles (when in the same directory)
158
-help            - Show this help message"""
243
+cd <directory>    - Change directory (supports ~, -, and ..)
244
+cd                - Go to home directory
245
+pushd <directory> - Push directory onto stack and change to it
246
+popd              - Pop directory from stack and change to it
247
+dirs              - Display directory stack
248
+ls [-la]          - List directory contents
249
+pwd               - Print working directory
250
+echo <text>       - Display text (supports $HOME, $PWD, $OLDPWD)
251
+killall moles     - Eliminate moles (when in the same directory)
252
+help              - Show this help message
253
+
254
+Special paths:
255
+~   - Home directory
256
+-   - Previous directory
257
+..  - Parent directory"""
159258
             response_data['success'] = True
160259
         
161260
         else:
frontend/src/components/Game.tsxmodified
@@ -63,6 +63,7 @@ const Game: React.FC = () => {
6363
       
6464
       // Create a more dynamic starting message based on random location
6565
       const startLocation = response.tree.player_location;
66
+      const homeDir = response.home_directory || '/home';
6667
       let locationContext = '';
6768
       
6869
       if (startLocation.startsWith('/home')) {
@@ -79,7 +80,7 @@ const Game: React.FC = () => {
7980
       
8081
       setCommandHistory([{
8182
         command: 'Hunt started!',
82
-        output: `${response.mole_hint}\n${locationContext}Use 'pwd' to see where you are.\nType "help" for available commands.`,
83
+        output: `${response.mole_hint}\n${locationContext}Your home directory is ${homeDir}.\nUse 'pwd' to see where you are, 'cd ~' to go home.\nType "help" for available commands.`,
8384
         success: true,
8485
       }]);
8586
       setHints([]);
frontend/src/lib/api.tsmodified
@@ -43,6 +43,7 @@ export interface GameCreationResponse {
4343
   tree: FileSystemTree;
4444
   session_id: number;
4545
   mole_hint: string;
46
+  home_directory: string;
4647
 }
4748
 
4849
 export interface HintResponse {