Python · 11873 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 # Cache the tree structure
56 self.cache_tree()
57 self.save()
58
59 def _create_fhs_structure(self, root):
60 """Create standard FHS directory structure"""
61 fhs_dirs = [
62 {"name": "bin", "desc": "Essential command binaries"},
63 {"name": "boot", "desc": "Static files of the boot loader"},
64 {"name": "dev", "desc": "Device files"},
65 {"name": "etc", "desc": "Host-specific system configuration"},
66 {"name": "home", "desc": "User home directories"},
67 {"name": "lib", "desc": "Essential shared libraries and kernel modules"},
68 {"name": "media", "desc": "Mount points for removable media"},
69 {"name": "mnt", "desc": "Mount point for temporarily mounted filesystems"},
70 {"name": "opt", "desc": "Add-on application software packages"},
71 {"name": "proc", "desc": "Virtual filesystem for process information"},
72 {"name": "root", "desc": "Home directory for the root user"},
73 {"name": "run", "desc": "Data relevant to running processes"},
74 {"name": "sbin", "desc": "Essential system binaries"},
75 {"name": "srv", "desc": "Data for services provided by this system"},
76 {"name": "sys", "desc": "Virtual filesystem for system information"},
77 {"name": "tmp", "desc": "Temporary files"},
78 {"name": "usr", "desc": "Secondary hierarchy"},
79 {"name": "var", "desc": "Variable data"},
80 ]
81
82 for dir_info in fhs_dirs:
83 DirectoryNode.objects.create(
84 tree=self,
85 name=dir_info["name"],
86 path=f"/{dir_info['name']}",
87 parent=root,
88 is_fhs_standard=True,
89 description=dir_info["desc"]
90 )
91
92 # Create some standard subdirectories
93 usr = DirectoryNode.objects.get(tree=self, path="/usr")
94 for subdir in ["bin", "lib", "local", "share", "src"]:
95 DirectoryNode.objects.create(
96 tree=self,
97 name=subdir,
98 path=f"/usr/{subdir}",
99 parent=usr,
100 is_fhs_standard=True,
101 description=f"User {subdir} directory"
102 )
103
104 # Create user directories
105 home = DirectoryNode.objects.get(tree=self, path="/home")
106 for username in ["alice", "bob", "charlie"]:
107 user_home = DirectoryNode.objects.create(
108 tree=self,
109 name=username,
110 path=f"/home/{username}",
111 parent=home,
112 is_fhs_standard=False,
113 description=f"Home directory for {username}"
114 )
115
116 # Add some standard user directories
117 for userdir in ["Documents", "Downloads", "Desktop", "Pictures"]:
118 DirectoryNode.objects.create(
119 tree=self,
120 name=userdir,
121 path=f"/home/{username}/{userdir}",
122 parent=user_home,
123 is_fhs_standard=False,
124 description=f"{username}'s {userdir}"
125 )
126
127 def _add_procedural_directories(self, max_depth, dirs_per_level):
128 """Add procedurally generated directories to make the tree interesting"""
129 # Common directory names for procedural generation
130 dir_names = [
131 "projects", "workspace", "temp", "backup", "archive", "data",
132 "config", "logs", "cache", "build", "dist", "assets",
133 "scripts", "tools", "utils", "resources", "public", "private",
134 "old", "new", "test", "prod", "dev", "staging",
135 "alpha", "beta", "gamma", "delta", "epsilon", "zeta",
136 "red", "blue", "green", "yellow", "purple", "orange",
137 "cat", "dog", "fish", "bird", "mouse", "rabbit"
138 ]
139
140 # Add procedural dirs to certain locations
141 base_paths = [
142 "/home/alice", "/home/bob", "/home/charlie",
143 "/opt", "/var", "/usr/local"
144 ]
145
146 for base_path in base_paths:
147 try:
148 base_node = DirectoryNode.objects.get(tree=self, path=base_path)
149 self._generate_subtree(base_node, max_depth-2, dirs_per_level, dir_names)
150 except DirectoryNode.DoesNotExist:
151 continue
152
153 def _generate_subtree(self, parent, depth, dirs_per_level, name_pool):
154 """Recursively generate random subdirectories"""
155 if depth <= 0:
156 return
157
158 # Random number of directories at this level
159 num_dirs = random.randint(1, dirs_per_level)
160 used_names = set()
161
162 for _ in range(num_dirs):
163 # Pick a unique name for this level
164 name = random.choice(name_pool)
165 while name in used_names:
166 name = random.choice(name_pool)
167 used_names.add(name)
168
169 # Create the directory
170 path = f"{parent.path}/{name}" if parent.path != "/" else f"/{name}"
171 new_dir = DirectoryNode.objects.create(
172 tree=self,
173 name=name,
174 path=path,
175 parent=parent,
176 is_fhs_standard=False,
177 description=f"Procedurally generated directory"
178 )
179
180 # Randomly decide whether to create subdirectories
181 if random.random() > 0.3: # 70% chance of subdirectories
182 self._generate_subtree(new_dir, depth-1, dirs_per_level, name_pool)
183
184 def _place_mole(self):
185 """Place the mole in a random non-FHS directory"""
186 candidates = DirectoryNode.objects.filter(
187 tree=self,
188 is_fhs_standard=False
189 ).exclude(path__in=[
190 "/home", "/home/alice", "/home/bob", "/home/charlie"
191 ])
192
193 if candidates.exists():
194 mole_dir = random.choice(candidates)
195 self.mole_location = mole_dir.path
196
197 def cache_tree(self):
198 """Cache the tree structure for efficient retrieval"""
199 def build_tree_dict(node):
200 children = DirectoryNode.objects.filter(parent=node)
201 return {
202 "name": node.name,
203 "path": node.path,
204 "is_fhs": node.is_fhs_standard,
205 "description": node.description,
206 "has_mole": node.path == self.mole_location,
207 "children": [build_tree_dict(child) for child in children]
208 }
209
210 root = DirectoryNode.objects.get(tree=self, path="/")
211 self.tree_data = build_tree_dict(root)
212
213 def move_player(self, target_path):
214 """Move player to a new location if valid"""
215 # Normalize path
216 if not target_path.startswith('/'):
217 # Relative path
218 if target_path == "..":
219 # Go up one directory
220 if self.player_location == "/":
221 return False, "Already at root directory"
222 self.player_location = "/".join(self.player_location.split("/")[:-1]) or "/"
223 else:
224 # Go to subdirectory
225 new_path = f"{self.player_location}/{target_path}" if self.player_location != "/" else f"/{target_path}"
226 if DirectoryNode.objects.filter(tree=self, path=new_path).exists():
227 self.player_location = new_path
228 else:
229 return False, f"Directory not found: {target_path}"
230 else:
231 # Absolute path
232 if DirectoryNode.objects.filter(tree=self, path=target_path).exists():
233 self.player_location = target_path
234 else:
235 return False, f"Directory not found: {target_path}"
236
237 self.save()
238 return True, f"Moved to {self.player_location}"
239
240 def check_win_condition(self):
241 """Check if player is in the same directory as the mole"""
242 return self.player_location == self.mole_location
243
244
245 class DirectoryNode(models.Model):
246 """A directory in the filesystem tree"""
247 tree = models.ForeignKey(FileSystemTree, on_delete=models.CASCADE, related_name='nodes')
248 parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='children')
249
250 name = models.CharField(max_length=100)
251 path = models.CharField(max_length=500, db_index=True)
252 is_fhs_standard = models.BooleanField(default=False)
253 description = models.TextField(blank=True)
254
255 class Meta:
256 unique_together = ('tree', 'path')
257 ordering = ['path']
258
259 def __str__(self):
260 return f"{self.path} ({'FHS' if self.is_fhs_standard else 'Generated'})"
261
262 @property
263 def depth(self):
264 """Calculate directory depth"""
265 return self.path.count('/')
266
267 def get_contents(self):
268 """Get immediate children of this directory"""
269 return DirectoryNode.objects.filter(parent=self).order_by('name')
270
271
272 class GameSession(models.Model):
273 """Track game sessions and scores"""
274 tree = models.ForeignKey(FileSystemTree, on_delete=models.CASCADE, related_name='sessions')
275 player_name = models.CharField(max_length=100, default="Anonymous")
276 started_at = models.DateTimeField(auto_now_add=True)
277 completed_at = models.DateTimeField(null=True, blank=True)
278
279 # Game metrics
280 commands_used = models.IntegerField(default=0)
281 directories_visited = models.IntegerField(default=0)
282 time_taken = models.DurationField(null=True, blank=True)
283
284 # Command history
285 command_history = models.JSONField(default=list)
286
287 def __str__(self):
288 return f"{self.player_name} - {self.tree.name}"
289
290 def add_command(self, command):
291 """Add a command to the history"""
292 self.command_history.append({
293 'command': command,
294 'timestamp': str(timezone.now())
295 })
296 self.commands_used += 1
297 self.save()