ServicesQuest

Expeditions and Puzzle Chains

The other two quest kinds. Both go through the same accept / turn-in lifecycle as objective quests; what differs is how steps complete: expeditions resolve on a timed check-in, puzzles on validated answers.

Expeditions

An expedition definition has no event objectives - instead it carries a dispatch block (party size, duration, optional early return) and a resolution block (how outcomes are rolled):

{
  "quest_id": "ruins",
  "name": "Ruins Run",
  "kind": "expedition",
  "dispatch": {
    "party": { "min": 1, "max": 3 },
    "duration_seconds": 14400,
    "early_return": { "allowed_after_seconds": 7200 }
  },
  "resolution": { "outcome_kinds": ["treasure", "injury", "nothing"], "rolls": 2 }
}

Accept the quest, then dispatch a party of member identifiers:

snug quest accept --quest-id ruins
snug quest dispatch --quest-id ruins --party hero-1,scout-7
# { "quest_id": "ruins", "party": ["hero-1", "scout-7"], "resolves_at": 1787923755 }

Party members are locked while out: dispatching a member who is already on any expedition - even another user's - fails with 400 invalid_party ("member hero-1 is already on an expedition"). Locks release when the expedition resolves.

There is no scheduler; resolution is lazy. Check in at or after resolves_at and the outcomes are rolled and persisted:

snug quest check-in --quest-id ruins
# { "quest_id": "ruins", "resolved": true, "outcomes": ["treasure", "treasure"] }

Verified gating, in order:

  • Check-in before resolves_at is 409 expedition_pending.
  • check-in --early before allowed_after_seconds have elapsed is 409 early_return_not_allowed; after that it resolves immediately at full value.
  • Once resolved, the quest is ready to turn in, and the resolved party, timestamps, and outcomes stay on the state's expedition object in the quest log.

Outcomes are rolls draws over the definition's outcome_kinds - a deterministic seeded roll by default, or weighted draws from a Lottery table when resolution names one in lottery_table.

Puzzle chains

Puzzle steps carry an answer spec instead of an event objective, plus optional hints, throttling, and hiding:

{
  "quest_id": "cipher",
  "name": "Cipher Hunt",
  "kind": "puzzle",
  "step_mode": "ordered",
  "steps": [
    { "id": "riddle", "name": "First riddle",
      "answer": { "mode": "case_insensitive", "pattern": "the raven" },
      "hints": [
        { "order": 1, "text": "It is a bird", "after_seconds": 3600 },
        { "order": 2, "text": "Poe wrote about it",
          "treasury_price": { "currency": "gold", "amount": 10 } }
      ],
      "attempt_throttle": { "max_attempts": 3, "window_seconds": 60 } },
    { "id": "vault", "name": "Vault code", "hidden": true,
      "answer": { "mode": "regex", "pattern": "^\\d{4}$" } }
  ]
}

Answer modes are exact, case_insensitive, and regex (patterns are length-bounded at definition time). Answers are checked server-side only:

snug quest answer --quest-id cipher --step-id riddle --text "The RAVEN"
{ "quest_id": "cipher", "step_id": "riddle", "correct": true,
  "step_completed": true, "ready_for_turn_in": false, "attempts_remaining": 1 }

Anti-spoiler and anti-bruteforce behaviors:

  • Patterns never ship to clients - in every definition response the answer.pattern comes back null.
  • Hidden steps are masked - a hidden: true step's name reads ??? and its description is dropped in definition responses. It still accepts answers by its real step id once reached.
  • Attempt throttling - with the throttle above, the fourth wrong answer inside the window is 429 answer_throttled ("max 3 attempts per 60s window"). attempts_remaining counts down per window; without a throttle it is a u32::MAX sentinel, not a real count.

Hints

hints lists a step's hints with locked text withheld:

snug quest hints --quest-id cipher --step-id riddle
{ "hints": [
    { "order": 1, "text": null, "unlocked": false, "unlock_condition": "unlocks after 3600s" },
    { "order": 2, "text": null, "unlocked": false, "unlock_condition": "costs 10 gold" }
] }

Time-gated hints unlock themselves: once after_seconds have passed since accept, the listing returns their text with no call needed - and unlock-hint on one is rejected with 400 hint_not_purchasable. The unlock endpoint is for paid hints:

snug quest unlock-hint --quest-id cipher --step-id riddle --order 2

charges the caller's Treasury wallet and returns the hint text; without the funds it is 409 insufficient_funds ("need 10 gold") and nothing is charged. Unlocked hints are remembered on the quest state.

Parameters and error codes for every endpoint are in the Quest API reference.

On this page