feat(bt): Rename repeat to cycle and refactor runner
Rework task execution to be synchronous within a single tick. Scheduling boundaries are now defined by `cycle` nodes, which replace the previous `repeat` decorator. All tasks are executed immediately when scheduled, and parent/child propagation is handled inline.
This commit is contained in:
+74
-121
@@ -11,7 +11,7 @@ import {
|
||||
TaskRunner,
|
||||
buildTree,
|
||||
leaf,
|
||||
repeat,
|
||||
cycle,
|
||||
sequential,
|
||||
} from "../src/bt/index";
|
||||
|
||||
@@ -44,9 +44,9 @@ function makeRandom(world: World, parent?: Entity): Entity {
|
||||
return e;
|
||||
}
|
||||
|
||||
function makeRepeat(world: World, parent?: Entity): Entity {
|
||||
function makeCycle(world: World, parent?: Entity): Entity {
|
||||
const e = world.spawn();
|
||||
world.add(e, Task, { kind: "repeat" });
|
||||
world.add(e, Task, { kind: "cycle" });
|
||||
if (parent) world.relate(e, ChildOf, parent);
|
||||
return e;
|
||||
}
|
||||
@@ -90,15 +90,15 @@ describe("Entity task factories", () => {
|
||||
expect(world.has(root, Succeeded)).toBe(true);
|
||||
});
|
||||
|
||||
it("repeat works with component child entities beside its task child", () => {
|
||||
const Label = defineComponent("repeatLabel", { value: "" });
|
||||
it("cycle works with component child entities beside its task child", () => {
|
||||
const Label = defineComponent("cycleLabel", { value: "" });
|
||||
const world = new World();
|
||||
let calls = 0;
|
||||
|
||||
const runner = buildTree(
|
||||
world,
|
||||
repeat([
|
||||
entity(Label, { value: "repeat metadata" }),
|
||||
cycle([
|
||||
entity(Label, { value: "cycle metadata" }),
|
||||
leaf(() => {
|
||||
calls++;
|
||||
}),
|
||||
@@ -218,30 +218,20 @@ describe("Sequential tasks", () => {
|
||||
runner.onLeaf = (_w, e) => leafCalls.push(e);
|
||||
|
||||
runner.schedule(seq);
|
||||
runner.tick(); // schedules first child
|
||||
expect(world.has(a, Scheduled)).toBe(true);
|
||||
expect(world.has(b, Scheduled)).toBe(false);
|
||||
expect(world.has(c, Scheduled)).toBe(false);
|
||||
|
||||
runner.tick(); // runs a
|
||||
runner.succeed(a);
|
||||
|
||||
// parent should be re-scheduled
|
||||
runner.tick(); // schedules next child
|
||||
expect(world.has(b, Scheduled)).toBe(true);
|
||||
|
||||
runner.tick(); // runs b
|
||||
runner.succeed(b);
|
||||
|
||||
runner.tick(); // schedules c
|
||||
expect(world.has(c, Scheduled)).toBe(true);
|
||||
|
||||
runner.tick(); // runs c
|
||||
runner.succeed(c);
|
||||
|
||||
// parent should now be scheduled and succeed
|
||||
runner.tick();
|
||||
expect(world.has(a, Running)).toBe(true);
|
||||
expect(world.has(b, Running)).toBe(false);
|
||||
expect(world.has(c, Running)).toBe(false);
|
||||
|
||||
runner.succeed(a);
|
||||
expect(world.has(b, Running)).toBe(true);
|
||||
|
||||
runner.succeed(b);
|
||||
expect(world.has(c, Running)).toBe(true);
|
||||
|
||||
runner.succeed(c);
|
||||
expect(world.has(seq, Succeeded)).toBe(true);
|
||||
expect(leafCalls).toEqual([a, b, c]);
|
||||
});
|
||||
|
||||
it("fails immediately when a child fails", () => {
|
||||
@@ -319,7 +309,7 @@ describe("Parallel tasks", () => {
|
||||
runner = new TaskRunner(world);
|
||||
});
|
||||
|
||||
it("schedules all children at once", () => {
|
||||
it("starts all children at once", () => {
|
||||
const par = makeParallel(world);
|
||||
const a = makeLeaf(world, par);
|
||||
const b = makeLeaf(world, par);
|
||||
@@ -328,9 +318,9 @@ describe("Parallel tasks", () => {
|
||||
runner.schedule(par);
|
||||
runner.tick();
|
||||
|
||||
expect(world.has(a, Scheduled)).toBe(true);
|
||||
expect(world.has(b, Scheduled)).toBe(true);
|
||||
expect(world.has(c, Scheduled)).toBe(true);
|
||||
expect(world.has(a, Running)).toBe(true);
|
||||
expect(world.has(b, Running)).toBe(true);
|
||||
expect(world.has(c, Running)).toBe(true);
|
||||
});
|
||||
|
||||
it("succeeds when all children succeed", () => {
|
||||
@@ -407,15 +397,13 @@ describe("Random tasks", () => {
|
||||
runner.schedule(rand);
|
||||
runner.tick();
|
||||
|
||||
// Exactly one child should be scheduled
|
||||
const scheduled = [world.has(a, Scheduled), world.has(b, Scheduled)];
|
||||
expect(scheduled.filter(Boolean)).toHaveLength(1);
|
||||
// Exactly one child should be running
|
||||
const running = [world.has(a, Running), world.has(b, Running)];
|
||||
expect(running.filter(Boolean)).toHaveLength(1);
|
||||
|
||||
const picked = world.has(a, Scheduled) ? a : b;
|
||||
runner.tick(); // runs picked leaf
|
||||
const picked = world.has(a, Running) ? a : b;
|
||||
runner.succeed(picked);
|
||||
|
||||
runner.tick(); // random sees child done → succeeds
|
||||
expect(world.has(rand, Succeeded)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -444,8 +432,8 @@ describe("Random tasks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Repeat ──────────────────────────────────────────
|
||||
describe("Repeat tasks", () => {
|
||||
// ── Cycle ──────────────────────────────────────────
|
||||
describe("Cycle tasks", () => {
|
||||
let world: World;
|
||||
let runner: TaskRunner;
|
||||
|
||||
@@ -455,110 +443,98 @@ describe("Repeat tasks", () => {
|
||||
});
|
||||
|
||||
it("re-runs child after it succeeds", () => {
|
||||
const rep = makeRepeat(world);
|
||||
const leaf = makeLeaf(world, rep);
|
||||
const cyc = makeCycle(world);
|
||||
const leaf = makeLeaf(world, cyc);
|
||||
|
||||
let leafCount = 0;
|
||||
runner.onLeaf = () => leafCount++;
|
||||
|
||||
runner.schedule(rep);
|
||||
runner.schedule(cyc);
|
||||
|
||||
// First run
|
||||
runner.tick(); // repeat schedules leaf
|
||||
runner.tick(); // leaf runs
|
||||
runner.tick();
|
||||
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
|
||||
// Completion schedules the next cycle at a tick boundary.
|
||||
expect(world.has(leaf, Scheduled)).toBe(true);
|
||||
runner.tick();
|
||||
expect(leafCount).toBe(2);
|
||||
runner.succeed(leaf);
|
||||
|
||||
// And again
|
||||
runner.tick(); // repeat resets
|
||||
runner.tick(); // leaf runs
|
||||
runner.tick();
|
||||
expect(leafCount).toBe(3);
|
||||
});
|
||||
|
||||
it("re-runs child after it fails", () => {
|
||||
const rep = makeRepeat(world);
|
||||
const leaf = makeLeaf(world, rep);
|
||||
const cyc = makeCycle(world);
|
||||
const leaf = makeLeaf(world, cyc);
|
||||
|
||||
let leafCount = 0;
|
||||
runner.onLeaf = () => leafCount++;
|
||||
|
||||
runner.schedule(rep);
|
||||
runner.tick(); // repeat schedules leaf
|
||||
runner.tick(); // leaf runs
|
||||
runner.schedule(cyc);
|
||||
runner.tick();
|
||||
runner.fail(leaf);
|
||||
|
||||
// Repeat re-scheduled, resets leaf
|
||||
runner.tick(); // repeat resets leaf
|
||||
runner.tick(); // leaf runs again
|
||||
expect(world.has(leaf, Scheduled)).toBe(true);
|
||||
runner.tick();
|
||||
expect(leafCount).toBe(2);
|
||||
});
|
||||
|
||||
it("never terminates on its own", () => {
|
||||
const rep = makeRepeat(world);
|
||||
const leaf = makeLeaf(world, rep);
|
||||
const cyc = makeCycle(world);
|
||||
const leaf = makeLeaf(world, cyc);
|
||||
|
||||
runner.schedule(rep);
|
||||
runner.tick(); // schedules leaf
|
||||
runner.tick(); // runs leaf
|
||||
runner.schedule(cyc);
|
||||
runner.tick();
|
||||
runner.succeed(leaf);
|
||||
|
||||
// After many cycles, repeat is still not terminal
|
||||
// After many cycles, cycle is still not terminal
|
||||
for (let i = 0; i < 5; i++) {
|
||||
runner.tick(); // repeat resets + schedules leaf
|
||||
runner.tick(); // leaf runs
|
||||
runner.tick();
|
||||
runner.succeed(leaf);
|
||||
}
|
||||
|
||||
expect(world.has(rep, Succeeded)).toBe(false);
|
||||
expect(world.has(rep, Failed)).toBe(false);
|
||||
expect(world.has(rep, Cancelled)).toBe(false);
|
||||
expect(world.has(cyc, Succeeded)).toBe(false);
|
||||
expect(world.has(cyc, Failed)).toBe(false);
|
||||
expect(world.has(cyc, Cancelled)).toBe(false);
|
||||
});
|
||||
|
||||
it("can be cancelled", () => {
|
||||
const rep = makeRepeat(world);
|
||||
const leaf = makeLeaf(world, rep);
|
||||
const cyc = makeCycle(world);
|
||||
const leaf = makeLeaf(world, cyc);
|
||||
|
||||
runner.schedule(rep);
|
||||
runner.tick(); // schedules leaf
|
||||
runner.tick(); // runs leaf
|
||||
runner.schedule(cyc);
|
||||
runner.tick();
|
||||
|
||||
runner.cancel(rep);
|
||||
runner.cancel(cyc);
|
||||
|
||||
expect(world.has(rep, Cancelled)).toBe(true);
|
||||
expect(world.has(cyc, Cancelled)).toBe(true);
|
||||
expect(world.has(leaf, Cancelled)).toBe(true);
|
||||
});
|
||||
|
||||
it("empty repeat does nothing", () => {
|
||||
const rep = makeRepeat(world);
|
||||
it("empty cycle does nothing", () => {
|
||||
const cyc = makeCycle(world);
|
||||
|
||||
runner.schedule(rep);
|
||||
runner.schedule(cyc);
|
||||
runner.tick();
|
||||
|
||||
// No child, so nothing happens
|
||||
expect(world.has(rep, Succeeded)).toBe(false);
|
||||
expect(world.has(rep, Failed)).toBe(false);
|
||||
expect(world.has(cyc, Succeeded)).toBe(false);
|
||||
expect(world.has(cyc, Failed)).toBe(false);
|
||||
});
|
||||
|
||||
it("repeat inside sequential advances parent when cancelled", () => {
|
||||
it("cycle inside sequential advances parent when cancelled", () => {
|
||||
const seq = makeSequential(world);
|
||||
const rep = makeRepeat(world, seq);
|
||||
const leaf = makeLeaf(world, rep);
|
||||
const cyc = makeCycle(world, seq);
|
||||
const leaf = makeLeaf(world, cyc);
|
||||
const after = makeLeaf(world, seq);
|
||||
|
||||
runner.schedule(seq);
|
||||
runner.tick(); // seq schedules rep
|
||||
runner.tick(); // rep schedules leaf
|
||||
runner.tick(); // leaf runs
|
||||
runner.tick();
|
||||
|
||||
// Cancel the repeat
|
||||
runner.cancel(rep);
|
||||
runner.tick(); // seq sees rep cancelled, seq becomes cancelled
|
||||
// Cancel the cycle
|
||||
runner.cancel(cyc);
|
||||
|
||||
expect(world.has(seq, Cancelled)).toBe(true);
|
||||
expect(world.has(after, Scheduled)).toBe(false);
|
||||
@@ -604,19 +580,12 @@ describe("Selector tasks", () => {
|
||||
runner.tick(); // runs a
|
||||
runner.fail(a);
|
||||
|
||||
runner.tick(); // selector schedules b
|
||||
expect(world.has(b, Scheduled)).toBe(true);
|
||||
expect(world.has(b, Running)).toBe(true);
|
||||
|
||||
runner.tick(); // runs b
|
||||
runner.fail(b);
|
||||
expect(world.has(c, Running)).toBe(true);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -656,8 +625,7 @@ describe("Selector tasks", () => {
|
||||
runner.tick(); // runs a
|
||||
runner.cancel(a);
|
||||
|
||||
runner.tick(); // selector sees a cancelled, tries b
|
||||
expect(world.has(b, Scheduled)).toBe(true);
|
||||
expect(world.has(b, Running)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -756,10 +724,9 @@ describe("Multi-frame leaves", () => {
|
||||
expect(world.has(b, Scheduled)).toBe(false);
|
||||
expect(world.has(b, Running)).toBe(false);
|
||||
|
||||
// Finish a
|
||||
// Finish a; parent propagation starts b immediately.
|
||||
runner.succeed(a);
|
||||
runner.tick(); // seq schedules b
|
||||
expect(world.has(b, Scheduled)).toBe(true);
|
||||
expect(world.has(b, Running)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -804,25 +771,11 @@ describe("Edge cases", () => {
|
||||
|
||||
runner.schedule(root);
|
||||
|
||||
// Tick 1: root schedules mid
|
||||
runner.tick();
|
||||
expect(world.has(mid, Scheduled)).toBe(true);
|
||||
|
||||
// Tick 2: mid schedules leaf
|
||||
runner.tick();
|
||||
expect(world.has(leaf, Scheduled)).toBe(true);
|
||||
|
||||
// Tick 3: leaf runs
|
||||
runner.tick();
|
||||
expect(world.has(leaf, Running)).toBe(true);
|
||||
|
||||
runner.succeed(leaf);
|
||||
// leaf done → mid scheduled
|
||||
|
||||
runner.tick(); // mid sees leaf done → mid succeeds
|
||||
expect(world.has(mid, Succeeded)).toBe(true);
|
||||
|
||||
runner.tick(); // root sees mid done → root succeeds
|
||||
expect(world.has(root, Succeeded)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user