///|
pub(all) struct TreeStats {
nodes : Int
leaves : Int
composites : Int
decorators : Int
conditions : Int
actions : Int
waits : Int
max_children : Int
max_depth : Int
} derive(Debug, Eq)
///|
pub fn TreeStats::to_line(self : TreeStats) -> String {
"nodes=" +
self.nodes.to_string() +
", leaves=" +
self.leaves.to_string() +
", composites=" +
self.composites.to_string() +
", decorators=" +
self.decorators.to_string() +
", conditions=" +
self.conditions.to_string() +
", actions=" +
self.actions.to_string() +
", waits=" +
self.waits.to_string() +
", max_children=" +
self.max_children.to_string() +
", max_depth=" +
self.max_depth.to_string()
}
///|
pub fn analyze_tree(tree : BehaviorTree) -> TreeStats {
let mut leaves = 0
let mut composites = 0
let mut decorators = 0
let mut conditions = 0
let mut actions = 0
let mut waits = 0
let mut max_children = 0
let mut i = 0
while i < tree.nodes.length() {
let n = tree.nodes[i]
if n.children.length() == 0 {
leaves = leaves + 1
}
if n.children.length() > max_children {
max_children = n.children.length()
}
match n.kind {
Sequence | Selector | ParallelAll | ParallelAny => composites = composites + 1
Inverter | Succeeder | Failer | Repeat(_) | Retry(_) => decorators = decorators + 1
Condition(_, _, _) => conditions = conditions + 1
ActionPlan(_, _, _) => actions = actions + 1
Wait(_) => waits = waits + 1
_ => ()
}
i = i + 1
}
{
nodes: tree.nodes.length(),
leaves,
composites,
decorators,
conditions,
actions,
waits,
max_children,
max_depth: tree_depth(tree),
}
}
///|
pub(all) struct LintIssue {
severity : String
node_id : String
message : String
} derive(Debug, Eq)
///|
pub fn LintIssue::to_line(self : LintIssue) -> String {
self.severity + " " + self.node_id + ": " + self.message
}
///|
pub(all) struct LintReport {
ok : Bool
issues : Array[LintIssue]
} derive(Debug, Eq)
///|
pub fn LintReport::summary(self : LintReport) -> String {
if self.ok {
"lint ok"
} else {
"lint issues=" + self.issues.length().to_string()
}
}
///|
pub fn LintReport::lines(self : LintReport) -> Array[String] {
let lines = Array::new()
if self.ok {
lines.push("lint ok")
} else {
let mut i = 0
while i < self.issues.length() {
lines.push(self.issues[i].to_line())
i = i + 1
}
}
lines
}
///|
pub fn lint_tree(tree : BehaviorTree) -> LintReport {
let issues = Array::new()
let validation = tree.validate()
let mut v = 0
while v < validation.issues.length() {
issues.push({ severity: "error", node_id: tree.root, message: validation.issues[v] })
v = v + 1
}
let mut i = 0
while i < tree.nodes.length() {
let n = tree.nodes[i]
if n.name == "" {
issues.push({ severity: "warning", node_id: n.id, message: "node name is empty" })
}
if n.children.length() > 8 {
issues.push({
severity: "info",
node_id: n.id,
message: "node has many children; consider grouping for readable traces",
})
}
match n.kind {
Wait(ticks) =>
if ticks > 120 {
issues.push({
severity: "info",
node_id: n.id,
message: "long wait may hide intent in frame-based games",
})
}
ActionPlan(_, statuses, _) => {
if statuses.length() > 20 {
issues.push({
severity: "info",
node_id: n.id,
message: "scripted action has a long status sequence",
})
}
if action_never_finishes(statuses) {
issues.push({
severity: "warning",
node_id: n.id,
message: "action status script never reaches success or failure",
})
}
}
Repeat(count) | Retry(count) =>
if count > 20 {
issues.push({
severity: "info",
node_id: n.id,
message: "large decorator count can make tests slow",
})
}
_ => ()
}
i = i + 1
}
{ ok: issues.length() == 0, issues }
}
///|
pub(all) struct RunSummary {
name : String
final_status : BtStatus
ticks : Int
trace_events : Int
digest : String
ok : Bool
detail : String
} derive(Debug, Eq)
///|
pub fn RunSummary::to_line(self : RunSummary) -> String {
self.name +
": status=" +
self.final_status.to_text() +
", ticks=" +
self.ticks.to_string() +
", events=" +
self.trace_events.to_string() +
", digest=" +
self.digest +
", ok=" +
(if self.ok { "true" } else { "false" }) +
", detail=" +
self.detail
}
///|
pub fn smoke_run(
name : String,
tree : BehaviorTree,
board : Blackboard,
expected : BtStatus,
max_ticks? : Int,
) -> RunSummary {
let engine = new_engine(tree, blackboard=board, config=default_tick_config())
match engine.run_until_done(max_ticks=max_ticks.unwrap_or(32)) {
Ok(result) => {
let ok = result.status == expected
{
name,
final_status: result.status,
ticks: result.tick,
trace_events: engine.trace.length(),
digest: engine.trace_digest(),
ok,
detail: if ok { "matched expected status" } else { "unexpected final status" },
}
}
Err(err) => {
{
name,
final_status: Failure,
ticks: engine.tick_count,
trace_events: engine.trace.length(),
digest: engine.trace_digest(),
ok: false,
detail: err.message(),
}
}
}
}
///|
pub(all) struct MarkdownReport {
title : String
lines : Array[String]
} derive(Debug, Eq)
///|
pub fn MarkdownReport::text(self : MarkdownReport) -> String {
join_strings(self.lines, "\n")
}
///|
pub fn tree_markdown_report(
title : String,
tree : BehaviorTree,
board : Blackboard,
) -> MarkdownReport {
let stats = analyze_tree(tree)
let lint = lint_tree(tree)
let lines = Array::new()
lines.push("# " + title)
lines.push("")
lines.push("## Summary")
lines.push("")
lines.push("- Root: `" + tree.root + "`")
lines.push("- Stats: " + stats.to_line())
lines.push("- Blackboard entries: " + board.len().to_string())
lines.push("- Lint: " + lint.summary())
lines.push("")
lines.push("## Nodes")
lines.push("")
let mut i = 0
while i < tree.nodes.length() {
let n = tree.nodes[i]
lines.push(
"- `" +
n.id +
"` " +
n.kind.to_text() +
" children=[" +
join_strings(n.children, ", ") +
"]",
)
i = i + 1
}
if board.len() > 0 {
lines.push("")
lines.push("## Blackboard")
lines.push("")
let board_lines = board.to_lines()
let mut b = 0
while b < board_lines.length() {
lines.push("- " + board_lines[b])
b = b + 1
}
}
if !lint.ok {
lines.push("")
lines.push("## Lint")
lines.push("")
let lint_lines = lint.lines()
let mut l = 0
while l < lint_lines.length() {
lines.push("- " + lint_lines[l])
l = l + 1
}
}
{ title, lines }
}
///|
pub fn explain_trace(events : Array[TickEvent]) -> Array[String] {
let lines = Array::new()
let mut i = 0
while i < events.length() {
let event = events[i]
lines.push(
event.tick.to_string() +
". " +
event.node_id +
" evaluated as " +
event.status.to_text() +
" via " +
event.kind,
)
i = i + 1
}
lines
}
///|
pub fn count_status(events : Array[TickEvent], status : BtStatus) -> Int {
let mut count = 0
let mut i = 0
while i < events.length() {
if events[i].status == status {
count = count + 1
}
i = i + 1
}
count
}
///|
pub fn trace_contains(events : Array[TickEvent], node_id : String, status : BtStatus) -> Bool {
let mut i = 0
while i < events.length() {
if events[i].node_id == node_id && events[i].status == status {
return true
}
i = i + 1
}
false
}
///|
pub fn board_contains(board : Array[(String, BtValue)], key : String, value : BtValue) -> Bool {
let mut i = 0
while i < board.length() {
if board[i].0 == key && board[i].1 == value {
return true
}
i = i + 1
}
false
}
///|
fn action_never_finishes(statuses : Array[BtStatus]) -> Bool {
if statuses.length() == 0 {
return true
}
let mut i = 0
while i < statuses.length() {
if statuses[i] != Running {
return false
}
i = i + 1
}
true
}
///|
fn tree_depth(tree : BehaviorTree) -> Int {
if !tree.has_node(tree.root) {
0
} else {
depth_at(tree, tree.root, 1)
}
}
///|
fn depth_at(tree : BehaviorTree, id : String, depth : Int) -> Int {
match tree.node(id) {
Some(n) => {
let mut best = depth
let mut i = 0
while i < n.children.length() {
let child_depth = depth_at(tree, n.children[i], depth + 1)
if child_depth > best {
best = child_depth
}
i = i + 1
}
best
}
None => depth
}
}