Challenges
Summary
Implement challenge creation, completion, and results comparison on mobile. Challenges are social motivators — pick a drill, invite friends, compare best scores.
Context
A challenge references a drill via drill_id. Scores also reference drills via drill_id. Participant scores are queried by joining on the shared drill_id + shooter_id — no schema changes needed. “Best score” is determined by the drill’s calculation method (e.g., highest hit factor, lowest time).
Scoring semantics (MVP): post-challenge derived best. A participant’s standing is their best non-voided score for the drill recorded after the challenge existed for them — scores recorded before the challenge are ignored. This is a deliberate MVP choice that differs from the legacy “dedicated one-off run with a deadline” model; see Challenge Scoring Semantics for the decision and the deferred Option A.
Participants are added immediately on challenge creation (no pending/accept state in the schema). The mobile UI reflects this — challenges appear in your list when invited, no accept/decline flow.
Requirements
- Create challenge (name, select drill, invite friends)
- Challenge list with tabs for active (open/in-progress) and completed
- Challenge detail view with participants, display names, and best scores
- Complete challenge (creator only, idempotent)
Details
New API work: GetChallenge RPC
Added to ChallengeService proto. Returns hydrated challenge detail in a single round-trip.
Request: challenge_id
Response: challenge + list of ChallengeParticipantDetail
ChallengeParticipantDetail fields: user_id, display_name, joined_at, optional score_id, points, time, hit_factor
Authorization: caller must be a participant or creator. Returns PermissionDenied otherwise.
Best score selection: Server joins drills to get calculation method, then orders the LATERAL score query:
- HitFactor: highest
hit_factor - TimePlus/TimeOnly: lowest
time - PointsOnly: highest
points - PassFail:
pass = truefirst, then highestpoints - Voided scores excluded
- Challenge window only: only scores with
timestamp >= GREATEST(challenge.created_at, participant.joined_at)count; once the challenge is completed, alsotimestamp <= challenge.completed_at(standings freeze at completion). MVP post-challenge derived best — see Challenge Scoring Semantics
Participant ordering: ORDER BY joined_at ASC, user_id ASC for deterministic rendering.
Integration tests (minimum):
- Participant can fetch detail
- Non-participant forbidden
- Best-score selection ignores voided scores
- Best-score selection ignores pre-challenge scores (recorded before
GREATEST(created_at, joined_at)) - Completed challenge freezes standings: scores recorded after
completed_atdon’t change the result - No-score participant still returned
- Completed challenge still fetchable
Challenge flow
- Create: User picks a drill, names the challenge, selects friends to invite. Invited users become participants immediately (no pending state). Push notification sent.
- List: Tabs for “Active” (open + in_progress) and “Completed”. Paginated.
- Detail: Challenge name, drill name, status, participant list with display names and best scores. Creator sees “Complete” button.
- Complete: Creator-only. Marks challenge as completed. Sends push notification.
Navigation
Accessible from Training tab (toolbar entry point).
Architecture
- New
ChallengeService(iOS/Android) — RPCs: ListChallenges, GetChallenge, CreateChallenge, CompleteChallenge - New
ChallengeViewModel— challenge list state, detail state, create flow - Drill picker reuses existing drill list from
ScoringViewModel - Friend picker queries
FriendServicefor invitation selection - Proto changes: new
GetChallengeRPC,GetChallengeRequest,GetChallengeResponse,ChallengeParticipantDetailmessages
Not in scope
- Accept/decline UI (participants are immediate on creation, AcceptChallenge RPC is idempotent no-op)
- Score entry within challenge context (users score drills normally)
- Challenge media (deferred to mobile-media)
- Challenge expiration/auto-complete
Open Questions
-
Challenge drill typesAll drills in the user’s list are valid. API validates server-side. -
Completed challenge detail viewGetChallenge RPC returns participants + best scores via drill_id join. -
Personal completed challengesListChallenges with status filter. Tabs in UI. -
Challenge mediaDeferred to mobile-media (cross-reference added).