feat: add rule engine & tic tac toe test
This commit is contained in:
@@ -35,10 +35,10 @@ describe('CommandParser', () => {
|
||||
|
||||
it('should parse command with flag', () => {
|
||||
const result = parser.parse('shuffle discard --seed=2026');
|
||||
|
||||
|
||||
expect(result.commandName).toBe('shuffle');
|
||||
expect(result.args.positional).toEqual(['discard']);
|
||||
expect(result.args.flags).toEqual({ seed: 2026 });
|
||||
expect(result.args.flags).toEqual({ seed: '2026' });
|
||||
});
|
||||
|
||||
it('should parse command with multiple flags', () => {
|
||||
@@ -67,10 +67,10 @@ describe('CommandParser', () => {
|
||||
|
||||
it('should parse command with short flag and value', () => {
|
||||
const result = parser.parse('shuffle d1 --seed=2026');
|
||||
|
||||
|
||||
expect(result.commandName).toBe('shuffle');
|
||||
expect(result.args.positional).toEqual(['d1']);
|
||||
expect(result.args.flags).toEqual({ seed: 2026 });
|
||||
expect(result.args.flags).toEqual({ seed: '2026' });
|
||||
});
|
||||
|
||||
it('should parse command with string number value', () => {
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createGameState } from '../../src/core/GameState';
|
||||
import { RuleEngine } from '../../src/rules/RuleEngine';
|
||||
import { RegionType } from '../../src/core/Region';
|
||||
import type { Player, TicTacToeMetadata } from '../../src/games/tictactoe/TicTacToeState';
|
||||
import {
|
||||
getCellId,
|
||||
getAllCellIds,
|
||||
getWinningCombinations,
|
||||
} from '../../src/games/tictactoe/TicTacToeState';
|
||||
import {
|
||||
ticTacToeRules,
|
||||
startGameCommand,
|
||||
createMarkCellCommand,
|
||||
resetGameCommand,
|
||||
} from '../../src/games/tictactoe';
|
||||
|
||||
describe('Tic Tac Toe', () => {
|
||||
let gameState: ReturnType<typeof createGameState>;
|
||||
let ruleEngine: RuleEngine;
|
||||
|
||||
beforeEach(async () => {
|
||||
gameState = createGameState({
|
||||
id: 'tictactoe-game',
|
||||
name: 'Tic Tac Toe',
|
||||
metadata: {
|
||||
ticTacToe: {
|
||||
currentPlayer: 'X' as Player,
|
||||
gameEnded: false,
|
||||
winner: null,
|
||||
moveHistory: [],
|
||||
totalMoves: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleEngine = new RuleEngine(gameState, { gameType: 'tictactoe' });
|
||||
ruleEngine.registerRules(ticTacToeRules);
|
||||
|
||||
// Start the game
|
||||
await ruleEngine.executeCommand(startGameCommand);
|
||||
});
|
||||
|
||||
describe('game initialization', () => {
|
||||
it('should create the board region', () => {
|
||||
const board = gameState.getRegion('board');
|
||||
expect(board).toBeDefined();
|
||||
expect(board?.type).toBe(RegionType.Keyed);
|
||||
});
|
||||
|
||||
it('should initialize all cells', () => {
|
||||
const cellIds = getAllCellIds(3);
|
||||
expect(cellIds.length).toBe(9);
|
||||
|
||||
const board = gameState.getRegion('board');
|
||||
expect(board).toBeDefined();
|
||||
});
|
||||
|
||||
it('should set initial game state', () => {
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.currentPlayer).toBe('X');
|
||||
expect(metadata.gameEnded).toBe(false);
|
||||
expect(metadata.winner).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('marking cells', () => {
|
||||
it('should allow player X to mark an empty cell', async () => {
|
||||
const command = createMarkCellCommand('cell-A1', 'X');
|
||||
const result = await ruleEngine.executeCommand(command);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const placement = gameState.getPlacement('cell-A1');
|
||||
expect(placement).toBeDefined();
|
||||
expect(placement?.metadata?.player).toBe('X');
|
||||
});
|
||||
|
||||
it('should switch to player O after X moves', async () => {
|
||||
const command = createMarkCellCommand('cell-A1', 'X');
|
||||
await ruleEngine.executeCommand(command);
|
||||
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.currentPlayer).toBe('O');
|
||||
});
|
||||
|
||||
it('should record move history', async () => {
|
||||
const command = createMarkCellCommand('cell-A1', 'X');
|
||||
await ruleEngine.executeCommand(command);
|
||||
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.moveHistory.length).toBe(1);
|
||||
expect(metadata.moveHistory[0].player).toBe('X');
|
||||
expect(metadata.moveHistory[0].cellId).toBe('cell-A1');
|
||||
});
|
||||
|
||||
it('should not allow marking an occupied cell', async () => {
|
||||
const command1 = createMarkCellCommand('cell-A1', 'X');
|
||||
await ruleEngine.executeCommand(command1);
|
||||
|
||||
const command2 = createMarkCellCommand('cell-A1', 'O');
|
||||
const result = await ruleEngine.executeCommand(command2);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('already occupied');
|
||||
});
|
||||
|
||||
it('should not allow wrong player to move', async () => {
|
||||
// Try to place O when it's X's turn
|
||||
const command = createMarkCellCommand('cell-A1', 'O');
|
||||
const result = await ruleEngine.executeCommand(command);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("It is X's turn");
|
||||
});
|
||||
|
||||
it('should not allow moves after game ends', async () => {
|
||||
// Set up a winning scenario
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A3', 'X'));
|
||||
|
||||
// Game should end with X winning
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.gameEnded).toBe(true);
|
||||
expect(metadata.winner).toBe('X');
|
||||
|
||||
// Try to make another move
|
||||
const command = createMarkCellCommand('cell-C1', 'O');
|
||||
const result = await ruleEngine.executeCommand(command);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Game has already ended');
|
||||
});
|
||||
});
|
||||
|
||||
describe('win conditions', () => {
|
||||
it('should detect horizontal win for X', async () => {
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A3', 'X'));
|
||||
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.gameEnded).toBe(true);
|
||||
expect(metadata.winner).toBe('X');
|
||||
expect(metadata.winningCombination).toEqual(['cell-A1', 'cell-A2', 'cell-A3']);
|
||||
});
|
||||
|
||||
it('should detect horizontal win for O', async () => {
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-C2', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B3', 'O'));
|
||||
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.gameEnded).toBe(true);
|
||||
expect(metadata.winner).toBe('O');
|
||||
expect(metadata.winningCombination).toEqual(['cell-B1', 'cell-B2', 'cell-B3']);
|
||||
});
|
||||
|
||||
it('should detect vertical win', async () => {
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-C2', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-C3', 'O'));
|
||||
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.gameEnded).toBe(true);
|
||||
expect(metadata.winner).toBe('O');
|
||||
expect(metadata.winningCombination).toEqual(['cell-C1', 'cell-C2', 'cell-C3']);
|
||||
});
|
||||
|
||||
it('should detect main diagonal win', async () => {
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-C3', 'X'));
|
||||
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.gameEnded).toBe(true);
|
||||
expect(metadata.winner).toBe('X');
|
||||
expect(metadata.winningCombination).toEqual(['cell-A1', 'cell-B2', 'cell-C3']);
|
||||
});
|
||||
|
||||
it('should detect anti-diagonal win', async () => {
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A3', 'X'));
|
||||
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.gameEnded).toBe(true);
|
||||
expect(metadata.winner).toBe('X');
|
||||
expect(metadata.winningCombination).toEqual(['cell-A3', 'cell-B2', 'cell-C1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('draw condition', () => {
|
||||
it('should detect a draw when all cells are filled without winner', async () => {
|
||||
// Fill the board with no winner
|
||||
const moves = [
|
||||
['cell-A1', 'X'],
|
||||
['cell-A2', 'O'],
|
||||
['cell-A3', 'X'],
|
||||
['cell-B1', 'O'],
|
||||
['cell-B3', 'X'],
|
||||
['cell-B2', 'O'],
|
||||
['cell-C2', 'X'],
|
||||
['cell-C1', 'O'],
|
||||
['cell-C3', 'X'],
|
||||
] as [string, Player][];
|
||||
|
||||
for (const [cell, player] of moves) {
|
||||
const command = createMarkCellCommand(cell, player);
|
||||
await ruleEngine.executeCommand(command);
|
||||
}
|
||||
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.gameEnded).toBe(true);
|
||||
expect(metadata.winner).toBe(null); // Draw
|
||||
expect(metadata.totalMoves).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset game', () => {
|
||||
it('should reset the board for a new game', async () => {
|
||||
// Make some moves
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
||||
|
||||
// Reset
|
||||
await ruleEngine.executeCommand(resetGameCommand);
|
||||
|
||||
// Check that cells are empty
|
||||
const board = gameState.getRegion('board');
|
||||
expect(board).toBeDefined();
|
||||
|
||||
// Game should be reset
|
||||
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
||||
expect(metadata.currentPlayer).toBe('X');
|
||||
expect(metadata.gameEnded).toBe(false);
|
||||
expect(metadata.winner).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rule engine integration', () => {
|
||||
it('should execute all rules in correct order', async () => {
|
||||
const command = createMarkCellCommand('cell-B2', 'X');
|
||||
const result = await ruleEngine.executeCommand(command);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.validationRules.length).toBeGreaterThan(0);
|
||||
expect(result.effectRules.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that validation rules ran
|
||||
const validationRuleIds = result.validationRules.map((r) => r.ruleId);
|
||||
expect(validationRuleIds).toContain('tictactoe-validate-turn');
|
||||
expect(validationRuleIds).toContain('tictactoe-validate-cell-empty');
|
||||
expect(validationRuleIds).toContain('tictactoe-validate-game-not-ended');
|
||||
|
||||
// Check that effect rules ran
|
||||
const effectRuleIds = result.effectRules.map((r) => r.ruleId);
|
||||
expect(effectRuleIds).toContain('tictactoe-switch-turn');
|
||||
expect(effectRuleIds).toContain('tictactoe-record-history');
|
||||
});
|
||||
|
||||
it('should trigger win condition check after each move', async () => {
|
||||
// Set up a winning scenario
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'X'));
|
||||
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O'));
|
||||
|
||||
const winningMove = createMarkCellCommand('cell-A3', 'X');
|
||||
const result = await ruleEngine.executeCommand(winningMove);
|
||||
|
||||
// Check that trigger rules ran
|
||||
const triggerRuleIds = result.triggerRules.map((r) => r.ruleId);
|
||||
expect(triggerRuleIds).toContain('tictactoe-check-win');
|
||||
});
|
||||
});
|
||||
|
||||
describe('helper functions', () => {
|
||||
it('should generate correct cell IDs', () => {
|
||||
expect(getCellId(0, 0)).toBe('cell-A1');
|
||||
expect(getCellId(1, 1)).toBe('cell-B2');
|
||||
expect(getCellId(2, 2)).toBe('cell-C3');
|
||||
});
|
||||
|
||||
it('should return correct winning combinations', () => {
|
||||
const combinations = getWinningCombinations(3);
|
||||
expect(combinations.length).toBe(8); // 3 rows + 3 columns + 2 diagonals
|
||||
|
||||
// Check rows
|
||||
expect(combinations[0]).toEqual(['cell-A1', 'cell-A2', 'cell-A3']);
|
||||
expect(combinations[1]).toEqual(['cell-B1', 'cell-B2', 'cell-B3']);
|
||||
expect(combinations[2]).toEqual(['cell-C1', 'cell-C2', 'cell-C3']);
|
||||
|
||||
// Check columns
|
||||
expect(combinations[3]).toEqual(['cell-A1', 'cell-B1', 'cell-C1']);
|
||||
expect(combinations[4]).toEqual(['cell-A2', 'cell-B2', 'cell-C2']);
|
||||
expect(combinations[5]).toEqual(['cell-A3', 'cell-B3', 'cell-C3']);
|
||||
|
||||
// Check diagonals
|
||||
expect(combinations[6]).toEqual(['cell-A1', 'cell-B2', 'cell-C3']);
|
||||
expect(combinations[7]).toEqual(['cell-A3', 'cell-B2', 'cell-C1']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,595 @@
|
||||
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<typeof createGameState>;
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user