@aura3d/scripting 1.3.3 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/HTN.ts DELETED
@@ -1,178 +0,0 @@
1
- import { WorldState, type GOAPStateShape } from "./GOAP";
2
-
3
- export type HTNTaskResult = "success" | "running" | "failure";
4
- export type HTNTaskType = "primitive" | "compound";
5
-
6
- export interface HTNTaskMethod {
7
- readonly name: string;
8
- readonly preconditions?: GOAPStateShape;
9
- readonly subtasks: readonly HTNTask[];
10
- readonly priority?: number;
11
- readonly check?: (state: WorldState, context: unknown) => boolean;
12
- }
13
-
14
- export interface HTNPrimitiveTaskOptions {
15
- readonly name: string;
16
- readonly preconditions?: GOAPStateShape;
17
- readonly effects?: GOAPStateShape;
18
- readonly execute?: (state: WorldState, context: unknown) => HTNTaskResult;
19
- }
20
-
21
- export interface HTNCompoundTaskOptions {
22
- readonly name: string;
23
- readonly methods?: readonly HTNTaskMethod[];
24
- }
25
-
26
- export interface HTNPlannerOptions {
27
- readonly maxDepth?: number;
28
- readonly maxIterations?: number;
29
- }
30
-
31
- export interface HTNPlan {
32
- readonly valid: boolean;
33
- readonly rootTask: string;
34
- readonly tasks: readonly string[];
35
- readonly methodTrace: readonly string[];
36
- readonly decompositions: number;
37
- readonly maxDepthReached: number;
38
- readonly iterations: number;
39
- readonly finalState: GOAPStateShape;
40
- }
41
-
42
- type DecomposeStats = {
43
- decompositions: number;
44
- iterations: number;
45
- maxDepthReached: number;
46
- methodTrace: string[];
47
- };
48
-
49
- type DecomposeResult = {
50
- readonly valid: boolean;
51
- readonly tasks: readonly HTNTask[];
52
- readonly state: WorldState;
53
- };
54
-
55
- export class HTNTask {
56
- readonly name: string;
57
- readonly type: HTNTaskType;
58
- readonly preconditions: GOAPStateShape;
59
- readonly effects: GOAPStateShape;
60
- private readonly methods: HTNTaskMethod[];
61
- private readonly executeCallback?: (state: WorldState, context: unknown) => HTNTaskResult;
62
-
63
- private constructor(options: {
64
- readonly name: string;
65
- readonly type: HTNTaskType;
66
- readonly preconditions?: GOAPStateShape;
67
- readonly effects?: GOAPStateShape;
68
- readonly methods?: readonly HTNTaskMethod[];
69
- readonly execute?: (state: WorldState, context: unknown) => HTNTaskResult;
70
- }) {
71
- if (options.name.trim().length === 0) throw new Error("HTN task name cannot be empty.");
72
- this.name = options.name;
73
- this.type = options.type;
74
- this.preconditions = options.preconditions ?? {};
75
- this.effects = options.effects ?? {};
76
- this.methods = [...(options.methods ?? [])].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0) || left.name.localeCompare(right.name));
77
- this.executeCallback = options.execute;
78
- }
79
-
80
- static primitive(options: HTNPrimitiveTaskOptions): HTNTask {
81
- return new HTNTask({ ...options, type: "primitive" });
82
- }
83
-
84
- static compound(options: HTNCompoundTaskOptions): HTNTask {
85
- return new HTNTask({ ...options, type: "compound" });
86
- }
87
-
88
- getMethods(): readonly HTNTaskMethod[] {
89
- return this.methods;
90
- }
91
-
92
- canRun(state: WorldState): boolean {
93
- return state.matches(this.preconditions);
94
- }
95
-
96
- applyEffects(state: WorldState): WorldState {
97
- return state.apply(this.effects);
98
- }
99
-
100
- execute(state: WorldState, context: unknown): HTNTaskResult {
101
- if (this.type !== "primitive") return "failure";
102
- return this.executeCallback?.(state, context) ?? "success";
103
- }
104
- }
105
-
106
- export class HTNPlanner {
107
- private readonly maxDepth: number;
108
- private readonly maxIterations: number;
109
-
110
- constructor(options: HTNPlannerOptions = {}) {
111
- this.maxDepth = options.maxDepth ?? 12;
112
- this.maxIterations = options.maxIterations ?? 128;
113
- if (!Number.isInteger(this.maxDepth) || this.maxDepth < 1) throw new RangeError("HTN maxDepth must be a positive integer.");
114
- if (!Number.isInteger(this.maxIterations) || this.maxIterations < 1) throw new RangeError("HTN maxIterations must be a positive integer.");
115
- }
116
-
117
- plan(rootTask: HTNTask, initialState: WorldState, context: unknown = {}): HTNPlan {
118
- const stats: DecomposeStats = {
119
- decompositions: 0,
120
- iterations: 0,
121
- maxDepthReached: 0,
122
- methodTrace: []
123
- };
124
- const result = this.decompose([rootTask], initialState.clone(), context, 0, stats);
125
- return {
126
- valid: result.valid,
127
- rootTask: rootTask.name,
128
- tasks: result.tasks.map((task) => task.name),
129
- methodTrace: stats.methodTrace,
130
- decompositions: stats.decompositions,
131
- maxDepthReached: stats.maxDepthReached,
132
- iterations: stats.iterations,
133
- finalState: result.state.toObject()
134
- };
135
- }
136
-
137
- isPlanValid(plan: HTNPlan, rootTask: HTNTask, initialState: WorldState, context: unknown = {}): boolean {
138
- if (!plan.valid) return false;
139
- const nextPlan = this.plan(rootTask, initialState, context);
140
- return nextPlan.valid && nextPlan.tasks.join(">") === plan.tasks.join(">");
141
- }
142
-
143
- private decompose(tasks: readonly HTNTask[], state: WorldState, context: unknown, depth: number, stats: DecomposeStats): DecomposeResult {
144
- stats.maxDepthReached = Math.max(stats.maxDepthReached, depth);
145
- if (depth > this.maxDepth || stats.iterations >= this.maxIterations) {
146
- return { valid: false, tasks: [], state };
147
- }
148
- if (tasks.length === 0) {
149
- return { valid: true, tasks: [], state };
150
- }
151
- stats.iterations += 1;
152
- const [currentTask, ...remainingTasks] = tasks;
153
- if (!currentTask) return { valid: true, tasks: [], state };
154
-
155
- if (currentTask.type === "primitive") {
156
- if (!currentTask.canRun(state)) return { valid: false, tasks: [], state };
157
- const executed = currentTask.execute(state, context);
158
- if (executed === "failure") return { valid: false, tasks: [], state };
159
- const nextState = currentTask.applyEffects(state);
160
- const remainder = this.decompose(remainingTasks, nextState, context, depth, stats);
161
- return remainder.valid
162
- ? { valid: true, tasks: [currentTask, ...remainder.tasks], state: remainder.state }
163
- : { valid: false, tasks: [], state };
164
- }
165
-
166
- stats.decompositions += 1;
167
- for (const method of currentTask.getMethods()) {
168
- const preconditionsMet = method.check ? method.check(state, context) : state.matches(method.preconditions ?? {});
169
- if (!preconditionsMet) continue;
170
- const traceLength = stats.methodTrace.length;
171
- stats.methodTrace.push(`${currentTask.name}:${method.name}`);
172
- const result = this.decompose([...method.subtasks, ...remainingTasks], state, context, depth + 1, stats);
173
- if (result.valid) return result;
174
- stats.methodTrace.length = traceLength;
175
- }
176
- return { valid: false, tasks: [], state };
177
- }
178
- }
@@ -1,184 +0,0 @@
1
- export interface LearningAgentFixtureOptions {
2
- readonly seed?: number;
3
- readonly targetDistance?: number;
4
- readonly health?: number;
5
- readonly energy?: number;
6
- readonly nearbyCount?: number;
7
- }
8
-
9
- export interface LearningAgentFixture {
10
- readonly source: "origin-master-ml-agent-adapted";
11
- readonly observation: {
12
- readonly featureSize: number;
13
- readonly normalizedPosition: readonly [number, number, number];
14
- readonly normalizedVelocity: readonly [number, number, number];
15
- readonly targetDistance: number;
16
- readonly health: number;
17
- readonly energy: number;
18
- readonly nearbyEntitySlots: number;
19
- readonly featureHash: string;
20
- };
21
- readonly behaviorCloning: {
22
- readonly demonstrations: number;
23
- readonly trainLoss: number;
24
- readonly validationLoss: number;
25
- readonly trainAccuracy: number;
26
- readonly validationAccuracy: number;
27
- readonly earlyStoppingEpoch: number;
28
- readonly selectedAction: "move-to-target" | "hold-position" | "recover";
29
- };
30
- readonly reinforcementLearning: {
31
- readonly gamma: number;
32
- readonly lambda: number;
33
- readonly clipRange: number;
34
- readonly policyLoss: number;
35
- readonly valueLoss: number;
36
- readonly totalLoss: number;
37
- readonly avgAdvantage: number;
38
- readonly avgReturn: number;
39
- readonly entropy: number;
40
- readonly klDivergence: number;
41
- readonly explainedVariance: number;
42
- };
43
- readonly reward: {
44
- readonly progress: number;
45
- readonly survival: number;
46
- readonly energyPenalty: number;
47
- readonly proximityBonus: number;
48
- readonly total: number;
49
- };
50
- readonly productionReadiness: {
51
- readonly featureExtractionTelemetry: true;
52
- readonly behaviorCloningTelemetry: true;
53
- readonly ppoStatsTelemetry: true;
54
- readonly rewardBreakdownTelemetry: true;
55
- };
56
- readonly blockedClaims: readonly string[];
57
- readonly claimBoundary: string;
58
- readonly hash: string;
59
- }
60
-
61
- export function sampleLearningAgentFixture(options: LearningAgentFixtureOptions = {}): LearningAgentFixture {
62
- const seed = normalizeSeed(options.seed ?? 0x1ea5);
63
- const targetDistance = positive(options.targetDistance ?? 6.2, "targetDistance");
64
- const health = clamp01(options.health ?? 0.72);
65
- const energy = clamp01(options.energy ?? 0.58);
66
- const nearbyEntitySlots = Math.max(0, Math.min(5, Math.trunc(options.nearbyCount ?? 3)));
67
- const normalizedPosition: readonly [number, number, number] = [
68
- Number((seededUnit(seed) * 0.12).toFixed(4)),
69
- 0,
70
- Number((seededUnit(seed ^ 0x5167) * 0.08).toFixed(4))
71
- ];
72
- const normalizedVelocity: readonly [number, number, number] = [
73
- Number((0.06 + seededUnit(seed ^ 0x78ab) * 0.04).toFixed(4)),
74
- 0,
75
- Number((seededUnit(seed ^ 0x4312) * 0.02).toFixed(4))
76
- ];
77
- const featureCore = [
78
- ...normalizedPosition,
79
- ...normalizedVelocity,
80
- 1,
81
- 0,
82
- 0,
83
- Number((targetDistance / 100).toFixed(4)),
84
- health,
85
- energy,
86
- nearbyEntitySlots
87
- ];
88
- const progress = Number(Math.max(0, 1 - targetDistance / 12).toFixed(4));
89
- const survival = Number((health * 0.35).toFixed(4));
90
- const energyPenalty = Number(((1 - energy) * -0.18).toFixed(4));
91
- const proximityBonus = Number((nearbyEntitySlots > 0 ? 0.08 : 0).toFixed(4));
92
- const totalReward = Number((progress + survival + energyPenalty + proximityBonus).toFixed(4));
93
- const selectedAction = health < 0.35 ? "recover" : targetDistance > 1.5 ? "move-to-target" : "hold-position";
94
- const behaviorCloning = {
95
- demonstrations: 24 + nearbyEntitySlots * 4,
96
- trainLoss: Number((0.18 - progress * 0.06).toFixed(4)),
97
- validationLoss: Number((0.22 - progress * 0.05).toFixed(4)),
98
- trainAccuracy: Number((0.74 + progress * 0.12).toFixed(4)),
99
- validationAccuracy: Number((0.7 + progress * 0.1).toFixed(4)),
100
- earlyStoppingEpoch: 7 + nearbyEntitySlots,
101
- selectedAction
102
- } satisfies LearningAgentFixture["behaviorCloning"];
103
- const reinforcementLearning = {
104
- gamma: 0.99,
105
- lambda: 0.95,
106
- clipRange: 0.2,
107
- policyLoss: Number((-0.04 - progress * 0.03).toFixed(4)),
108
- valueLoss: Number((0.21 - totalReward * 0.08).toFixed(4)),
109
- totalLoss: Number((0.15 - totalReward * 0.05).toFixed(4)),
110
- avgAdvantage: Number((totalReward * 0.42).toFixed(4)),
111
- avgReturn: totalReward,
112
- entropy: Number((0.52 - progress * 0.08).toFixed(4)),
113
- klDivergence: Number((0.009 + seededUnit(seed ^ 0xacab) * 0.004).toFixed(4)),
114
- explainedVariance: Number((0.66 + progress * 0.18).toFixed(4))
115
- };
116
- const core = {
117
- featureCore,
118
- behaviorCloning,
119
- reinforcementLearning,
120
- reward: { progress, survival, energyPenalty, proximityBonus, total: totalReward }
121
- };
122
- return {
123
- source: "origin-master-ml-agent-adapted",
124
- observation: {
125
- featureSize: 27,
126
- normalizedPosition,
127
- normalizedVelocity,
128
- targetDistance,
129
- health,
130
- energy,
131
- nearbyEntitySlots,
132
- featureHash: stableHash(JSON.stringify(featureCore))
133
- },
134
- behaviorCloning,
135
- reinforcementLearning,
136
- reward: core.reward,
137
- productionReadiness: {
138
- featureExtractionTelemetry: true,
139
- behaviorCloningTelemetry: true,
140
- ppoStatsTelemetry: true,
141
- rewardBreakdownTelemetry: true
142
- },
143
- blockedClaims: [
144
- "real neural-network training",
145
- "ONNX runtime inference parity",
146
- "Unity ML-Agents parity",
147
- "Unreal Learning Agents parity",
148
- "production reinforcement-learning deployment"
149
- ],
150
- claimBoundary: "This fixture ports old ML-agent concepts into deterministic feature-extraction, behavior-cloning, PPO-stat, and reward telemetry. It does not train neural networks, run ONNX inference, or claim Unity ML-Agents or Unreal Learning Agents parity.",
151
- hash: stableHash(JSON.stringify(core))
152
- };
153
- }
154
-
155
- function positive(value: number, name: string): number {
156
- if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative finite number.`);
157
- return Number(value.toFixed(4));
158
- }
159
-
160
- function normalizeSeed(seed: number): number {
161
- if (!Number.isInteger(seed) || seed < 0) throw new Error("seed must be a non-negative integer.");
162
- return seed >>> 0;
163
- }
164
-
165
- function clamp01(value: number): number {
166
- return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0));
167
- }
168
-
169
- function seededUnit(seed: number): number {
170
- let value = seed >>> 0;
171
- value ^= value << 13;
172
- value ^= value >>> 17;
173
- value ^= value << 5;
174
- return (value >>> 0) / 0xffffffff;
175
- }
176
-
177
- function stableHash(value: string): string {
178
- let hash = 2166136261;
179
- for (let index = 0; index < value.length; index += 1) {
180
- hash ^= value.charCodeAt(index);
181
- hash = Math.imul(hash, 16777619);
182
- }
183
- return (hash >>> 0).toString(16).padStart(8, "0");
184
- }