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
This commit is contained in:
+100
@@ -0,0 +1,100 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user