@holoscript/framework 6.0.3
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/ALL-test-results.json +1 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/ROADMAP.md +175 -0
- package/dist/AgentManifest-CB4xM-Ma.d.cts +704 -0
- package/dist/AgentManifest-CB4xM-Ma.d.ts +704 -0
- package/dist/BehaviorTree-BrBFECv5.d.cts +103 -0
- package/dist/BehaviorTree-BrBFECv5.d.ts +103 -0
- package/dist/InvisibleWallet-BB6tFvRA.d.cts +1732 -0
- package/dist/InvisibleWallet-rtRrBOA8.d.ts +1732 -0
- package/dist/OrchestratorAgent-BvWgf9uw.d.cts +798 -0
- package/dist/OrchestratorAgent-Q_CbVTmO.d.ts +798 -0
- package/dist/agents/index.cjs +4790 -0
- package/dist/agents/index.d.cts +1788 -0
- package/dist/agents/index.d.ts +1788 -0
- package/dist/agents/index.js +4695 -0
- package/dist/ai/index.cjs +5347 -0
- package/dist/ai/index.d.cts +1753 -0
- package/dist/ai/index.d.ts +1753 -0
- package/dist/ai/index.js +5244 -0
- package/dist/behavior.cjs +449 -0
- package/dist/behavior.d.cts +130 -0
- package/dist/behavior.d.ts +130 -0
- package/dist/behavior.js +407 -0
- package/dist/economy/index.cjs +3659 -0
- package/dist/economy/index.d.cts +747 -0
- package/dist/economy/index.d.ts +747 -0
- package/dist/economy/index.js +3617 -0
- package/dist/implementations-D9T3un9D.d.cts +236 -0
- package/dist/implementations-D9T3un9D.d.ts +236 -0
- package/dist/index.cjs +24550 -0
- package/dist/index.d.cts +1729 -0
- package/dist/index.d.ts +1729 -0
- package/dist/index.js +24277 -0
- package/dist/learning/index.cjs +219 -0
- package/dist/learning/index.d.cts +104 -0
- package/dist/learning/index.d.ts +104 -0
- package/dist/learning/index.js +189 -0
- package/dist/negotiation/index.cjs +970 -0
- package/dist/negotiation/index.d.cts +610 -0
- package/dist/negotiation/index.d.ts +610 -0
- package/dist/negotiation/index.js +931 -0
- package/dist/skills/index.cjs +1118 -0
- package/dist/skills/index.d.cts +289 -0
- package/dist/skills/index.d.ts +289 -0
- package/dist/skills/index.js +1079 -0
- package/dist/swarm/index.cjs +5268 -0
- package/dist/swarm/index.d.cts +2433 -0
- package/dist/swarm/index.d.ts +2433 -0
- package/dist/swarm/index.js +5221 -0
- package/dist/training/index.cjs +2745 -0
- package/dist/training/index.d.cts +1734 -0
- package/dist/training/index.d.ts +1734 -0
- package/dist/training/index.js +2687 -0
- package/extract-failures.js +10 -0
- package/package.json +82 -0
- package/src/__tests__/bounty-marketplace.test.ts +374 -0
- package/src/__tests__/delegation.test.ts +144 -0
- package/src/__tests__/distributed-claimer.test.ts +147 -0
- package/src/__tests__/done-log-audit.test.ts +342 -0
- package/src/__tests__/framework.test.ts +865 -0
- package/src/__tests__/goal-synthesizer.test.ts +236 -0
- package/src/__tests__/presence.test.ts +223 -0
- package/src/__tests__/protocol-agent.test.ts +254 -0
- package/src/__tests__/revenue-splitter.test.ts +114 -0
- package/src/__tests__/scenario-driven-todo.test.ts +197 -0
- package/src/__tests__/self-improve.test.ts +349 -0
- package/src/__tests__/service-lifecycle.test.ts +237 -0
- package/src/__tests__/skill-router.test.ts +121 -0
- package/src/agents/AgentManifest.ts +493 -0
- package/src/agents/AgentRegistry.ts +475 -0
- package/src/agents/AgentTypes.ts +585 -0
- package/src/agents/AgentWalletRegistry.ts +83 -0
- package/src/agents/AuthenticatedCRDT.ts +388 -0
- package/src/agents/CapabilityMatcher.ts +453 -0
- package/src/agents/CrossRealityHandoff.ts +305 -0
- package/src/agents/CulturalMemory.ts +454 -0
- package/src/agents/FederatedRegistryAdapter.ts +429 -0
- package/src/agents/NormEngine.ts +450 -0
- package/src/agents/OrchestratorAgent.ts +414 -0
- package/src/agents/SkillWorkflowEngine.ts +472 -0
- package/src/agents/TaskDelegationService.ts +551 -0
- package/src/agents/__tests__/AgentManifest.prod.test.ts +134 -0
- package/src/agents/__tests__/AgentManifest.test.ts +182 -0
- package/src/agents/__tests__/AgentModule.test.ts +864 -0
- package/src/agents/__tests__/AgentRegistry.prod.test.ts +125 -0
- package/src/agents/__tests__/AgentRegistry.test.ts +148 -0
- package/src/agents/__tests__/AgentTypes.test.ts +534 -0
- package/src/agents/__tests__/AgentWalletRegistry.test.ts +152 -0
- package/src/agents/__tests__/AuthenticatedCRDT.test.ts +558 -0
- package/src/agents/__tests__/CapabilityMatcher.prod.test.ts +117 -0
- package/src/agents/__tests__/CapabilityMatcher.test.ts +178 -0
- package/src/agents/__tests__/CrossRealityHandoff.test.ts +402 -0
- package/src/agents/__tests__/CulturalMemory.test.ts +200 -0
- package/src/agents/__tests__/FederatedRegistryAdapter.test.ts +409 -0
- package/src/agents/__tests__/NormEngine.test.ts +276 -0
- package/src/agents/__tests__/OrchestratorAgent.test.ts +182 -0
- package/src/agents/__tests__/SkillWorkflowEngine.test.ts +357 -0
- package/src/agents/__tests__/TaskDelegationService.test.ts +446 -0
- package/src/agents/index.ts +107 -0
- package/src/agents/spatial-comms/Layer1RealTime.ts +621 -0
- package/src/agents/spatial-comms/Layer2A2A.ts +661 -0
- package/src/agents/spatial-comms/Layer3MCP.ts +651 -0
- package/src/agents/spatial-comms/ProtocolTypes.ts +543 -0
- package/src/agents/spatial-comms/SpatialCommClient.ts +483 -0
- package/src/agents/spatial-comms/__tests__/performance-benchmark.test.ts +465 -0
- package/src/agents/spatial-comms/examples/multi-agent-world-creation.ts +409 -0
- package/src/agents/spatial-comms/index.ts +66 -0
- package/src/ai/AIAdapter.ts +313 -0
- package/src/ai/AICopilot.ts +331 -0
- package/src/ai/AIOutputValidator.ts +203 -0
- package/src/ai/BTNodes.ts +239 -0
- package/src/ai/BehaviorSelector.ts +135 -0
- package/src/ai/BehaviorTree.ts +153 -0
- package/src/ai/Blackboard.ts +165 -0
- package/src/ai/GenerationAnalytics.ts +461 -0
- package/src/ai/GenerationCache.ts +265 -0
- package/src/ai/GoalPlanner.ts +165 -0
- package/src/ai/HoloScriptGenerator.ts +580 -0
- package/src/ai/InfluenceMap.ts +180 -0
- package/src/ai/NavMesh.ts +168 -0
- package/src/ai/PerceptionSystem.ts +178 -0
- package/src/ai/PromptTemplates.ts +453 -0
- package/src/ai/SemanticSearchService.ts +80 -0
- package/src/ai/StateMachine.ts +196 -0
- package/src/ai/SteeringBehavior.ts +150 -0
- package/src/ai/SteeringBehaviors.ts +244 -0
- package/src/ai/TrainingDataGenerator.ts +1082 -0
- package/src/ai/UtilityAI.ts +145 -0
- package/src/ai/__tests__/AIAdapter.prod.test.ts +259 -0
- package/src/ai/__tests__/AIAdapter.test.ts +109 -0
- package/src/ai/__tests__/AICopilot.prod.test.ts +341 -0
- package/src/ai/__tests__/AICopilot.test.ts +178 -0
- package/src/ai/__tests__/AIOutputValidator.prod.test.ts +226 -0
- package/src/ai/__tests__/AIOutputValidator.test.ts +138 -0
- package/src/ai/__tests__/BTNodes.prod.test.ts +391 -0
- package/src/ai/__tests__/BTNodes.test.ts +263 -0
- package/src/ai/__tests__/BehaviorSelector.prod.test.ts +129 -0
- package/src/ai/__tests__/BehaviorSelector.test.ts +132 -0
- package/src/ai/__tests__/BehaviorTree.prod.test.ts +266 -0
- package/src/ai/__tests__/BehaviorTree.test.ts +216 -0
- package/src/ai/__tests__/Blackboard.prod.test.ts +339 -0
- package/src/ai/__tests__/Blackboard.test.ts +183 -0
- package/src/ai/__tests__/GenerationAnalytics.prod.test.ts +141 -0
- package/src/ai/__tests__/GenerationAnalytics.test.ts +165 -0
- package/src/ai/__tests__/GenerationCache.prod.test.ts +144 -0
- package/src/ai/__tests__/GenerationCache.test.ts +171 -0
- package/src/ai/__tests__/GoalPlanner.prod.test.ts +189 -0
- package/src/ai/__tests__/GoalPlanner.test.ts +137 -0
- package/src/ai/__tests__/GoalPlannerDepth.prod.test.ts +217 -0
- package/src/ai/__tests__/HoloScriptGenerator.test.ts +125 -0
- package/src/ai/__tests__/InfluenceMap.prod.test.ts +146 -0
- package/src/ai/__tests__/InfluenceMap.test.ts +149 -0
- package/src/ai/__tests__/NavMesh.prod.test.ts +141 -0
- package/src/ai/__tests__/NavMesh.test.ts +159 -0
- package/src/ai/__tests__/PerceptionSystem.prod.test.ts +135 -0
- package/src/ai/__tests__/PerceptionSystem.test.ts +250 -0
- package/src/ai/__tests__/PromptTemplates.prod.test.ts +313 -0
- package/src/ai/__tests__/PromptTemplates.test.ts +146 -0
- package/src/ai/__tests__/SemanticSearch.test.ts +37 -0
- package/src/ai/__tests__/StateMachine.prod.test.ts +162 -0
- package/src/ai/__tests__/StateMachine.test.ts +163 -0
- package/src/ai/__tests__/SteeringBehavior.prod.test.ts +251 -0
- package/src/ai/__tests__/SteeringBehavior.test.ts +135 -0
- package/src/ai/__tests__/SteeringBehaviors.prod.test.ts +133 -0
- package/src/ai/__tests__/SteeringBehaviors.test.ts +151 -0
- package/src/ai/__tests__/TrainingDataGenerator.prod.test.ts +286 -0
- package/src/ai/__tests__/TrainingDataGenerator.test.ts +286 -0
- package/src/ai/__tests__/UtilityAI.prod.test.ts +207 -0
- package/src/ai/__tests__/UtilityAI.test.ts +155 -0
- package/src/ai/__tests__/adapters.prod.test.ts +263 -0
- package/src/ai/__tests__/adapters.test.ts +320 -0
- package/src/ai/adapters.ts +1585 -0
- package/src/ai/index.ts +130 -0
- package/src/behavior/BehaviorPresets.ts +140 -0
- package/src/behavior/BehaviorTree.ts +236 -0
- package/src/behavior/StateMachine.ts +176 -0
- package/src/behavior/StateTrait.ts +67 -0
- package/src/behavior/index.ts +8 -0
- package/src/behavior.ts +8 -0
- package/src/board/audit.ts +284 -0
- package/src/board/board-ops.ts +336 -0
- package/src/board/board-types.ts +302 -0
- package/src/board/index.ts +69 -0
- package/src/define-agent.ts +46 -0
- package/src/define-team.ts +33 -0
- package/src/delegation.ts +265 -0
- package/src/distributed-claimer.ts +228 -0
- package/src/economy/AgentBudgetEnforcer.ts +464 -0
- package/src/economy/BountyManager.ts +185 -0
- package/src/economy/CreatorRevenueAggregator.ts +460 -0
- package/src/economy/InvisibleWallet.ts +82 -0
- package/src/economy/KnowledgeMarketplace.ts +193 -0
- package/src/economy/PaymentWebhookService.ts +512 -0
- package/src/economy/RevenueSplitter.ts +156 -0
- package/src/economy/SubscriptionManager.ts +546 -0
- package/src/economy/UnifiedBudgetOptimizer.ts +635 -0
- package/src/economy/UsageMeter.ts +440 -0
- package/src/economy/_core-stubs.ts +219 -0
- package/src/economy/index.ts +100 -0
- package/src/economy/x402-facilitator.ts +1978 -0
- package/src/index.ts +348 -0
- package/src/knowledge/__tests__/knowledge-consolidator.test.ts +444 -0
- package/src/knowledge/__tests__/knowledge-store-vector.test.ts +291 -0
- package/src/knowledge/brain.ts +167 -0
- package/src/knowledge/consolidation.ts +581 -0
- package/src/knowledge/knowledge-consolidator.ts +510 -0
- package/src/knowledge/knowledge-store.ts +616 -0
- package/src/learning/MemoryConsolidator.ts +102 -0
- package/src/learning/MemoryScorer.ts +69 -0
- package/src/learning/ProceduralCompiler.ts +45 -0
- package/src/learning/SemanticClusterer.ts +66 -0
- package/src/learning/index.ts +8 -0
- package/src/llm/llm-adapter.ts +159 -0
- package/src/mesh/index.ts +309 -0
- package/src/negotiation/NegotiationProtocol.ts +694 -0
- package/src/negotiation/NegotiationTypes.ts +473 -0
- package/src/negotiation/VotingMechanisms.ts +691 -0
- package/src/negotiation/index.ts +49 -0
- package/src/protocol/goal-synthesizer.ts +317 -0
- package/src/protocol/implementations.ts +474 -0
- package/src/protocol/micro-phase-decomposer.ts +299 -0
- package/src/protocol/micro-step-decomposer.test.ts +306 -0
- package/src/protocol-agent.test.ts +353 -0
- package/src/protocol-agent.ts +670 -0
- package/src/self-improve/absorb-scanner.ts +252 -0
- package/src/self-improve/evolution-engine.ts +149 -0
- package/src/self-improve/framework-absorber.ts +214 -0
- package/src/self-improve/index.ts +50 -0
- package/src/self-improve/prompt-optimizer.ts +212 -0
- package/src/self-improve/test-generator.ts +175 -0
- package/src/skill-router.ts +186 -0
- package/src/skills/index.ts +5 -0
- package/src/skills/skill-md-bridge.ts +1699 -0
- package/src/swarm/ACOEngine.ts +261 -0
- package/src/swarm/CollectiveIntelligence.ts +383 -0
- package/src/swarm/ContributionSynthesizer.ts +481 -0
- package/src/swarm/LeaderElection.ts +393 -0
- package/src/swarm/PSOEngine.ts +206 -0
- package/src/swarm/QuorumPolicy.ts +173 -0
- package/src/swarm/SwarmCoordinator.ts +335 -0
- package/src/swarm/SwarmManager.ts +442 -0
- package/src/swarm/SwarmMembership.ts +456 -0
- package/src/swarm/VotingRound.ts +255 -0
- package/src/swarm/__tests__/ACOEngine.prod.test.ts +164 -0
- package/src/swarm/__tests__/ACOEngine.test.ts +117 -0
- package/src/swarm/__tests__/CollectiveIntelligence.prod.test.ts +296 -0
- package/src/swarm/__tests__/CollectiveIntelligence.test.ts +457 -0
- package/src/swarm/__tests__/ContributionSynthesizer.prod.test.ts +269 -0
- package/src/swarm/__tests__/ContributionSynthesizer.test.ts +254 -0
- package/src/swarm/__tests__/LeaderElection.prod.test.ts +196 -0
- package/src/swarm/__tests__/LeaderElection.test.ts +151 -0
- package/src/swarm/__tests__/PSOEngine.prod.test.ts +162 -0
- package/src/swarm/__tests__/PSOEngine.test.ts +106 -0
- package/src/swarm/__tests__/QuorumPolicy.prod.test.ts +216 -0
- package/src/swarm/__tests__/QuorumPolicy.test.ts +177 -0
- package/src/swarm/__tests__/SwarmCoordinator.prod.test.ts +186 -0
- package/src/swarm/__tests__/SwarmCoordinator.test.ts +167 -0
- package/src/swarm/__tests__/SwarmManager.prod.test.ts +308 -0
- package/src/swarm/__tests__/SwarmManager.test.ts +373 -0
- package/src/swarm/__tests__/SwarmMembership.prod.test.ts +273 -0
- package/src/swarm/__tests__/SwarmMembership.test.ts +264 -0
- package/src/swarm/__tests__/VotingRound.prod.test.ts +233 -0
- package/src/swarm/__tests__/VotingRound.test.ts +174 -0
- package/src/swarm/analytics/SwarmInspector.ts +476 -0
- package/src/swarm/analytics/SwarmMetrics.ts +449 -0
- package/src/swarm/analytics/__tests__/SwarmInspector.prod.test.ts +366 -0
- package/src/swarm/analytics/__tests__/SwarmInspector.test.ts +454 -0
- package/src/swarm/analytics/__tests__/SwarmMetrics.prod.test.ts +254 -0
- package/src/swarm/analytics/__tests__/SwarmMetrics.test.ts +370 -0
- package/src/swarm/analytics/index.ts +7 -0
- package/src/swarm/index.ts +69 -0
- package/src/swarm/messaging/BroadcastChannel.ts +509 -0
- package/src/swarm/messaging/GossipProtocol.ts +565 -0
- package/src/swarm/messaging/SwarmEventBus.ts +443 -0
- package/src/swarm/messaging/__tests__/BroadcastChannel.prod.test.ts +331 -0
- package/src/swarm/messaging/__tests__/BroadcastChannel.test.ts +333 -0
- package/src/swarm/messaging/__tests__/GossipProtocol.prod.test.ts +356 -0
- package/src/swarm/messaging/__tests__/GossipProtocol.test.ts +437 -0
- package/src/swarm/messaging/__tests__/SwarmEventBus.prod.test.ts +191 -0
- package/src/swarm/messaging/__tests__/SwarmEventBus.test.ts +247 -0
- package/src/swarm/messaging/index.ts +8 -0
- package/src/swarm/spatial/FlockingBehavior.ts +462 -0
- package/src/swarm/spatial/FormationController.ts +500 -0
- package/src/swarm/spatial/Vector3.ts +170 -0
- package/src/swarm/spatial/ZoneClaiming.ts +509 -0
- package/src/swarm/spatial/__tests__/FlockingBehavior.prod.test.ts +239 -0
- package/src/swarm/spatial/__tests__/FlockingBehavior.test.ts +298 -0
- package/src/swarm/spatial/__tests__/FormationController.prod.test.ts +240 -0
- package/src/swarm/spatial/__tests__/FormationController.test.ts +297 -0
- package/src/swarm/spatial/__tests__/Vector3.prod.test.ts +283 -0
- package/src/swarm/spatial/__tests__/Vector3.test.ts +224 -0
- package/src/swarm/spatial/__tests__/ZoneClaiming.prod.test.ts +246 -0
- package/src/swarm/spatial/__tests__/ZoneClaiming.test.ts +374 -0
- package/src/swarm/spatial/index.ts +28 -0
- package/src/team.ts +1245 -0
- package/src/training/LRScheduler.ts +377 -0
- package/src/training/QualityScoringPipeline.ts +139 -0
- package/src/training/SoftDedup.ts +461 -0
- package/src/training/SparsityMonitor.ts +685 -0
- package/src/training/SparsityMonitorTypes.ts +209 -0
- package/src/training/SpatialTrainingDataGenerator.ts +1526 -0
- package/src/training/SpatialTrainingDataTypes.ts +216 -0
- package/src/training/TrainingPipelineConfig.ts +215 -0
- package/src/training/constants.ts +94 -0
- package/src/training/index.ts +138 -0
- package/src/training/schema.ts +147 -0
- package/src/training/scripts/generate-novel-use-cases-dataset.ts +272 -0
- package/src/training/scripts/generate-spatial-dataset.ts +521 -0
- package/src/training/training/data/novel-use-cases.jsonl +153 -0
- package/src/training/training/data/spatial-reasoning-10k.jsonl +9354 -0
- package/src/training/trainingmonkey/TrainingMonkeyIntegration.ts +477 -0
- package/src/training/trainingmonkey/TrainingMonkeyTypes.ts +230 -0
- package/src/training/trainingmonkey/index.ts +26 -0
- package/src/training/trait-mappings.ts +157 -0
- package/src/types/core-stubs.d.ts +113 -0
- package/src/types.ts +304 -0
- package/test-output.txt +0 -0
- package/test-result.json +1 -0
- package/tsc-errors.txt +4 -0
- package/tsc_output.txt +0 -0
- package/tsconfig.json +14 -0
- package/tsup-learning-esm.config.ts +12 -0
- package/tsup.config.ts +21 -0
- package/typescript-errors-2.txt +0 -0
- package/typescript-errors.txt +22 -0
- package/vitest-log-utf8.txt +268 -0
- package/vitest-log.txt +0 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { StateMachine, type StateConfig, type TransitionConfig } from '../StateMachine';
|
|
3
|
+
|
|
4
|
+
describe('StateMachine', () => {
|
|
5
|
+
let sm: StateMachine;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
sm = new StateMachine();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// State Management
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
it('addState registers a state', () => {
|
|
16
|
+
sm.addState({ id: 'idle' });
|
|
17
|
+
expect(sm.getStateCount()).toBe(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('removeState unregisters a state', () => {
|
|
21
|
+
sm.addState({ id: 'idle' });
|
|
22
|
+
sm.removeState('idle');
|
|
23
|
+
expect(sm.getStateCount()).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('setInitialState sets current state and calls onEnter', () => {
|
|
27
|
+
const onEnter = vi.fn();
|
|
28
|
+
sm.addState({ id: 'idle', onEnter });
|
|
29
|
+
sm.setInitialState('idle');
|
|
30
|
+
expect(sm.getCurrentState()).toBe('idle');
|
|
31
|
+
expect(onEnter).toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Transitions
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
it('send transitions to new state', () => {
|
|
39
|
+
sm.addState({ id: 'idle' });
|
|
40
|
+
sm.addState({ id: 'walk' });
|
|
41
|
+
sm.addTransition({ from: 'idle', to: 'walk', event: 'move' });
|
|
42
|
+
sm.setInitialState('idle');
|
|
43
|
+
expect(sm.send('move')).toBe(true);
|
|
44
|
+
expect(sm.getCurrentState()).toBe('walk');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('send returns false for unmatched event', () => {
|
|
48
|
+
sm.addState({ id: 'idle' });
|
|
49
|
+
sm.setInitialState('idle');
|
|
50
|
+
expect(sm.send('nonexistent')).toBe(false);
|
|
51
|
+
expect(sm.getCurrentState()).toBe('idle');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('send calls onExit and onEnter', () => {
|
|
55
|
+
const onExit = vi.fn();
|
|
56
|
+
const onEnter = vi.fn();
|
|
57
|
+
sm.addState({ id: 'a', onExit });
|
|
58
|
+
sm.addState({ id: 'b', onEnter });
|
|
59
|
+
sm.addTransition({ from: 'a', to: 'b', event: 'go' });
|
|
60
|
+
sm.setInitialState('a');
|
|
61
|
+
sm.send('go');
|
|
62
|
+
expect(onExit).toHaveBeenCalled();
|
|
63
|
+
expect(onEnter).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('send respects guard condition', () => {
|
|
67
|
+
sm.addState({ id: 'locked' });
|
|
68
|
+
sm.addState({ id: 'open' });
|
|
69
|
+
sm.addTransition({
|
|
70
|
+
from: 'locked',
|
|
71
|
+
to: 'open',
|
|
72
|
+
event: 'unlock',
|
|
73
|
+
guard: (ctx) => ctx['hasKey'] === true,
|
|
74
|
+
});
|
|
75
|
+
sm.setInitialState('locked');
|
|
76
|
+
|
|
77
|
+
expect(sm.send('unlock')).toBe(false); // no key
|
|
78
|
+
sm.setContext('hasKey', true);
|
|
79
|
+
expect(sm.send('unlock')).toBe(true);
|
|
80
|
+
expect(sm.getCurrentState()).toBe('open');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('transition action is called', () => {
|
|
84
|
+
const action = vi.fn();
|
|
85
|
+
sm.addState({ id: 'a' });
|
|
86
|
+
sm.addState({ id: 'b' });
|
|
87
|
+
sm.addTransition({ from: 'a', to: 'b', event: 'go', action });
|
|
88
|
+
sm.setInitialState('a');
|
|
89
|
+
sm.send('go');
|
|
90
|
+
expect(action).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Update
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
it('update calls onUpdate of current state', () => {
|
|
98
|
+
const onUpdate = vi.fn();
|
|
99
|
+
sm.addState({ id: 'idle', onUpdate });
|
|
100
|
+
sm.setInitialState('idle');
|
|
101
|
+
sm.update();
|
|
102
|
+
expect(onUpdate).toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('update does nothing with no current state', () => {
|
|
106
|
+
// Should not throw
|
|
107
|
+
sm.update();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Context
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
it('setContext / getContext stores values', () => {
|
|
115
|
+
sm.setContext('hp', 100);
|
|
116
|
+
expect(sm.getContext('hp')).toBe(100);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Queries
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
it('isInState checks current state', () => {
|
|
124
|
+
sm.addState({ id: 'idle' });
|
|
125
|
+
sm.setInitialState('idle');
|
|
126
|
+
expect(sm.isInState('idle')).toBe(true);
|
|
127
|
+
expect(sm.isInState('walk')).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('getHistory returns state transition history', () => {
|
|
131
|
+
sm.addState({ id: 'a' });
|
|
132
|
+
sm.addState({ id: 'b' });
|
|
133
|
+
sm.addTransition({ from: 'a', to: 'b', event: 'go' });
|
|
134
|
+
sm.setInitialState('a');
|
|
135
|
+
sm.send('go');
|
|
136
|
+
expect(sm.getHistory()).toEqual(['a', 'b']);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Hierarchy
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
it('getChildStates returns child states', () => {
|
|
144
|
+
sm.addState({ id: 'combat' });
|
|
145
|
+
sm.addState({ id: 'attack', parent: 'combat' });
|
|
146
|
+
sm.addState({ id: 'defend', parent: 'combat' });
|
|
147
|
+
const children = sm.getChildStates('combat');
|
|
148
|
+
expect(children).toContain('attack');
|
|
149
|
+
expect(children).toContain('defend');
|
|
150
|
+
expect(children).toHaveLength(2);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('parent transitions bubble up', () => {
|
|
154
|
+
sm.addState({ id: 'combat' });
|
|
155
|
+
sm.addState({ id: 'attack', parent: 'combat' });
|
|
156
|
+
sm.addState({ id: 'idle' });
|
|
157
|
+
sm.addTransition({ from: 'combat', to: 'idle', event: 'disengage' });
|
|
158
|
+
sm.setInitialState('attack');
|
|
159
|
+
// Should find transition from parent 'combat'
|
|
160
|
+
expect(sm.send('disengage')).toBe(true);
|
|
161
|
+
expect(sm.getCurrentState()).toBe('idle');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SteeringBehavior — Production Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests: seek, flee, arrive, wander, avoid, blend
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { SteeringBehavior } from '../SteeringBehavior';
|
|
9
|
+
import type { SteeringAgent } from '../SteeringBehavior';
|
|
10
|
+
|
|
11
|
+
function makeAgent(overrides: Partial<SteeringAgent> = {}): SteeringAgent {
|
|
12
|
+
return {
|
|
13
|
+
position: { x: 0, z: 0 },
|
|
14
|
+
velocity: { x: 0, z: 0 },
|
|
15
|
+
maxSpeed: 5,
|
|
16
|
+
maxForce: 10,
|
|
17
|
+
mass: 1,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --- seek ---
|
|
23
|
+
describe('SteeringBehavior.seek', () => {
|
|
24
|
+
it('returns zero force when already at target', () => {
|
|
25
|
+
const agent = makeAgent({ position: { x: 3, z: 4 } });
|
|
26
|
+
const force = SteeringBehavior.seek(agent, { x: 3, z: 4 });
|
|
27
|
+
expect(force.x).toBe(0);
|
|
28
|
+
expect(force.z).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('desired velocity length equals maxSpeed minus current velocity', () => {
|
|
32
|
+
const agent = makeAgent({ maxSpeed: 5 });
|
|
33
|
+
const force = SteeringBehavior.seek(agent, { x: 10, z: 0 });
|
|
34
|
+
// desired = (10/10)*5 = 5, velocity = 0 → force.x = 5
|
|
35
|
+
expect(force.x).toBeCloseTo(5, 4);
|
|
36
|
+
expect(force.z).toBeCloseTo(0, 4);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('force accounts for existing velocity', () => {
|
|
40
|
+
const agent = makeAgent({ velocity: { x: 2, z: 0 }, maxSpeed: 5 });
|
|
41
|
+
const force = SteeringBehavior.seek(agent, { x: 10, z: 0 });
|
|
42
|
+
// desired.x=5, velocity.x=2 → force.x=3
|
|
43
|
+
expect(force.x).toBeCloseTo(3, 4);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('seeks in negative direction', () => {
|
|
47
|
+
const agent = makeAgent();
|
|
48
|
+
const force = SteeringBehavior.seek(agent, { x: -10, z: 0 });
|
|
49
|
+
expect(force.x).toBeLessThan(0);
|
|
50
|
+
expect(force.z).toBeCloseTo(0, 4);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns normalized direction toward diagonal target', () => {
|
|
54
|
+
const agent = makeAgent({ maxSpeed: 1 });
|
|
55
|
+
const force = SteeringBehavior.seek(agent, { x: 1, z: 1 });
|
|
56
|
+
const mag = Math.sqrt(force.x ** 2 + force.z ** 2);
|
|
57
|
+
// desired mag = 1 (maxSpeed), velocity=0 → force mag ≈ 1
|
|
58
|
+
expect(mag).toBeCloseTo(1, 4);
|
|
59
|
+
expect(force.x).toBeCloseTo(force.z, 4);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// --- flee ---
|
|
64
|
+
describe('SteeringBehavior.flee', () => {
|
|
65
|
+
it('is exact negation of seek result', () => {
|
|
66
|
+
const agent = makeAgent();
|
|
67
|
+
const target = { x: 5, z: 3 };
|
|
68
|
+
const seek = SteeringBehavior.seek(agent, target);
|
|
69
|
+
const flee = SteeringBehavior.flee(agent, target);
|
|
70
|
+
expect(flee.x).toBeCloseTo(-seek.x, 6);
|
|
71
|
+
expect(flee.z).toBeCloseTo(-seek.z, 6);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('points away from target', () => {
|
|
75
|
+
const agent = makeAgent({ position: { x: 0, z: 0 } });
|
|
76
|
+
const force = SteeringBehavior.flee(agent, { x: 5, z: 0 });
|
|
77
|
+
// should steer left (negative x)
|
|
78
|
+
expect(force.x).toBeLessThan(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// --- arrive ---
|
|
83
|
+
describe('SteeringBehavior.arrive', () => {
|
|
84
|
+
it('returns zero force at target', () => {
|
|
85
|
+
const agent = makeAgent({ position: { x: 5, z: 0 } });
|
|
86
|
+
const force = SteeringBehavior.arrive(agent, { x: 5, z: 0 });
|
|
87
|
+
expect(force.x).toBe(0);
|
|
88
|
+
expect(force.z).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('outside slowRadius speed equals maxSpeed', () => {
|
|
92
|
+
const agent = makeAgent({ maxSpeed: 5, velocity: { x: 0, z: 0 } });
|
|
93
|
+
const slowRadius = 3;
|
|
94
|
+
// target at (100,0) is far outside slowRadius
|
|
95
|
+
const force = SteeringBehavior.arrive(agent, { x: 100, z: 0 }, slowRadius);
|
|
96
|
+
// desired.x = maxSpeed = 5 → force.x = 5
|
|
97
|
+
expect(force.x).toBeCloseTo(5, 4);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('inside slowRadius speed proportional to distance', () => {
|
|
101
|
+
const agent = makeAgent({ maxSpeed: 10, velocity: { x: 0, z: 0 } });
|
|
102
|
+
const slowRadius = 10;
|
|
103
|
+
// target at (5, 0) → dist=5 < slowRadius=10 → speed=10*(5/10)=5
|
|
104
|
+
const force = SteeringBehavior.arrive(agent, { x: 5, z: 0 }, slowRadius);
|
|
105
|
+
expect(force.x).toBeCloseTo(5, 4);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('deceleration force is less than full-speed seek force', () => {
|
|
109
|
+
const agent = makeAgent({ maxSpeed: 10, velocity: { x: 0, z: 0 } });
|
|
110
|
+
const seekForce = SteeringBehavior.seek(agent, { x: 1, z: 0 });
|
|
111
|
+
const arriveForce = SteeringBehavior.arrive(agent, { x: 1, z: 0 }, 20);
|
|
112
|
+
expect(Math.abs(arriveForce.x)).toBeLessThan(Math.abs(seekForce.x));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('uses default slowRadius of 5', () => {
|
|
116
|
+
const agent = makeAgent({ maxSpeed: 10, velocity: { x: 0, z: 0 } });
|
|
117
|
+
// dist=2 < slowRadius=5 → speed=10*(2/5)=4
|
|
118
|
+
const force = SteeringBehavior.arrive(agent, { x: 2, z: 0 });
|
|
119
|
+
expect(force.x).toBeCloseTo(4, 4);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// --- wander ---
|
|
124
|
+
describe('SteeringBehavior.wander', () => {
|
|
125
|
+
it('returns a Vec2 object with x and z', () => {
|
|
126
|
+
const agent = makeAgent({ velocity: { x: 1, z: 0 } });
|
|
127
|
+
const force = SteeringBehavior.wander(agent);
|
|
128
|
+
expect(typeof force.x).toBe('number');
|
|
129
|
+
expect(typeof force.z).toBe('number');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('does not produce infinite or NaN values', () => {
|
|
133
|
+
const agent = makeAgent({ velocity: { x: 0, z: 0 } });
|
|
134
|
+
const force = SteeringBehavior.wander(agent);
|
|
135
|
+
expect(isFinite(force.x)).toBe(true);
|
|
136
|
+
expect(isFinite(force.z)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('multiple calls produce varied results (randomness)', () => {
|
|
140
|
+
const agent = makeAgent({ velocity: { x: 1, z: 0 } });
|
|
141
|
+
const forces = Array.from({ length: 10 }, () => SteeringBehavior.wander(agent));
|
|
142
|
+
const unique = new Set(forces.map((f) => `${f.x.toFixed(5)},${f.z.toFixed(5)}`));
|
|
143
|
+
// extremely unlikely all 10 are identical
|
|
144
|
+
expect(unique.size).toBeGreaterThan(1);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// --- avoid ---
|
|
149
|
+
describe('SteeringBehavior.avoid', () => {
|
|
150
|
+
it('returns zero when no obstacles', () => {
|
|
151
|
+
const agent = makeAgent();
|
|
152
|
+
const force = SteeringBehavior.avoid(agent, []);
|
|
153
|
+
expect(force.x).toBe(0);
|
|
154
|
+
expect(force.z).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('returns zero when obstacle is beyond lookAhead', () => {
|
|
158
|
+
const agent = makeAgent({ maxForce: 10 });
|
|
159
|
+
const force = SteeringBehavior.avoid(agent, [{ position: { x: 100, z: 0 }, radius: 1 }], 5);
|
|
160
|
+
expect(force.x).toBe(0);
|
|
161
|
+
expect(force.z).toBe(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('pushes away from nearby obstacle', () => {
|
|
165
|
+
const agent = makeAgent({ position: { x: 0, z: 0 }, maxForce: 10 });
|
|
166
|
+
// obstacle at (2, 0), within lookAhead=5
|
|
167
|
+
const force = SteeringBehavior.avoid(agent, [{ position: { x: 2, z: 0 }, radius: 1 }], 5);
|
|
168
|
+
// push should be in negative x direction
|
|
169
|
+
expect(force.x).toBeLessThan(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('larger obstacle radius causes stronger push', () => {
|
|
173
|
+
const agent = makeAgent({ position: { x: 0, z: 0 }, maxForce: 100 });
|
|
174
|
+
const small = SteeringBehavior.avoid(agent, [{ position: { x: 3, z: 0 }, radius: 0.5 }], 5);
|
|
175
|
+
const large = SteeringBehavior.avoid(agent, [{ position: { x: 3, z: 0 }, radius: 3 }], 5);
|
|
176
|
+
expect(Math.abs(large.x)).toBeGreaterThan(Math.abs(small.x));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('combines forces from multiple obstacles', () => {
|
|
180
|
+
const agent = makeAgent({ position: { x: 0, z: 0 }, maxForce: 10 });
|
|
181
|
+
const oneObs = SteeringBehavior.avoid(agent, [{ position: { x: 2, z: 0 }, radius: 1 }], 5);
|
|
182
|
+
const twoObs = SteeringBehavior.avoid(
|
|
183
|
+
agent,
|
|
184
|
+
[
|
|
185
|
+
{ position: { x: 2, z: 0 }, radius: 1 },
|
|
186
|
+
{ position: { x: -2, z: 0 }, radius: 1 },
|
|
187
|
+
],
|
|
188
|
+
5
|
|
189
|
+
);
|
|
190
|
+
// x-components partially cancel, but total force changes
|
|
191
|
+
expect(Math.abs(twoObs.x)).toBeLessThan(Math.abs(oneObs.x));
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// --- blend ---
|
|
196
|
+
describe('SteeringBehavior.blend', () => {
|
|
197
|
+
it('returns zero for empty outputs', () => {
|
|
198
|
+
const result = SteeringBehavior.blend([], 10);
|
|
199
|
+
expect(result.x).toBe(0);
|
|
200
|
+
expect(result.z).toBe(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('single output with weight=1 passes through unclamped', () => {
|
|
204
|
+
const result = SteeringBehavior.blend(
|
|
205
|
+
[{ force: { x: 3, z: 4 }, type: 'seek', weight: 1 }],
|
|
206
|
+
100
|
|
207
|
+
);
|
|
208
|
+
expect(result.x).toBeCloseTo(3, 4);
|
|
209
|
+
expect(result.z).toBeCloseTo(4, 4);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('clamps result magnitude to maxForce', () => {
|
|
213
|
+
const result = SteeringBehavior.blend(
|
|
214
|
+
[{ force: { x: 100, z: 0 }, type: 'seek', weight: 1 }],
|
|
215
|
+
5
|
|
216
|
+
);
|
|
217
|
+
expect(result.x).toBeCloseTo(5, 4);
|
|
218
|
+
expect(result.z).toBeCloseTo(0, 4);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('weight scales force contribution', () => {
|
|
222
|
+
const result = SteeringBehavior.blend(
|
|
223
|
+
[{ force: { x: 10, z: 0 }, type: 'seek', weight: 0.5 }],
|
|
224
|
+
100
|
|
225
|
+
);
|
|
226
|
+
expect(result.x).toBeCloseTo(5, 4);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('multiple outputs are summed with weights', () => {
|
|
230
|
+
const result = SteeringBehavior.blend(
|
|
231
|
+
[
|
|
232
|
+
{ force: { x: 4, z: 0 }, type: 'seek', weight: 1 },
|
|
233
|
+
{ force: { x: 6, z: 0 }, type: 'flee', weight: 1 },
|
|
234
|
+
],
|
|
235
|
+
100
|
|
236
|
+
);
|
|
237
|
+
expect(result.x).toBeCloseTo(10, 4);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('opposing forces can cancel', () => {
|
|
241
|
+
const result = SteeringBehavior.blend(
|
|
242
|
+
[
|
|
243
|
+
{ force: { x: 5, z: 0 }, type: 'seek', weight: 1 },
|
|
244
|
+
{ force: { x: -5, z: 0 }, type: 'flee', weight: 1 },
|
|
245
|
+
],
|
|
246
|
+
100
|
|
247
|
+
);
|
|
248
|
+
expect(result.x).toBeCloseTo(0, 4);
|
|
249
|
+
expect(result.z).toBeCloseTo(0, 4);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { SteeringBehavior, SteeringAgent, SteeringOutput, Vec2 } from '../SteeringBehavior';
|
|
3
|
+
|
|
4
|
+
function agent(pos: Vec2 = { x: 0, z: 0 }, vel: Vec2 = { x: 0, z: 0 }): SteeringAgent {
|
|
5
|
+
return { position: pos, velocity: vel, maxSpeed: 10, maxForce: 5, mass: 1 };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function mag(v: Vec2): number {
|
|
9
|
+
return Math.sqrt(v.x ** 2 + v.z ** 2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('SteeringBehavior', () => {
|
|
13
|
+
// --- Seek ---
|
|
14
|
+
it('seek steers toward target', () => {
|
|
15
|
+
const f = SteeringBehavior.seek(agent(), { x: 10, z: 0 });
|
|
16
|
+
expect(f.x).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('seek returns zero at target', () => {
|
|
20
|
+
const f = SteeringBehavior.seek(agent({ x: 5, z: 5 }), { x: 5, z: 5 });
|
|
21
|
+
expect(f.x).toBe(0);
|
|
22
|
+
expect(f.z).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('seek accounts for current velocity', () => {
|
|
26
|
+
const a = agent({ x: 0, z: 0 }, { x: 5, z: 0 });
|
|
27
|
+
const f = SteeringBehavior.seek(a, { x: 10, z: 0 });
|
|
28
|
+
// Desired is 10 (maxSpeed), current is 5, so steering = 5
|
|
29
|
+
expect(f.x).toBe(5);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// --- Flee ---
|
|
33
|
+
it('flee steers away from target', () => {
|
|
34
|
+
const f = SteeringBehavior.flee(agent(), { x: 10, z: 0 });
|
|
35
|
+
expect(f.x).toBeLessThan(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('flee is opposite of seek', () => {
|
|
39
|
+
const a = agent();
|
|
40
|
+
const seek = SteeringBehavior.seek(a, { x: 5, z: 5 });
|
|
41
|
+
const flee = SteeringBehavior.flee(a, { x: 5, z: 5 });
|
|
42
|
+
expect(flee.x).toBeCloseTo(-seek.x, 5);
|
|
43
|
+
expect(flee.z).toBeCloseTo(-seek.z, 5);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// --- Arrive ---
|
|
47
|
+
it('arrive matches seek when far away', () => {
|
|
48
|
+
const a = agent();
|
|
49
|
+
const target = { x: 100, z: 0 };
|
|
50
|
+
const arrive = SteeringBehavior.arrive(a, target, 5);
|
|
51
|
+
const seek = SteeringBehavior.seek(a, target);
|
|
52
|
+
// When far from slowRadius, arrive ≈ seek
|
|
53
|
+
expect(Math.abs(arrive.x - seek.x)).toBeLessThan(0.1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('arrive decelerates near target', () => {
|
|
57
|
+
const far = SteeringBehavior.arrive(agent(), { x: 100, z: 0 }, 5);
|
|
58
|
+
const near = SteeringBehavior.arrive(agent(), { x: 2, z: 0 }, 5);
|
|
59
|
+
expect(mag(near)).toBeLessThan(mag(far));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('arrive returns zero at target', () => {
|
|
63
|
+
const f = SteeringBehavior.arrive(agent({ x: 5, z: 5 }), { x: 5, z: 5 });
|
|
64
|
+
expect(f.x).toBe(0);
|
|
65
|
+
expect(f.z).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// --- Wander ---
|
|
69
|
+
it('wander produces a force', () => {
|
|
70
|
+
const f = SteeringBehavior.wander(agent({ x: 0, z: 0 }, { x: 1, z: 0 }));
|
|
71
|
+
expect(mag(f)).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('wander produces varied results', () => {
|
|
75
|
+
const a = agent({ x: 0, z: 0 }, { x: 1, z: 0 });
|
|
76
|
+
const results = new Set<string>();
|
|
77
|
+
for (let i = 0; i < 20; i++) {
|
|
78
|
+
const f = SteeringBehavior.wander(a);
|
|
79
|
+
results.add(`${f.x.toFixed(2)}_${f.z.toFixed(2)}`);
|
|
80
|
+
}
|
|
81
|
+
// Should produce more than one unique result over 20 calls
|
|
82
|
+
expect(results.size).toBeGreaterThan(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// --- Avoid ---
|
|
86
|
+
it('avoid pushes away from obstacle', () => {
|
|
87
|
+
const a = agent({ x: 0, z: 0 }, { x: 1, z: 0 });
|
|
88
|
+
const obstacles = [{ position: { x: 3, z: 0 }, radius: 1 }];
|
|
89
|
+
const f = SteeringBehavior.avoid(a, obstacles, 5);
|
|
90
|
+
expect(f.x).toBeLessThan(0); // Push back
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('avoid returns zero when no obstacles in range', () => {
|
|
94
|
+
const a = agent();
|
|
95
|
+
const obstacles = [{ position: { x: 100, z: 100 }, radius: 1 }];
|
|
96
|
+
const f = SteeringBehavior.avoid(a, obstacles, 5);
|
|
97
|
+
expect(f.x).toBe(0);
|
|
98
|
+
expect(f.z).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('avoid handles multiple obstacles', () => {
|
|
102
|
+
const a = agent({ x: 5, z: 5 });
|
|
103
|
+
const obstacles = [
|
|
104
|
+
{ position: { x: 6, z: 5 }, radius: 1 },
|
|
105
|
+
{ position: { x: 5, z: 6 }, radius: 1 },
|
|
106
|
+
];
|
|
107
|
+
const f = SteeringBehavior.avoid(a, obstacles, 5);
|
|
108
|
+
// Should push in -x and -z
|
|
109
|
+
expect(f.x).toBeLessThan(0);
|
|
110
|
+
expect(f.z).toBeLessThan(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// --- Blend ---
|
|
114
|
+
it('blend combines weighted outputs', () => {
|
|
115
|
+
const outputs: SteeringOutput[] = [
|
|
116
|
+
{ force: { x: 10, z: 0 }, type: 'seek', weight: 0.5 },
|
|
117
|
+
{ force: { x: 0, z: 10 }, type: 'flee', weight: 0.5 },
|
|
118
|
+
];
|
|
119
|
+
const f = SteeringBehavior.blend(outputs, 100);
|
|
120
|
+
expect(f.x).toBeCloseTo(5);
|
|
121
|
+
expect(f.z).toBeCloseTo(5);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('blend truncates to maxForce', () => {
|
|
125
|
+
const outputs: SteeringOutput[] = [{ force: { x: 100, z: 0 }, type: 'seek', weight: 1 }];
|
|
126
|
+
const f = SteeringBehavior.blend(outputs, 5);
|
|
127
|
+
expect(mag(f)).toBeCloseTo(5, 1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('blend with empty produces zero', () => {
|
|
131
|
+
const f = SteeringBehavior.blend([], 10);
|
|
132
|
+
expect(f.x).toBe(0);
|
|
133
|
+
expect(f.z).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SteeringBehaviors — Production Test Suite
|
|
3
|
+
*
|
|
4
|
+
* All methods are static. Vec3 helpers and separation/alignment/cohesion
|
|
5
|
+
* are private, so we test them indirectly through the public API:
|
|
6
|
+
* seek, flee, arrive, wander, flock, obstacleAvoidance, applyForce.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { SteeringBehaviors, type SteeringAgent } from '../SteeringBehaviors';
|
|
10
|
+
|
|
11
|
+
function agent(px = 0, py = 0, pz = 0, vx = 0, vy = 0, vz = 0): SteeringAgent {
|
|
12
|
+
return {
|
|
13
|
+
position: { x: px, y: py, z: pz },
|
|
14
|
+
velocity: { x: vx, y: vy, z: vz },
|
|
15
|
+
maxSpeed: 10,
|
|
16
|
+
maxForce: 5,
|
|
17
|
+
mass: 1,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function len(v: { x: number; y: number; z: number }): number {
|
|
22
|
+
return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('SteeringBehaviors — Production', () => {
|
|
26
|
+
// ─── Seek ─────────────────────────────────────────────────────────
|
|
27
|
+
it('seek returns force toward target', () => {
|
|
28
|
+
const force = SteeringBehaviors.seek(agent(), { x: 10, y: 0, z: 0 });
|
|
29
|
+
expect(force.x).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('seek force is capped by maxForce', () => {
|
|
33
|
+
const force = SteeringBehaviors.seek(agent(), { x: 1000, y: 0, z: 0 });
|
|
34
|
+
expect(len(force)).toBeLessThanOrEqual(5.01);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─── Flee ─────────────────────────────────────────────────────────
|
|
38
|
+
it('flee returns force away from threat', () => {
|
|
39
|
+
const force = SteeringBehaviors.flee(agent(), { x: 10, y: 0, z: 0 });
|
|
40
|
+
expect(force.x).toBeLessThan(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ─── Arrive ───────────────────────────────────────────────────────
|
|
44
|
+
it('arrive force decreases near target', () => {
|
|
45
|
+
const farForce = SteeringBehaviors.arrive(agent(0, 0, 0), { x: 10, y: 0, z: 0 }, 5);
|
|
46
|
+
const nearForce = SteeringBehaviors.arrive(agent(9, 0, 0), { x: 10, y: 0, z: 0 }, 5);
|
|
47
|
+
expect(len(nearForce)).toBeLessThanOrEqual(len(farForce) + 0.01);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('arrive returns zero at target', () => {
|
|
51
|
+
const force = SteeringBehaviors.arrive(agent(10, 0, 0), { x: 10, y: 0, z: 0 }, 5);
|
|
52
|
+
expect(len(force)).toBeCloseTo(0, 2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ─── Wander ───────────────────────────────────────────────────────
|
|
56
|
+
it('wander returns force and new angle', () => {
|
|
57
|
+
const result = SteeringBehaviors.wander(agent(0, 0, 0, 1, 0, 0), 5, 2, 0.5, 0);
|
|
58
|
+
expect(result.force).toBeDefined();
|
|
59
|
+
expect(typeof result.newAngle).toBe('number');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── Flock ────────────────────────────────────────────────────────
|
|
63
|
+
it('flock combines separation/alignment/cohesion', () => {
|
|
64
|
+
const a = agent(0, 0, 0, 1, 0, 0);
|
|
65
|
+
const neighbors = [agent(2, 0, 0, 1, 0, 0), agent(-2, 0, 0, 1, 0, 0)];
|
|
66
|
+
const config = {
|
|
67
|
+
separationWeight: 1,
|
|
68
|
+
alignmentWeight: 1,
|
|
69
|
+
cohesionWeight: 1,
|
|
70
|
+
neighborRadius: 10,
|
|
71
|
+
};
|
|
72
|
+
const force = SteeringBehaviors.flock(a, neighbors, config);
|
|
73
|
+
expect(typeof force.x).toBe('number');
|
|
74
|
+
expect(typeof force.y).toBe('number');
|
|
75
|
+
expect(typeof force.z).toBe('number');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('flock returns zero with no neighbors in range', () => {
|
|
79
|
+
const a = agent(0, 0, 0);
|
|
80
|
+
const neighbors = [agent(1000, 0, 0)]; // way out of range
|
|
81
|
+
const config = {
|
|
82
|
+
separationWeight: 1,
|
|
83
|
+
alignmentWeight: 1,
|
|
84
|
+
cohesionWeight: 1,
|
|
85
|
+
neighborRadius: 5,
|
|
86
|
+
};
|
|
87
|
+
const force = SteeringBehaviors.flock(a, neighbors, config);
|
|
88
|
+
expect(force.x).toBe(0);
|
|
89
|
+
expect(force.y).toBe(0);
|
|
90
|
+
expect(force.z).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('flock excludes self from neighbors', () => {
|
|
94
|
+
const a = agent(0, 0, 0);
|
|
95
|
+
const config = {
|
|
96
|
+
separationWeight: 1,
|
|
97
|
+
alignmentWeight: 1,
|
|
98
|
+
cohesionWeight: 1,
|
|
99
|
+
neighborRadius: 10,
|
|
100
|
+
};
|
|
101
|
+
const force = SteeringBehaviors.flock(a, [a], config); // self is neighbor
|
|
102
|
+
expect(force.x).toBe(0); // should be zero since self filtered out
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ─── Obstacle Avoidance ───────────────────────────────────────────
|
|
106
|
+
it('obstacleAvoidance returns zero when no obstacles near', () => {
|
|
107
|
+
const a = agent(0, 0, 0, 1, 0, 0);
|
|
108
|
+
const obstacles = [{ center: { x: 100, y: 0, z: 0 }, radius: 1 }];
|
|
109
|
+
const force = SteeringBehaviors.obstacleAvoidance(a, obstacles, 5);
|
|
110
|
+
expect(len(force)).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('obstacleAvoidance returns force near obstacle', () => {
|
|
114
|
+
const a = agent(0, 0, 0, 1, 0, 0);
|
|
115
|
+
const obstacles = [{ center: { x: 3, y: 0, z: 0 }, radius: 10 }]; // huge obstacle right ahead
|
|
116
|
+
const force = SteeringBehaviors.obstacleAvoidance(a, obstacles, 5);
|
|
117
|
+
expect(len(force)).toBeGreaterThan(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ─── Apply Force ──────────────────────────────────────────────────
|
|
121
|
+
it('applyForce updates position and velocity', () => {
|
|
122
|
+
const a = agent(0, 0, 0, 0, 0, 0);
|
|
123
|
+
SteeringBehaviors.applyForce(a, { x: 10, y: 0, z: 0 }, 1);
|
|
124
|
+
expect(a.velocity.x).toBeGreaterThan(0);
|
|
125
|
+
expect(a.position.x).toBeGreaterThan(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('applyForce caps velocity to maxSpeed', () => {
|
|
129
|
+
const a = agent(0, 0, 0, 0, 0, 0);
|
|
130
|
+
SteeringBehaviors.applyForce(a, { x: 1000, y: 0, z: 0 }, 1);
|
|
131
|
+
expect(len(a.velocity)).toBeLessThanOrEqual(a.maxSpeed + 0.01);
|
|
132
|
+
});
|
|
133
|
+
});
|