feat(bt): add repeat and selector task kinds
This commit is contained in:
+231
@@ -40,6 +40,20 @@ function makeRandom(world: World, parent?: Entity): Entity {
|
||||
return e;
|
||||
}
|
||||
|
||||
function makeRepeat(world: World, parent?: Entity): Entity {
|
||||
const e = world.spawn();
|
||||
world.add(e, Task, { kind: "repeat" });
|
||||
if (parent) world.relate(e, ChildOf, parent);
|
||||
return e;
|
||||
}
|
||||
|
||||
function makeSelector(world: World, parent?: Entity): Entity {
|
||||
const e = world.spawn();
|
||||
world.add(e, Task, { kind: "selector" });
|
||||
if (parent) world.relate(e, ChildOf, parent);
|
||||
return e;
|
||||
}
|
||||
|
||||
// ── Leaf tasks ──────────────────────────────────────
|
||||
describe("Leaf tasks", () => {
|
||||
let world: World;
|
||||
@@ -372,6 +386,223 @@ describe("Random tasks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Repeat ──────────────────────────────────────────
|
||||
describe("Repeat tasks", () => {
|
||||
let world: World;
|
||||
let runner: TaskRunner;
|
||||
|
||||
beforeEach(() => {
|
||||
world = new World();
|
||||
runner = new TaskRunner(world);
|
||||
});
|
||||
|
||||
it("re-runs child after it succeeds", () => {
|
||||
const rep = makeRepeat(world);
|
||||
const leaf = makeLeaf(world, rep);
|
||||
|
||||
let leafCount = 0;
|
||||
runner.onLeaf = () => leafCount++;
|
||||
|
||||
runner.schedule(rep);
|
||||
|
||||
// First run
|
||||
runner.tick(); // repeat schedules leaf
|
||||
runner.tick(); // leaf runs
|
||||
expect(leafCount).toBe(1);
|
||||
runner.succeed(leaf);
|
||||
|
||||
// Repeat re-scheduled
|
||||
runner.tick(); // repeat sees leaf done, resets and schedules again
|
||||
runner.tick(); // leaf runs again
|
||||
expect(leafCount).toBe(2);
|
||||
runner.succeed(leaf);
|
||||
|
||||
// And again
|
||||
runner.tick(); // repeat resets
|
||||
runner.tick(); // leaf runs
|
||||
expect(leafCount).toBe(3);
|
||||
});
|
||||
|
||||
it("re-runs child after it fails", () => {
|
||||
const rep = makeRepeat(world);
|
||||
const leaf = makeLeaf(world, rep);
|
||||
|
||||
let leafCount = 0;
|
||||
runner.onLeaf = () => leafCount++;
|
||||
|
||||
runner.schedule(rep);
|
||||
runner.tick(); // repeat schedules leaf
|
||||
runner.tick(); // leaf runs
|
||||
runner.fail(leaf);
|
||||
|
||||
// Repeat re-scheduled, resets leaf
|
||||
runner.tick(); // repeat resets leaf
|
||||
runner.tick(); // leaf runs again
|
||||
expect(leafCount).toBe(2);
|
||||
});
|
||||
|
||||
it("never terminates on its own", () => {
|
||||
const rep = makeRepeat(world);
|
||||
const leaf = makeLeaf(world, rep);
|
||||
|
||||
runner.schedule(rep);
|
||||
runner.tick(); // schedules leaf
|
||||
runner.tick(); // runs leaf
|
||||
runner.succeed(leaf);
|
||||
|
||||
// After many cycles, repeat is still not terminal
|
||||
for (let i = 0; i < 5; i++) {
|
||||
runner.tick(); // repeat resets + schedules leaf
|
||||
runner.tick(); // leaf runs
|
||||
runner.succeed(leaf);
|
||||
}
|
||||
|
||||
expect(world.has(rep, Succeeded)).toBe(false);
|
||||
expect(world.has(rep, Failed)).toBe(false);
|
||||
expect(world.has(rep, Cancelled)).toBe(false);
|
||||
});
|
||||
|
||||
it("can be cancelled", () => {
|
||||
const rep = makeRepeat(world);
|
||||
const leaf = makeLeaf(world, rep);
|
||||
|
||||
runner.schedule(rep);
|
||||
runner.tick(); // schedules leaf
|
||||
runner.tick(); // runs leaf
|
||||
|
||||
runner.cancel(rep);
|
||||
|
||||
expect(world.has(rep, Cancelled)).toBe(true);
|
||||
expect(world.has(leaf, Cancelled)).toBe(true);
|
||||
});
|
||||
|
||||
it("empty repeat does nothing", () => {
|
||||
const rep = makeRepeat(world);
|
||||
|
||||
runner.schedule(rep);
|
||||
runner.tick();
|
||||
|
||||
// No child, so nothing happens
|
||||
expect(world.has(rep, Succeeded)).toBe(false);
|
||||
expect(world.has(rep, Failed)).toBe(false);
|
||||
});
|
||||
|
||||
it("repeat inside sequential advances parent when cancelled", () => {
|
||||
const seq = makeSequential(world);
|
||||
const rep = makeRepeat(world, seq);
|
||||
const leaf = makeLeaf(world, rep);
|
||||
const after = makeLeaf(world, seq);
|
||||
|
||||
runner.schedule(seq);
|
||||
runner.tick(); // seq schedules rep
|
||||
runner.tick(); // rep schedules leaf
|
||||
runner.tick(); // leaf runs
|
||||
|
||||
// Cancel the repeat
|
||||
runner.cancel(rep);
|
||||
runner.tick(); // seq sees rep cancelled, seq becomes cancelled
|
||||
|
||||
expect(world.has(seq, Cancelled)).toBe(true);
|
||||
expect(world.has(after, Scheduled)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Selector ────────────────────────────────────────
|
||||
describe("Selector tasks", () => {
|
||||
let world: World;
|
||||
let runner: TaskRunner;
|
||||
|
||||
beforeEach(() => {
|
||||
world = new World();
|
||||
runner = new TaskRunner(world);
|
||||
});
|
||||
|
||||
it("succeeds on first child that succeeds", () => {
|
||||
const sel = makeSelector(world);
|
||||
const a = makeLeaf(world, sel);
|
||||
const b = makeLeaf(world, sel);
|
||||
|
||||
runner.schedule(sel);
|
||||
runner.tick(); // schedules a
|
||||
runner.tick(); // runs a
|
||||
runner.succeed(a);
|
||||
|
||||
// Selector re-scheduled, sees a succeeded
|
||||
runner.tick();
|
||||
expect(world.has(sel, Succeeded)).toBe(true);
|
||||
// b was never touched
|
||||
expect(world.has(b, Scheduled)).toBe(false);
|
||||
expect(world.has(b, Running)).toBe(false);
|
||||
});
|
||||
|
||||
it("tries next child when previous fails", () => {
|
||||
const sel = makeSelector(world);
|
||||
const a = makeLeaf(world, sel);
|
||||
const b = makeLeaf(world, sel);
|
||||
const c = makeLeaf(world, sel);
|
||||
|
||||
runner.schedule(sel);
|
||||
runner.tick(); // schedules a
|
||||
runner.tick(); // runs a
|
||||
runner.fail(a);
|
||||
|
||||
runner.tick(); // selector schedules b
|
||||
expect(world.has(b, Scheduled)).toBe(true);
|
||||
|
||||
runner.tick(); // runs b
|
||||
runner.fail(b);
|
||||
|
||||
runner.tick(); // selector schedules c
|
||||
expect(world.has(c, Scheduled)).toBe(true);
|
||||
|
||||
runner.tick(); // runs c
|
||||
runner.succeed(c);
|
||||
|
||||
runner.tick(); // selector succeeds
|
||||
expect(world.has(sel, Succeeded)).toBe(true);
|
||||
});
|
||||
|
||||
it("fails when all children fail", () => {
|
||||
const sel = makeSelector(world);
|
||||
const a = makeLeaf(world, sel);
|
||||
const b = makeLeaf(world, sel);
|
||||
|
||||
runner.schedule(sel);
|
||||
runner.tick(); // schedules a
|
||||
runner.tick(); // runs a
|
||||
runner.fail(a);
|
||||
runner.tick(); // schedules b
|
||||
runner.tick(); // runs b
|
||||
runner.fail(b);
|
||||
runner.tick(); // all failed
|
||||
|
||||
expect(world.has(sel, Failed)).toBe(true);
|
||||
});
|
||||
|
||||
it("empty selector fails immediately", () => {
|
||||
const sel = makeSelector(world);
|
||||
|
||||
runner.schedule(sel);
|
||||
runner.tick();
|
||||
|
||||
expect(world.has(sel, Failed)).toBe(true);
|
||||
});
|
||||
|
||||
it("skips cancelled children and continues", () => {
|
||||
const sel = makeSelector(world);
|
||||
const a = makeLeaf(world, sel);
|
||||
const b = makeLeaf(world, sel);
|
||||
|
||||
runner.schedule(sel);
|
||||
runner.tick(); // schedules a
|
||||
runner.tick(); // runs a
|
||||
runner.cancel(a);
|
||||
|
||||
runner.tick(); // selector sees a cancelled, tries b
|
||||
expect(world.has(b, Scheduled)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Cancel ──────────────────────────────────────────
|
||||
describe("Cancel", () => {
|
||||
let world: World;
|
||||
|
||||
Reference in New Issue
Block a user