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_atis409 expedition_pending. check-in --earlybeforeallowed_after_secondshave elapsed is409 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
expeditionobject 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.patterncomes backnull. - Hidden steps are masked - a
hidden: truestep'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_remainingcounts down per window; without a throttle it is au32::MAXsentinel, 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 2charges 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.