- Digging: hold E (or DIG touch button) by a crate wall, 2s rooted channel, once per tile. Loot: 70% +200 vibe, 25% gold record, 4% grail (full vibe), 1% bootleg (every bin on the level erupts). - Treasure rooms: every 3rd level, 20s bonus room strewn with gold, no enemies, booth exits instantly, "CLOSING TIME" when the timer dies. - THE ARCHIVE (4th theme): FLUX-generated dark tileset, real darkness via RenderTexture with radial glow holes at player + lamps, dust mites spawn in packs of 3, Mite Queen miniboss (12 HP, broods mites every 5s, airhorn-immune, drops 3 gold + 1000 pts). Archive tune pool: 195 dnb/hardcore/breakbeat/ electro 12"s (718 total tunes now). - Infinite: levels past the 4 authored maps are deterministic seeded mazes (recursive backtracker + loops + rooms), difficulty scales with depth. All verified in-browser via engine stepping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.8 KiB
Python
99 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract the trainspotting tune pools from local discogs_full -> public/tunes.json.
|
|
|
|
Canon = most-pressed masters (repress count across 10.4M releases = mass-owned classic).
|
|
The club pool additionally requires the main release to be a 12" — keeps it club wax,
|
|
not Madonna albums tagged House. Grail flag = top ~8% of each chunk (certified classics,
|
|
gold flash in-game). Null label renders as "White Label", which is exactly right.
|
|
|
|
Read-only against discogs_full (canonical). release.master_id is indexed; each chunk
|
|
query returns in ~1s.
|
|
|
|
python3 assets/extract_tunes.py
|
|
"""
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
PSQL = "/opt/homebrew/opt/postgresql@16/bin/psql"
|
|
OUT = Path(__file__).resolve().parent.parent / "public" / "tunes.json"
|
|
|
|
# pool -> chunks of (tag, is_genre, year_lo, year_hi, n, twelve_inch_only)
|
|
POOLS = {
|
|
"shop": [ # RECORD SHOP: disco / soul / funk
|
|
("Disco", False, 1968, 1989, 70, False),
|
|
("Soul", False, 1962, 1985, 70, False),
|
|
("Funk", False, 1968, 1985, 70, False),
|
|
],
|
|
"fair": [ # RECORD FAIR: hip hop / jazz-funk
|
|
("Hip Hop", True, 1979, 2005, 100, False),
|
|
("Jazz-Funk", False, 1968, 1995, 70, False),
|
|
],
|
|
"club": [ # CLUB NIGHT: house / techno / jungle, 12"s only
|
|
("House", False, 1985, 2005, 70, True),
|
|
("Techno", False, 1985, 2005, 70, True),
|
|
("Jungle", False, 1990, 2000, 50, True),
|
|
],
|
|
"archive": [ # THE ARCHIVE: the deep stuff, 12"s only
|
|
("Drum n Bass", False, 1992, 2005, 60, True),
|
|
("Hardcore", False, 1990, 1997, 50, True),
|
|
("Breakbeat", False, 1988, 2005, 50, True),
|
|
("Electro", False, 1982, 2005, 50, True),
|
|
],
|
|
}
|
|
GRAIL_FRACTION = 0.08 # top of each ranked chunk = certified classic
|
|
|
|
|
|
def q(sql):
|
|
r = subprocess.run([PSQL, "-d", "discogs_full", "--csv", "-t", "-c", sql],
|
|
capture_output=True, text=True, check=True)
|
|
import csv, io
|
|
return list(csv.reader(io.StringIO(r.stdout)))
|
|
|
|
|
|
def fetch(tag, is_genre, ylo, yhi, limit, twelve):
|
|
join = ("JOIN master_genre mg ON mg.master_id=m.id AND mg.genre=" if is_genre
|
|
else "JOIN master_style ms ON ms.master_id=m.id AND ms.style=")
|
|
twelve_sql = ("""AND EXISTS (SELECT 1 FROM release_format rf
|
|
WHERE rf.release_id=m.main_release AND rf.descriptions LIKE '%12"%')"""
|
|
if twelve else "")
|
|
rows = q(f"""
|
|
SELECT m.title, m.year, COUNT(r.id) AS presses,
|
|
(SELECT ma.artist_name FROM master_artist ma WHERE ma.master_id=m.id
|
|
ORDER BY ma.position NULLS LAST LIMIT 1),
|
|
(SELECT rl.label_name FROM release_label rl WHERE rl.release_id=m.main_release LIMIT 1)
|
|
FROM master m {join}'{tag}'
|
|
JOIN release r ON r.master_id=m.id
|
|
WHERE m.year BETWEEN {ylo} AND {yhi} {twelve_sql}
|
|
GROUP BY m.id ORDER BY presses DESC LIMIT {limit}""")
|
|
grails = max(1, int(len(rows) * GRAIL_FRACTION))
|
|
out = []
|
|
for i, r in enumerate(rows):
|
|
if len(r) < 5 or not r[0] or not r[3]:
|
|
continue
|
|
out.append({
|
|
"a": r[3], "t": r[0],
|
|
"l": r[4] or "White Label",
|
|
"y": int(r[1]) if r[1] else None,
|
|
**({"g": 1} if i < grails else {}),
|
|
})
|
|
return out
|
|
|
|
|
|
pools = {}
|
|
for pool, chunks in POOLS.items():
|
|
seen = set()
|
|
tunes = []
|
|
for chunk in chunks:
|
|
for tune in fetch(*chunk):
|
|
key = (tune["a"].lower(), tune["t"].lower())
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
tunes.append(tune)
|
|
pools[pool] = tunes
|
|
print(f"{pool}: {len(tunes)} tunes ({sum(1 for t in tunes if t.get('g'))} grails)")
|
|
|
|
OUT.write_text(json.dumps(pools, ensure_ascii=False, separators=(",", ":")))
|
|
print(f"wrote {OUT} ({OUT.stat().st_size // 1024}KB, {sum(len(v) for v in pools.values())} total)")
|