Skip to content

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