Skip to content

Dynamic badges/achievements system with tiered milestones - #128

Open
JanMikes wants to merge 26 commits into
mainfrom
feature/dynamic-badges-system
Open

Dynamic badges/achievements system with tiered milestones#128
JanMikes wants to merge 26 commits into
mainfrom
feature/dynamic-badges-system

Conversation

@JanMikes

Copy link
Copy Markdown
Member

Summary

  • Plug-in badge system with BadgeConditionInterface + tagged iterator — adding a new badge = one enum case + one class file
  • Five flagship tiered badges (Puzzles Solved, Pieces Solved, 500pc Speed, Streak, Team Player) with Bronze→Diamond tiers, plus existing Supporter as admin-granted single-tier
  • Async recalculation triggered on every solve-time add/edit/delete; backfill via myspeedpuzzling:recalculate-badges --backfill with DelayStamp staggering
  • Profile rendering via BadgesProfileSection Twig component (membership-gated), catalog page at /en/badges with progress bars

Closes #127

Linked feature request: https://myspeedpuzzling.com/en/feature-requests/019d1d34-8b4e-71f1-81cd-a3da75d28c38

Test plan

  • Run myspeedpuzzling:recalculate-badges --backfill in dev and verify badges are awarded + emails land in Mailpit
  • Visit a member's profile — badges section renders with tier medallions and "NEW" pills
  • Visit a non-member's profile — no badges section shown at all
  • Visit /en/badges logged in — per-badge progress bars visible
  • Visit /en/badges logged out — catalog visible without progress
  • Add a puzzle solving time near a threshold — verify async recalc triggers
  • Existing Supporter badge (if any) still renders correctly with tier=NULL

@JanMikes
JanMikes force-pushed the feature/dynamic-badges-system branch from f6fc773 to f2cac13 Compare July 11, 2026 11:57
JanMikes added 6 commits July 13, 2026 13:25
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
JanMikes force-pushed the feature/dynamic-badges-system branch from fd724ea to 37849d6 Compare July 13, 2026 11:25
JanMikes and others added 20 commits July 13, 2026 14:03
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dynamic badges/achievements system with tiered milestones

1 participant