///|
pub(all) struct FixtureSpec {
name : String
description : String
dsl : String
expected : BtStatus
max_ticks : Int
} derive(Debug, Eq)
///|
pub fn FixtureSpec::summary(self : FixtureSpec) -> String {
self.name +
": expected=" +
self.expected.to_text() +
", max_ticks=" +
self.max_ticks.to_string() +
", description=" +
self.description
}
///|
pub fn fixture_catalog() -> Array[FixtureSpec] {
[
guarded_attack_fixture(),
patrol_fixture(),
robot_delivery_fixture(),
agent_tool_fixture(),
boss_phase_fixture(),
tutorial_hint_fixture(),
stealth_guard_fixture(),
harvest_worker_fixture(),
qa_agent_fixture(),
mission_selector_fixture(),
]
}
///|
pub fn fixture_names() -> Array[String] {
let specs = fixture_catalog()
let names = Array::new()
let mut i = 0
while i < specs.length() {
names.push(specs[i].name)
i = i + 1
}
names
}
///|
pub fn find_fixture(name : String) -> FixtureSpec? {
let specs = fixture_catalog()
let mut i = 0
while i < specs.length() {
if specs[i].name == name {
return Some(specs[i])
}
i = i + 1
}
None
}
///|
pub fn load_fixture(name : String) -> Result[DslDocument, BtError] {
match find_fixture(name) {
Some(spec) => parse_dsl(spec.dsl)
None => Err(ParseError("unknown fixture: " + name))
}
}
///|
pub fn run_fixture(name : String) -> RunSummary {
match find_fixture(name) {
Some(spec) =>
match parse_dsl(spec.dsl) {
Ok(doc) =>
smoke_run(spec.name, doc.tree, doc.blackboard, spec.expected, max_ticks=spec.max_ticks)
Err(err) =>
{
name,
final_status: Failure,
ticks: 0,
trace_events: 0,
digest: "parse-error",
ok: false,
detail: err.message(),
}
}
None =>
{
name,
final_status: Failure,
ticks: 0,
trace_events: 0,
digest: "missing-fixture",
ok: false,
detail: "unknown fixture",
}
}
}
///|
pub fn run_fixture_catalog() -> Array[RunSummary] {
let specs = fixture_catalog()
let out = Array::new()
let mut i = 0
while i < specs.length() {
out.push(run_fixture(specs[i].name))
i = i + 1
}
out
}
///|
pub fn fixture_report_lines() -> Array[String] {
let summaries = run_fixture_catalog()
let lines = Array::new()
lines.push("MoonBTKit fixture report")
let mut i = 0
while i < summaries.length() {
lines.push(summaries[i].to_line())
i = i + 1
}
lines
}
///|
pub fn guarded_attack_fixture() -> FixtureSpec {
{
name: "guarded_attack",
description: "NPC checks resources before aiming and firing at a visible enemy.",
dsl: guarded_attack_dsl(),
expected: Success,
max_ticks: 6,
}
}
///|
pub fn guarded_attack_dsl() -> String {
"root root\n" +
"blackboard enemy_visible true\n" +
"blackboard ammo 3\n" +
"blackboard stamina 8\n" +
"selector root engage fallback\n" +
"sequence engage see_enemy has_ammo has_stamina aim fire mark_done\n" +
"condition see_enemy enemy_visible eq true\n" +
"condition has_ammo ammo gt 0\n" +
"condition has_stamina stamina ge 5\n" +
"action aim aim_weapon running,success mode=aiming\n" +
"action fire fire_weapon success ammo=2 last_action=fire\n" +
"set mark_done combat_state engaged\n" +
"sequence fallback emit_idle idle_wait\n" +
"emit emit_idle idle\n" +
"wait idle_wait 1\n"
}
///|
pub fn patrol_fixture() -> FixtureSpec {
{
name: "patrol",
description: "Guard patrols when no enemy is visible and records the patrol route.",
dsl: patrol_dsl(),
expected: Success,
max_ticks: 8,
}
}
///|
pub fn patrol_dsl() -> String {
"root root\n" +
"blackboard enemy_visible false\n" +
"blackboard waypoint 1\n" +
"selector root engage patrol\n" +
"sequence engage see_enemy attack\n" +
"condition see_enemy enemy_visible eq true\n" +
"action attack attack_target success state=attack\n" +
"sequence patrol choose_route walk_a wait_a walk_b remember\n" +
"emit choose_route route_selected\n" +
"action walk_a walk_to_a running,success waypoint=2\n" +
"wait wait_a 1\n" +
"action walk_b walk_to_b running,success waypoint=3\n" +
"set remember state patrolling\n"
}
///|
pub fn robot_delivery_fixture() -> FixtureSpec {
{
name: "robot_delivery",
description: "Warehouse robot prefers charging before pickup when battery is low.",
dsl: robot_delivery_dsl(),
expected: Success,
max_ticks: 12,
}
}
///|
pub fn robot_delivery_dsl() -> String {
"root root\n" +
"blackboard battery 18\n" +
"blackboard package_ready true\n" +
"selector root charge_branch deliver_branch\n" +
"sequence charge_branch low_battery dock charge undock\n" +
"condition low_battery battery lt 25\n" +
"action dock dock_to_station running,success docked=true\n" +
"action charge charge_battery running,running,success battery=90\n" +
"action undock leave_station success docked=false\n" +
"sequence deliver_branch package_ready_node pickup navigate dropoff\n" +
"condition package_ready_node package_ready eq true\n" +
"action pickup pickup_package running,success carrying=true\n" +
"action navigate navigate_to_target running,running,success location=target\n" +
"action dropoff drop_package success carrying=false delivered=true\n"
}
///|
pub fn agent_tool_fixture() -> FixtureSpec {
{
name: "agent_tool",
description: "AI agent chooses a safe search tool before a higher-risk shell action.",
dsl: agent_tool_dsl(),
expected: Success,
max_ticks: 8,
}
}
///|
pub fn agent_tool_dsl() -> String {
"root root\n" +
"blackboard needs_current_info true\n" +
"blackboard shell_allowed false\n" +
"selector root web_plan shell_plan fallback\n" +
"sequence web_plan needs_info search summarize\n" +
"condition needs_info needs_current_info eq true\n" +
"action search web_search running,success evidence=found\n" +
"action summarize summarize_evidence success answer=ready\n" +
"sequence shell_plan can_shell run_shell\n" +
"condition can_shell shell_allowed eq true\n" +
"action run_shell shell_command success answer=ready\n" +
"set fallback answer manual_review\n"
}
///|
pub fn boss_phase_fixture() -> FixtureSpec {
{
name: "boss_phase",
description: "Game boss changes phase based on health and runs a telegraphed attack.",
dsl: boss_phase_dsl(),
expected: Success,
max_ticks: 10,
}
}
///|
pub fn boss_phase_dsl() -> String {
"root root\n" +
"blackboard hp 35\n" +
"blackboard player_visible true\n" +
"selector root phase_two phase_one recover\n" +
"sequence phase_two low_hp visible roar charge slam\n" +
"condition low_hp hp le 40\n" +
"condition visible player_visible eq true\n" +
"emit roar enraged\n" +
"action charge charge_attack running,running,success attack=charged\n" +
"action slam ground_slam running,success damage=heavy\n" +
"sequence phase_one healthy basic_attack\n" +
"condition healthy hp gt 40\n" +
"action basic_attack basic_attack success damage=light\n" +
"action recover recover_posture success state=recover\n"
}
///|
pub fn tutorial_hint_fixture() -> FixtureSpec {
{
name: "tutorial_hint",
description: "Tutorial system waits, checks whether the player succeeded, then shows a hint.",
dsl: tutorial_hint_dsl(),
expected: Success,
max_ticks: 6,
}
}
///|
pub fn tutorial_hint_dsl() -> String {
"root root\n" +
"blackboard objective_done false\n" +
"selector root already_done hint_path\n" +
"sequence already_done done_cond celebrate\n" +
"condition done_cond objective_done eq true\n" +
"emit celebrate completed\n" +
"sequence hint_path grace_wait still_not_done show_hint\n" +
"wait grace_wait 2\n" +
"inverter still_not_done done_cond\n" +
"action show_hint show_context_hint success hint=shown\n"
}
///|
pub fn stealth_guard_fixture() -> FixtureSpec {
{
name: "stealth_guard",
description: "Stealth guard escalates from suspicion to alarm after repeated sighting.",
dsl: stealth_guard_dsl(),
expected: Success,
max_ticks: 10,
}
}
///|
pub fn stealth_guard_dsl() -> String {
"root root\n" +
"blackboard noise_heard true\n" +
"blackboard player_spotted true\n" +
"selector root alarm investigate idle\n" +
"sequence alarm spotted radio chase\n" +
"condition spotted player_spotted eq true\n" +
"action radio radio_alarm running,success alarm=true\n" +
"action chase chase_player running,success state=chasing\n" +
"sequence investigate heard turn search\n" +
"condition heard noise_heard eq true\n" +
"action turn turn_toward_noise success state=suspicious\n" +
"action search search_area running,success state=searching\n" +
"set idle state idle\n"
}
///|
pub fn harvest_worker_fixture() -> FixtureSpec {
{
name: "harvest_worker",
description: "Simulation worker repeats harvest steps and stores resource progress.",
dsl: harvest_worker_dsl(),
expected: Success,
max_ticks: 12,
}
}
///|
pub fn harvest_worker_dsl() -> String {
"root root\n" +
"blackboard tool_ready true\n" +
"sequence root ready repeat_cycle store\n" +
"condition ready tool_ready eq true\n" +
"repeat repeat_cycle 3 cycle\n" +
"sequence cycle swing collect\n" +
"action swing swing_tool running,success animation=swing\n" +
"action collect collect_resource success resource=wood\n" +
"set store task harvest_complete\n"
}
///|
pub fn qa_agent_fixture() -> FixtureSpec {
{
name: "qa_agent",
description: "Agent triages whether to answer directly or ask for clarification.",
dsl: qa_agent_dsl(),
expected: Success,
max_ticks: 5,
}
}
///|
pub fn qa_agent_dsl() -> String {
"root root\n" +
"blackboard has_context true\n" +
"blackboard ambiguity 1\n" +
"selector root answer clarify\n" +
"sequence answer context_ok ambiguity_low draft verify\n" +
"condition context_ok has_context eq true\n" +
"condition ambiguity_low ambiguity le 2\n" +
"action draft draft_answer running,success draft=ready\n" +
"action verify verify_answer success answer=ready\n" +
"action clarify ask_clarification success answer=needs_input\n"
}
///|
pub fn mission_selector_fixture() -> FixtureSpec {
{
name: "mission_selector",
description: "Quest controller picks emergency, main quest, or side quest by priority.",
dsl: mission_selector_dsl(),
expected: Success,
max_ticks: 8,
}
}
///|
pub fn mission_selector_dsl() -> String {
"root root\n" +
"blackboard emergency false\n" +
"blackboard main_available true\n" +
"blackboard reputation 7\n" +
"selector root emergency_branch main_branch side_branch\n" +
"sequence emergency_branch is_emergency evacuate\n" +
"condition is_emergency emergency eq true\n" +
"action evacuate start_evacuation success mission=emergency\n" +
"sequence main_branch main_ok enough_rep start_main\n" +
"condition main_ok main_available eq true\n" +
"condition enough_rep reputation ge 5\n" +
"action start_main start_main_quest running,success mission=main\n" +
"action side_branch start_side_quest success mission=side\n"
}