import { describe, it, expect, beforeEach } from 'vitest'; import { createGameState } from '../../src/core/GameState'; import { RuleEngine } from '../../src/rules/RuleEngine'; import { createValidationRule, createEffectRule, createTriggerRule } from '../../src/rules/Rule'; import type { RuleResult, RuleContext } from '../../src/rules/Rule'; import { Command, CommandActionType } from '../../src/commands/Command'; import { RegionType } from '../../src/core/Region'; describe('RuleEngine', () => { let gameState: ReturnType; let ruleEngine: RuleEngine; beforeEach(() => { gameState = createGameState({ id: 'test-game', name: 'Test Game' }); ruleEngine = new RuleEngine(gameState); }); describe('registerRule', () => { it('should register a validation rule', () => { const rule = createValidationRule({ id: 'test-validation', name: 'Test Validation', priority: 1, validate: async () => ({ success: true }), }); ruleEngine.registerRule(rule); const rules = ruleEngine.getRules(); expect(rules.length).toBe(1); expect(rules[0].id).toBe('test-validation'); }); it('should register an effect rule', () => { const rule = createEffectRule({ id: 'test-effect', name: 'Test Effect', priority: 1, apply: async () => ({ success: true }), }); ruleEngine.registerRule(rule); const rules = ruleEngine.getRules(); expect(rules.length).toBe(1); expect(rules[0].id).toBe('test-effect'); }); it('should register a trigger rule', () => { const rule = createTriggerRule({ id: 'test-trigger', name: 'Test Trigger', priority: 1, condition: async () => true, action: async () => ({ success: true }), }); ruleEngine.registerRule(rule); const rules = ruleEngine.getRules(); expect(rules.length).toBe(1); expect(rules[0].id).toBe('test-trigger'); }); it('should sort rules by priority', () => { const rule1 = createValidationRule({ id: 'rule-1', name: 'Rule 1', priority: 3, validate: async () => ({ success: true }), }); const rule2 = createValidationRule({ id: 'rule-2', name: 'Rule 2', priority: 1, validate: async () => ({ success: true }), }); const rule3 = createValidationRule({ id: 'rule-3', name: 'Rule 3', priority: 2, validate: async () => ({ success: true }), }); ruleEngine.registerRules([rule1, rule2, rule3]); const rules = ruleEngine.getRules(); expect(rules[0].id).toBe('rule-2'); expect(rules[1].id).toBe('rule-3'); expect(rules[2].id).toBe('rule-1'); }); }); describe('executeCommand with validation rules', () => { it('should execute command when all validation rules pass', async () => { ruleEngine.registerRule( createValidationRule({ id: 'always-pass', name: 'Always Pass', priority: 1, validate: async () => ({ success: true }), }) ); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; const result = await ruleEngine.executeCommand(command); expect(result.success).toBe(true); expect(gameState.getPart('meeple-1')).toBeDefined(); }); it('should block command when validation rule fails', async () => { ruleEngine.registerRule( createValidationRule({ id: 'always-fail', name: 'Always Fail', priority: 1, validate: async () => ({ success: false, error: 'Validation failed', }), }) ); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; const result = await ruleEngine.executeCommand(command); expect(result.success).toBe(false); expect(result.error).toBe('Validation failed'); expect(gameState.getPart('meeple-1')).toBeUndefined(); }); it('should block command when rule sets blockCommand', async () => { ruleEngine.registerRule( createValidationRule({ id: 'block-command', name: 'Block Command', priority: 1, validate: async () => ({ success: true, blockCommand: true, }), }) ); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; const result = await ruleEngine.executeCommand(command); expect(result.success).toBe(false); expect(result.error).toContain('blocked by rule'); }); it('should apply state updates from validation rules', async () => { ruleEngine.registerRule( createValidationRule({ id: 'set-metadata', name: 'Set Metadata', priority: 1, validate: async (context) => ({ success: true, stateUpdates: { validated: true }, }), }) ); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; const result = await ruleEngine.executeCommand(command); expect(result.success).toBe(true); }); }); describe('executeCommand with effect rules', () => { it('should execute effect rules after command', async () => { let effectExecuted = false; ruleEngine.registerRules([ createValidationRule({ id: 'validation', name: 'Validation', priority: 1, validate: async () => ({ success: true }), }), createEffectRule({ id: 'effect', name: 'Effect', priority: 1, apply: async () => { effectExecuted = true; return { success: true }; }, }), ]); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; await ruleEngine.executeCommand(command); expect(effectExecuted).toBe(true); }); it('should apply state updates from effect rules', async () => { ruleEngine.registerRule( createEffectRule({ id: 'update-metadata', name: 'Update Metadata', priority: 1, apply: async () => ({ success: true, stateUpdates: { effectApplied: true }, }), }) ); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; await ruleEngine.executeCommand(command); // Effect rule state updates are stored in metadata expect(gameState.data.value.metadata).toBeDefined(); }); }); describe('executeCommand with trigger rules', () => { it('should execute trigger rules when condition is met', async () => { let triggerExecuted = false; ruleEngine.registerRule( createTriggerRule({ id: 'trigger', name: 'Trigger', priority: 1, condition: async () => true, action: async () => { triggerExecuted = true; return { success: true }; }, }) ); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; await ruleEngine.executeCommand(command); expect(triggerExecuted).toBe(true); }); it('should not execute trigger rules when condition is not met', async () => { let triggerExecuted = false; ruleEngine.registerRule( createTriggerRule({ id: 'trigger', name: 'Trigger', priority: 1, condition: async () => false, action: async () => { triggerExecuted = true; return { success: true }; }, }) ); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; await ruleEngine.executeCommand(command); expect(triggerExecuted).toBe(false); }); it('should trigger commands from trigger rules', async () => { ruleEngine.registerRule( createTriggerRule({ id: 'trigger-command', name: 'Trigger Command', priority: 1, condition: async () => true, action: async () => ({ success: true, triggeredCommands: [ { id: 'triggered', name: 'Triggered Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'triggered-meeple', color: 'blue' }, }, ], }, ], }), }) ); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; const result = await ruleEngine.executeCommand(command); expect(result.triggeredCommands.length).toBe(1); expect(gameState.getPart('triggered-meeple')).toBeDefined(); }); }); describe('rule logging', () => { it('should log rule executions', async () => { ruleEngine.registerRule( createValidationRule({ id: 'logged-rule', name: 'Logged Rule', priority: 1, validate: async () => ({ success: true }), }) ); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; await ruleEngine.executeCommand(command); const logs = ruleEngine.getLogs(); expect(logs.length).toBe(1); expect(logs[0].ruleId).toBe('logged-rule'); expect(logs[0].ruleType).toBe('validation'); }); it('should clear logs', async () => { ruleEngine.registerRule( createValidationRule({ id: 'logged-rule', name: 'Logged Rule', priority: 1, validate: async () => ({ success: true }), }) ); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; await ruleEngine.executeCommand(command); expect(ruleEngine.getLogs().length).toBe(1); ruleEngine.clearLogs(); expect(ruleEngine.getLogs().length).toBe(0); }); }); describe('game type filtering', () => { it('should only apply rules matching the game type', async () => { const gameTypeRuleEngine = new RuleEngine(gameState, { gameType: 'tictactoe' }); let tictactoeRuleExecuted = false; let otherRuleExecuted = false; gameTypeRuleEngine.registerRules([ createValidationRule({ id: 'tictactoe-rule', name: 'Tic Tac Toe Rule', priority: 1, gameType: 'tictactoe', validate: async () => { tictactoeRuleExecuted = true; return { success: true }; }, }), createValidationRule({ id: 'other-rule', name: 'Other Rule', priority: 1, gameType: 'chess', validate: async () => { otherRuleExecuted = true; return { success: true }; }, }), ]); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; await gameTypeRuleEngine.executeCommand(command); expect(tictactoeRuleExecuted).toBe(true); expect(otherRuleExecuted).toBe(false); }); it('should apply rules without game type to all games', async () => { const gameTypeRuleEngine = new RuleEngine(gameState, { gameType: 'tictactoe' }); let globalRuleExecuted = false; gameTypeRuleEngine.registerRules([ createValidationRule({ id: 'global-rule', name: 'Global Rule', priority: 1, validate: async () => { globalRuleExecuted = true; return { success: true }; }, }), ]); const command: Command = { id: 'test-command', name: 'Test Command', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; await gameTypeRuleEngine.executeCommand(command); expect(globalRuleExecuted).toBe(true); }); }); describe('command filtering', () => { it('should only apply rules to applicable commands', async () => { let ruleExecuted = false; ruleEngine.registerRule( createValidationRule({ id: 'specific-command-rule', name: 'Specific Command Rule', priority: 1, applicableCommands: ['specificCommand'], validate: async () => { ruleExecuted = true; return { success: true }; }, }) ); const command: Command = { id: 'test-command', name: 'otherCommand', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; await ruleEngine.executeCommand(command); expect(ruleExecuted).toBe(false); }); it('should apply rules to matching commands', async () => { let ruleExecuted = false; ruleEngine.registerRule( createValidationRule({ id: 'specific-command-rule', name: 'Specific Command Rule', priority: 1, applicableCommands: ['testCommand'], validate: async () => { ruleExecuted = true; return { success: true }; }, }) ); const command: Command = { id: 'test-command', name: 'testCommand', steps: [ { action: CommandActionType.CreateMeeple, params: { id: 'meeple-1', color: 'red' }, }, ], }; await ruleEngine.executeCommand(command); expect(ruleExecuted).toBe(true); }); }); });