| 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() |