Unlock Rules

The Hunt Configuration Language is a domain-specific language used by PuzzleSpring to define puzzle unlocking rules, point rewards, and hint allocations in a puzzle hunt.

Quick Start

Here’s a simple example that demonstrates basic unlocking rules:

# Unlock puzzle 2 when puzzle 1 is solved
P2 <= P1

# Give 10 points when puzzle 1 is solved
10 POINTS <= P1

# Unlock puzzle 3 when either puzzle 1 or 2 is solved
P3 <= (P1 OR P2)

# Give 2 hints every 30 minutes if the team has at least 10 points
2 HINTS <= EVERY 30 MINUTES IF 10 POINTS

Basic Concepts

Puzzle IDs

Puzzles can be referenced in two ways:

By hex ID: Use P followed by the puzzle’s hexadecimal ID:

  • P1 refers to puzzle with ID 1
  • PA5 refers to puzzle with ID A5

By order number: Use P# followed by the puzzle’s order number:

  • P#1 refers to the first puzzle (order number 1)
  • P#10 refers to the tenth puzzle (order number 10)

When using a puzzle reference in a non-time-based rule condition, you can use just the puzzle reference (e.g. P1 or P#1) as a shortcut for P1 SOLVE or P#1 SOLVE. For example, P2 <= P#1 is equivalent to P2 <= P#1 SOLVE.

The special identifier PX can be used to create rules that apply to all puzzles. When PX appears multiple times in a single rule, each instance refers to the same puzzle when the rule is evaluated for each puzzle.

For example, the rule 2 PX HINTS <= PX UNLOCK will give 2 hints for puzzle A when puzzle A is unlocked, 2 hints for puzzle B when puzzle B is unlocked, and so on.

Round Numbers

Rounds are referenced with R followed by the round’s number, as shown on the Rounds page:

  • R1 refers to round number 1
  • R2 refers to round number 2

Rule Structure

Each rule follows the format:

<unlockable> <= <condition>

where <unlockable> specifies what should be unlocked or awarded, and <condition> specifies when this should happen.

Comments

The language supports shell-style comments using the # character. Everything after a # on a line is treated as a comment and ignored by the parser:

# This entire line is a comment
P2 <= P1  # This is an end-of-line comment

Unlockables

Puzzles

P1 <= <condition>  # Unlock puzzle 1
[P1, P2] <= <condition>  # Unlock multiple puzzles

Rounds

R1 <= <condition>  # Unlock round 1
[P1, R1] <= <condition>  # Unlock puzzle 1 and round 1 together

A puzzle that belongs to a round can’t be unlocked until its round is. If a puzzle’s rule is met while its round is still locked, the puzzle stays locked, then unlocks as soon as the round does. Puzzles that aren’t in any round are unaffected.

Points

10 POINTS <= <condition>  # Award 10 points
[5 POINTS, P1] <= <condition>  # Award 5 points and unlock P1
10 POINTS <= [P1, P2, P3]  # Award 10 points for solving any of these puzzles

Hints

2 HINTS <= <condition>  # Award 2 hints
3 P1 HINTS <= <condition>  # Award 3 hints for puzzle 1

Badges

"🏆" BADGE <= <condition> # Award a trophy badge
"FINISHER!" BADGE <= <condition> # Award a badge that says "FINISHER!"

Rule Conditions

Single-Use Rules

These rules are evaluated once and trigger when their condition is met:

Basic Conditions

  • Puzzle solve: P1 SOLVE
  • Puzzle unlock: P1 UNLOCK
  • Round unlock: R1 UNLOCK (or just R1)
  • Point threshold: 10 POINTS
  • Time since hunt start: +1:30 (1 hour 30 minutes)

Logical Operators

# AND operator
P3 <= (P1 AND P2)

# OR operator
P3 <= (P1 OR P2)

# N of M condition
P4 <= 2 OF (P1, P2, P3)

Multi-Use Rules

These rules can trigger multiple times based on intervals:

Time Intervals

# Every N minutes/hours
2 HINTS <= EVERY 30 MINUTES
5 POINTS <= EVERY 2 HOURS

# Intervals after events
2 HINTS <= EVERY 30 MINUTES AFTER P1 SOLVE
5 POINTS <= EVERY 1 HOUR AFTER +1:00

# Conditional intervals
2 HINTS <= EVERY 30 MINUTES IF 10 POINTS

# Limited intervals
2 HINTS <= EVERY 30 MINUTES LIMIT 3
5 POINTS <= EVERY 1 HOUR AFTER +1:00 LIMIT 2

When using multiple clauses in a multi-use rule, they must appear in this order: AFTER (optional), IF (optional), LIMIT (optional). For example: 2 HINTS <= EVERY 30 MINUTES AFTER P1 SOLVE IF 10 POINTS LIMIT 3 is valid, but 2 HINTS <= EVERY 30 MINUTES IF 10 POINTS AFTER P1 SOLVE is invalid because IF appears before AFTER.

Delayed Actions (One-Time)

For actions that should trigger exactly once after a delay from a puzzle event, use this simplified syntax:

# Give a hint 30 minutes after P1 is solved
1 HINT <= 30 MINUTES AFTER P1 SOLVE

# Unlock P2 one hour after P1 is unlocked
P2 <= 1 HOUR AFTER P1 UNLOCK

# Unlock round 3 thirty minutes after round 2 unlocks
R3 <= 30 MINUTES AFTER R2 UNLOCK

This is equivalent to but more readable than:

1 HINT <= EVERY 30 MINUTES AFTER P1 SOLVE LIMIT 1

You can also add conditions:

# Give a hint 30 minutes after P1 solve, but only if team has 10 points
1 HINT <= 30 MINUTES AFTER P1 SOLVE IF 10 POINTS

Best Practices

  • Plan puzzle dependencies carefully
  • Consider multiple paths to progress
  • Balance point requirements
  • Test unlocking sequences thoroughly using the Config Tester

Formal Grammar

The language follows this simplified grammar:

rule := unlockable "<=" condition

unlockable := puzzle_id | round_num | point_reward | hint_reward | badge | "[" unlockable_list "]"
unlockable_list := unlockable ("," unlockable)*

puzzle_id := "P" hex_number
round_num := "R" number
point_reward := number "POINTS"
hint_reward := number ["puzzle_id"] "HINTS"
badge := "\"" string "\""

condition := single_use_condition | multi_use_condition

single_use_condition := puzzle_solve | puzzle_id | puzzle_unlock | round_unlock | round_num |
                       time_since_start | point_threshold | logical_expression | delayed_action
logical_expression := "(" condition ("AND"|"OR") condition ")" |
                     number "OF" "(" condition_list ")"
delayed_action := number ("MINUTES"|"HOURS") "AFTER" point_in_time ["IF" single_use_condition]

multi_use_condition := time_interval ["IF" single_use_condition]
time_interval := "EVERY" number ("MINUTES"|"HOURS") ["AFTER" point_in_time]
point_in_time := time_since_start | puzzle_solve | puzzle_unlock | round_unlock

puzzle_solve := puzzle_id "SOLVE"
puzzle_unlock := puzzle_id "UNLOCK"
round_unlock := round_num "UNLOCK"
time_since_start := "+" hours ":" minutes
point_threshold := number "POINTS"