Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Generates a variable reward system with randomized rewards, daily bonuses, mystery box mechanics, and ethical engagement caps. Use when user wants daily spins, mystery boxes, random rewards, or gamification reward systems.
.claude/skills/rshankras-variable-rewards/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 192% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 101% | 0% |
| case-23 | ✓→✗ | ▼ Worse | 97% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 100% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 122% | 0% |
Generate a variable reward system — randomized rewards (daily spins, mystery boxes, bonus points) that leverage variable-ratio reinforcement to increase engagement. Implements ethical engagement patterns with daily/weekly caps, transparent probability disclosure, and no pay-to-play mechanics.
Use this skill when the user:
Search for existing reward or points systems:
Glob: **/*Reward*.swift, **/*Points*.swift, **/*DailySpin*.swift, **/*MysteryBox*.swift
Grep: "reward" or "dailySpin" or "mysteryBox" or "rewardPool" or "lootBox"If existing reward system found:
Determine if generating for iOS or macOS or both (cross-platform). The templates use SwiftUI and are cross-platform by default.
Ask user via AskUserQuestion:
Read templates.md for production Swift code.
Generate these files:
Reward.swift — Model for reward type, value, rarity, display metadataRewardPool.swift — Weighted probability distribution with seeded random for testabilityRewardManager.swift — @Observable class managing claims, daily resets, caps, history via SwiftDataDailySpinView.swift — Animated spin wheel with disabled state when already claimedMysteryBoxView.swift — Card-flip / chest-open animation with matchedGeometryEffectRewardHistoryView.swift — List of past rewards grouped by dayRewardNotificationView.swift — Toast/banner overlay for reward availabilityCheck project structure:
Sources/ exists -> Sources/VariableRewards/App/ exists -> App/VariableRewards/VariableRewards/After generation, provide:
VariableRewards/
├── Reward.swift # Reward model with type, rarity, value
├── RewardPool.swift # Weighted random selection engine
├── RewardManager.swift # Core manager: claims, caps, history
├── DailySpinView.swift # Animated spin wheel
├── MysteryBoxView.swift # Card-flip / chest-open reveal
├── RewardHistoryView.swift # Past rewards grouped by day
└── RewardNotificationView.swift # Toast overlay for available rewardsSet up the model container (SwiftData):
swift@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [RewardClaim.self]) } }
Add the daily spin to your home screen:
swiftstruct HomeView: View { @Environment(\.modelContext) private var modelContext @State private var rewardManager: RewardManager? var body: some View { VStack { if let manager = rewardManager { DailySpinView(manager: manager) } } .onAppear { rewardManager = RewardManager(modelContext: modelContext) } } }
Trigger a mystery box reveal:
swiftstruct MilestoneView: View { @Environment(\.modelContext) private var modelContext @State private var rewardManager: RewardManager? @State private var showMysteryBox = false var body: some View { VStack { Button("Open Mystery Box") { showMysteryBox = true } .disabled(rewardManager?.canClaimToday == false) } .sheet(isPresented: $showMysteryBox) { if let manager = rewardManager { MysteryBoxView(manager: manager) } } .onAppear { rewardManager = RewardManager(modelContext: modelContext) } } }
Show a reward notification toast:
swiftstruct ContentView: View { @Environment(\.modelContext) private var modelContext @State private var rewardManager: RewardManager? var body: some View { ZStack { MainTabView() if let manager = rewardManager { RewardNotificationView(manager: manager) } } .onAppear { rewardManager = RewardManager(modelContext: modelContext) } } }
View reward history:
swiftstruct ProfileView: View { @Environment(\.modelContext) private var modelContext @State private var rewardManager: RewardManager? var body: some View { NavigationStack { if let manager = rewardManager { RewardHistoryView(manager: manager) } } .onAppear { rewardManager = RewardManager(modelContext: modelContext) } } }
swiftimport Testing import SwiftData @Test func dailySpinGrantsReward() async throws { let container = try ModelContainer( for: RewardClaim.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true) ) let context = ModelContext(container) let pool = RewardPool(seed: 42) // Deterministic for testing let manager = RewardManager(modelContext: context, rewardPool: pool) let reward = try await manager.claimDailySpin() #expect(reward != nil) #expect(manager.canClaimToday == false) // Already claimed } @Test func dailyCapEnforced() async throws { let container = try ModelContainer( for: RewardClaim.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true) ) let context = ModelContext(container) let pool = RewardPool(seed: 42) let manager = RewardManager(modelContext: context, rewardPool: pool, dailyCap: 1) _ = try await manager.claimDailySpin() let second = try await manager.claimDailySpin() #expect(second == nil) // Cap reached } @Test func deterministicSeedProducesSameReward() { let pool1 = RewardPool(seed: 123) let pool2 = RewardPool(seed: 123) let reward1 = pool1.drawReward() let reward2 = pool2.drawReward() #expect(reward1.type == reward2.type) #expect(reward1.rarity == reward2.rarity) } @Test func weeklyCapResetsAfterWeek() async throws { let container = try ModelContainer( for: RewardClaim.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true) ) let context = ModelContext(container) let pool = RewardPool(seed: 42) let manager = RewardManager(modelContext: context, rewardPool: pool, weeklyCap: 3) // Simulate 3 claims on different days within the same week let calendar = Calendar.current for daysAgo in (0...2).reversed() { let date = calendar.date(byAdding: .day, value: -daysAgo, to: .now)! _ = try await manager.claimDailySpin(date: date) } #expect(manager.weeklyClaimCount == 3) #expect(manager.canClaimThisWeek == false) }
swift// Check if user can spin today if rewardManager.canClaimToday { let reward = try await rewardManager.claimDailySpin() // Show reward animation } // Time until next spin let timeRemaining = rewardManager.timeUntilNextClaim // Returns TimeInterval — use for countdown display
swift// Claim via mystery box mechanism let reward = try await rewardManager.claimMysteryBox() // Claim via random bonus (triggered by app action) let bonus = try await rewardManager.claimRandomBonus() // All claim methods enforce daily/weekly caps automatically
swift// Get all past rewards let history = rewardManager.claimHistory // [RewardClaim] // Grouped by day for display let grouped = rewardManager.claimHistoryByDay // [Date: [RewardClaim]] // Filter by rarity let rareRewards = rewardManager.claims(withRarity: .rare)
swiftlet canClaim = rewardManager.canClaimToday // Daily cap not reached let canClaimWeek = rewardManager.canClaimThisWeek // Weekly cap not reached let countdown = rewardManager.timeUntilNextClaim // Seconds until midnight reset let todayCount = rewardManager.dailyClaimCount // Claims made today let weekCount = rewardManager.weeklyClaimCount // Claims made this week
Variable-ratio reinforcement is a powerful engagement mechanic. The templates enforce ethical boundaries:
RewardPool exposes its rarity weights so you can display them in a "Reward Rates" info sheet (required by some App Store regions).Users can change their device clock to claim extra rewards. Mitigations:
Date() with Calendar.current.startOfDay(for:) — accept that determined users can game it.timeProvider protocol for injecting server time.Date timestamps alongside normalized day values so you can detect anomalies (claims with createdAt before a previously recorded claim).RewardPool accepts a seed parameter that initializes the random number generator deterministically. In tests, always pass a fixed seed so assertions are stable:
swiftlet pool = RewardPool(seed: 42) let reward = pool.drawReward() // Always produces the same reward for seed 42
In production, omit the seed to use system entropy.
The default rarity weights (common: 60%, uncommon: 25%, rare: 10%, epic: 5%) produce one epic reward roughly every 20 claims. Adjust weights in RewardPool.defaultWeights to match your engagement goals. Verify with a histogram test:
swift@Test func rarityDistributionMatchesWeights() { let pool = RewardPool(seed: 0) var counts: [Reward.Rarity: Int] = [:] for _ in 0..<10_000 { let reward = pool.drawReward() counts[reward.rarity, default: 0] += 1 } // Epic should be roughly 5% (500 +/- tolerance) #expect(counts[.epic]! > 300 && counts[.epic]! < 700) }
SwiftData stores persist across app updates but are lost on reinstall. For valuable reward inventories, sync to CloudKit or a backend. The RewardManager includes a lastSyncDate hook for this purpose.
If a user claims a reward at 11:59:59 PM and the daily reset fires at midnight, ensure the claim is attributed to the correct day. The templates use Calendar.current.startOfDay(for: claimDate) to normalize all claims to their calendar day, avoiding off-by-one errors at the boundary.
app-store/originality-check before submitting.generators/milestone-celebration — Celebrate reward milestones with animationsgenerators/streak-tracker — Combine streaks with daily rewards for compounding engagementgenerators/tipkit-generator — Coach marks to introduce the reward system to new usersOther measured skills in the registry, with their headline benchmark lift.