diff --git a/mcp/bigmind/bigmind/profile_builder.py b/mcp/bigmind/bigmind/profile_builder.py index 3b370e3..c6c77f7 100644 --- a/mcp/bigmind/bigmind/profile_builder.py +++ b/mcp/bigmind/bigmind/profile_builder.py @@ -435,10 +435,10 @@ def compute_achievements(user_id: str) -> list[dict]: # ── Assemble ────────────────────────────────────────────────────────────── A = [] - def _add(id_, icon, name, desc, unlocked, unlocked_at, condition, extra=None): + def _add(id_, icon, name, desc, unlocked, unlocked_at, condition, extra=None, image=None): A.append(dict(id=id_, icon=icon, name=name, description=desc, unlocked=unlocked, unlocked_at=unlocked_at, - condition=condition, extra=extra)) + condition=condition, extra=extra, image=image)) _add("first_breath", "🌱", "First Breath", "Opened the very first session", @@ -539,6 +539,138 @@ def compute_achievements(user_id: str) -> list[dict]: sniper_row is not None, _dt(sniper_row[0]) if sniper_row else None, "Save 500,000+ tokens in a single operation") + # ── Tiered Achievement Badges (20 PNG) ──────────────────────────────────── + # NOTE: conn is already closed above; open a fresh connection for tiered queries + + tiers = ["bronze", "silver", "gold", "platinum"] + tier_names = ["Bronze", "Silver", "Gold", "Platinum"] + + with db() as conn2: + # Networker (people directory) + try: + people_count = conn2.execute( + "SELECT COUNT(*) FROM people WHERE user_id=?", (user_id,) + ).fetchone()[0] + except Exception: + people_count = 0 + for i, thresh in enumerate([1, 5, 25, 100]): + unlocked = people_count >= thresh + unlocked_at = None + if unlocked: + try: + row = conn2.execute( + "SELECT created_at FROM people WHERE user_id=?" + " ORDER BY created_at ASC LIMIT 1 OFFSET ?", + (user_id, thresh - 1) + ).fetchone() + except Exception: + row = None + unlocked_at = _dt(row[0]) if row else None + _add( + f"networker_{tiers[i]}", None, f"Networker {tier_names[i]}", + f"Added your {thresh:,}+ person to the directory", + unlocked, unlocked_at, + f"Reach {thresh:,} people (now: {people_count:,})", + image=f"static/achievements/networker_{tiers[i]}.png" + ) + + # Token Sniper (max single token save) + try: + max_token = conn2.execute( + "SELECT COALESCE(MAX(tokens_saved_estimate), 0) FROM token_saves WHERE user_id=?", + (user_id,) + ).fetchone()[0] + except Exception: + max_token = 0 + for i, thresh in enumerate([10000, 50000, 250000, 1000000]): + unlocked = max_token >= thresh + unlocked_at = None + if unlocked: + try: + row = conn2.execute( + "SELECT created_at FROM token_saves" + " WHERE user_id=? AND tokens_saved_estimate >= ?" + " ORDER BY created_at ASC LIMIT 1", + (user_id, thresh) + ).fetchone() + except Exception: + row = None + unlocked_at = _dt(row[0]) if row else None + _add( + f"tokensniper_{tiers[i]}", None, f"Token Sniper {tier_names[i]}", + f"Single shot saved {thresh:,}+ tokens", + unlocked, unlocked_at, + f"Max single save {thresh:,}+ (current max: {max_token:,})", + image=f"static/achievements/tokensniper_{tiers[i]}.png" + ) + + # Hypothesis Master (confirmed hypotheses) + try: + confirmed_hyp_count = conn2.execute( + "SELECT COUNT(*) FROM hypotheses WHERE user_id=? AND status='confirmed'", + (user_id,) + ).fetchone()[0] + except Exception: + confirmed_hyp_count = 0 + for i, thresh in enumerate([3, 10, 25, 100]): + unlocked = confirmed_hyp_count >= thresh + unlocked_at = None + if unlocked: + row = conn2.execute( + "SELECT resolved_at FROM hypotheses" + " WHERE user_id=? AND status='confirmed'" + " ORDER BY resolved_at ASC LIMIT 1 OFFSET ?", + (user_id, thresh - 1) + ).fetchone() + unlocked_at = _dt(row[0]) if row else None + _add( + f"hypothesismaster_{tiers[i]}", None, f"Hypothesis Master {tier_names[i]}", + f"Confirmed {thresh:,}+ predictions right", + unlocked, unlocked_at, + f"Confirm {thresh:,}+ hypotheses (now: {confirmed_hyp_count:,})", + image=f"static/achievements/hypothesismaster_{tiers[i]}.png" + ) + + # Memory Architect (facts stored — fact_count already computed above) + for i, thresh in enumerate([25, 100, 500, 2500]): + unlocked = fact_count >= thresh + unlocked_at = None + if unlocked: + row = conn2.execute( + "SELECT created_at FROM facts" + " WHERE user_id=? AND (deprecated IS NULL OR deprecated=0)" + " ORDER BY created_at ASC LIMIT 1 OFFSET ?", + (user_id, thresh - 1) + ).fetchone() + unlocked_at = _dt(row[0]) if row else None + _add( + f"memoryarchitect_{tiers[i]}", None, f"Memory Architect {tier_names[i]}", + f"Stored {thresh:,}+ facts in your brain", + unlocked, unlocked_at, + f"Store {thresh:,}+ facts (now: {fact_count:,})", + image=f"static/achievements/memoryarchitect_{tiers[i]}.png" + ) + + # Session Veteran (session_count already computed above) + for i, thresh in enumerate([50, 250, 1000, 5000]): + unlocked = session_count >= thresh + unlocked_at = None + if unlocked: + row = conn2.execute( + "SELECT started_at FROM sessions" + " WHERE user_id=? AND ended_at IS NOT NULL" + " ORDER BY started_at ASC LIMIT 1 OFFSET ?", + (user_id, thresh - 1) + ).fetchone() + unlocked_at = _dt(row[0]) if row else None + _add( + f"sessionveteran_{tiers[i]}", None, f"Session Veteran {tier_names[i]}", + f"Completed {thresh:,}+ sessions", + unlocked, unlocked_at, + f"Complete {thresh:,}+ sessions (now: {session_count:,})", + image=f"static/achievements/sessionveteran_{tiers[i]}.png" + ) + return A diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_145807.png b/mcp/bigmind/bigmind/static/achievements/20260404_145807.png new file mode 100644 index 0000000..2b14220 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_145807.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_145811.png b/mcp/bigmind/bigmind/static/achievements/20260404_145811.png new file mode 100644 index 0000000..017a5f1 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_145811.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150053.png b/mcp/bigmind/bigmind/static/achievements/20260404_150053.png new file mode 100644 index 0000000..47c0f29 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150053.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150057.png b/mcp/bigmind/bigmind/static/achievements/20260404_150057.png new file mode 100644 index 0000000..6e8135b Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150057.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150101.png b/mcp/bigmind/bigmind/static/achievements/20260404_150101.png new file mode 100644 index 0000000..5b92880 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150101.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150106.png b/mcp/bigmind/bigmind/static/achievements/20260404_150106.png new file mode 100644 index 0000000..16d9aa6 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150106.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150117.png b/mcp/bigmind/bigmind/static/achievements/20260404_150117.png new file mode 100644 index 0000000..7cfba70 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150117.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150122.png b/mcp/bigmind/bigmind/static/achievements/20260404_150122.png new file mode 100644 index 0000000..0274615 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150122.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150126.png b/mcp/bigmind/bigmind/static/achievements/20260404_150126.png new file mode 100644 index 0000000..8e9c607 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150126.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150130.png b/mcp/bigmind/bigmind/static/achievements/20260404_150130.png new file mode 100644 index 0000000..b07dbb0 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150130.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150153.png b/mcp/bigmind/bigmind/static/achievements/20260404_150153.png new file mode 100644 index 0000000..b7ab2b6 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150153.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150158.png b/mcp/bigmind/bigmind/static/achievements/20260404_150158.png new file mode 100644 index 0000000..331afd0 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150158.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150202.png b/mcp/bigmind/bigmind/static/achievements/20260404_150202.png new file mode 100644 index 0000000..cfb682f Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150202.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150207.png b/mcp/bigmind/bigmind/static/achievements/20260404_150207.png new file mode 100644 index 0000000..40b35b0 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150207.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150224.png b/mcp/bigmind/bigmind/static/achievements/20260404_150224.png new file mode 100644 index 0000000..d47606a Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150224.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150228.png b/mcp/bigmind/bigmind/static/achievements/20260404_150228.png new file mode 100644 index 0000000..9a3d249 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150228.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150233.png b/mcp/bigmind/bigmind/static/achievements/20260404_150233.png new file mode 100644 index 0000000..90eb785 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150233.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150238.png b/mcp/bigmind/bigmind/static/achievements/20260404_150238.png new file mode 100644 index 0000000..6a0300d Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150238.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150252.png b/mcp/bigmind/bigmind/static/achievements/20260404_150252.png new file mode 100644 index 0000000..c1f058c Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150252.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150257.png b/mcp/bigmind/bigmind/static/achievements/20260404_150257.png new file mode 100644 index 0000000..53b9bdd Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150257.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/20260404_150302.png b/mcp/bigmind/bigmind/static/achievements/20260404_150302.png new file mode 100644 index 0000000..539be40 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/20260404_150302.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/hypothesismaster_bronze.png b/mcp/bigmind/bigmind/static/achievements/hypothesismaster_bronze.png new file mode 100644 index 0000000..6b255f9 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/hypothesismaster_bronze.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/hypothesismaster_gold.png b/mcp/bigmind/bigmind/static/achievements/hypothesismaster_gold.png new file mode 100644 index 0000000..8cdd649 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/hypothesismaster_gold.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/hypothesismaster_platinum.png b/mcp/bigmind/bigmind/static/achievements/hypothesismaster_platinum.png new file mode 100644 index 0000000..eabecb4 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/hypothesismaster_platinum.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/hypothesismaster_silver.png b/mcp/bigmind/bigmind/static/achievements/hypothesismaster_silver.png new file mode 100644 index 0000000..8819a25 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/hypothesismaster_silver.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/memoryarchitect_bronze.png b/mcp/bigmind/bigmind/static/achievements/memoryarchitect_bronze.png new file mode 100644 index 0000000..16f5c1a Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/memoryarchitect_bronze.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/memoryarchitect_gold.png b/mcp/bigmind/bigmind/static/achievements/memoryarchitect_gold.png new file mode 100644 index 0000000..cb45c07 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/memoryarchitect_gold.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/memoryarchitect_platinum.png b/mcp/bigmind/bigmind/static/achievements/memoryarchitect_platinum.png new file mode 100644 index 0000000..91239c6 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/memoryarchitect_platinum.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/memoryarchitect_silver.png b/mcp/bigmind/bigmind/static/achievements/memoryarchitect_silver.png new file mode 100644 index 0000000..eae525a Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/memoryarchitect_silver.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/networker_gold.png b/mcp/bigmind/bigmind/static/achievements/networker_gold.png new file mode 100644 index 0000000..3d209ee Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/networker_gold.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/networker_platinum.png b/mcp/bigmind/bigmind/static/achievements/networker_platinum.png new file mode 100644 index 0000000..3fb1f3d Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/networker_platinum.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/sessionveteran_bronze.png b/mcp/bigmind/bigmind/static/achievements/sessionveteran_bronze.png new file mode 100644 index 0000000..0290678 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/sessionveteran_bronze.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/sessionveteran_gold.png b/mcp/bigmind/bigmind/static/achievements/sessionveteran_gold.png new file mode 100644 index 0000000..4362202 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/sessionveteran_gold.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/sessionveteran_platinum.png b/mcp/bigmind/bigmind/static/achievements/sessionveteran_platinum.png new file mode 100644 index 0000000..795694b Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/sessionveteran_platinum.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/sessionveteran_silver.png b/mcp/bigmind/bigmind/static/achievements/sessionveteran_silver.png new file mode 100644 index 0000000..bdfbb56 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/sessionveteran_silver.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/tokensniper_bronze.png b/mcp/bigmind/bigmind/static/achievements/tokensniper_bronze.png new file mode 100644 index 0000000..04275c0 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/tokensniper_bronze.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/tokensniper_gold.png b/mcp/bigmind/bigmind/static/achievements/tokensniper_gold.png new file mode 100644 index 0000000..4a84cc6 Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/tokensniper_gold.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/tokensniper_platinum.png b/mcp/bigmind/bigmind/static/achievements/tokensniper_platinum.png new file mode 100644 index 0000000..65951ff Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/tokensniper_platinum.png differ diff --git a/mcp/bigmind/bigmind/static/achievements/tokensniper_silver.png b/mcp/bigmind/bigmind/static/achievements/tokensniper_silver.png new file mode 100644 index 0000000..4ae862f Binary files /dev/null and b/mcp/bigmind/bigmind/static/achievements/tokensniper_silver.png differ diff --git a/mcp/bigmind/bigmind/web.py b/mcp/bigmind/bigmind/web.py index 34f2ed2..c39b614 100644 --- a/mcp/bigmind/bigmind/web.py +++ b/mcp/bigmind/bigmind/web.py @@ -159,6 +159,22 @@ def _create_app(): return jsonify(final[:15]) + @app.route('/static/achievements/') + def achievements_image(filename: str): + from pathlib import Path + safe_name = Path(filename).name + img_path = Path('static') / 'achievements' / safe_name + if img_path.exists() and img_path.suffix.lower() in ['.png', '.jpg', '.jpeg', '.webp', '.gif']: + mimetype = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.webp': 'image/webp', + '.gif': 'image/gif', + }.get(img_path.suffix.lower(), 'image/png') + return send_file(str(img_path), mimetype=mimetype) + abort(404) + return app diff --git a/mcp/bigmind/bigmind/web_render.py b/mcp/bigmind/bigmind/web_render.py index 32df094..3a29621 100644 --- a/mcp/bigmind/bigmind/web_render.py +++ b/mcp/bigmind/bigmind/web_render.py @@ -29,18 +29,24 @@ def _render_achievements(achievements: list) -> str: def _esc(s): return (s or "").replace('"', """).replace("'", "'") - lock_overlay = "" if a["unlocked"] else '🔒' + lock_overlay = '🔒' if not a["unlocked"] else '' + + if a.get("image"): + tier = a["id"].rsplit("_", 1)[-1] + visual_html = f'
{lock_overlay}
' + else: + visual_html = f'
{a["icon"]}{lock_overlay}
' return ( - f'
' - f'
{a["icon"]}{lock_overlay}
' + f'{visual_html}' f'
{a["name"]}
' f'{date_html}' f'{countdown_html}' @@ -283,11 +289,65 @@ def _render_html(data: dict) -> str: .ach-card:not(.locked):hover {{ border-color: var(--accent); transform: translateY(-2px); }} .ach-card.locked {{ opacity: 0.35; filter: grayscale(0.6); }} .ach-card.locked:hover {{ opacity: 0.55; border-color: var(--muted); }} + + .ach-image {{ + width: 64px; + height: 64px; + border-radius: 50%; + margin: 0 auto 8px; + background-size: cover; + background-position: center; + position: relative; + }} + + .tier-bronze {{ + box-shadow: 0 0 8px rgba(205, 127, 50, 0.7); + border: 3px solid #cd7f32; + }} + + .tier-silver {{ + box-shadow: 0 0 8px rgba(170, 169, 173, 0.7); + border: 3px solid #aaa9ad; + }} + + .tier-gold {{ + box-shadow: 0 0 12px rgba(255, 215, 0, 0.8); + border: 3px solid #ffd700; + }} + + .tier-platinum {{ + box-shadow: 0 0 12px rgba(229, 228, 226, 0.8); + border: 3px solid #e5e4e2; + }} + + .ach-card.locked::after {{ + content: '🔒'; + position: absolute; + top: 8px; + right: 8px; + font-size: 20px; + opacity: 0.8; + z-index: 1; + }} + + .ach-card.locked .ach-icon, + .ach-card.locked .ach-image {{ + opacity: 0.5; + }} .ach-icon {{ font-size: 28px; line-height: 1; margin-bottom: 6px; position: relative; display: inline-block; }} .ach-lock {{ position: absolute; bottom: -4px; right: -6px; font-size: 12px; }} .ach-name {{ font-size: 10px; font-weight: 600; color: var(--text); line-height: 1.3; word-break: break-word; }} .ach-date {{ font-size: 9px; color: var(--muted); margin-top: 3px; }} .ach-countdown {{ font-size: 9px; color: var(--yellow); margin-top: 3px; font-weight: 500; }} + .ap-image {{ + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + display: block; + margin: 0 auto 8px; + }} + /* Achievement popup panel */ #ach-popup {{ display: none; position: fixed; z-index: 200; @@ -299,6 +359,15 @@ def _render_html(data: dict) -> str: #ach-popup.pinned {{ pointer-events: auto; }} #ach-popup.visible {{ display: block; }} .ap-icon {{ font-size: 40px; text-align: center; margin-bottom: 8px; }} + + .ap-image {{ + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + display: block; + margin: 0 auto 8px; + }} .ap-name {{ font-size: 15px; font-weight: 700; text-align: center; margin-bottom: 6px; }} .ap-badge {{ display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px; @@ -557,7 +626,12 @@ def _render_html(data: dict) -> str: function showPopup(card, pin) {{ var d = card.dataset; - document.getElementById('ap-icon').textContent = d.icon; + var tier = d.id.split('_').pop(); + if (d.image) {{ + document.getElementById('ap-icon').innerHTML = "\""; + }} else {{ + document.getElementById('ap-icon').textContent = d.icon; + }} document.getElementById('ap-name').textContent = d.name; var badge = document.getElementById('ap-badge'); if (d.unlocked === '1') {{ diff --git a/mcp/bigmind/tests/test_profile_builder.py b/mcp/bigmind/tests/test_profile_builder.py index 63e598e..57c6c4c 100644 --- a/mcp/bigmind/tests/test_profile_builder.py +++ b/mcp/bigmind/tests/test_profile_builder.py @@ -7,6 +7,7 @@ BIGMIND_DB_PATH + BIGMIND_USER to a fresh SQLite file per test. import pytest from datetime import datetime, timezone, timedelta from bigmind import memory_store +from bigmind.db import db from bigmind.profile_builder import compute_achievements, build_profile_data @@ -44,6 +45,11 @@ class TestComputeAchievements: "on_fire", "storyteller", "night_owl", "speed_thinker", "first_handshake", "birthday", "shared_mind", "frugal_mind", "quarter_million", "token_millionaire", "sniper", + "networker_bronze", "networker_silver", "networker_gold", "networker_platinum", + "tokensniper_bronze", "tokensniper_silver", "tokensniper_gold", "tokensniper_platinum", + "hypothesismaster_bronze", "hypothesismaster_silver", "hypothesismaster_gold", "hypothesismaster_platinum", + "memoryarchitect_bronze", "memoryarchitect_silver", "memoryarchitect_gold", "memoryarchitect_platinum", + "sessionveteran_bronze", "sessionveteran_silver", "sessionveteran_gold", "sessionveteran_platinum", } assert expected == ids @@ -325,4 +331,60 @@ class TestComputeAchievements: # At minimum: first_breath + first_handshake = 2 assert len(unlocked) >= 2 +class TestTieredAchievements: + def test_networker_bronze(self): + uid = _uid() + with db() as conn: + conn.execute("INSERT INTO people (user_id, username) VALUES (?, ?)", (uid, "test")) + conn.commit() + achs = compute_achievements(uid) + bronze = next(a for a in achs if a['id'] == 'networker_bronze') + assert bronze['unlocked'] is True + assert bronze['image'].endswith('networker_bronze.png') + def test_tokensniper_silver(self): + uid = _uid() + sid = memory_store.create_session(uid) + memory_store.log_token_save(sid, uid, "big save", 60000, "grep") + achs = compute_achievements(uid) + silver = next(a for a in achs if a['id'] == 'tokensniper_silver') + assert silver['unlocked'] is True + + def test_hypothesismaster_bronze(self): + uid = _uid() + sid = memory_store.create_session(uid) + for _ in range(3): + hid = memory_store.add_hypothesis(uid, sid, "test", 0.8) + memory_store.resolve_hypothesis(hid, uid, "confirmed", "yes") + achs = compute_achievements(uid) + bronze = next(a for a in achs if a['id'] == 'hypothesismaster_bronze') + assert bronze['unlocked'] is True + + def test_memoryarchitect_silver(self): + uid = _uid() + for _ in range(100): + memory_store.store_fact(uid, "test", f"fact {_}") + achs = compute_achievements(uid) + silver = next(a for a in achs if a['id'] == 'memoryarchitect_silver') + assert silver['unlocked'] is True + + def test_sessionveteran_bronze(self): + uid = _uid() + for _ in range(50): + sid = memory_store.create_session(uid) + _close_session(sid) + achs = compute_achievements(uid) + bronze = next(a for a in achs if a['id'] == 'sessionveteran_bronze') + assert bronze['unlocked'] is True + + def test_tiered_achievements_have_image(self): + uid = _uid() + achs = compute_achievements(uid) + tiered_ids = [ + f"{cat}_{tier}" for cat in ["networker", "tokensniper", "hypothesismaster", "memoryarchitect", "sessionveteran"] + for tier in ["bronze", "silver", "gold", "platinum"] + ] + for tid in tiered_ids: + a = next(aa for aa in achs if aa['id'] == tid) + assert a['image'] is not None + assert a['image'].endswith(tid + '.png')