|
<div class="docs-content" data-render>
|
|
# Gamification: XP, levels, badges, and achievements
|
|
|
|
> Audience: administrators. This page is hidden from members and guests in the sidebar, the search
|
|
> index, and the documentation export. It is the complete reference for the progression system:
|
|
> experience points, levels, the full badge catalogue, the activity-tracking engine behind
|
|
> first-use and usage-tier badges, every place a badge is awarded, and how to extend it.
|
|
|
|
DevPlace rewards participation with three layered mechanics, all centralised in the `devplacepy/utils`
|
|
package (`utils/rewards.py` for XP and milestones, `utils/badges.py` for the catalogue and tracking)
|
|
and wired into the existing content, engagement, and feature hooks. Nothing writes badges or XP
|
|
directly - every award goes through the helpers below so milestone checks and notifications are never
|
|
skipped.
|
|
|
|
- **XP and levels** measure overall contribution.
|
|
- **Badges** are one-time, never-revoked awards grouped into themes and shown on the profile.
|
|
- **Achievements** is the umbrella term for the badge catalogue plus the per-user progress shown in
|
|
the profile showcase.
|
|
|
|
All reward work is deferred to the background task queue, so it never sits on the request path; in
|
|
the test suite the queue runs inline so awards are deterministic.
|
|
|
|
## XP and levels
|
|
|
|
Members earn XP at the existing content and engagement hook points. The amounts are constants in
|
|
`utils/rewards.py`:
|
|
|
|
| Action | Constant | XP |
|
|
|--------|----------|----|
|
|
| Publish a post | `XP_POST` | 10 |
|
|
| Write a comment | `XP_COMMENT` | 2 |
|
|
| Publish a project | `XP_PROJECT` | 15 |
|
|
| Publish a gist | `XP_GIST` | 5 |
|
|
| Receive an upvote | `XP_UPVOTE` | 5 |
|
|
| Gain a follower | `XP_FOLLOW` | 5 |
|
|
|
|
Awards for received upvotes and followers go to the content owner or the followed user. The level
|
|
formula is `level = 1 + xp // 100` (`LEVEL_XP = 100`). `award_xp(user_uid, amount)` clamps XP at zero,
|
|
recomputes the level, invalidates the user cache, and on a level-up fires a `level` notification and
|
|
any level badge. `award_rewards(user_uid, amount, first_badge=None)` is the single entry point that
|
|
chains the optional first-time badge, `award_xp`, and `check_milestone_badges`.
|
|
|
|
## The badge system: two complementary mechanisms
|
|
|
|
Badges come from two sources, chosen by whether the achievement can be recomputed from existing data:
|
|
|
|
1. **Source-derived milestones** - counts that already live in a table (posts, comments, projects,
|
|
gists, stars, followers, following, activity streak). These need no tracking: `check_milestone_badges`
|
|
recomputes them and is accurate even retroactively. Defined in the `_COUNT_MILESTONES` list and the
|
|
`LEVEL_BADGES` map.
|
|
2. **Activity-tracked badges** - first-time feature use and usage tiers that are *not* derivable from
|
|
any table (read N documentation pages, ran a DeepSearch, sent a message, used Devii). These are
|
|
counted in a dedicated activity table and awarded by `track_action`.
|
|
|
|
The four original first-content badges (First Post, First Comment, First Project, First Gist) are a
|
|
third, simplest case: they are passed as the `first_badge` argument of `award_rewards` at the moment
|
|
the content is created (in `content.py`).
|
|
|
|
## Badge catalogue
|
|
|
|
Every badge, its icon, the group it belongs to, and how it is earned. Metadata lives in
|
|
`BADGE_CATALOG`; the group order is `BADGE_GROUPS`.
|
|
|
|
### First steps
|
|
|
|
| Icon | Badge | How to earn |
|
|
|------|-------|-------------|
|
|
| ✎ | First Post | Publish your first post (`award_rewards` first_badge) |
|
|
| ❝ | First Comment | Write your first comment (`award_rewards` first_badge) |
|
|
| ⬢ | First Project | Share your first project (`award_rewards` first_badge) |
|
|
| ❡ | First Gist | Share your first gist (`award_rewards` first_badge) |
|
|
| 📎 | First Upload | Upload your first attachment (`upload`, 1) |
|
|
| 📄 | File Creator | Create a file in a project (`project_file`, 1) |
|
|
| 📇 | Profiled | Fill in any profile field (`profile`, 1) |
|
|
|
|
### Explorer
|
|
|
|
| Icon | Badge | How to earn |
|
|
|------|-------|-------------|
|
|
| 📖 | Curious | Read 1 documentation page (`docs.read`, distinct) |
|
|
| 📚 | Studious | Read 5 documentation pages (`docs.read`, distinct) |
|
|
| 🎓 | Scholar | Read 15 documentation pages (`docs.read`, distinct) |
|
|
| 🤖 | AI Curious | Talk to Devii for the first time (`devii`, 1) |
|
|
| ✨ | AI Whisperer | Hold 25 conversations with Devii (`devii`, 25) |
|
|
| 🍴 | First Fork | Fork a project (`fork`, 1) |
|
|
| 🌿 | Forker | Fork 5 projects (`fork`, 5) |
|
|
| 🗜 | Archivist | Download a project archive (`zip`, 1) |
|
|
| 🔍 | SEO Auditor | Run an SEO diagnostics audit (`seo`, 1) |
|
|
| 🔬 | Researcher | Run a DeepSearch investigation (`deepsearch`, 1) |
|
|
| 🌊 | Deep Diver | Run 10 DeepSearch investigations (`deepsearch`, 10) |
|
|
| 📦 | Container Captain | Create a container instance (`container`, 1) |
|
|
|
|
### Engagement
|
|
|
|
| Icon | Badge | How to earn |
|
|
|------|-------|-------------|
|
|
| 🔖 | Bookmarker | Bookmark your first item (`bookmark`, 1) |
|
|
| 📌 | Curator | Bookmark 25 items (`bookmark`, 25) |
|
|
| 💛 | First Reaction | React to content for the first time (`reaction`, 1) |
|
|
| 🎉 | Cheerleader | React 50 times (`reaction`, 50) |
|
|
| ⭐ | First Star Given | Star someone's work for the first time (`vote`, 1) |
|
|
| 👍 | Supporter | Give 50 stars (`vote`, 50) |
|
|
| 🤝 | Patron | Give 250 stars (`vote`, 250) |
|
|
| 🗳 | Pollster | Vote in a poll for the first time (`poll`, 1) |
|
|
|
|
### Content
|
|
|
|
| Icon | Badge | How to earn |
|
|
|------|-------|-------------|
|
|
| ✺ | Prolific | Publish 10 posts |
|
|
| 🖋 | Wordsmith | Publish 50 posts |
|
|
| 🏆 | Veteran | Publish 100 posts |
|
|
| 💬 | Conversationalist | Write 25 comments |
|
|
| 📣 | Debater | Write 100 comments |
|
|
| 🔧 | Builder | Share 5 projects |
|
|
| 🏛 | Architect | Share 10 projects |
|
|
| 📑 | Snippet Collector | Share 5 gists |
|
|
|
|
### Community
|
|
|
|
| Icon | Badge | How to earn |
|
|
|------|-------|-------------|
|
|
| 🔗 | Connector | Follow 10 people |
|
|
| ✉ | Messenger | Send your first direct message (`message`, 1) |
|
|
| 📨 | Chatterbox | Send 100 direct messages (`message`, 100) |
|
|
| 👋 | Friendly | Follow someone for the first time (`follow`, 1) |
|
|
| 🐛 | Bug Reporter | File your first issue (`issue`, 1) |
|
|
|
|
### Reputation
|
|
|
|
| Icon | Badge | How to earn |
|
|
|------|-------|-------------|
|
|
| ☆ | Rising Star | Earn 25 stars |
|
|
| ★ | Star Author | Earn 100 stars |
|
|
| 🌟 | Superstar | Earn 500 stars |
|
|
| ◎ | Popular | Reach 10 followers |
|
|
| 📢 | Influencer | Reach 50 followers |
|
|
| 👑 | Celebrity | Reach 100 followers |
|
|
|
|
### Dedication
|
|
|
|
| Icon | Badge | How to earn |
|
|
|------|-------|-------------|
|
|
| 🔥 | On Fire | Maintain a 7-day activity streak |
|
|
| 📅 | Dedicated | Maintain a 30-day activity streak |
|
|
| 🚀 | Unstoppable | Maintain a 100-day activity streak |
|
|
|
|
### Code Farm
|
|
|
|
See [Code Farm](/docs/code-farm.html) for the game itself.
|
|
|
|
| Icon | Badge | How to earn |
|
|
|------|-------|-------------|
|
|
| 🌱 | Green Thumb | Harvest your first build on the Code Farm (`harvest`, 1) |
|
|
| 🌾 | Master Farmer | Harvest 50 builds on the Code Farm (`harvest`, 50) |
|
|
| 💧 | Good Neighbor | Water a neighbour's build (`water`, 1) |
|
|
| 🥷 | Cat Burglar | Steal a ready build from another farm (`harvest_stolen`, 1) |
|
|
| 🚨 | Robbed | Have a build stolen from your farm (`got_stolen_from`, 1) |
|
|
|
|
### Levels and membership
|
|
|
|
| Icon | Badge | How to earn |
|
|
|------|-------|-------------|
|
|
| ❖ | Level 5 / 10 / 25 / 50 / 100 | Reach the matching level (`LEVEL_BADGES`, group `Levels`) |
|
|
| ✦ | Member | Join DevPlace (granted at registration, group `Milestones`) |
|
|
|
|
## The activity-tracking engine
|
|
|
|
Activity-tracked badges are backed by two tables, both created and indexed in `init_db()`:
|
|
|
|
- `user_activity` - a per-user, per-action counter (`count`, `first_at`, `last_at`), unique on
|
|
`(user_uid, action)`. Used for repeatable actions (votes given, messages sent, forks).
|
|
- `user_activity_seen` - a per-user set of distinct targets, unique on `(user_uid, action, target)`.
|
|
Used for "N distinct things" achievements, currently only `docs.read` (distinct documentation
|
|
slugs), so refreshing the same page does not inflate the count.
|
|
|
|
`database.record_activity(user_uid, action)` upserts the counter and returns the new count;
|
|
`database.record_unique_activity(user_uid, action, target)` records a distinct target and returns the
|
|
new distinct count, or `None` if the target was already seen.
|
|
|
|
The single hook the rest of the codebase calls is `utils.track_action(user_uid, action, target=None)`.
|
|
It returns immediately for an unknown action, otherwise it submits the work to the background queue,
|
|
which records the activity (counter or distinct, per `UNIQUE_ACTIONS`) and then awards and notifies
|
|
every threshold crossed in the `ACHIEVEMENTS` registry. It is fully deferred and idempotent, so it is
|
|
safe to call inline at any feature's success point.
|
|
|
|
The `ACHIEVEMENTS` registry maps an action to its `(threshold, badge)` tiers (threshold 1 is a
|
|
first-use badge):
|
|
|
|
| Action | Counting | Tiers |
|
|
|--------|----------|-------|
|
|
| `docs.read` | distinct | 1 Curious, 5 Studious, 15 Scholar |
|
|
| `devii` | count | 1 AI Curious, 25 AI Whisperer |
|
|
| `fork` | count | 1 First Fork, 5 Forker |
|
|
| `zip` | count | 1 Archivist |
|
|
| `seo` | count | 1 SEO Auditor |
|
|
| `deepsearch` | count | 1 Researcher, 10 Deep Diver |
|
|
| `container` | count | 1 Container Captain |
|
|
| `message` | count | 1 Messenger, 100 Chatterbox |
|
|
| `bookmark` | count | 1 Bookmarker, 25 Curator |
|
|
| `reaction` | count | 1 First Reaction, 50 Cheerleader |
|
|
| `vote` | count | 1 First Star Given, 50 Supporter, 250 Patron |
|
|
| `follow` | count | 1 Friendly |
|
|
| `upload` | count | 1 First Upload |
|
|
| `project_file` | count | 1 File Creator |
|
|
| `issue` | count | 1 Bug Reporter |
|
|
| `poll` | count | 1 Pollster |
|
|
| `profile` | count | 1 Profiled |
|
|
| `harvest` | count | 1 Green Thumb, 50 Master Farmer |
|
|
| `water` | count | 1 Good Neighbor |
|
|
| `harvest_stolen` | count | 1 Cat Burglar |
|
|
| `got_stolen_from` | count | 1 Robbed |
|
|
|
|
## Where each badge is awarded
|
|
|
|
`track_action` is called at the success point of each feature:
|
|
|
|
| Action | Source location | Trigger |
|
|
|--------|-----------------|---------|
|
|
| `vote` | `content.apply_vote` | an upvote (not a toggle-off) |
|
|
| `bookmark` | `content.set_bookmark` | a save (not a removal) |
|
|
| `reaction` | `routers/reactions.py` | a reaction added |
|
|
| `follow` | `routers/follow.py` | following someone (follower side) |
|
|
| `message` | `services/messaging/persist.py` | a message sent (sender) |
|
|
| `docs.read` | `routers/docs/views.py` | an authenticated doc page view, per slug |
|
|
| `fork` / `zip` | `routers/projects/index.py` | fork or project-zip enqueue |
|
|
| `zip` / `project_file` | `routers/projects/files.py` | subtree-zip enqueue, file create |
|
|
| `seo` | `routers/tools/seo.py` | SEO audit enqueue (signed-in owner) |
|
|
| `deepsearch` | `routers/tools/deepsearch.py` | DeepSearch enqueue (signed-in owner) |
|
|
| `container` | `services/containers/api.create_instance` | instance created by a user actor |
|
|
| `upload` | `routers/uploads.py` | attachment upload |
|
|
| `issue` | `routers/issues/create.py` | issue filing enqueue |
|
|
| `poll` | `routers/polls.py` | a poll vote cast |
|
|
| `profile` | `routers/profile/index.py` | profile saved with a non-empty field |
|
|
| `devii` | `services/devii/session/core.py` | a completed Devii turn (user owner, non-docs channel) |
|
|
| `harvest` | `routers/game/index.py` | harvest a ready build on your own farm |
|
|
| `water` / `harvest_stolen` / `got_stolen_from` | `routers/game/farm.py` | water a neighbour, steal a build (thief), or be stolen from (victim) |
|
|
|
|
Guests are never tracked: the hooks only fire for an authenticated user. The Docii documentation
|
|
channel is excluded from the `devii` badge.
|
|
|
|
## Milestone badges (`check_milestone_badges`)
|
|
|
|
`check_milestone_badges(user_uid)` is called by `award_rewards` after every post, comment, project,
|
|
gist, upvote, and follow. It loads the badges the user already holds, then evaluates only the unheld
|
|
entries of `_COUNT_MILESTONES`, resolving each metric lazily through a memoised resolver so a metric
|
|
is queried only when an unheld badge needs it. Metric keys: `posts`, `comments`, `projects`, `gists`,
|
|
`stars`, `followers`, `following`, `streak`. Streak badges additionally emit a
|
|
`reward.streak.milestone` audit event.
|
|
|
|
## Notifications and audit
|
|
|
|
- Earning any badge fires a `badge` notification (`notify_badge`), and a level-up fires a `level`
|
|
notification. Both carry `related_uid == user_uid`, so they render with the recipient's own avatar.
|
|
- Every grant records an audit event: `reward.badge.award`, `reward.xp.grant`, `reward.level.up`, and
|
|
`reward.streak.milestone`. These appear in the admin Audit Log.
|
|
|
|
## The profile achievements showcase
|
|
|
|
`utils.build_achievements(held_names)` returns the full catalogue grouped by theme, each badge marked
|
|
earned or locked, with per-group `earned` and `total` counts. The profile route passes it as
|
|
`achievements` (plus `badge_earned` and `badge_total`) and `profile.html` renders a collapsible
|
|
**Achievements** panel above the profile tabs: earned badges are highlighted, locked ones are dimmed
|
|
and show their unlock condition, and the summary shows the overall earned-of-total count. This makes
|
|
every badge discoverable so there is always a next goal.
|
|
|
|
## Existing accounts and backfill
|
|
|
|
`_backfill_gamification()` runs at the end of `init_db()`. For accounts still at the default `xp = 0`
|
|
it computes XP from prior activity, sets the level, and runs `check_milestone_badges`, so historic
|
|
contributors are not stranded at level 1. Source-derived milestone badges are also awarded on a
|
|
user's next rewarded action. Activity-tracked badges accrue from the next time each feature is used;
|
|
they are intentionally not backfilled, because the activity counters do not exist for past actions.
|
|
|
|
## Extending the system
|
|
|
|
To add a **first-use or usage-tier** badge:
|
|
|
|
1. Add the badge to `BADGE_CATALOG` with an `icon`, `description`, and `group`.
|
|
2. Add an `ACHIEVEMENTS` entry mapping the action to its `(threshold, badge)` tiers. Add the action to
|
|
`UNIQUE_ACTIONS` if it counts distinct targets.
|
|
3. Call `track_action(user_uid, action[, target])` inline at the feature's success point. Do not wrap
|
|
it in `background.submit` - it defers itself.
|
|
|
|
To add a **source-derived milestone** badge:
|
|
|
|
1. Add the badge to `BADGE_CATALOG`.
|
|
2. Add a row to `_COUNT_MILESTONES` with an existing metric key (extend `_milestone_metrics` if a new
|
|
metric is needed). It is awarded the next time `check_milestone_badges` runs.
|
|
|
|
Keep all badge logic in these helpers; never insert into the `badges` table or compute thresholds
|
|
inline elsewhere.
|
|
</div>
|