Design updates

- gameplay draft
- core architecture (Python core)
This commit is contained in:
Mikkeli Matlock
2026-01-10 00:02:01 +09:00
parent 5c8e80da13
commit 8635e4788a
6 changed files with 2311 additions and 1 deletions

145
tools/filetree.py Normal file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""
Create an ASCII file tree while respecting .gitignore when possible.
Usage:
python tools/filetree.py [output_path]
"""
from __future__ import annotations
import fnmatch
import os
import subprocess
import sys
from typing import Dict, Iterable, List
def find_repo_root(start: str) -> str:
cur = os.path.abspath(start)
while True:
if os.path.isdir(os.path.join(cur, ".git")):
return cur
parent = os.path.dirname(cur)
if parent == cur:
return os.path.abspath(start)
cur = parent
def git_available() -> bool:
try:
subprocess.run(["git", "--version"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return True
except Exception:
return False
def list_files_via_git(root: str) -> List[str]:
result = subprocess.run(
["git", "-C", root, "ls-files", "--others", "--cached", "--exclude-standard"],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
files = [line.strip() for line in result.stdout.splitlines() if line.strip()]
return files
def parse_gitignore(root: str) -> List[str]:
path = os.path.join(root, ".gitignore")
if not os.path.isfile(path):
return []
patterns: List[str] = []
with open(path, "r", encoding="utf-8") as f:
for raw in f:
line = raw.strip()
if not line or line.startswith("#"):
continue
patterns.append(line)
return patterns
def is_ignored(path_rel: str, is_dir: bool, patterns: List[str]) -> bool:
# Minimal .gitignore matching: supports *, ?, **, leading /, trailing /
path_rel = path_rel.replace(os.sep, "/")
ignored = False
for pat in patterns:
negate = pat.startswith("!")
if negate:
pat = pat[1:]
if pat.startswith("/"):
pat = pat[1:]
if pat.endswith("/") and not is_dir:
continue
pat = pat.rstrip("/")
if fnmatch.fnmatch(path_rel, pat) or fnmatch.fnmatch(os.path.basename(path_rel), pat):
ignored = not negate
return ignored
def list_files_with_fallback(root: str) -> List[str]:
patterns = parse_gitignore(root)
files: List[str] = []
for dirpath, dirnames, filenames in os.walk(root):
rel_dir = os.path.relpath(dirpath, root)
if rel_dir == ".":
rel_dir = ""
# Prune ignored directories early
pruned = []
for d in dirnames:
rel_path = os.path.join(rel_dir, d) if rel_dir else d
if is_ignored(rel_path, True, patterns):
continue
pruned.append(d)
dirnames[:] = pruned
for name in filenames:
rel_path = os.path.join(rel_dir, name) if rel_dir else name
if is_ignored(rel_path, False, patterns):
continue
files.append(rel_path.replace(os.sep, "/"))
return files
def build_tree(paths: Iterable[str]) -> Dict[str, dict]:
root: Dict[str, dict] = {}
for path in paths:
parts = [p for p in path.split("/") if p]
node = root
for part in parts:
node = node.setdefault(part, {})
return root
def render_tree(node: Dict[str, dict], prefix: str = "") -> List[str]:
lines: List[str] = []
entries = sorted(node.keys())
for i, name in enumerate(entries):
is_last = i == len(entries) - 1
connector = "\\-- " if is_last else "|-- "
lines.append(f"{prefix}{connector}{name}")
child = node[name]
if child:
extension = " " if is_last else "| "
lines.extend(render_tree(child, prefix + extension))
return lines
def main() -> int:
out_path = sys.argv[1] if len(sys.argv) > 1 else "FILETREE.txt"
root = find_repo_root(os.getcwd())
if os.path.isdir(os.path.join(root, ".git")) and git_available():
files = list_files_via_git(root)
else:
files = list_files_with_fallback(root)
tree = build_tree(files)
lines = ["."]
lines.extend(render_tree(tree))
out_abs = os.path.join(root, out_path)
with open(out_abs, "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())