Files
44r0n7andClaude Opus 5.5 b270c4e818 Deepgrain Foundry: move from a claude.ai artifact to a local repository
The game as of artifact version 24, ready for local development with
coding agents.

- Source split from three large files into 52 small ones under src/sim,
  src/render and src/ui, joined in the order in src/manifest.json by
  build.py. The joined code is unchanged apart from comments; every
  headless suite gives the same output, and screenshots in both looks
  are pixel-identical to before.
- Light tidying with no change in behaviour: one movement scan loop,
  shared hazard-stripe and belt shader helpers, unused code removed, a
  shared check helper for the test suites.
- World files can be exported as an ordinary download outside claude.ai.
- Tests: broken old scripts removed, experiments moved to tools/,
  test/run_all.py added. Tools: shot.py (repeatable screenshots),
  where.py, map.py.
- Docs: AGENTS.md, CLAUDE.md, NOTES.md (the owner's to-do inbox),
  docs/PROJECT_MAP.md, DESIGN.md, TESTING.md, systems notes, the
  owner's requests and an agent log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9kLmYADKSEjuwUN3rFxDA
2026-10-23 16:05:31 -04:00

101 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Regenerate the file and symbol index at the end of docs/PROJECT_MAP.md.
python3 tools/map.py rewrite the index (everything after the INDEX marker)
python3 tools/map.py --check exit 1 if the index is out of date (nothing is written)
The index lists every source file in build order with its line count, its header comment, and
the top-level functions and variables it declares, so a name can be found without searching.
Run it after adding, moving or renaming files or top-level functions. The hand-written part of
the map above the marker is kept as it is.
"""
import json, pathlib, re, sys
ROOT = pathlib.Path(__file__).resolve().parent.parent
MAP = ROOT / 'docs' / 'PROJECT_MAP.md'
MARK = '<!-- INDEX: generated by tools/map.py, do not edit below this line -->'
def header(text):
"""The file's leading comment (// lines or one /* block */), as one line."""
out, block = [], False
for line in text.split('\n'):
s = line.strip()
if block:
done = s.endswith('*/')
s = s.rstrip('/').rstrip('*').strip(' =')
if s: out.append(s)
if done: break
continue
if s in ("'use strict';", '') and not out: continue
if s.startswith('/*'):
block = not s.endswith('*/')
s = s.lstrip('/').lstrip('*').rstrip('/').rstrip('*').strip(' =')
if s: out.append(s)
if not block: break
elif s.startswith('//') and not s.startswith('// ----'):
out.append(s[2:].strip())
else:
break
return re.sub(r'\s+', ' ', ' '.join(out)).strip()
def symbols(text):
funcs = re.findall(r'^(?:async\s+)?function\s+([A-Za-z0-9_$]+)', text, re.M)
names = []
for m in re.finditer(r'^(?:const|let|var)\s+(.+)$', text, re.M):
decl = m.group(1)
# names at the top level of the declaration list (skip what is inside brackets)
depth, cur, parts = 0, '', []
for ch in decl:
if ch in '([{': depth += 1
elif ch in ')]}': depth -= 1
if ch == ',' and depth == 0: parts.append(cur); cur = ''
else: cur += ch
if ch == ';' and depth == 0: break
parts.append(cur)
for p in parts:
n = re.match(r'\s*([A-Za-z_$][A-Za-z0-9_$]*)', p)
if n and n.group(1) not in names: names.append(n.group(1))
return funcs, names
def index():
man = json.loads((ROOT / 'src' / 'manifest.json').read_text())
lines = [MARK, '', '## Index of source files (build order)', '']
for f in man['sim'] + man['client']:
text = (ROOT / 'src' / f).read_text()
funcs, names = symbols(text)
lines.append(f'### src/{f} ({text.count(chr(10))} lines)')
h = header(text)
if h: lines.append(h)
if funcs: lines.append('- functions: ' + ', '.join(funcs))
if names: lines.append('- variables: ' + ', '.join(names))
lines.append('')
lines += ['## Tests and tools', '']
for d in ['test', 'tools', 'tools/experiments', 'tools/scenes']:
for p in sorted((ROOT / d).glob('*')):
if p.suffix not in ('.js', '.py') : continue
text = p.read_text()
m = re.search(r'"""(.+?)(?:\n\n|""")', text, re.S) if p.suffix == '.py' else None
h = re.sub(r'\s+', ' ', m.group(1)).strip() if m else header(text)
lines.append(f'- `{d}/{p.name}`: {h}')
return '\n'.join(lines).rstrip() + '\n'
def main():
cur = MAP.read_text() if MAP.exists() else '# Project map\n\n'
head = cur.split(MARK)[0].rstrip() + '\n\n'
new = head + index()
if '--check' in sys.argv:
ok = new == cur
print('index is up to date' if ok else 'index is out of date: run python3 tools/map.py')
sys.exit(0 if ok else 1)
MAP.parent.mkdir(exist_ok=True)
MAP.write_text(new)
print(f'wrote {MAP.relative_to(ROOT)}')
if __name__ == '__main__':
main()