mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 03:41:40 +00:00
feat(career): genre families — sub-genres inherit the family drill (#951)
The enrichment fallback made the passport rack real (hundreds of MB
sub-genres) but only the five exact umbrella keys carried Virtuoso
drills. Genres now resolve to a family by keyword substring (MB's
vocabulary is open — 'metalcore' must hit metal without an alias),
first-match-wins in list order ('blues rock' → blues), and inherit the
family's requirement from the same genres map. Exact entries still win;
per-instrument scoping unchanged; unmatched genres stay songs-only.
Data: families for metal (incl. djent/grindcore/thrash/doom), blues,
jazz (bebop/swing/bossa), funk (disco), rock (punk/grunge/shoegaze).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
329cc86315
commit
6cc0312661
@@ -3,6 +3,13 @@
|
|||||||
"songs": 5,
|
"songs": 5,
|
||||||
"min_stars": 2
|
"min_stars": 2
|
||||||
},
|
},
|
||||||
|
"families": [
|
||||||
|
{ "key": "metal", "match": ["metal", "djent", "grindcore", "thrash", "doom"] },
|
||||||
|
{ "key": "blues", "match": ["blues"] },
|
||||||
|
{ "key": "jazz", "match": ["jazz", "bebop", "swing", "bossa"] },
|
||||||
|
{ "key": "funk", "match": ["funk", "disco"] },
|
||||||
|
{ "key": "rock", "match": ["rock", "punk", "grunge", "shoegaze"] }
|
||||||
|
],
|
||||||
"genres": {
|
"genres": {
|
||||||
"blues": { "virtuoso_nodes": { "guitar": ["blues_shuffle"] } },
|
"blues": { "virtuoso_nodes": { "guitar": ["blues_shuffle"] } },
|
||||||
"rock": { "virtuoso_nodes": { "guitar": ["rock_power_backbeat"] } },
|
"rock": { "virtuoso_nodes": { "guitar": ["rock_power_backbeat"] } },
|
||||||
|
|||||||
@@ -276,12 +276,33 @@ def _library_genres():
|
|||||||
key=lambda r: (-r["songs_in_library"], r["genre_key"]))
|
key=lambda r: (-r["songs_in_library"], r["genre_key"]))
|
||||||
|
|
||||||
|
|
||||||
|
def _genre_family(gkey):
|
||||||
|
"""First family whose keyword appears in the genre key (substring — MB's
|
||||||
|
vocabulary is open: 'metalcore' must hit the 'metal' family without an
|
||||||
|
exact alias). List order decides ambiguity: families are checked top to
|
||||||
|
bottom, so 'blues rock' lands on whichever of blues/rock is listed first."""
|
||||||
|
for fam in _state["passports_content"].get("families") or []:
|
||||||
|
if not isinstance(fam, dict):
|
||||||
|
continue
|
||||||
|
for kw in fam.get("match") or []:
|
||||||
|
if isinstance(kw, str) and kw and kw in gkey:
|
||||||
|
return fam.get("key")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _badge_requirement(gkey, instrument="guitar"):
|
def _badge_requirement(gkey, instrument="guitar"):
|
||||||
cfg = _state["passports_content"]
|
cfg = _state["passports_content"]
|
||||||
req = dict(cfg.get("badge_requirement") or {})
|
req = dict(cfg.get("badge_requirement") or {})
|
||||||
req.setdefault("songs", 5)
|
req.setdefault("songs", 5)
|
||||||
req.setdefault("min_stars", 2)
|
req.setdefault("min_stars", 2)
|
||||||
override = (cfg.get("genres") or {}).get(gkey)
|
# Exact per-genre override wins; otherwise the genre inherits its FAMILY's
|
||||||
|
# requirement — so 'death metal' / 'metalcore' passports carry the metal
|
||||||
|
# drill without curating every MB sub-genre by hand.
|
||||||
|
genres_cfg = cfg.get("genres") or {}
|
||||||
|
override = genres_cfg.get(gkey)
|
||||||
|
if not isinstance(override, dict):
|
||||||
|
family = _genre_family(gkey)
|
||||||
|
override = genres_cfg.get(family) if family else None
|
||||||
if isinstance(override, dict):
|
if isinstance(override, dict):
|
||||||
req.update(override)
|
req.update(override)
|
||||||
# virtuoso_nodes: {instrument: [node_ids]} — a passport only carries its
|
# virtuoso_nodes: {instrument: [node_ids]} — a passport only carries its
|
||||||
|
|||||||
@@ -208,3 +208,40 @@ def test_drill_state_merge_is_gained_only(client, meta_db):
|
|||||||
p = _passport(client)
|
p = _passport(client)
|
||||||
assert p["drills"]["cleared"] == ["blues_shuffle"]
|
assert p["drills"]["cleared"] == ["blues_shuffle"]
|
||||||
assert p["badge"] == "earned"
|
assert p["badge"] == "earned"
|
||||||
|
|
||||||
|
|
||||||
|
def test_genre_families_inherit_drills(client, meta_db):
|
||||||
|
# 'death metal' has no exact entry — it inherits the metal family's drill.
|
||||||
|
for i in range(5):
|
||||||
|
meta_db.add(f"dm{i}.feedpak", 0, 0.9, genre="Death Metal", arrangements=LEAD)
|
||||||
|
_open(client, "guitar", "Death Metal")
|
||||||
|
p = _passport(client, "guitar", "death metal")
|
||||||
|
assert p["drills"]["required"] == ["melodic_metal_gallop"]
|
||||||
|
assert p["badge"] == "in_progress"
|
||||||
|
# 'metalcore' (single word) matches by substring, no alias needed.
|
||||||
|
_open(client, "guitar", "Metalcore")
|
||||||
|
assert _passport(client, "guitar", "metalcore")["drills"]["required"] == \
|
||||||
|
["melodic_metal_gallop"]
|
||||||
|
# 'blues rock' resolves by family LIST ORDER: blues comes before rock.
|
||||||
|
_open(client, "guitar", "Blues Rock")
|
||||||
|
assert _passport(client, "guitar", "blues rock")["drills"]["required"] == \
|
||||||
|
["blues_shuffle"]
|
||||||
|
# A genre outside every family stays songs-only.
|
||||||
|
_open(client, "guitar", "Reggae")
|
||||||
|
assert _passport(client, "guitar", "reggae")["drills"]["required"] == []
|
||||||
|
# Exact per-genre entries still beat the family (the shipped 'metal' entry
|
||||||
|
# IS the exact entry for genre key 'metal').
|
||||||
|
_open(client, "guitar", "Metal")
|
||||||
|
assert _passport(client, "guitar", "metal")["drills"]["required"] == \
|
||||||
|
["melodic_metal_gallop"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_family_drills_stay_per_instrument(client, meta_db):
|
||||||
|
# Family inheritance must not leak guitar drills onto other instruments.
|
||||||
|
keys_arr = [{"type": "lead", "name": "Keys"}]
|
||||||
|
for i in range(5):
|
||||||
|
meta_db.add(f"kdm{i}.feedpak", 0, 0.9, genre="Death Metal", arrangements=keys_arr)
|
||||||
|
_open(client, "keys", "Death Metal")
|
||||||
|
p = _passport(client, "keys", "death metal")
|
||||||
|
assert p["drills"]["required"] == []
|
||||||
|
assert p["badge"] == "earned"
|
||||||
|
|||||||
Reference in New Issue
Block a user