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":
|
||||
this._executeRandom(entity);
|
||||
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
|
||||
}
|
||||
|
||||
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 ────────────────────────
|
||||
|
||||
private _finish(
|
||||
|
|
|
|||
|
|
@ -13,12 +13,22 @@ import { defineRelationship } from "../relationship";
|
|||
* Succeeds when all children succeed; fails when any child fails.
|
||||
* - `"random"` — picks one child at random each time it's scheduled.
|
||||
* 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", {
|
||||
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) ──
|
||||
/** 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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue