feat: language affinity system — persistent XP + tier progression
Adds LANGUAGE_TIERS with 6 tiers: discovering → familiar → comfortable → proficient → expert → master (thresholds: 0/50/150/350/700/1200 XP). add_language_affinity() writes to roster.json['language_affinities'], accumulating across sessions. Returns (leveled_up, old_tier, new_tier) so the Edit/Write branch can fire a level-up message immediately (Edit PostToolUse additionalContext surfaces fine). Session-level languages_seen remains for the one-time Explorer bonus. Roster skill view updated to show language affinity section.
This commit is contained in:
parent
d2006727a1
commit
a9c5610914
2 changed files with 90 additions and 1 deletions
|
|
@ -77,6 +77,54 @@ def add_session_xp(amount: int):
|
|||
save_json(roster_file, roster)
|
||||
|
||||
|
||||
LANGUAGE_TIERS = [
|
||||
(0, "discovering"),
|
||||
(50, "familiar"),
|
||||
(150, "comfortable"),
|
||||
(350, "proficient"),
|
||||
(700, "expert"),
|
||||
(1200, "master"),
|
||||
]
|
||||
|
||||
|
||||
def _tier_for_xp(xp: int) -> tuple[int, str]:
|
||||
"""Return (level_index, tier_label) for a given XP total."""
|
||||
level = 0
|
||||
label = LANGUAGE_TIERS[0][1]
|
||||
for i, (threshold, name) in enumerate(LANGUAGE_TIERS):
|
||||
if xp >= threshold:
|
||||
level = i
|
||||
label = name
|
||||
return level, label
|
||||
|
||||
|
||||
def get_language_affinity(lang: str) -> dict:
|
||||
"""Return the affinity entry for lang from roster.json, or a fresh one."""
|
||||
roster = load_json(BUDDYMON_DIR / "roster.json")
|
||||
return roster.get("language_affinities", {}).get(lang, {"xp": 0, "level": 0, "tier": "discovering"})
|
||||
|
||||
|
||||
def add_language_affinity(lang: str, xp_amount: int) -> tuple[bool, str, str]:
|
||||
"""Add XP to lang's affinity. Returns (leveled_up, old_tier, new_tier)."""
|
||||
roster_file = BUDDYMON_DIR / "roster.json"
|
||||
roster = load_json(roster_file)
|
||||
affinities = roster.setdefault("language_affinities", {})
|
||||
entry = affinities.get(lang, {"xp": 0, "level": 0, "tier": "discovering"})
|
||||
|
||||
old_level, old_tier = _tier_for_xp(entry["xp"])
|
||||
entry["xp"] = entry.get("xp", 0) + xp_amount
|
||||
new_level, new_tier = _tier_for_xp(entry["xp"])
|
||||
entry["level"] = new_level
|
||||
entry["tier"] = new_tier
|
||||
|
||||
affinities[lang] = entry
|
||||
roster["language_affinities"] = affinities
|
||||
save_json(roster_file, roster)
|
||||
|
||||
leveled_up = new_level > old_level
|
||||
return leveled_up, old_tier, new_tier
|
||||
|
||||
|
||||
def get_languages_seen():
|
||||
session = load_json(BUDDYMON_DIR / "session.json")
|
||||
return set(session.get("languages_seen", []))
|
||||
|
|
@ -285,6 +333,23 @@ def format_new_language_message(lang: str, buddy_display: str) -> str:
|
|||
)
|
||||
|
||||
|
||||
def format_language_levelup_message(lang: str, old_tier: str, new_tier: str, total_xp: int, buddy_display: str) -> str:
|
||||
tier_emojis = {
|
||||
"discovering": "🔭",
|
||||
"familiar": "📖",
|
||||
"comfortable": "🛠️",
|
||||
"proficient": "⚡",
|
||||
"expert": "🎯",
|
||||
"master": "👑",
|
||||
}
|
||||
emoji = tier_emojis.get(new_tier, "⬆️")
|
||||
return (
|
||||
f"\n{emoji} **{lang} affinity: {old_tier} → {new_tier}!**\n"
|
||||
f" {buddy_display} has grown more comfortable in {lang}.\n"
|
||||
f" *Total {lang} XP: {total_xp}*\n"
|
||||
)
|
||||
|
||||
|
||||
def format_commit_message(streak: int, buddy_display: str) -> str:
|
||||
if streak < 5:
|
||||
return ""
|
||||
|
|
@ -394,13 +459,14 @@ def main():
|
|||
commit_xp = 20
|
||||
add_session_xp(commit_xp)
|
||||
|
||||
# ── Write / Edit: new language detection + test file encounters ───────
|
||||
# ── Write / Edit: new language detection + affinity + test file encounters ─
|
||||
elif tool_name in ("Write", "Edit", "MultiEdit"):
|
||||
file_path = tool_input.get("file_path", "")
|
||||
if file_path:
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
lang = KNOWN_EXTENSIONS.get(ext)
|
||||
if lang:
|
||||
# Session-level "first encounter" bonus
|
||||
seen = get_languages_seen()
|
||||
if lang not in seen:
|
||||
add_language_seen(lang)
|
||||
|
|
@ -408,6 +474,13 @@ def main():
|
|||
msg = format_new_language_message(lang, buddy_display)
|
||||
messages.append(msg)
|
||||
|
||||
# Persistent affinity XP — always accumulates
|
||||
leveled_up, old_tier, new_tier = add_language_affinity(lang, 3)
|
||||
if leveled_up:
|
||||
affinity = get_language_affinity(lang)
|
||||
msg = format_language_levelup_message(lang, old_tier, new_tier, affinity["xp"], buddy_display)
|
||||
messages.append(msg)
|
||||
|
||||
# TestSpecter: editing a test file with no active encounter
|
||||
if not get_active_encounter():
|
||||
enc = match_test_file_encounter(file_path, catalog)
|
||||
|
|
|
|||
|
|
@ -259,8 +259,24 @@ Read roster and display:
|
|||
🌐 CORSCurse — caught 2026-03-28
|
||||
|
||||
❓ ??? — [n] more creatures to discover...
|
||||
|
||||
🗺️ Language Affinities
|
||||
──────────────────────────────────────────
|
||||
🛠️ Python comfortable (Lv.2 · 183 XP)
|
||||
📖 TypeScript familiar (Lv.1 · 72 XP)
|
||||
🔭 Rust discovering (Lv.0 · 9 XP)
|
||||
```
|
||||
|
||||
Tier emoji mapping:
|
||||
- 🔭 discovering (0 XP)
|
||||
- 📖 familiar (50 XP)
|
||||
- 🛠️ comfortable (150 XP)
|
||||
- ⚡ proficient (350 XP)
|
||||
- 🎯 expert (700 XP)
|
||||
- 👑 master (1200 XP)
|
||||
|
||||
Read `roster.json` → `language_affinities`. Skip this section if empty.
|
||||
|
||||
---
|
||||
|
||||
## `help`
|
||||
|
|
|
|||
Loading…
Reference in a new issue