Skip to content

Selectors & values

Two small types thread through every condition and action: selectors (which players) and values (what to compare or store). Understanding them — and the binding model that connects them — is the difference between rules that work and rules that do the right thing to the wrong player.

type PlayerSelector =
| "any"
| "same"
| "other"
| { byId: string }
| { byTeam: string };
SelectorMeans
anyEvery player (used in conditions to iterate / match anyone)
sameThe primary bound player of the current match
otherThe partner bound by a two-party condition (or the item, in item interactions)
{ byTeam: "blue" }All players on that team
{ byId: "abc123" }One specific player by id

Selectors appear in conditions (who, a, b) to decide who matches, and in actions (target, to) to decide who’s affected.

- { kind: increment_state, target: { byTeam: blue }, key: score, by: 1 } # whole team
- { kind: send_event, to: same, event: { type: toast, text: "Hi" } } # just this player

A binding is the set of players a condition match carries into the rule’s actions and nested conditions. The rule: the condition decides who same and other are; everything downstream refers to them.

ConditionBinds sameBinds other
entered_zone / left_zonethe crossing player
state_equals / count_state / compareeach matching player
proximity { a, b }a player matching aa player matching b
interaction (player target)the initiatorthe target player
interaction (item target)the initiatorthe matched item

Inside an and, the first player-binding child establishes same/other; later children (state_equals who: same, compare ... other.teamId) filter that binding. This is why order matters in an and.

when:
kind: and
children:
- { kind: proximity, a: any, b: any, meters: 5 } # same = a, other = b
- { kind: state_equals, who: same, key: hasFlag, value: red }
- { kind: compare, op: eq, left: { ref: other.teamId }, right: red }
do:
- { kind: set_state, target: same, key: hasFlag, value: null } # acts on the carrier
- { kind: spawn_item, item: red_flag, at: same }

In a worldItem/heldItem interaction the item is bound as other. You can read item fields by path and the custody actions default to it:

- id: pickup_coin
when: { kind: interaction, verb: grab_coin }
do:
- { kind: set_state, target: same, key: hasCoin, value: true }
- { kind: set_state, target: same, key: coinId, value: { ref: other.id } } # remember which coin

Source: apps/wage-engine/src/games/pirates_booty/game.yaml.

type Value =
| string | number | boolean | null // a bare literal
| { lit: unknown } // an explicit literal
| { ref: string }; // a path into the current binding

A Value is what goes on either side of a compare, or into a set_state / set_state_delayed.

  • Bare primitive5, "red", true, null — used directly as a literal.
  • { lit: ... } — an explicit literal. Use it when the literal is itself an object, or when its shape might be mistaken for a { ref }/{ lit }.
  • { ref: "path" } — read a value from the current binding’s players.

A ref path is (same|other).(id|teamId|name|state.KEY):

PathReads
same.id / other.idthe player’s id
same.teamId / other.teamIdthe player’s team
same.name / other.namethe player’s display name
same.state.scorea state key on that player
other.state.targetIda state key on the partner

An unresolvable path yields undefined.

# Same-team check.
- { kind: compare, op: eq, left: { ref: same.teamId }, right: { ref: other.teamId } }
# Threshold against live state.
- { kind: compare, op: gte, left: { ref: same.state.score }, right: 30 }
# Identity check (is the partner my assigned target?).
- { kind: compare, op: eq, left: { ref: other.id }, right: { ref: same.state.targetId } }
# Copy a value across players.
- { kind: set_state, target: same, key: targetId, value: { ref: other.state.targetId } }

These examples are drawn from king_of_the_hill, capture_the_flag, and assassin under apps/wage-engine/src/games/.