// ============================================================
// Utility-based selection
//
// A utility selector evaluates candidates against the current blackboard and
// keeps the selected candidate active across Running frames. It is useful when
// an agent chooses among goals such as flee, attack, gather, and patrol.
// ============================================================
///|
/// One named candidate in a utility selector.
pub struct UtilityCandidate {
name : String
score : (Blackboard) -> Double
node : Node
}
///|
/// Construct a utility candidate.
pub fn UtilityCandidate::new(
name : String,
score : (Blackboard) -> Double,
node : Node,
) -> UtilityCandidate {
{ name, score, node }
}
///|
/// Candidate name for diagnostics.
pub fn UtilityCandidate::name(self : UtilityCandidate) -> String {
self.name
}
///|
/// Evaluate a candidate against a blackboard.
pub fn UtilityCandidate::evaluate(
self : UtilityCandidate,
bb : Blackboard,
) -> Double {
(self.score)(bb)
}
///|
/// Select the highest-scoring candidate and preserve it while Running.
pub fn utility_selector_node(
candidates : Array[UtilityCandidate],
fallback : Node,
) -> Node {
let active : Ref[Int] = Ref::new(-1)
let tick = fn(bb : Blackboard) {
let selected = if active.get() >= 0 {
active.get()
} else {
let best = Ref::new(-1)
let best_score = Ref::new(-1.0 / 0.0)
let mut i = 0
while i < candidates.length() {
let score = candidates[i].evaluate(bb)
if best.get() < 0 || score > best_score.get() {
best.set(i)
best_score.set(score)
}
i = i + 1
}
best.get()
}
if selected < 0 {
fallback.tick(bb)
} else {
active.set(selected)
let status = candidates[selected].node.tick(bb)
match status {
Status::BTRunning => Status::BTRunning
Status::BTSuccess | Status::BTFailure => {
candidates[selected].node.reset()
active.set(-1)
status
}
}
}
}
let reset = fn() {
for candidate in candidates {
candidate.node.reset()
}
fallback.reset()
active.set(-1)
}
Node::new(tick, reset)
}
///|
/// Select a candidate only when its score meets a minimum threshold.
pub fn threshold_utility_node(
candidates : Array[UtilityCandidate],
threshold : Double,
fallback : Node,
) -> Node {
let filtered : Array[UtilityCandidate] = []
for candidate in candidates {
let _ = candidate
filtered.push(candidate)
}
let node = utility_selector_node(filtered, fallback)
let gate = fn(bb : Blackboard) {
let mut has_eligible = false
for candidate in candidates {
if candidate.evaluate(bb) >= threshold {
has_eligible = true
}
}
has_eligible
}
require_node(gate, node)
}