Game Rules — Domain Spec¶
Rules describe what it takes to complete a challenge. This page is the authoring contract for rule expressions — vocabulary, validation, triggers, scope. See Game Engine for the runtime.
Mental model¶
A rule is an expression tree stored on challenges.rule. Two node
shapes:
- Leaf
{ kind, params }— one typed measurement against the user's state (counts of things, distinct sums, presence days, etc.). 13 leaf kinds registered today. - Composite
{ op, children[, n] }— combines child results via an operator. Four operators.
Tree depth is capped at 4 — deeper is almost certainly an authoring mistake. Validation runs at write time and rejects malformed trees with path-precise errors.
Operators¶
| Op | Semantics | Concurrency |
|---|---|---|
all_of |
Every child must complete | Parallel |
any_of |
Any child completing satisfies it | Parallel |
n_of |
At least N children must complete (requires n field) |
Parallel |
sequence |
Children must complete strictly in order (later children skip evaluation until predecessor completes) | Serial — short-circuits |
Only sequence-eligible leaves can appear under (or transitively under)
a sequence operator. Cumulative leaves (presence_days,
geographic_breadth, category_breadth, unique_location_count) aren't
sequence-eligible — their "since T" semantic isn't meaningful.
Leaf catalog¶
| Kind | Sequence-eligible | Triggers | What it counts |
|---|---|---|---|
visit_count |
✓ | [travel_log] |
Travel-log rows; filters cover source / category / tags / location_ids / location_scope / time_window |
event_count |
✓ | [event_attend, event_host] |
Event participations or hosts; role, categories, capacity_filled |
friend_count |
✓ | [friend] |
Accepted friendships |
friend_diversity |
✓ | [friend] |
Distinct geographic units among friends' countries; distinct_by: country\|region\|continent |
first_at_location |
✓ | [travel_log] |
Locations the user was first to discover |
presence_days |
✗ | [travel_log, user_activated] |
Distinct days the user appeared (visit or app open); consecutive, window_days |
geographic_breadth |
✗ | [travel_log] |
Distinct values of unit: country\|region\|city\|district\|continent; location_scope, countries whitelist |
category_breadth |
✗ | [travel_log, event_host] |
Distinct location or event categories; kind: location\|event, level: top\|sub |
unique_location_count |
✗ | [travel_log] |
Distinct location IDs; location_scope, tags |
stamp_earned_count |
✓ | [stamp_mint] |
Curated stamps earned (meta-progression); scope: city\|region\|country\|multi_country |
curio_earned_count |
✓ | [travel_log, friend, event_host, user_activated] |
Curios already earned; category |
challenge_completed |
✓ | [travel_log, friend, event_host, user_activated] |
Other completed challenges; slug, series_slug, cadence, parent_challenge_id |
co_experience (stub) |
✓ | [tag_accept, travel_log] |
Co-tagged moments with friends; returns 0 until tagging ships |
Triggers¶
Each leaf declares which ActionKind values can possibly move it. At
challenge publish time, the union across all leaves is computed and
persisted to challenges.triggers text[]. The engine's candidate query
filters by triggers && $signal_kinds so signals only re-evaluate
challenges that could possibly move.
Most rules end up with [travel_log] in their triggers (the workhorse
leaf is visit-related). Non-visit signals (friend, event_host,
user_activated) reduce the candidate set to the small subset that
actually cares.
Scope¶
| Scope | EvalContext | Status |
|---|---|---|
user |
UserContext{user_id} — leaves query WHERE user_id = ANY({uid}) |
Active |
circle |
CircleContext{circle_id, member_ids} — leaves query WHERE user_id = ANY(member_ids) |
Reserved; lands when circles ship |
The same leaf code serves both — the EvalContext interface abstracts the member-set query.
Authoring examples¶
Visit 10 countries¶
{ "kind": "geographic_breadth", "params": { "unit": "country", "count": 10 } }
10 nightlife visits after 6pm¶
{
"kind": "visit_count",
"params": {
"count": 10,
"category": "nightlife",
"time_window": { "after": 18, "before": 5 }
}
}
Visit all 50 US states¶
{
"kind": "geographic_breadth",
"params": {
"unit": "region",
"count": 50,
"location_scope": { "country": "US" }
}
}
20 friends from different countries¶
{ "kind": "friend_diversity", "params": { "distinct_by": "country", "count": 20 } }
Curated "East Asia" set¶
{
"kind": "geographic_breadth",
"params": {
"unit": "country",
"count": 4,
"countries": ["JP", "KR", "CN", "TW", "MN"]
}
}
Both events AND friends¶
{
"op": "all_of",
"children": [
{ "kind": "event_count", "params": { "count": 3 } },
{ "kind": "friend_count", "params": { "count": 5 } }
]
}
See also¶
- Challenges — the engagement unit rules sit on
- Tomoda Collection — where rewards land
- Game Engine service — runtime