Skip to content

18. Selectors

EBNF
selector = selector_header ":" selector_body .
selector_header = [ reserved_construct_marker ] selector_type identifier .
selector_type = "action-selector" | "plan-selector" .

A selector definition, or just selector, specifies a set of candidate constructs and a policy for choosing among them. There are two kinds: action selectors choose among actions, and plan selectors choose among plans. Selectors are named with an identifier, and each selector in a content bundle must have a unique name.

EBNF
selector_header = [ "reserved" ] selector_type identifier .
selector_type = "action-selector" | "plan-selector" .

The selector header introduces a selector definition. It declares the selector type and its name. Action selectors may optionally include a reserved marker:

action-selector choose-greeting:
...
plan-selector choose-revenge-plan:
...
reserved action-selector special-response:
...

When an action selector is marked reserved, it may only be targeted via a reaction or through another selector, just like a reserved action. Plan selectors cannot be marked reserved, since plans are always targeted via reactions.

EBNF
selector_body = (* unordered; target group required, others optional *)
[ selector_roles ]
[ selector_conditions ]
selector_target_group .
selector_roles = "roles" ":" role+ .
selector_conditions = "conditions" ":" statements .

The selector body contains a required target group and two optional fields: roles and conditions.

EBNF
selector_roles = "roles" ":" role+ .

The optional roles field specifies one or more role definitions that parameterize the selector:

action-selector choose-social:
roles:
@person:
as: initiator
@bystander:
as: bystander
...

For action selectors, an initiator role is always required—but it does not always need to be declared explicitly. See initiator pass-through below.

Like actions, an action selector requires an initiator role. The most common use of an action selector is to select an action for a given initiator during general action targeting—for instance, choosing between greet, wave, and nod for whoever the initiator happens to be. In this common case, the selector itself has no roles beyond the initiator, and each candidate receives the same initiator.

To support this, an action selector MAY omit the roles field entirely. When it does, the compiler creates a virtual initiator role and automatically passes the initiator from the targeting context through to each candidate:

// The initiator is passed through to whichever candidate is selected
action-selector choose-greeting:
target randomly:
greet;
wave;
nod;

If a selector needs additional roles beyond the initiator—for example, a bystander that parameterizes the choice of action—the roles field MUST be present, and it MUST include an explicit initiator role alongside the additional roles:

action-selector choose-social:
roles:
@person:
as: initiator
@bystander:
as: bystander
target randomly:
greet;
wave;

Omitting the roles field signals that the initiator is the only role and should be passed through implicitly; including it signals that all roles—including the initiator—are explicitly declared.

Plans do not have initiators, so there is no initiator pass-through for plan selectors. A plan selector MAY still omit the roles field—but the meaning is different: a role-less plan selector is pure dispatch, and the selected plan’s roles are cast entirely from the world state at execution time. When a plan selector does define roles, those roles can be used to parameterize the selection or to pre-bind candidate plan roles via bindings:

// Pure dispatch: plan roles are cast at execution time
plan-selector choose-plan:
target randomly:
revenge-plan;
forgiveness-plan;
// Parameterized: the selector pre-binds a role in each candidate plan
plan-selector choose-plan-for:
roles:
@hero:
as: character
target randomly:
revenge-plan:
with partial:
@avenger: @hero
forgiveness-plan:
with partial:
@forgiver: @hero
EBNF
selector_conditions = "conditions" ":" statements .

The optional conditions field specifies a block of statements that must evaluate to truthy values for the selector to proceed.

EBNF
selector_target_group = "target" selector_policy ":" selector_candidates .
selector_policy = "randomly" | "with" "weights" | "in" "order" .
selector_candidates = selector_candidate+ .
selector_candidate = [ selector_candidate_weight ] selector_candidate_name ";"
| [ selector_candidate_weight ] selector_candidate_name ":" bindings .
selector_candidate_name = [ "selector" ] identifier .
selector_candidate_weight = "(" expression ")" .

The target group specifies the set of candidate constructs and the policy for choosing among them. It is introduced by the target keyword, followed by a target policy and a colon, and then one or more candidate entries.

The target policy determines how a candidate is selected from the group:

PolicyBehavior
randomlyA candidate is chosen uniformly at random.
with weightsA candidate is chosen with probability proportional to its weight.
in orderCandidates are tried in the order listed; the first one whose targeting succeeds is selected.
action-selector random-greeting:
target randomly:
greet;
wave;
nod;

A candidate entry names a construct to consider. By default, the candidate names an action (for action selectors) or a plan (for plan selectors). If prefixed with the selector keyword, the candidate names another selector, enabling chaining:

action-selector master-selector:
target in order:
selector urgent-responses;
selector social-actions;
idle;

A candidate entry terminates with ; if it has no bindings, or with : followed by a bindings block if it does:

action-selector choose-response:
roles:
@person:
as: initiator
target randomly:
// No bindings
wave;
// With bindings
greet:
with partial:
@greeter: @person

For action selectors, each candidate MUST precast the selector’s initiator role in the candidate’s corresponding initiator role. This ensures the initiator is consistent across the selector and all of its candidates.

When the with weights policy is used, each candidate MAY have a weight expression enclosed in parentheses before the candidate name. The expression SHOULD evaluate to a numeric value:

action-selector weighted-social:
roles:
@person:
as: initiator
target with weights:
(80) greet;
(15) wave;
(5) ignore;

If a weight expression evaluates to zero or a negative number, the candidate is excluded from selection.