wifi-densepose/vendor/sublinear-time-solver/npx/goalie/test/goap-planner.test.ts

287 lines
8.8 KiB
TypeScript

/**
* GOAP Planner Tests
* Comprehensive testing for the GOAP planning system
*/
import { GoapPlanner } from '../src/goap/planner';
import { GoapAction, WorldState, GoapGoal, PlanningContext } from '../src/core/types';
describe('GoapPlanner', () => {
let planner: GoapPlanner;
beforeEach(() => {
planner = new GoapPlanner();
});
// Test actions for planning
const testActions: GoapAction[] = [
{
name: 'get_key',
cost: 1,
preconditions: [],
effects: [{ key: 'has_key', value: true, operation: 'set' }],
async execute(state: WorldState) {
return {
success: true,
newState: { ...state, has_key: true }
};
}
},
{
name: 'unlock_door',
cost: 2,
preconditions: [{ key: 'has_key', value: true, operator: 'equals' }],
effects: [{ key: 'door_unlocked', value: true, operation: 'set' }],
async execute(state: WorldState) {
if (!state.has_key) {
return {
success: false,
newState: state,
error: 'No key available'
};
}
return {
success: true,
newState: { ...state, door_unlocked: true }
};
}
},
{
name: 'enter_room',
cost: 1,
preconditions: [{ key: 'door_unlocked', value: true, operator: 'equals' }],
effects: [{ key: 'in_room', value: true, operation: 'set' }],
async execute(state: WorldState) {
if (!state.door_unlocked) {
return {
success: false,
newState: state,
error: 'Door is locked'
};
}
return {
success: true,
newState: { ...state, in_room: true }
};
}
}
];
describe('Plan Creation', () => {
test('should create a plan to achieve goal', async () => {
const initialState: WorldState = {};
const goal: GoapGoal = {
name: 'enter_room',
conditions: [{ key: 'in_room', value: true, operator: 'equals' }],
priority: 1
};
const context: PlanningContext = {
currentState: initialState,
goal,
availableActions: testActions
};
const plan = await planner.createPlan(context);
expect(plan).toBeTruthy();
expect(plan!.steps).toHaveLength(3);
expect(plan!.steps[0].action.name).toBe('get_key');
expect(plan!.steps[1].action.name).toBe('unlock_door');
expect(plan!.steps[2].action.name).toBe('enter_room');
expect(plan!.totalCost).toBe(4); // 1 + 2 + 1
});
test('should return null when no plan exists', async () => {
const initialState: WorldState = {};
const goal: GoapGoal = {
name: 'impossible_goal',
conditions: [{ key: 'impossible', value: true, operator: 'equals' }],
priority: 1
};
const context: PlanningContext = {
currentState: initialState,
goal,
availableActions: testActions
};
const plan = await planner.createPlan(context);
expect(plan).toBeNull();
});
test('should return empty plan when goal already satisfied', async () => {
const initialState: WorldState = { in_room: true };
const goal: GoapGoal = {
name: 'enter_room',
conditions: [{ key: 'in_room', value: true, operator: 'equals' }],
priority: 1
};
const context: PlanningContext = {
currentState: initialState,
goal,
availableActions: testActions
};
const plan = await planner.createPlan(context);
expect(plan).toBeTruthy();
expect(plan!.steps).toHaveLength(0);
expect(plan!.totalCost).toBe(0);
});
});
describe('Plan Execution', () => {
test('should execute plan successfully', async () => {
const initialState: WorldState = {};
const goal: GoapGoal = {
name: 'enter_room',
conditions: [{ key: 'in_room', value: true, operator: 'equals' }],
priority: 1
};
const context: PlanningContext = {
currentState: initialState,
goal,
availableActions: testActions
};
const plan = await planner.createPlan(context);
expect(plan).toBeTruthy();
const result = await planner.executePlan(plan!, testActions);
expect(result.success).toBe(true);
expect(result.executedSteps).toBe(3);
expect(result.finalState.in_room).toBe(true);
expect(result.finalState.has_key).toBe(true);
expect(result.finalState.door_unlocked).toBe(true);
});
test('should handle action failures with replanning', async () => {
// Create a failing action
const failingActions: GoapAction[] = [
{
name: 'get_key',
cost: 1,
preconditions: [],
effects: [{ key: 'has_key', value: true, operation: 'set' }],
async execute(state: WorldState) {
return {
success: false,
newState: state,
error: 'Key not found'
};
}
},
...testActions.slice(1) // Keep other actions
];
const initialState: WorldState = {};
const goal: GoapGoal = {
name: 'enter_room',
conditions: [{ key: 'in_room', value: true, operator: 'equals' }],
priority: 1
};
const context: PlanningContext = {
currentState: initialState,
goal,
availableActions: failingActions
};
const plan = await planner.createPlan(context);
expect(plan).toBeTruthy();
const result = await planner.executePlan(plan!, failingActions);
expect(result.success).toBe(false);
expect(result.error).toContain('Key not found');
});
});
describe('Precondition Evaluation', () => {
test('should evaluate different precondition operators', () => {
const state: WorldState = {
count: 5,
items: ['a', 'b', 'c'],
flag: true,
missing: undefined
};
// Test equals
expect(planner['evaluatePrecondition']({ key: 'flag', value: true, operator: 'equals' }, state)).toBe(true);
expect(planner['evaluatePrecondition']({ key: 'flag', value: false, operator: 'equals' }, state)).toBe(false);
// Test exists
expect(planner['evaluatePrecondition']({ key: 'count', value: null, operator: 'exists' }, state)).toBe(true);
expect(planner['evaluatePrecondition']({ key: 'missing', value: null, operator: 'exists' }, state)).toBe(false);
// Test not_exists
expect(planner['evaluatePrecondition']({ key: 'missing', value: null, operator: 'not_exists' }, state)).toBe(true);
expect(planner['evaluatePrecondition']({ key: 'count', value: null, operator: 'not_exists' }, state)).toBe(false);
// Test greater/less
expect(planner['evaluatePrecondition']({ key: 'count', value: 3, operator: 'greater' }, state)).toBe(true);
expect(planner['evaluatePrecondition']({ key: 'count', value: 10, operator: 'greater' }, state)).toBe(false);
expect(planner['evaluatePrecondition']({ key: 'count', value: 10, operator: 'less' }, state)).toBe(true);
// Test contains
expect(planner['evaluatePrecondition']({ key: 'items', value: 'b', operator: 'contains' }, state)).toBe(true);
expect(planner['evaluatePrecondition']({ key: 'items', value: 'x', operator: 'contains' }, state)).toBe(false);
});
});
describe('Effect Application', () => {
test('should apply different effect operations', () => {
const state: WorldState = {
count: 5,
items: ['a', 'b'],
flag: false
};
// Test set
planner['applyEffect']({ key: 'flag', value: true, operation: 'set' }, state);
expect(state.flag).toBe(true);
// Test add
planner['applyEffect']({ key: 'items', value: 'c', operation: 'add' }, state);
expect(state.items).toEqual(['a', 'b', 'c']);
// Test remove
planner['applyEffect']({ key: 'items', value: 'b', operation: 'remove' }, state);
expect(state.items).toEqual(['a', 'c']);
// Test increment
planner['applyEffect']({ key: 'count', value: 2, operation: 'increment' }, state);
expect(state.count).toBe(7);
// Test decrement
planner['applyEffect']({ key: 'count', value: 3, operation: 'decrement' }, state);
expect(state.count).toBe(4);
});
});
describe('Heuristic Calculation', () => {
test('should calculate heuristic distance to goal', () => {
const state: WorldState = {
has_key: true,
door_unlocked: false,
in_room: false
};
const goal: GoapGoal = {
name: 'test_goal',
conditions: [
{ key: 'door_unlocked', value: true, operator: 'equals' },
{ key: 'in_room', value: true, operator: 'equals' }
],
priority: 1
};
const heuristic = planner['calculateHeuristic'](state, goal);
expect(heuristic).toBe(2); // Two unsatisfied conditions
});
});
});