Dynamic badges/achievements system with tiered milestones - #128
Open
JanMikes wants to merge 26 commits into
Open
Dynamic badges/achievements system with tiered milestones#128JanMikes wants to merge 26 commits into
JanMikes wants to merge 26 commits into
Conversation
JanMikes
force-pushed
the
feature/dynamic-badges-system
branch
from
July 11, 2026 11:57
f6fc773 to
f2cac13
Compare
Plug-in architecture: BadgeConditionInterface + tagged iterator. Five flagship tiered badges (Puzzles Solved, Pieces Solved, 500pc Speed, Streak, Team Player) plus existing Supporter as admin-granted single-tier. - BadgeEvaluator orchestrates recalculation with gap-filling (tier 3 jump also persists tiers 1+2) and a single TemplatedEmail per recalc pass. - Live trigger: RecalculateBadgesForPlayer dispatched from the three solving-time handlers to the async messenger transport. - Console command with --player and --backfill (DelayStamp staggering). - Profile: BadgesProfileSection component with tier medallions + NEW pill. - Catalog page at /en/badges with per-badge progress bars. - SCSS: Bronze/Silver/Gold/Platinum/Diamond radial-gradient medallions.
Filter out null entries from jsonb_array_elements when team puzzler records lack a player_id field.
GetAllPlayerIdsWithSolveTimes, GetPlayerStatsSnapshot, and GetBadges now have DB-backed tests that verify SQL syntax, null-safety, and return-type correctness against real fixture data.
The recalc handler was sending emails inside the doctrine_transaction boundary — a mailer failure rolled back badge rows too. Now the handler dispatches a separate SendBadgeNotificationEmail message to async, so badge writes commit independently of email delivery.
Use DISTINCT ON (type) with tier DESC to return one row per badge type — always the highest earned tier. Profile and email no longer list intermediate tiers.
JanMikes
force-pushed
the
feature/dynamic-badges-system
branch
from
July 13, 2026 11:25
fd724ea to
37849d6
Compare
- Construct single-consumer query services directly (inlined out of the compiled container, so container->get() fails PHPStan's symfony extension) - Narrow array-shape offsets with assertArrayHasKey before access - Move MessageBusSpy to tests/TestDouble (PSR1 one-class-per-file) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… surfaces Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ics, drift guards Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lity bar Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dge surfaces Badge persistence keeps running for everyone; profile badges section, badges catalog page and congratulation emails are admin-only while the xp-system flag is active. Leak inventory + feature-flag docs added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lator, deterministic recompute xp_entry table with partial unique idempotency anchors (player-aware for team solves), Player xp_total/level/experienceSystemOptedOut, pieces snapshot on solving times with migration backfill, exhaustively tested XpCalculator implementing the locked §1.2 decomposition, XpLedger with denormalized totals invariant, and RecalculateXpForPlayer recompute proven idempotent by integration test (incl. team chains and relax repeats). Suspicious solves excluded platform-consistently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ensation on delete, settlements, achievement XP XpChainRecomputer drives the solve lifecycle via async messages; every registered team participant earns through their own occurrence chain. Weekly boost / daily warm-up counters read the ledger in canonical order. Settlement command myspeedpuzzling:settle-xp-bonuses grants frozen-forever difficulty/speed bonuses via the same calculator paths. BadgeEvaluator emits achievement XP per newly persisted tier (backfill mode keeps it out of weekly delta + suppresses congratulation emails); badge email is now members-only. Delete dialog warns about XP loss (flag-gated). Fixes pre-existing evaluator crash on re-evaluation of multi-tier badges (GetBadges::allEarnedTiers). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…py, AP query, Early Adopter rename PlayerStatsSnapshot gains 11 metrics loaded in 4 fixed queries (owner counters batched into one FILTER-aggregate SQL; quarterly streak island detection with the year-0024 guard). Condition classes with exhaustive unit tests mirror the locked §1.6 thresholds; Speed Demon 1000 uses the descending pattern. GetAchievementPoints sums AP with a drift-guard test against BadgeTier::points(). Supporter displays as Early Adopter (enum value unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eal, puzzle estimate (all flag-gated) XpSolveReceipt renders ledger lines 1:1 with repeat/relax discounts folded into the base label and dim pending-settlement lines; the XpRecapCelebration LiveComponent polls once to bridge async awards, pops the level-up interstitial (golden Lv50 fork for everyone) and the members-only achievement toast. Shared XpRing gives profile + header their CSS-only milestone rings. First-click badge reveal flips the medallion with confetti (revealed_at migration; lower tiers flip along), reachable from the membership page invite. Puzzle detail gets the calculator-backed estimate line with personalized repeat and unrated-pending notes. XpSurfacesTest proves every surface invisible to non-admins while flagged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n approval) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…udit page, explainer, fair-play, launch reveal, share cards Catalog lives at /achievements (301 from /badges) with AP chips and free-user earned-waiting states; holders directory lists members per tier with first-to-earn highlight, newest earners and country filter (badge.type indexed). Leaderboard: weekly ledger delta / all-time / AP ladder tabs with filters and pinned self-row. Audit page renders the full ledger with puzzle/badge context. Explainer + fair-play static pages (copy pending Jan approval, no guard thresholds). One-time launch reveal page (DismissedHint-backed) with XP counter spin and share CTA; Intervention-rendered 800x800 share cards for launch + level-up. Every route 404s for non-admins while flagged (XpPagesTest, 24 tests). Fixed GetBadgeCatalog highest-tier-only earned map. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…unsubscribe, audit hardening ContentDigestLog + player frequency (default weekly per launch decision), digest_emails Doctrine queue with backoff, staggered dispatch command (weekly only, gate-suppressed), handler with direct transport send, staleness guard, eligibility re-check incl. XP opt-outs, never-two-quiet-digests rule and 55x failure classification. Inky template with XP/achievements headline (member/free variants), week-in-numbers, streak, favorites, next-achievements and community blocks + no-activity variant. Signed one-click unsubscribe (RFC 8058 headers), digest+XP preferences in settings (hidden while flagged). Email audit: digest bodies/debug skipped, 30-day digest retention, batched cleanup via message re-dispatch, (email_type, sent_at) index. 24 new tests incl. full-pipeline rendering of all three variants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erification, reveal emails, runbook myspeedpuzzling:xp-backfill rebuilds all ledgers then re-evaluates achievements in backfill mode (staggered, idempotent, email-free). myspeedpuzzling:xp-distribution prints the level pyramid with the locked calibration invariants (~115 Lv50 +-10, median L13-14) inline. myspeedpuzzling:send-xp-reveal-emails fans out the one-time launch email (hero inline, transactional transport, RFC 8058 headers, content_digest_log idempotency, refuses while flagged). Feature README with production cron block + step-by-step launch runbook incl. rollback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…locale fill, deferred issues XpLeakTest sweeps every gated surface as anonymous and as a logged non-admin non-member (deleted on launch day with the flag); XpAntiAbuseTest proves the cap, 3-solver median requirement, PPM guard (with positive control) and relax-repeat-zero end-to-end. All 379 new translation keys filled for cs/de/es/fr/ja via the missing-translations workflow; stale badge-title/supporter stubs renamed to Achievements/Early Adopter terminology across locales (cs names flagged for native review in the launch checklist). Feature README + CLAUDE.md pointer + feature-flag registry finalized; GitHub issues #148-#158 cover every deferred item. STATE=DONE. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
player.achievement_points is maintained by BadgeEvaluator as an absolute re-anchor on every evaluation — the only badge write path, so the 15-minute cron self-heals drift (proven by test with manually inserted badges). AP ladder, Lv50 chips and GetAchievementPoints now read the indexed column instead of aggregating the badge table. Migration backfills existing holders (prod Supporters). New indexes: player.xp_total + player.achievement_points (ladder ordering and self-rank counts), xp_entry.solving_time_id (receipt/delete/edit lookups), and partial covering custom_xp_entry_weekly_delta (earned_at, player_id, amount) WHERE in_weekly_delta so the weekly tab index-only-scans the current week instead of seq-scanning a forever-growing ledger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, bake-off plan Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tic frames + flood-fill cutout Full Speed Demon set (5 tiers) produced end-to-end. Key findings: prompt-only geometry fails; neural matting destroys flat-art interior whites (flood fill wins); Klein edit passes preserve geometry 11/11 and obey text over reference (per-tier fill prompts required). Scripts + evidence sheets included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
BadgeConditionInterface+ tagged iterator — adding a new badge = one enum case + one class filemyspeedpuzzling:recalculate-badges --backfillwith DelayStamp staggeringBadgesProfileSectionTwig component (membership-gated), catalog page at/en/badgeswith progress barsCloses #127
Linked feature request: https://myspeedpuzzling.com/en/feature-requests/019d1d34-8b4e-71f1-81cd-a3da75d28c38
Test plan
myspeedpuzzling:recalculate-badges --backfillin dev and verify badges are awarded + emails land in Mailpit/en/badgeslogged in — per-badge progress bars visible/en/badgeslogged out — catalog visible without progress