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
101 lines
3.9 KiB
Python
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()
|