Python · 13583 bytes Raw Blame History
1 # apps/trees/models.py
2 from django.db import models
3 from django.utils import timezone
4 import json
5 import random
6 import string
7
8 class FileSystemTree(models.Model):
9 """A complete filesystem tree for a game session"""
10 name = models.CharField(max_length=100, default="FHS Tree")
11 created_at = models.DateTimeField(auto_now_add=True)
12 seed = models.IntegerField(default=0)
13
14 # Game state
15 mole_location = models.CharField(max_length=500, blank=True) # Path to mole
16 player_location = models.CharField(max_length=500, default="/home")
17 is_completed = models.BooleanField(default=False)
18 completed_at = models.DateTimeField(null=True, blank=True)
19
20 # Cached tree structure
21 tree_data = models.JSONField(null=True, blank=True)
22
23 def __str__(self):
24 return f"{self.name} - {'Completed' if self.is_completed else 'Active'}"
25
26 def generate_tree(self, max_depth=5, directories_per_level=3):
27 """Generate a procedural Unix filesystem tree"""
28 if self.seed == 0:
29 self.seed = random.randint(1, 1000000)
30
31 random.seed(self.seed)
32
33 # Clear existing nodes
34 self.nodes.all().delete()
35
36 # Create root
37 root = DirectoryNode.objects.create(
38 tree=self,
39 name="",
40 path="/",
41 parent=None,
42 is_fhs_standard=True,
43 description="Root directory"
44 )
45
46 # Create standard FHS directories
47 self._create_fhs_structure(root)
48
49 # Add procedural directories to some locations
50 self._add_procedural_directories(max_depth, directories_per_level)
51
52 # Place the mole in a random directory (not in standard FHS locations)
53 self._place_mole()
54
55 # Set random starting position for player
56 self._set_random_start_position()
57
58 # Cache the tree structure
59 self.cache_tree()
60 self.save()
61
62 def _create_fhs_structure(self, root):
63 """Create standard FHS directory structure"""
64 fhs_dirs = [
65 {"name": "bin", "desc": "Essential command binaries"},
66 {"name": "boot", "desc": "Static files of the boot loader"},
67 {"name": "dev", "desc": "Device files"},
68 {"name": "etc", "desc": "Host-specific system configuration"},
69 {"name": "home", "desc": "User home directories"},
70 {"name": "lib", "desc": "Essential shared libraries and kernel modules"},
71 {"name": "media", "desc": "Mount points for removable media"},
72 {"name": "mnt", "desc": "Mount point for temporarily mounted filesystems"},
73 {"name": "opt", "desc": "Add-on application software packages"},
74 {"name": "proc", "desc": "Virtual filesystem for process information"},
75 {"name": "root", "desc": "Home directory for the root user"},
76 {"name": "run", "desc": "Data relevant to running processes"},
77 {"name": "sbin", "desc": "Essential system binaries"},
78 {"name": "srv", "desc": "Data for services provided by this system"},
79 {"name": "sys", "desc": "Virtual filesystem for system information"},
80 {"name": "tmp", "desc": "Temporary files"},
81 {"name": "usr", "desc": "Secondary hierarchy"},
82 {"name": "var", "desc": "Variable data"},
83 ]
84
85 for dir_info in fhs_dirs:
86 DirectoryNode.objects.create(
87 tree=self,
88 name=dir_info["name"],
89 path=f"/{dir_info['name']}",
90 parent=root,
91 is_fhs_standard=True,
92 description=dir_info["desc"]
93 )
94
95 # Create some standard subdirectories
96 usr = DirectoryNode.objects.get(tree=self, path="/usr")
97 for subdir in ["bin", "lib", "local", "share", "src"]:
98 DirectoryNode.objects.create(
99 tree=self,
100 name=subdir,
101 path=f"/usr/{subdir}",
102 parent=usr,
103 is_fhs_standard=True,
104 description=f"User {subdir} directory"
105 )
106
107 # Create user directories
108 home = DirectoryNode.objects.get(tree=self, path="/home")
109 for username in ["alice", "bob", "charlie"]:
110 user_home = DirectoryNode.objects.create(
111 tree=self,
112 name=username,
113 path=f"/home/{username}",
114 parent=home,
115 is_fhs_standard=False,
116 description=f"Home directory for {username}"
117 )
118
119 # Add some standard user directories
120 for userdir in ["Documents", "Downloads", "Desktop", "Pictures"]:
121 DirectoryNode.objects.create(
122 tree=self,
123 name=userdir,
124 path=f"/home/{username}/{userdir}",
125 parent=user_home,
126 is_fhs_standard=False,
127 description=f"{username}'s {userdir}"
128 )
129
130 def _add_procedural_directories(self, max_depth, dirs_per_level):
131 """Add procedurally generated directories to make the tree interesting"""
132 # Common directory names for procedural generation
133 dir_names = [
134 "projects", "workspace", "temp", "backup", "archive", "data",
135 "config", "logs", "cache", "build", "dist", "assets",
136 "scripts", "tools", "utils", "resources", "public", "private",
137 "old", "new", "test", "prod", "dev", "staging",
138 "alpha", "beta", "gamma", "delta", "epsilon", "zeta",
139 "red", "blue", "green", "yellow", "purple", "orange",
140 "cat", "dog", "fish", "bird", "mouse", "rabbit"
141 ]
142
143 # Add procedural dirs to certain locations
144 base_paths = [
145 "/home/alice", "/home/bob", "/home/charlie",
146 "/opt", "/var", "/usr/local"
147 ]
148
149 for base_path in base_paths:
150 try:
151 base_node = DirectoryNode.objects.get(tree=self, path=base_path)
152 self._generate_subtree(base_node, max_depth-2, dirs_per_level, dir_names)
153 except DirectoryNode.DoesNotExist:
154 continue
155
156 def _generate_subtree(self, parent, depth, dirs_per_level, name_pool):
157 """Recursively generate random subdirectories"""
158 if depth <= 0:
159 return
160
161 # Random number of directories at this level
162 num_dirs = random.randint(1, dirs_per_level)
163 used_names = set()
164
165 for _ in range(num_dirs):
166 # Pick a unique name for this level
167 name = random.choice(name_pool)
168 while name in used_names:
169 name = random.choice(name_pool)
170 used_names.add(name)
171
172 # Create the directory
173 path = f"{parent.path}/{name}" if parent.path != "/" else f"/{name}"
174 new_dir = DirectoryNode.objects.create(
175 tree=self,
176 name=name,
177 path=path,
178 parent=parent,
179 is_fhs_standard=False,
180 description=f"Procedurally generated directory"
181 )
182
183 # Randomly decide whether to create subdirectories
184 if random.random() > 0.3: # 70% chance of subdirectories
185 self._generate_subtree(new_dir, depth-1, dirs_per_level, name_pool)
186
187 def _place_mole(self):
188 """Place the mole in a random non-FHS directory"""
189 candidates = DirectoryNode.objects.filter(
190 tree=self,
191 is_fhs_standard=False
192 ).exclude(path__in=[
193 "/home", "/home/alice", "/home/bob", "/home/charlie"
194 ])
195
196 if candidates.exists():
197 mole_dir = random.choice(candidates)
198 self.mole_location = mole_dir.path
199
200 def _set_random_start_position(self):
201 """Set a random starting position for the player"""
202 # Get all directories that could be valid starting positions
203 # Exclude root and very deep directories (depth > 3)
204 candidates = DirectoryNode.objects.filter(
205 tree=self
206 ).exclude(
207 path="/" # Don't start at root
208 )
209
210 # Filter to reasonable starting positions
211 valid_starts = []
212 for node in candidates:
213 depth = node.path.count('/')
214 # Prefer directories at depth 1-3
215 if 1 <= depth <= 3:
216 # Don't start at the mole location
217 if node.path != self.mole_location:
218 valid_starts.append(node)
219
220 if valid_starts:
221 # Weight selection towards common starting areas but allow anywhere
222 weights = []
223 for node in valid_starts:
224 if node.path.startswith('/home'):
225 weights.append(3) # Higher weight for home directories
226 elif node.path.startswith('/usr'):
227 weights.append(2) # Medium weight for usr directories
228 else:
229 weights.append(1) # Lower weight for other directories
230
231 # Select random starting position with weights
232 start_node = random.choices(valid_starts, weights=weights, k=1)[0]
233 self.player_location = start_node.path
234 else:
235 # Fallback to /home if no valid candidates
236 self.player_location = "/home"
237
238 def cache_tree(self):
239 """Cache the tree structure for efficient retrieval"""
240 def build_tree_dict(node):
241 children = DirectoryNode.objects.filter(parent=node)
242 return {
243 "name": node.name,
244 "path": node.path,
245 "is_fhs": node.is_fhs_standard,
246 "description": node.description,
247 "has_mole": node.path == self.mole_location,
248 "children": [build_tree_dict(child) for child in children]
249 }
250
251 root = DirectoryNode.objects.get(tree=self, path="/")
252 self.tree_data = build_tree_dict(root)
253
254 def move_player(self, target_path):
255 """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
261 if self.player_location == "/":
262 return False, "Already at root directory"
263 self.player_location = "/".join(self.player_location.split("/")[:-1]) or "/"
264 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}"
271 else:
272 # Absolute path
273 if DirectoryNode.objects.filter(tree=self, path=target_path).exists():
274 self.player_location = target_path
275 else:
276 return False, f"Directory not found: {target_path}"
277
278 self.save()
279 return True, f"Moved to {self.player_location}"
280
281 def check_win_condition(self):
282 """Check if player is in the same directory as the mole"""
283 return self.player_location == self.mole_location
284
285
286 class DirectoryNode(models.Model):
287 """A directory in the filesystem tree"""
288 tree = models.ForeignKey(FileSystemTree, on_delete=models.CASCADE, related_name='nodes')
289 parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='children')
290
291 name = models.CharField(max_length=100)
292 path = models.CharField(max_length=500, db_index=True)
293 is_fhs_standard = models.BooleanField(default=False)
294 description = models.TextField(blank=True)
295
296 class Meta:
297 unique_together = ('tree', 'path')
298 ordering = ['path']
299
300 def __str__(self):
301 return f"{self.path} ({'FHS' if self.is_fhs_standard else 'Generated'})"
302
303 @property
304 def depth(self):
305 """Calculate directory depth"""
306 return self.path.count('/')
307
308 def get_contents(self):
309 """Get immediate children of this directory"""
310 return DirectoryNode.objects.filter(parent=self).order_by('name')
311
312
313 class GameSession(models.Model):
314 """Track game sessions and scores"""
315 tree = models.ForeignKey(FileSystemTree, on_delete=models.CASCADE, related_name='sessions')
316 player_name = models.CharField(max_length=100, default="Anonymous")
317 started_at = models.DateTimeField(auto_now_add=True)
318 completed_at = models.DateTimeField(null=True, blank=True)
319
320 # Game metrics
321 commands_used = models.IntegerField(default=0)
322 directories_visited = models.IntegerField(default=0)
323 time_taken = models.DurationField(null=True, blank=True)
324
325 # Command history
326 command_history = models.JSONField(default=list)
327
328 def __str__(self):
329 return f"{self.player_name} - {self.tree.name}"
330
331 def add_command(self, command):
332 """Add a command to the history"""
333 self.command_history.append({
334 'command': command,
335 'timestamp': str(timezone.now())
336 })
337 self.commands_used += 1
338 self.save()