feat(bt): add repeat and selector task kinds
This commit is contained in:
parent
4e37e03d3f
commit
3620e80807
|
|
@ -150,6 +150,12 @@ export class TaskRunner {
|
||||||
case "random":
|
case "random":
|
||||||
this._executeRandom(entity);
|
this._executeRandom(entity);
|
||||||
break;
|
break;
|
||||||
|
case "repeat":
|
||||||
|
this._executeRepeat(entity);
|
||||||
|
break;
|
||||||
|
case "selector":
|
||||||
|
this._executeSelector(entity);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,6 +250,56 @@ export class TaskRunner {
|
||||||
// If no eligible children (all running), wait for one to finish
|
// If no eligible children (all running), wait for one to finish
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _executeRepeat(entity: Entity): void {
|
||||||
|
const children = childrenOf(this._world, entity);
|
||||||
|
|
||||||
|
// Repeat expects exactly one child
|
||||||
|
if (children.length === 0) return;
|
||||||
|
|
||||||
|
const child = children[0];
|
||||||
|
|
||||||
|
// If child reached a terminal, reset it and schedule again
|
||||||
|
if (isTerminal(this._world, child)) {
|
||||||
|
clearStatus(this._world, child);
|
||||||
|
this._world.add(child, Scheduled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule child if not already running or scheduled
|
||||||
|
if (
|
||||||
|
!this._world.has(child, Running) &&
|
||||||
|
!this._world.has(child, Scheduled)
|
||||||
|
) {
|
||||||
|
this._world.add(child, Scheduled);
|
||||||
|
}
|
||||||
|
// Repeat itself never terminates — it just keeps the child going
|
||||||
|
}
|
||||||
|
|
||||||
|
private _executeSelector(entity: Entity): void {
|
||||||
|
const children = childrenOf(this._world, entity);
|
||||||
|
|
||||||
|
// Find the first non-terminal child
|
||||||
|
for (const child of children) {
|
||||||
|
if (isTerminal(this._world, child)) {
|
||||||
|
const status = terminalStatus(this._world, child)!;
|
||||||
|
if (status === "succeeded") {
|
||||||
|
// First success — selector succeeds
|
||||||
|
this._finish(entity, Succeeded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// failed or cancelled — continue to next child
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found a child that hasn't run yet — schedule it
|
||||||
|
this._world.add(child, Scheduled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All children failed
|
||||||
|
this._finish(entity, Failed);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Completion propagation ────────────────────────
|
// ── Completion propagation ────────────────────────
|
||||||
|
|
||||||
private _finish(
|
private _finish(
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,22 @@ import { defineRelationship } from "../relationship";
|
||||||
* Succeeds when all children succeed; fails when any child fails.
|
* Succeeds when all children succeed; fails when any child fails.
|
||||||
* - `"random"` — picks one child at random each time it's scheduled.
|
* - `"random"` — picks one child at random each time it's scheduled.
|
||||||
* Succeeds/fails with that child's result.
|
* Succeeds/fails with that child's result.
|
||||||
|
* - `"repeat"` — runs its single child. When the child finishes, resets it
|
||||||
|
* and runs again. Never terminates on its own (only via cancel).
|
||||||
|
* - `"selector"` — runs children left to right. Succeeds on the first
|
||||||
|
* child that succeeds; fails only if all children fail.
|
||||||
*/
|
*/
|
||||||
export const Task = defineComponent("task", {
|
export const Task = defineComponent("task", {
|
||||||
kind: "leaf" as "leaf" | "sequential" | "parallel" | "random",
|
kind: "leaf" as
|
||||||
|
| "leaf"
|
||||||
|
| "sequential"
|
||||||
|
| "parallel"
|
||||||
|
| "random"
|
||||||
|
| "repeat"
|
||||||
|
| "selector",
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TaskKind = typeof Task.type["kind"];
|
export type TaskKind = (typeof Task.type)["kind"];
|
||||||
|
|
||||||
// ── Status tags (zero-size — presence is the signal) ──
|
// ── Status tags (zero-size — presence is the signal) ──
|
||||||
/** A task that should be executed this tick. */
|
/** A task that should be executed this tick. */
|
||||||
|
|
|
||||||
231
test/bt.test.ts
231
test/bt.test.ts
|
|
@ -40,6 +40,20 @@ function makeRandom(world: World, parent?: Entity): Entity {
|
||||||
return e;
|
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 ──────────────────────────────────────
|
// ── Leaf tasks ──────────────────────────────────────
|
||||||
describe("Leaf tasks", () => {
|
describe("Leaf tasks", () => {
|
||||||
let world: World;
|
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 ──────────────────────────────────────────
|
// ── Cancel ──────────────────────────────────────────
|
||||||
describe("Cancel", () => {
|
describe("Cancel", () => {
|
||||||
let world: World;
|
let world: World;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue