@layers/amba 1.1.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,316 @@
1
+ # Gamification
2
+
3
+ Five primitives that turn a flat app into something users come back to: XP rules (auto-award points on a tracked event), achievements (badges that unlock on criteria), streaks (consecutive-period qualification), leaderboards (rank by metric), and challenges (time-bounded goals with rewards). All five are define-once / use-many: you create the definitions via MCP, the SDK qualifies / claims / reads against them at runtime.
4
+
5
+ The pattern is always:
6
+
7
+ 1. Agent (via MCP): define the rule / achievement / streak / etc.
8
+ 2. Client SDK at runtime: track the event (`Amba.events.track(...)`) or qualify (`Amba.streaks.qualify(...)` / `Amba.challenges.claim(...)`).
9
+ 3. Server: auto-evaluate, mutate user state, return updated progress.
10
+
11
+ ## MCP tools
12
+
13
+ ### XP rules
14
+
15
+ | Tool | Purpose | Example args |
16
+ | --- | --- | --- |
17
+ | `amba_xp_rules_create` / `amba_create_xp_rule` | Auto-award XP on a matching event. | `{ project_id, name: "Workout Completed", event_name: "workout_completed", xp_amount: 50, max_per_day: 5, cooldown_seconds: 60 }` |
18
+ | `amba_xp_rules_list` / `amba_list_xp_rules` | List all XP rules. | `{ project_id }` |
19
+ | `amba_xp_update_rule` | Edit a rule. | `{ project_id, rule_id, xp_amount: 75 }` |
20
+ | `amba_xp_delete_rule` | Delete a rule. | `{ project_id, rule_id }` |
21
+ | `amba_xp_list_users` | List users sorted by XP (for an admin panel or smoke-test). | `{ project_id, limit: 50 }` |
22
+ | `amba_users_get_xp` / `amba_get_user_xp` | Read a specific user's XP total + level. | `{ project_id, user_id }` |
23
+
24
+ ### Achievements
25
+
26
+ | Tool | Purpose | Example args |
27
+ | --- | --- | --- |
28
+ | `amba_achievements_create` / `amba_create_achievement` | Define an achievement that unlocks on criteria. | `{ project_id, key: "first_workout", name: "First Workout", description: "Complete your first workout", xp_reward: 100, criteria: { event: "workout_completed", count: 1 } }` |
29
+ | `amba_achievements_list` / `amba_list_achievements` | List all achievements. | `{ project_id }` |
30
+ | `amba_achievements_get` / `amba_get_achievement` | Read one. | `{ project_id, achievement_id }` |
31
+ | `amba_achievements_update` / `amba_update_achievement` | Edit an achievement. | `{ project_id, achievement_id, xp_reward: 150 }` |
32
+ | `amba_achievements_delete` / `amba_delete_achievement` | Delete. | `{ project_id, achievement_id }` |
33
+
34
+ Common criteria shapes (the schema is permissive — the server validates against the available metrics):
35
+
36
+ ```jsonc
37
+ // Count-based
38
+ { "event": "workout_completed", "count": 5 }
39
+
40
+ // Streak-based
41
+ { "streak_key": "daily_workout", "min_length": 7 }
42
+
43
+ // XP-based
44
+ { "xp_total": 5000 }
45
+
46
+ // Catalog-item-based
47
+ { "item_owned": "premium_theme" }
48
+ ```
49
+
50
+ ### Streaks
51
+
52
+ | Tool | Purpose | Example args |
53
+ | --- | --- | --- |
54
+ | `amba_streaks_create` / `amba_create_streak` | Define a streak — what event qualifies, what period, freeze rules. | `{ project_id, key: "daily_workout", name: "Daily Workout", qualifying_event: "workout_completed", period: "daily", grace_period_hours: 6, freeze_enabled: true, max_freezes: 3 }` |
55
+ | `amba_streaks_list` | List streaks. | `{ project_id }` |
56
+ | `amba_streaks_update` | Edit a streak definition. | `{ project_id, streak_id, max_freezes: 5 }` |
57
+ | `amba_streaks_delete` | Delete. | `{ project_id, streak_id }` |
58
+
59
+ The `key` is **immutable** after creation (the SDK identifies streaks by key, not UUID — changing it breaks live `Amba.streaks.qualify(...)` calls). Keys: lowercase letters, digits, `_`, `-`, 1-64 chars.
60
+
61
+ ### Leaderboards
62
+
63
+ | Tool | Purpose | Example args |
64
+ | --- | --- | --- |
65
+ | `amba_leaderboards_create` / `amba_create_leaderboard` | Define a leaderboard. | `{ project_id, name: "Weekly XP", metric: "xp", period: "weekly", max_entries: 100 }` |
66
+ | `amba_leaderboards_list` / `amba_list_leaderboards` | List. | `{ project_id }` |
67
+ | `amba_leaderboards_get` / `amba_get_leaderboard` | Read the current top entries. | `{ project_id, leaderboard_id, limit: 50 }` |
68
+ | `amba_leaderboards_get_definition` / `amba_get_leaderboard_definition` | Read the definition without the entries (cheap). | `{ project_id, leaderboard_id }` |
69
+ | `amba_leaderboards_update` / `amba_update_leaderboard` | Edit the period / max_entries. | `{ project_id, leaderboard_id, max_entries: 250 }` |
70
+ | `amba_leaderboards_delete` / `amba_delete_leaderboard` | Delete. | `{ project_id, leaderboard_id }` |
71
+
72
+ Metrics: `xp`, `streak`, `custom`. For `custom`, pass `custom_event` (e.g. `"workout_completed"`) — the leaderboard ranks by the count of that event in the period. Periods: `all_time`, `daily`, `weekly`, `monthly`.
73
+
74
+ ### Challenges
75
+
76
+ | Tool | Purpose | Example args |
77
+ | --- | --- | --- |
78
+ | `amba_challenges_create` / `amba_create_challenge` | Define a time-bounded challenge with a goal + reward. | `{ project_id, name: "Spring Sprint", description: "5 workouts in 7 days", goal_event: "workout_completed", goal_count: 5, starts_at: "2026-06-01T00:00:00Z", ends_at: "2026-06-08T00:00:00Z", reward: { xp: 500, currency: { code: "gems", amount: 50 } } }` |
79
+ | `amba_challenges_list` / `amba_list_challenges` | List. | `{ project_id }` |
80
+ | `amba_challenges_get` / `amba_get_challenge` | Read. | `{ project_id, challenge_id }` |
81
+ | `amba_challenges_update` / `amba_update_challenge` | Edit. | `{ project_id, challenge_id, ends_at: "..." }` |
82
+ | `amba_challenges_delete` / `amba_delete_challenge` | Delete. | `{ project_id, challenge_id }` |
83
+ | `amba_challenges_list_participants` / `amba_list_challenge_participants` | List opt-in participants + progress. | `{ project_id, challenge_id, limit: 100 }` |
84
+
85
+ ## SDK init per stack
86
+
87
+ `Amba.configure(...)` runs first — see `identity.md`. The snippets below show only the gamification calls.
88
+
89
+ ### Expo
90
+
91
+ ```tsx
92
+ import { Amba } from '@layers/amba-expo';
93
+
94
+ // 1. Track the event that drives XP / achievements / streaks / leaderboards.
95
+ // XP rules + achievements + streaks + leaderboards all match on event_name,
96
+ // so a single track() call can fire all of them.
97
+ async function onWorkoutCompleted(durationMinutes: number) {
98
+ await Amba.events.track('workout_completed', { duration_minutes: durationMinutes });
99
+
100
+ // 2. Qualify the streak. The server keeps the consecutive-period count;
101
+ // the SDK returns the updated state.
102
+ const streak = await Amba.streaks.qualify('daily_workout');
103
+ // streak.current_count, streak.longest_count, streak.status, streak.freezes_remaining
104
+
105
+ // 3. Show the user any newly-unlocked achievements (poll progress).
106
+ const progress = await Amba.achievements.getProgress();
107
+ const newlyUnlocked = progress.filter(p => p.unlocked && p.unlocked_within_seconds < 5);
108
+
109
+ // 4. Show their current XP balance.
110
+ const xp = await Amba.xp.getBalance();
111
+ // xp.total, xp.level, xp.next_level_xp
112
+ }
113
+
114
+ // 5. Read the leaderboard for a UI screen.
115
+ async function loadLeaderboard(leaderboardKey: string) {
116
+ const entries = await Amba.leaderboards.getEntries(leaderboardKey, 50);
117
+ const myRank = await Amba.leaderboards.getMyRank(leaderboardKey);
118
+ }
119
+
120
+ // 6. Active challenges (e.g. for a "Challenges" tab).
121
+ async function loadChallenges() {
122
+ const active = await Amba.challenges.getActive();
123
+ for (const c of active) {
124
+ const progress = await Amba.challenges.getProgress(c.id);
125
+ if (progress.completed && !progress.claimed) {
126
+ await Amba.challenges.claim(c.id);
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### React Native (bare)
133
+
134
+ ```tsx
135
+ import { Amba } from '@layers/amba-react-native';
136
+
137
+ await Amba.events.track('workout_completed', { duration_minutes: 30 });
138
+ const streak = await Amba.streaks.qualify('daily_workout');
139
+ const achievements = await Amba.achievements.getProgress();
140
+ const entries = await Amba.leaderboards.getEntries('Weekly XP', 50);
141
+ const active = await Amba.challenges.getActive();
142
+ ```
143
+
144
+ ### Web (browser / Next.js / Vite)
145
+
146
+ ```ts
147
+ import { Amba } from '@layers/amba-web';
148
+
149
+ await Amba.events.track('lesson_completed', { course_id: 'algebra-1' });
150
+ const streak = await Amba.streaks.qualify('daily_lesson');
151
+ const xp = await Amba.xp.getBalance();
152
+ const top = await Amba.leaderboards.getEntries('Weekly XP', 100);
153
+ const myRank = await Amba.leaderboards.getMyRank('Weekly XP');
154
+ ```
155
+
156
+ With React hooks (`@layers/amba-react`) — there is no dedicated `useXp` / `useStreak` hook in 1.0, so wrap the SDK calls in your own `useEffect`:
157
+
158
+ ```tsx
159
+ import { useEffect, useState } from 'react';
160
+ import { Amba } from '@layers/amba-web';
161
+
162
+ export function useStreak(key: string) {
163
+ const [streak, setStreak] = useState<Streak | null>(null);
164
+ const refresh = async () => {
165
+ const all = await Amba.streaks.getAll();
166
+ setStreak(all.find(s => s.key === key) ?? null);
167
+ };
168
+ useEffect(() => { refresh(); }, [key]);
169
+ return { streak, refresh };
170
+ }
171
+ ```
172
+
173
+ ### iOS (Swift)
174
+
175
+ ```swift
176
+ import Amba
177
+
178
+ try await Amba.events.track("workout_completed", properties: ["duration_minutes": 30])
179
+ let streak = try await Amba.streaks.qualify(streakKey: "daily_workout")
180
+ let progress = try await Amba.achievements.getProgress()
181
+ let xp = try await Amba.xp.getBalance()
182
+ let entries = try await Amba.leaderboards.getEntries(key: "Weekly XP", limit: 50)
183
+ let myRank = try await Amba.leaderboards.getMyRank(key: "Weekly XP")
184
+ let active = try await Amba.challenges.getActive()
185
+ for challenge in active {
186
+ let p = try await Amba.challenges.getProgress(id: challenge.id)
187
+ if p.completed && !p.claimed {
188
+ _ = try await Amba.challenges.claim(id: challenge.id)
189
+ }
190
+ }
191
+ ```
192
+
193
+ ### Android (Kotlin)
194
+
195
+ ```kotlin
196
+ Amba.events.track("workout_completed", mapOf("duration_minutes" to 30))
197
+ val streak = Amba.streaks.qualify("daily_workout")
198
+ val progress = Amba.achievements.getProgress()
199
+ val xp = Amba.xp.getBalance()
200
+ val entries = Amba.leaderboards.getEntries("Weekly XP", limit = 50)
201
+ val myRank = Amba.leaderboards.getMyRank("Weekly XP")
202
+ val active = Amba.challenges.getActive()
203
+ for (c in active) {
204
+ val p = Amba.challenges.getProgress(c.id)
205
+ if (p.completed && !p.claimed) Amba.challenges.claim(c.id)
206
+ }
207
+ ```
208
+
209
+ ### Flutter
210
+
211
+ ```dart
212
+ import 'package:amba/amba.dart';
213
+
214
+ await Amba.events.track('workout_completed', {'duration_minutes': 30});
215
+ final streak = await Amba.streaks.qualify('daily_workout');
216
+ final progress = await Amba.achievements.getProgress();
217
+ final xp = await Amba.xp.getBalance();
218
+ final entries = await Amba.leaderboards.getEntries('Weekly XP', limit: 50);
219
+ final myRank = await Amba.leaderboards.getMyRank('Weekly XP');
220
+ final active = await Amba.challenges.getActive();
221
+ for (final c in active) {
222
+ final p = await Amba.challenges.getProgress(c.id);
223
+ if (p.completed && !p.claimed) {
224
+ await Amba.challenges.claim(c.id);
225
+ }
226
+ }
227
+ ```
228
+
229
+ ## Common follow-ups
230
+
231
+ Batch into one or two questions.
232
+
233
+ 1. **What's the qualifying event for XP / achievements / streaks?** Most preset answers below — accept the suggestion or override:
234
+ - fitness: `workout_completed`
235
+ - education: `lesson_completed`
236
+ - game: `level_completed` (or `match_played`)
237
+ - productivity: `task_completed`
238
+ - content_creator: `post_published`
239
+ - ai_chatbot: `prompt_sent`
240
+
241
+ 2. **Default XP per qualifying event?** (defaults to `50`)
242
+
243
+ 3. **Streak period?**
244
+ - Daily (recommended)
245
+ - Weekly
246
+ - None (skip streaks)
247
+
248
+ 4. **Streak freezes?**
249
+ - 3 freezes/month (recommended — covers the occasional missed day without breaking)
250
+ - Hard mode (no freezes)
251
+ - None — don't enable freezes
252
+
253
+ 5. **Leaderboard scope:** (multi-select)
254
+ - [x] Weekly (recommended — rotates, never gets stale)
255
+ - [ ] All-time
256
+ - [ ] Daily (high-engagement apps only — daily resets feel relentless)
257
+ - [ ] Monthly
258
+
259
+ 6. **Which achievements to seed?** Defaults per preset (offer to create, or skip):
260
+
261
+ **fitness:**
262
+ - `first_workout` — Complete 1 workout (+100 XP)
263
+ - `week_warrior` — 7-day streak (+250 XP)
264
+ - `century_club` — 100 workouts (+1000 XP)
265
+
266
+ **education:**
267
+ - `first_lesson` — Complete 1 lesson (+100 XP)
268
+ - `dedicated_learner` — 7-day streak (+250 XP)
269
+ - `course_complete` — Finish a course (+1000 XP, criteria: `event: course_completed, count: 1`)
270
+
271
+ **game:**
272
+ - `first_win` — Win 1 match (+100 XP)
273
+ - `streak_master` — 5 wins in a row (custom criteria)
274
+ - `level_50` — Reach level 50 (`xp_total: 50000`)
275
+
276
+ **productivity:**
277
+ - `first_task` — Complete 1 task (+100 XP)
278
+ - `inbox_zero` — Clear all tasks (criteria: `event: inbox_zeroed, count: 1`)
279
+ - `monthly_warrior` — 30-day streak (+1000 XP)
280
+
281
+ For other presets, ask the user to confirm 3 achievements they want or skip.
282
+
283
+ 7. **Challenges:** seed an example weekly challenge?
284
+ - Yes — "5 workouts this week" (or equivalent for the preset) ending next Sunday
285
+ - No
286
+
287
+ ## Re-run behavior
288
+
289
+ 1. Read `.amba/wired.json` `surfaces.gamification`:
290
+
291
+ ```json
292
+ {
293
+ "surfaces": {
294
+ "gamification": {
295
+ "xp_rules": ["Workout Completed"],
296
+ "achievements": ["first_workout", "week_warrior", "century_club"],
297
+ "streaks": ["daily_workout"],
298
+ "leaderboards": ["Weekly XP"],
299
+ "challenges": []
300
+ }
301
+ }
302
+ }
303
+ ```
304
+
305
+ 2. Before creating anything, list-then-diff:
306
+ - `amba_xp_rules_list` — match on `name`. If exists, ask to update (`amba_xp_update_rule`) or skip.
307
+ - `amba_achievements_list` — match on `key`. Achievement `key` is unique per project; collision → skip or update.
308
+ - `amba_streaks_list` — match on `key`. **Never recreate** a streak with an existing key — the SDK calls would silently target a stale definition. Update instead.
309
+ - `amba_leaderboards_list` — match on `name`. Collision → ask.
310
+ - `amba_challenges_list` — match on `name + starts_at`. New challenge with same name + different date is fine; same name + overlapping date → ask.
311
+
312
+ 3. In the entry file: detect existing `Amba.events.track('<event_name>')` calls. If they already exist, don't add another. If they're missing for an event tied to a new XP rule, add a sample comment / TODO showing where to call `Amba.events.track`.
313
+
314
+ 4. Update `wired.json` — append to each list, never replace.
315
+
316
+ 5. If the user asks to "remove gamification": offer a soft path — pause XP rules and unschedule challenges rather than deleting definitions (deletion is irreversible and loses historical user XP / unlocks).