Python · 18363 bytes Raw Blame History
1 # apps/trees/views.py
2 from django.http import HttpResponse
3 from rest_framework import viewsets, status
4 from rest_framework.decorators import action
5 from rest_framework.response import Response
6 from django.utils import timezone
7 from datetime import timedelta
8 from .models import FileSystemTree, DirectoryNode, GameSession
9 from .serializers import (
10 FileSystemTreeSerializer, DirectoryNodeSerializer,
11 GameSessionSerializer, GameCommandSerializer
12 )
13
14
15 class FileSystemTreeViewSet(viewsets.ModelViewSet):
16 """ViewSet for filesystem trees"""
17 queryset = FileSystemTree.objects.all()
18 serializer_class = FileSystemTreeSerializer
19
20 @action(detail=False, methods=['get'])
21 def fhs_reference(self, request):
22 """Get FHS directory reference"""
23 fhs_dirs = [
24 {"path": "/bin", "name": "bin", "desc": "Essential command binaries"},
25 {"path": "/boot", "name": "boot", "desc": "Static files of the boot loader"},
26 {"path": "/dev", "name": "dev", "desc": "Device files"},
27 {"path": "/etc", "name": "etc", "desc": "Host-specific system configuration"},
28 {"path": "/home", "name": "home", "desc": "User home directories"},
29 {"path": "/lib", "name": "lib", "desc": "Essential shared libraries and kernel modules"},
30 {"path": "/media", "name": "media", "desc": "Mount points for removable media"},
31 {"path": "/mnt", "name": "mnt", "desc": "Mount point for temporarily mounted filesystems"},
32 {"path": "/opt", "name": "opt", "desc": "Add-on application software packages"},
33 {"path": "/proc", "name": "proc", "desc": "Virtual filesystem for process information"},
34 {"path": "/root", "name": "root", "desc": "Home directory for the root user"},
35 {"path": "/run", "name": "run", "desc": "Data relevant to running processes"},
36 {"path": "/sbin", "name": "sbin", "desc": "Essential system binaries"},
37 {"path": "/srv", "name": "srv", "desc": "Data for services provided by this system"},
38 {"path": "/sys", "name": "sys", "desc": "Virtual filesystem for system information"},
39 {"path": "/tmp", "name": "tmp", "desc": "Temporary files"},
40 {"path": "/usr", "name": "usr", "desc": "Secondary hierarchy"},
41 {"path": "/var", "name": "var", "desc": "Variable data"},
42 ]
43 return Response({"directories": fhs_dirs})
44
45 @action(detail=False, methods=['post'])
46 def create_game(self, request):
47 """Create a new game with a generated filesystem tree"""
48 tree_name = request.data.get('name', 'FHS Game Tree')
49 max_depth = request.data.get('max_depth', 5)
50 dirs_per_level = request.data.get('dirs_per_level', 3)
51
52 # Create and generate tree
53 tree = FileSystemTree.objects.create(name=tree_name)
54 tree.generate_tree(max_depth=max_depth, directories_per_level=dirs_per_level)
55
56 # Create game session
57 player_name = request.data.get('player_name', 'Anonymous')
58 session = GameSession.objects.create(
59 tree=tree,
60 player_name=player_name
61 )
62
63 serializer = self.get_serializer(tree)
64 return Response({
65 'tree': serializer.data,
66 'session_id': session.id,
67 'mole_hint': f"The mole is hiding somewhere in the filesystem!",
68 'home_directory': tree.home_directory
69 }, status=status.HTTP_201_CREATED)
70
71 @action(detail=True, methods=['get'])
72 def current_directory(self, request, pk=None):
73 """Get current directory contents and player location"""
74 tree = self.get_object()
75
76 try:
77 current_dir = DirectoryNode.objects.get(
78 tree=tree,
79 path=tree.player_location
80 )
81 contents = current_dir.get_contents()
82
83 return Response({
84 'path': tree.player_location,
85 'contents': DirectoryNodeSerializer(contents, many=True).data,
86 'parent': current_dir.parent.path if current_dir.parent else None
87 })
88 except DirectoryNode.DoesNotExist:
89 return Response(
90 {'error': 'Current directory not found'},
91 status=status.HTTP_404_NOT_FOUND
92 )
93
94 @action(detail=True, methods=['post'])
95 def execute_command(self, request, pk=None):
96 """Execute a shell command in the game"""
97 tree = self.get_object()
98 command = request.data.get('command', '').strip()
99 session_id = request.data.get('session_id')
100
101 if not command:
102 return Response(
103 {'error': 'No command provided'},
104 status=status.HTTP_400_BAD_REQUEST
105 )
106
107 # Get session if provided
108 session = None
109 if session_id:
110 try:
111 session = GameSession.objects.get(id=session_id, tree=tree)
112 session.add_command(command)
113 except GameSession.DoesNotExist:
114 pass
115
116 # Parse and execute command
117 parts = command.split()
118 cmd = parts[0] if parts else ""
119
120 response_data = {
121 'command': command,
122 'success': False,
123 'output': '',
124 'current_path': tree.player_location
125 }
126
127 if cmd == 'cd':
128 if len(parts) < 2:
129 # cd with no args goes to home directory
130 success, message = tree.move_player("~")
131 response_data['success'] = success
132 response_data['output'] = message if not success else ""
133 response_data['current_path'] = tree.player_location
134 else:
135 target = parts[1]
136 success, message = tree.move_player(target)
137 response_data['success'] = success
138 response_data['output'] = message
139 response_data['current_path'] = tree.player_location
140
141 if success and session:
142 session.directories_visited += 1
143 session.save()
144
145 elif cmd == 'pushd':
146 if len(parts) < 2:
147 # pushd with no args swaps top two directories on stack
148 if tree.directory_stack:
149 success, message = tree.push_directory()
150 response_data['success'] = success
151 response_data['output'] = message
152 else:
153 response_data['output'] = "pushd: no other directory"
154 else:
155 target = parts[1]
156 success, message = tree.push_directory(target)
157 response_data['success'] = success
158 response_data['output'] = message
159 response_data['current_path'] = tree.player_location
160
161 if success and session:
162 session.directories_visited += 1
163 session.save()
164
165 elif cmd == 'popd':
166 success, message = tree.pop_directory()
167 response_data['success'] = success
168 response_data['output'] = message
169 response_data['current_path'] = tree.player_location
170
171 if success and session:
172 session.directories_visited += 1
173 session.save()
174
175 elif cmd == 'dirs':
176 # Show directory stack
177 stack = tree.get_directory_stack()
178 if stack:
179 response_data['output'] = ' '.join(stack)
180 else:
181 response_data['output'] = tree.player_location
182 response_data['success'] = True
183
184 elif cmd == 'ls':
185 # Handle ls with options
186 show_all = '-a' in parts or '-la' in parts or '-al' in parts
187 long_format = '-l' in parts or '-la' in parts or '-al' in parts
188
189 try:
190 current_dir = DirectoryNode.objects.get(
191 tree=tree,
192 path=tree.player_location
193 )
194 contents = current_dir.get_contents()
195
196 output_lines = []
197
198 if show_all:
199 # Add . and .. entries
200 if long_format:
201 output_lines.append("drwxr-xr-x . " + current_dir.description)
202 if current_dir.parent:
203 output_lines.append("drwxr-xr-x .. " + current_dir.parent.description)
204 else:
205 output_lines.extend(['.', '..'])
206
207 if contents:
208 for d in contents:
209 if long_format:
210 output_lines.append(f"drwxr-xr-x {d.name} {d.description}")
211 else:
212 output_lines.append(d.name)
213
214 if long_format:
215 response_data['output'] = '\n'.join(output_lines)
216 else:
217 # Format in columns for regular ls
218 if output_lines:
219 response_data['output'] = ' '.join(output_lines)
220 else:
221 response_data['output'] = ''
222
223 response_data['success'] = True
224 except DirectoryNode.DoesNotExist:
225 response_data['output'] = "ls: cannot access directory"
226
227 elif cmd == 'pwd':
228 response_data['output'] = tree.player_location
229 response_data['success'] = True
230
231 elif cmd == 'echo':
232 # Simple echo implementation
233 if len(parts) > 1:
234 # Handle special variables
235 echo_text = ' '.join(parts[1:])
236 if echo_text == '$HOME':
237 response_data['output'] = tree.home_directory
238 elif echo_text == '$PWD':
239 response_data['output'] = tree.player_location
240 elif echo_text == '$OLDPWD':
241 response_data['output'] = tree.previous_location or ''
242 else:
243 response_data['output'] = echo_text
244 else:
245 response_data['output'] = ''
246 response_data['success'] = True
247
248 elif cmd == 'tree':
249 # Parse options
250 show_all = '-a' in parts
251 max_depth = 3 # Default depth
252
253 # Check for -L option
254 if '-L' in parts:
255 try:
256 depth_index = parts.index('-L') + 1
257 if depth_index < len(parts):
258 max_depth = int(parts[depth_index])
259 max_depth = min(max(max_depth, 1), 5) # Limit between 1-5
260 except (ValueError, IndexError):
261 pass
262
263 try:
264 current_dir = DirectoryNode.objects.get(
265 tree=tree,
266 path=tree.player_location
267 )
268
269 # Build tree output
270 output_lines = [tree.player_location]
271
272 def build_tree_output(node, prefix="", is_last=True, depth=0):
273 if depth >= max_depth:
274 return
275
276 children = list(node.get_contents().order_by('name'))
277
278 for i, child in enumerate(children):
279 is_last_child = i == len(children) - 1
280
281 # Determine if this directory contains the mole
282 has_mole_in_subtree = False
283 if child.path == tree.mole_location:
284 has_mole_in_subtree = True
285 else:
286 # Check if mole is in any subdirectory
287 all_descendants = DirectoryNode.objects.filter(
288 tree=tree,
289 path__startswith=child.path + '/'
290 )
291 if any(d.path == tree.mole_location for d in all_descendants):
292 has_mole_in_subtree = True
293
294 # Build the tree branch characters
295 connector = "└── " if is_last_child else "├── "
296
297 # Format the directory name
298 if has_mole_in_subtree:
299 # Use X to indicate mole presence
300 dir_display = f"[X] {child.name}"
301 else:
302 dir_display = child.name
303
304 output_lines.append(f"{prefix}{connector}{dir_display}")
305
306 # Recurse into subdirectories
307 if depth + 1 < max_depth:
308 extension = " " if is_last_child else "│ "
309 build_tree_output(child, prefix + extension, is_last_child, depth + 1)
310
311 build_tree_output(current_dir)
312
313 # Add directory count at the end
314 total_shown = len(output_lines) - 1
315 if total_shown == 0:
316 output_lines.append("\n0 directories")
317 else:
318 dir_text = "directory" if total_shown == 1 else "directories"
319 output_lines.append(f"\n{total_shown} {dir_text}")
320
321 response_data['output'] = '\n'.join(output_lines)
322 response_data['success'] = True
323
324 except DirectoryNode.DoesNotExist:
325 response_data['output'] = "tree: cannot access directory"
326
327 elif cmd == 'killall' and len(parts) > 1 and parts[1] == 'moles':
328 if tree.check_win_condition():
329 tree.is_completed = True
330 tree.completed_at = timezone.now()
331 tree.save()
332
333 if session:
334 session.completed_at = timezone.now()
335 session.time_taken = session.completed_at - session.started_at
336 session.save()
337
338 response_data['output'] = "🎉 Congratulations! You found and eliminated the mole!"
339 response_data['success'] = True
340 response_data['game_won'] = True
341 else:
342 response_data['output'] = "No moles found in this directory."
343 response_data['success'] = True
344
345 elif cmd == 'help':
346 response_data['output'] = """Available commands:
347 cd <directory> - Change directory (supports ~, -, and ..)
348 cd - Go to home directory
349 pushd <directory> - Push directory onto stack and change to it
350 popd - Pop directory from stack and change to it
351 dirs - Display directory stack
352 ls [-la] - List directory contents
353 pwd - Print working directory
354 echo <text> - Display text (supports $HOME, $PWD, $OLDPWD)
355 tree [-L depth] - Display directory tree (use -L to limit depth)
356 killall moles - Eliminate moles (when in the same directory)
357 help - Show this help message
358
359 Special paths:
360 ~ - Home directory
361 - - Previous directory
362 .. - Parent directory"""
363 response_data['success'] = True
364
365 else:
366 response_data['output'] = f"bash: {cmd}: command not found"
367
368 return Response(response_data)
369
370 @action(detail=True, methods=['get'])
371 def hint(self, request, pk=None):
372 """Get a hint about the mole's location"""
373 tree = self.get_object()
374
375 if not tree.mole_location:
376 return Response({'hint': 'No mole in this tree!'})
377
378 mole_depth = tree.mole_location.count('/')
379 player_depth = tree.player_location.count('/')
380
381 hints = []
382 if mole_depth > player_depth:
383 hints.append("The mole is deeper in the filesystem than you are.")
384 elif mole_depth < player_depth:
385 hints.append("The mole is in a shallower directory than you are.")
386 else:
387 hints.append("You're at the same depth as the mole!")
388
389 # Give a path hint
390 mole_parts = tree.mole_location.split('/')
391 player_parts = tree.player_location.split('/')
392
393 # Find common path
394 common_parts = []
395 for i, (m, p) in enumerate(zip(mole_parts, player_parts)):
396 if m == p:
397 common_parts.append(m)
398 else:
399 break
400
401 if len(common_parts) == len(mole_parts):
402 hints.append("You're in the mole's directory! Use 'killall moles'!")
403 elif len(common_parts) > 1:
404 hints.append(f"You share a common path with the mole: {'/'.join(common_parts) or '/'}")
405
406 return Response({'hints': hints})
407
408
409 class DirectoryNodeViewSet(viewsets.ReadOnlyModelViewSet):
410 """ViewSet for browsing directory nodes"""
411 queryset = DirectoryNode.objects.all()
412 serializer_class = DirectoryNodeSerializer
413
414 def get_queryset(self):
415 queryset = super().get_queryset()
416 tree_id = self.request.query_params.get('tree', None)
417 if tree_id:
418 queryset = queryset.filter(tree_id=tree_id)
419 return queryset
420
421
422 class GameSessionViewSet(viewsets.ModelViewSet):
423 """ViewSet for game sessions"""
424 queryset = GameSession.objects.all()
425 serializer_class = GameSessionSerializer
426
427 @action(detail=False, methods=['get'])
428 def leaderboard(self, request):
429 """Get the leaderboard of fastest completions"""
430 completed_sessions = GameSession.objects.filter(
431 completed_at__isnull=False
432 ).order_by('time_taken', 'commands_used')[:20]
433
434 serializer = self.get_serializer(completed_sessions, many=True)
435 return Response(serializer.data)