zeroed-some/bashamole / 9c38f5f

Browse files

init backend

Authored by espadonne
SHA
9c38f5f501f4f391f875753fde2c716bddd510ff
Tree
0ddeb57

25 changed files

StatusFile+-
A .gitignore 51 0
A .tool-versions 1 0
A backend/.env.example 13 0
A backend/apps/__init__.py 0 0
A backend/apps/trees/__init__.py 0 0
A backend/apps/trees/admin.py 72 0
A backend/apps/trees/apps.py 6 0
A backend/apps/trees/management/__init__.py 0 0
A backend/apps/trees/management/commands/__init__.py 0 0
A backend/apps/trees/management/commands/populate_trees.py 51 0
A backend/apps/trees/migrations/0001_initial.py 105 0
A backend/apps/trees/migrations/__init__.py 0 0
A backend/apps/trees/models.py 297 0
A backend/apps/trees/serializers.py 47 0
A backend/apps/trees/tests.py 3 0
A backend/apps/trees/urls.py 13 0
A backend/apps/trees/views.py 231 0
A backend/bashamole/__init__.py 0 0
A backend/bashamole/asgi.py 16 0
A backend/bashamole/settings.py 138 0
A backend/bashamole/urls.py 25 0
A backend/bashamole/wsgi.py 16 0
A backend/manage.py 22 0
A backend/requirements.txt 5 0
A package.json 21 0
.gitignoreadded
@@ -0,0 +1,51 @@
1
+# Python
2
+backend/venv/
3
+backend/.molenv/*
4
+backend/env/
5
+backend/*.pyc
6
+backend/__pycache__/
7
+backend/**/__pycache__/
8
+backend/db.sqlite3
9
+backend/.env
10
+backend/*.log
11
+backend/.coverage
12
+backend/htmlcov/
13
+backend/media/
14
+backend/staticfiles/
15
+
16
+# Node
17
+frontend/node_modules/
18
+frontend/.next/
19
+frontend/out/
20
+frontend/dist/
21
+frontend/build/
22
+frontend/.env.local
23
+frontend/.env.*.local
24
+frontend/npm-debug.log*
25
+frontend/yarn-debug.log*
26
+frontend/yarn-error.log*
27
+
28
+# IDE
29
+.vscode/
30
+.idea/
31
+*.swp
32
+*.swo
33
+*~
34
+
35
+# OS
36
+.DS_Store
37
+Thumbs.db
38
+
39
+# Shared
40
+shared/node_modules/
41
+shared/dist/
42
+
43
+# Logs
44
+*.log
45
+logs/
46
+
47
+# Testing
48
+coverage/
49
+.nyc_output/
50
+*.coverage
51
+.pytest_cache/
.tool-versionsadded
@@ -0,0 +1,1 @@
1
+nodejs 23.11.0
backend/.env.exampleadded
@@ -0,0 +1,13 @@
1
+# backend/.env.example
2
+DEBUG=True
3
+SECRET_KEY=your-secret-key-here
4
+ALLOWED_HOSTS=localhost,127.0.0.1
5
+
6
+# Database (for production)
7
+# DATABASE_URL=postgresql://user:password@localhost/bashamole
8
+
9
+# CORS
10
+CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
11
+
12
+# API Settings
13
+API_PAGE_SIZE=100
backend/apps/__init__.pyadded
backend/apps/trees/__init__.pyadded
backend/apps/trees/admin.pyadded
@@ -0,0 +1,72 @@
1
+# apps/trees/admin.py
2
+from django.contrib import admin
3
+from .models import FileSystemTree, DirectoryNode, GameSession
4
+
5
+
6
+class DirectoryNodeInline(admin.TabularInline):
7
+    model = DirectoryNode
8
+    extra = 0
9
+    fields = ['name', 'path', 'is_fhs_standard', 'parent']
10
+    readonly_fields = ['path']
11
+    
12
+    def has_add_permission(self, request, obj=None):
13
+        return False
14
+
15
+
16
+@admin.register(FileSystemTree)
17
+class FileSystemTreeAdmin(admin.ModelAdmin):
18
+    list_display = ['name', 'created_at', 'is_completed', 'mole_location', 'player_location']
19
+    list_filter = ['is_completed', 'created_at']
20
+    readonly_fields = ['created_at', 'completed_at', 'tree_data', 'seed']
21
+    
22
+    fieldsets = (
23
+        ('Basic Info', {
24
+            'fields': ('name', 'seed', 'created_at')
25
+        }),
26
+        ('Game State', {
27
+            'fields': ('player_location', 'mole_location', 'is_completed', 'completed_at')
28
+        }),
29
+        ('Tree Data', {
30
+            'fields': ('tree_data',),
31
+            'classes': ('collapse',)
32
+        })
33
+    )
34
+    
35
+    actions = ['regenerate_trees']
36
+    
37
+    def regenerate_trees(self, request, queryset):
38
+        for tree in queryset:
39
+            tree.generate_tree()
40
+        self.message_user(request, f"Regenerated {queryset.count()} trees")
41
+    regenerate_trees.short_description = "Regenerate selected trees"
42
+
43
+
44
+@admin.register(DirectoryNode)
45
+class DirectoryNodeAdmin(admin.ModelAdmin):
46
+    list_display = ['path', 'name', 'tree', 'is_fhs_standard', 'parent']
47
+    list_filter = ['is_fhs_standard', 'tree']
48
+    search_fields = ['path', 'name']
49
+    raw_id_fields = ['tree', 'parent']
50
+    
51
+    def get_queryset(self, request):
52
+        return super().get_queryset(request).select_related('tree', 'parent')
53
+
54
+
55
+@admin.register(GameSession)
56
+class GameSessionAdmin(admin.ModelAdmin):
57
+    list_display = ['player_name', 'tree', 'started_at', 'completed_at', 'commands_used', 'time_taken']
58
+    list_filter = ['completed_at', 'started_at']
59
+    readonly_fields = ['started_at', 'completed_at', 'time_taken', 'command_history']
60
+    
61
+    fieldsets = (
62
+        ('Player Info', {
63
+            'fields': ('player_name', 'tree')
64
+        }),
65
+        ('Game Stats', {
66
+            'fields': ('started_at', 'completed_at', 'time_taken', 'commands_used', 'directories_visited')
67
+        }),
68
+        ('Command History', {
69
+            'fields': ('command_history',),
70
+            'classes': ('collapse',)
71
+        })
72
+    )
backend/apps/trees/apps.pyadded
@@ -0,0 +1,6 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class TreesConfig(AppConfig):
5
+    default_auto_field = "django.db.models.BigAutoField"
6
+    name = "apps.trees"
backend/apps/trees/management/__init__.pyadded
backend/apps/trees/management/commands/__init__.pyadded
backend/apps/trees/management/commands/populate_trees.pyadded
@@ -0,0 +1,51 @@
1
+# apps/trees/management/commands/populate_trees.py
2
+from django.core.management.base import BaseCommand
3
+from apps.trees.models import FileSystemTree
4
+
5
+
6
+class Command(BaseCommand):
7
+    help = 'Create sample filesystem trees for testing'
8
+    
9
+    def add_arguments(self, parser):
10
+        parser.add_argument(
11
+            '--count',
12
+            type=int,
13
+            default=3,
14
+            help='Number of trees to create'
15
+        )
16
+    
17
+    def handle(self, *args, **options):
18
+        count = options['count']
19
+        
20
+        self.stdout.write('Creating sample filesystem trees...')
21
+        
22
+        difficulties = [
23
+            {'name': 'Beginner Tree', 'max_depth': 3, 'dirs_per_level': 2},
24
+            {'name': 'Intermediate Tree', 'max_depth': 4, 'dirs_per_level': 3},
25
+            {'name': 'Advanced Tree', 'max_depth': 5, 'dirs_per_level': 4},
26
+            {'name': 'Expert Tree', 'max_depth': 6, 'dirs_per_level': 5},
27
+        ]
28
+        
29
+        created_count = 0
30
+        for i in range(count):
31
+            difficulty = difficulties[i % len(difficulties)]
32
+            
33
+            tree = FileSystemTree.objects.create(
34
+                name=f"{difficulty['name']} #{i+1}"
35
+            )
36
+            tree.generate_tree(
37
+                max_depth=difficulty['max_depth'],
38
+                directories_per_level=difficulty['dirs_per_level']
39
+            )
40
+            
41
+            created_count += 1
42
+            self.stdout.write(
43
+                self.style.SUCCESS(
44
+                    f"Created tree: {tree.name} with {tree.nodes.count()} directories"
45
+                )
46
+            )
47
+            self.stdout.write(f"  Mole hidden at: {tree.mole_location}")
48
+        
49
+        self.stdout.write(
50
+            self.style.SUCCESS(f'Successfully created {created_count} filesystem trees')
51
+        )
backend/apps/trees/migrations/0001_initial.pyadded
@@ -0,0 +1,105 @@
1
+# Generated by Django 5.2.3 on 2025-06-14 18:26
2
+
3
+import django.db.models.deletion
4
+from django.db import migrations, models
5
+
6
+
7
+class Migration(migrations.Migration):
8
+
9
+    initial = True
10
+
11
+    dependencies = []
12
+
13
+    operations = [
14
+        migrations.CreateModel(
15
+            name="FileSystemTree",
16
+            fields=[
17
+                (
18
+                    "id",
19
+                    models.BigAutoField(
20
+                        auto_created=True,
21
+                        primary_key=True,
22
+                        serialize=False,
23
+                        verbose_name="ID",
24
+                    ),
25
+                ),
26
+                ("name", models.CharField(default="FHS Tree", max_length=100)),
27
+                ("created_at", models.DateTimeField(auto_now_add=True)),
28
+                ("seed", models.IntegerField(default=0)),
29
+                ("mole_location", models.CharField(blank=True, max_length=500)),
30
+                ("player_location", models.CharField(default="/home", max_length=500)),
31
+                ("is_completed", models.BooleanField(default=False)),
32
+                ("completed_at", models.DateTimeField(blank=True, null=True)),
33
+                ("tree_data", models.JSONField(blank=True, null=True)),
34
+            ],
35
+        ),
36
+        migrations.CreateModel(
37
+            name="GameSession",
38
+            fields=[
39
+                (
40
+                    "id",
41
+                    models.BigAutoField(
42
+                        auto_created=True,
43
+                        primary_key=True,
44
+                        serialize=False,
45
+                        verbose_name="ID",
46
+                    ),
47
+                ),
48
+                ("player_name", models.CharField(default="Anonymous", max_length=100)),
49
+                ("started_at", models.DateTimeField(auto_now_add=True)),
50
+                ("completed_at", models.DateTimeField(blank=True, null=True)),
51
+                ("commands_used", models.IntegerField(default=0)),
52
+                ("directories_visited", models.IntegerField(default=0)),
53
+                ("time_taken", models.DurationField(blank=True, null=True)),
54
+                ("command_history", models.JSONField(default=list)),
55
+                (
56
+                    "tree",
57
+                    models.ForeignKey(
58
+                        on_delete=django.db.models.deletion.CASCADE,
59
+                        related_name="sessions",
60
+                        to="trees.filesystemtree",
61
+                    ),
62
+                ),
63
+            ],
64
+        ),
65
+        migrations.CreateModel(
66
+            name="DirectoryNode",
67
+            fields=[
68
+                (
69
+                    "id",
70
+                    models.BigAutoField(
71
+                        auto_created=True,
72
+                        primary_key=True,
73
+                        serialize=False,
74
+                        verbose_name="ID",
75
+                    ),
76
+                ),
77
+                ("name", models.CharField(max_length=100)),
78
+                ("path", models.CharField(db_index=True, max_length=500)),
79
+                ("is_fhs_standard", models.BooleanField(default=False)),
80
+                ("description", models.TextField(blank=True)),
81
+                (
82
+                    "parent",
83
+                    models.ForeignKey(
84
+                        blank=True,
85
+                        null=True,
86
+                        on_delete=django.db.models.deletion.CASCADE,
87
+                        related_name="children",
88
+                        to="trees.directorynode",
89
+                    ),
90
+                ),
91
+                (
92
+                    "tree",
93
+                    models.ForeignKey(
94
+                        on_delete=django.db.models.deletion.CASCADE,
95
+                        related_name="nodes",
96
+                        to="trees.filesystemtree",
97
+                    ),
98
+                ),
99
+            ],
100
+            options={
101
+                "ordering": ["path"],
102
+                "unique_together": {("tree", "path")},
103
+            },
104
+        ),
105
+    ]
backend/apps/trees/migrations/__init__.pyadded
backend/apps/trees/models.pyadded
@@ -0,0 +1,297 @@
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()
backend/apps/trees/serializers.pyadded
@@ -0,0 +1,47 @@
1
+# apps/trees/serializers.py
2
+from rest_framework import serializers
3
+from .models import FileSystemTree, DirectoryNode, GameSession
4
+
5
+
6
+class DirectoryNodeSerializer(serializers.ModelSerializer):
7
+    class Meta:
8
+        model = DirectoryNode
9
+        fields = ['id', 'name', 'path', 'is_fhs_standard', 'description', 'depth']
10
+        read_only_fields = ['depth']
11
+
12
+
13
+class FileSystemTreeSerializer(serializers.ModelSerializer):
14
+    total_directories = serializers.SerializerMethodField()
15
+    
16
+    class Meta:
17
+        model = FileSystemTree
18
+        fields = [
19
+            'id', 'name', 'created_at', 'seed', 
20
+            'player_location', 'is_completed', 'completed_at',
21
+            'tree_data', 'total_directories'
22
+        ]
23
+        read_only_fields = ['created_at', 'tree_data', 'total_directories']
24
+    
25
+    def get_total_directories(self, obj):
26
+        return obj.nodes.count()
27
+
28
+
29
+class GameSessionSerializer(serializers.ModelSerializer):
30
+    tree_name = serializers.CharField(source='tree.name', read_only=True)
31
+    
32
+    class Meta:
33
+        model = GameSession
34
+        fields = [
35
+            'id', 'tree', 'tree_name', 'player_name',
36
+            'started_at', 'completed_at', 'commands_used',
37
+            'directories_visited', 'time_taken', 'command_history'
38
+        ]
39
+        read_only_fields = [
40
+            'started_at', 'completed_at', 'time_taken', 'command_history'
41
+        ]
42
+
43
+
44
+class GameCommandSerializer(serializers.Serializer):
45
+    """Serializer for game commands"""
46
+    command = serializers.CharField(max_length=200)
47
+    session_id = serializers.IntegerField(required=False)
backend/apps/trees/tests.pyadded
@@ -0,0 +1,3 @@
1
+from django.test import TestCase
2
+
3
+# Create your tests here.
backend/apps/trees/urls.pyadded
@@ -0,0 +1,13 @@
1
+# apps/trees/urls.py
2
+from django.urls import path, include
3
+from rest_framework.routers import DefaultRouter
4
+from . import views
5
+
6
+router = DefaultRouter()
7
+router.register(r'filesystem-trees', views.FileSystemTreeViewSet)
8
+router.register(r'directories', views.DirectoryNodeViewSet)
9
+router.register(r'game-sessions', views.GameSessionViewSet)
10
+
11
+urlpatterns = [
12
+    path('', include(router.urls)),
13
+]
backend/apps/trees/views.pyadded
@@ -0,0 +1,231 @@
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=['post'])
21
+    def create_game(self, request):
22
+        """Create a new game with a generated filesystem tree"""
23
+        tree_name = request.data.get('name', 'FHS Game Tree')
24
+        max_depth = request.data.get('max_depth', 5)
25
+        dirs_per_level = request.data.get('dirs_per_level', 3)
26
+        
27
+        # Create and generate tree
28
+        tree = FileSystemTree.objects.create(name=tree_name)
29
+        tree.generate_tree(max_depth=max_depth, directories_per_level=dirs_per_level)
30
+        
31
+        # Create game session
32
+        player_name = request.data.get('player_name', 'Anonymous')
33
+        session = GameSession.objects.create(
34
+            tree=tree,
35
+            player_name=player_name
36
+        )
37
+        
38
+        serializer = self.get_serializer(tree)
39
+        return Response({
40
+            'tree': serializer.data,
41
+            'session_id': session.id,
42
+            'mole_hint': f"The mole is hiding somewhere in the filesystem!"
43
+        }, status=status.HTTP_201_CREATED)
44
+    
45
+    @action(detail=True, methods=['get'])
46
+    def current_directory(self, request, pk=None):
47
+        """Get current directory contents and player location"""
48
+        tree = self.get_object()
49
+        
50
+        try:
51
+            current_dir = DirectoryNode.objects.get(
52
+                tree=tree, 
53
+                path=tree.player_location
54
+            )
55
+            contents = current_dir.get_contents()
56
+            
57
+            return Response({
58
+                'path': tree.player_location,
59
+                'contents': DirectoryNodeSerializer(contents, many=True).data,
60
+                'parent': current_dir.parent.path if current_dir.parent else None
61
+            })
62
+        except DirectoryNode.DoesNotExist:
63
+            return Response(
64
+                {'error': 'Current directory not found'}, 
65
+                status=status.HTTP_404_NOT_FOUND
66
+            )
67
+    
68
+    @action(detail=True, methods=['post'])
69
+    def execute_command(self, request, pk=None):
70
+        """Execute a shell command in the game"""
71
+        tree = self.get_object()
72
+        command = request.data.get('command', '').strip()
73
+        session_id = request.data.get('session_id')
74
+        
75
+        if not command:
76
+            return Response(
77
+                {'error': 'No command provided'}, 
78
+                status=status.HTTP_400_BAD_REQUEST
79
+            )
80
+        
81
+        # Get session if provided
82
+        session = None
83
+        if session_id:
84
+            try:
85
+                session = GameSession.objects.get(id=session_id, tree=tree)
86
+                session.add_command(command)
87
+            except GameSession.DoesNotExist:
88
+                pass
89
+        
90
+        # Parse and execute command
91
+        parts = command.split()
92
+        cmd = parts[0] if parts else ""
93
+        
94
+        response_data = {
95
+            'command': command,
96
+            'success': False,
97
+            'output': '',
98
+            'current_path': tree.player_location
99
+        }
100
+        
101
+        if cmd == 'cd':
102
+            if len(parts) < 2:
103
+                response_data['output'] = "cd: missing operand"
104
+            else:
105
+                target = parts[1]
106
+                success, message = tree.move_player(target)
107
+                response_data['success'] = success
108
+                response_data['output'] = message
109
+                response_data['current_path'] = tree.player_location
110
+                
111
+                if success and session:
112
+                    session.directories_visited += 1
113
+                    session.save()
114
+        
115
+        elif cmd == 'ls':
116
+            try:
117
+                current_dir = DirectoryNode.objects.get(
118
+                    tree=tree, 
119
+                    path=tree.player_location
120
+                )
121
+                contents = current_dir.get_contents()
122
+                if contents:
123
+                    response_data['output'] = '\n'.join([d.name for d in contents])
124
+                else:
125
+                    response_data['output'] = ''
126
+                response_data['success'] = True
127
+            except DirectoryNode.DoesNotExist:
128
+                response_data['output'] = "ls: cannot access directory"
129
+        
130
+        elif cmd == 'pwd':
131
+            response_data['output'] = tree.player_location
132
+            response_data['success'] = True
133
+        
134
+        elif cmd == 'killall' and len(parts) > 1 and parts[1] == 'moles':
135
+            if tree.check_win_condition():
136
+                tree.is_completed = True
137
+                tree.completed_at = timezone.now()
138
+                tree.save()
139
+                
140
+                if session:
141
+                    session.completed_at = timezone.now()
142
+                    session.time_taken = session.completed_at - session.started_at
143
+                    session.save()
144
+                
145
+                response_data['output'] = "🎉 Congratulations! You found and eliminated the mole!"
146
+                response_data['success'] = True
147
+                response_data['game_won'] = True
148
+            else:
149
+                response_data['output'] = "No moles found in this directory."
150
+                response_data['success'] = True
151
+        
152
+        elif cmd == 'help':
153
+            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"""
159
+            response_data['success'] = True
160
+        
161
+        else:
162
+            response_data['output'] = f"bash: {cmd}: command not found"
163
+        
164
+        return Response(response_data)
165
+    
166
+    @action(detail=True, methods=['get'])
167
+    def hint(self, request, pk=None):
168
+        """Get a hint about the mole's location"""
169
+        tree = self.get_object()
170
+        
171
+        if not tree.mole_location:
172
+            return Response({'hint': 'No mole in this tree!'})
173
+        
174
+        mole_depth = tree.mole_location.count('/')
175
+        player_depth = tree.player_location.count('/')
176
+        
177
+        hints = []
178
+        if mole_depth > player_depth:
179
+            hints.append("The mole is deeper in the filesystem than you are.")
180
+        elif mole_depth < player_depth:
181
+            hints.append("The mole is in a shallower directory than you are.")
182
+        else:
183
+            hints.append("You're at the same depth as the mole!")
184
+        
185
+        # Give a path hint
186
+        mole_parts = tree.mole_location.split('/')
187
+        player_parts = tree.player_location.split('/')
188
+        
189
+        # Find common path
190
+        common_parts = []
191
+        for i, (m, p) in enumerate(zip(mole_parts, player_parts)):
192
+            if m == p:
193
+                common_parts.append(m)
194
+            else:
195
+                break
196
+        
197
+        if len(common_parts) == len(mole_parts):
198
+            hints.append("You're in the mole's directory! Use 'killall moles'!")
199
+        elif len(common_parts) > 1:
200
+            hints.append(f"You share a common path with the mole: {'/'.join(common_parts) or '/'}")
201
+        
202
+        return Response({'hints': hints})
203
+
204
+
205
+class DirectoryNodeViewSet(viewsets.ReadOnlyModelViewSet):
206
+    """ViewSet for browsing directory nodes"""
207
+    queryset = DirectoryNode.objects.all()
208
+    serializer_class = DirectoryNodeSerializer
209
+    
210
+    def get_queryset(self):
211
+        queryset = super().get_queryset()
212
+        tree_id = self.request.query_params.get('tree', None)
213
+        if tree_id:
214
+            queryset = queryset.filter(tree_id=tree_id)
215
+        return queryset
216
+
217
+
218
+class GameSessionViewSet(viewsets.ModelViewSet):
219
+    """ViewSet for game sessions"""
220
+    queryset = GameSession.objects.all()
221
+    serializer_class = GameSessionSerializer
222
+    
223
+    @action(detail=False, methods=['get'])
224
+    def leaderboard(self, request):
225
+        """Get the leaderboard of fastest completions"""
226
+        completed_sessions = GameSession.objects.filter(
227
+            completed_at__isnull=False
228
+        ).order_by('time_taken', 'commands_used')[:20]
229
+        
230
+        serializer = self.get_serializer(completed_sessions, many=True)
231
+        return Response(serializer.data)
backend/bashamole/__init__.pyadded
backend/bashamole/asgi.pyadded
@@ -0,0 +1,16 @@
1
+"""
2
+ASGI config for bashamole project.
3
+
4
+It exposes the ASGI callable as a module-level variable named ``application``.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
8
+"""
9
+
10
+import os
11
+
12
+from django.core.asgi import get_asgi_application
13
+
14
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bashamole.settings")
15
+
16
+application = get_asgi_application()
backend/bashamole/settings.pyadded
@@ -0,0 +1,138 @@
1
+"""
2
+Django settings for bashamole project.
3
+
4
+Generated by 'django-admin startproject' using Django 5.2.3.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/5.2/topics/settings/
8
+
9
+For the full list of settings and their values, see
10
+https://docs.djangoproject.com/en/5.2/ref/settings/
11
+"""
12
+
13
+from pathlib import Path
14
+
15
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
16
+BASE_DIR = Path(__file__).resolve().parent.parent
17
+
18
+
19
+# Quick-start development settings - unsuitable for production
20
+# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
21
+
22
+# SECURITY WARNING: keep the secret key used in production secret!
23
+SECRET_KEY = "django-insecure-xykf5$__a(xn*avfxs2njr#==cn7*+t8kg+4_lbi$(#+@5_j8g"
24
+
25
+# SECURITY WARNING: don't run with debug turned on in production!
26
+DEBUG = True
27
+
28
+ALLOWED_HOSTS = []
29
+
30
+
31
+# Application definition
32
+
33
+INSTALLED_APPS = [
34
+    "django.contrib.admin",
35
+    "django.contrib.auth",
36
+    "django.contrib.contenttypes",
37
+    "django.contrib.sessions",
38
+    "django.contrib.messages",
39
+    "django.contrib.staticfiles",
40
+    "rest_framework",
41
+    "corsheaders",
42
+    "apps.trees",
43
+]
44
+
45
+MIDDLEWARE = [
46
+    "django.middleware.security.SecurityMiddleware",
47
+    "django.contrib.sessions.middleware.SessionMiddleware",
48
+    "django.middleware.common.CommonMiddleware",
49
+    "django.middleware.csrf.CsrfViewMiddleware",
50
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
51
+    "django.contrib.messages.middleware.MessageMiddleware",
52
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
53
+    'corsheaders.middleware.CorsMiddleware',
54
+    'django.middleware.common.CommonMiddleware',
55
+]
56
+
57
+ROOT_URLCONF = "bashamole.urls"
58
+
59
+TEMPLATES = [
60
+    {
61
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
62
+        "DIRS": [],
63
+        "APP_DIRS": True,
64
+        "OPTIONS": {
65
+            "context_processors": [
66
+                "django.template.context_processors.request",
67
+                "django.contrib.auth.context_processors.auth",
68
+                "django.contrib.messages.context_processors.messages",
69
+            ],
70
+        },
71
+    },
72
+]
73
+
74
+WSGI_APPLICATION = "bashamole.wsgi.application"
75
+
76
+
77
+# Database
78
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
79
+
80
+DATABASES = {
81
+    "default": {
82
+        "ENGINE": "django.db.backends.sqlite3",
83
+        "NAME": BASE_DIR / "db.sqlite3",
84
+    }
85
+}
86
+
87
+
88
+# Password validation
89
+# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
90
+
91
+AUTH_PASSWORD_VALIDATORS = [
92
+    {
93
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
94
+    },
95
+    {
96
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
97
+    },
98
+    {
99
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
100
+    },
101
+    {
102
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
103
+    },
104
+]
105
+
106
+
107
+# Internationalization
108
+# https://docs.djangoproject.com/en/5.2/topics/i18n/
109
+
110
+LANGUAGE_CODE = "en-us"
111
+
112
+TIME_ZONE = "UTC"
113
+
114
+USE_I18N = True
115
+
116
+USE_TZ = True
117
+
118
+
119
+# Static files (CSS, JavaScript, Images)
120
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
121
+
122
+STATIC_URL = "static/"
123
+
124
+# Default primary key field type
125
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
126
+
127
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
128
+
129
+CORS_ALLOWED_ORIGINS = [
130
+    "http://localhost:3000",
131
+    "http://localhost:5173",
132
+]
133
+
134
+# API settings
135
+REST_FRAMEWORK = {
136
+    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
137
+    'PAGE_SIZE': 100
138
+}
backend/bashamole/urls.pyadded
@@ -0,0 +1,25 @@
1
+"""
2
+URL configuration for bashamole project.
3
+
4
+The `urlpatterns` list routes URLs to views. For more information please see:
5
+    https://docs.djangoproject.com/en/5.2/topics/http/urls/
6
+Examples:
7
+Function views
8
+    1. Add an import:  from my_app import views
9
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
10
+Class-based views
11
+    1. Add an import:  from other_app.views import Home
12
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
13
+Including another URLconf
14
+    1. Import the include() function: from django.urls import include, path
15
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
16
+"""
17
+
18
+from django.contrib import admin
19
+from django.urls import path, include
20
+
21
+urlpatterns = [
22
+    path('admin/', admin.site.urls),
23
+    path('api/trees/', include('apps.trees.urls')),
24
+    path('api-auth/', include('rest_framework.urls')),
25
+]
backend/bashamole/wsgi.pyadded
@@ -0,0 +1,16 @@
1
+"""
2
+WSGI config for bashamole project.
3
+
4
+It exposes the WSGI callable as a module-level variable named ``application``.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
8
+"""
9
+
10
+import os
11
+
12
+from django.core.wsgi import get_wsgi_application
13
+
14
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bashamole.settings")
15
+
16
+application = get_wsgi_application()
backend/manage.pyadded
@@ -0,0 +1,22 @@
1
+#!/usr/bin/env python
2
+"""Django's command-line utility for administrative tasks."""
3
+import os
4
+import sys
5
+
6
+
7
+def main():
8
+    """Run administrative tasks."""
9
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bashamole.settings")
10
+    try:
11
+        from django.core.management import execute_from_command_line
12
+    except ImportError as exc:
13
+        raise ImportError(
14
+            "Couldn't import Django. Are you sure it's installed and "
15
+            "available on your PYTHONPATH environment variable? Did you "
16
+            "forget to activate a virtual environment?"
17
+        ) from exc
18
+    execute_from_command_line(sys.argv)
19
+
20
+
21
+if __name__ == "__main__":
22
+    main()
backend/requirements.txtadded
@@ -0,0 +1,5 @@
1
+asgiref==3.8.1
2
+Django==5.2.3
3
+django-cors-headers==4.7.0
4
+djangorestframework==3.16.0
5
+sqlparse==0.5.3
package.jsonadded
@@ -0,0 +1,21 @@
1
+{
2
+  "name": "bashamole",
3
+  "version": "1.0.0",
4
+  "private": true,
5
+  "scripts": {
6
+    "dev:backend": "cd backend && python manage.py runserver",
7
+    "dev:frontend": "cd frontend && npm run dev",
8
+    "install:backend": "cd backend && pip install -r requirements.txt",
9
+    "install:frontend": "cd frontend && npm install"
10
+  },
11
+  "dependencies": {
12
+    "@types/d3": "^7.4.3",
13
+    "axios": "^1.10.0",
14
+    "d3": "^7.9.0"
15
+  },
16
+  "devDependencies": {
17
+    "autoprefixer": "^10.4.21",
18
+    "postcss": "^8.5.5",
19
+    "tailwindcss": "^4.1.10"
20
+  }
21
+}