chore: clean up

This commit is contained in:
hyper
2026-04-01 17:08:08 +08:00
parent 6740584fc8
commit ea337acacb
37 changed files with 0 additions and 7381 deletions
-329
View File
@@ -1,329 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createGameState } from '../../src/core/GameState';
import { CommandExecutor } from '../../src/commands/CommandExecutor';
import { Command, CommandActionType } from '../../src/commands/Command';
import { RegionType } from '../../src/core/Region';
describe('CommandExecutor', () => {
let gameState: ReturnType<typeof createGameState>;
let executor: CommandExecutor;
beforeEach(() => {
gameState = createGameState({ id: 'test-game', name: 'Test Game' });
executor = new CommandExecutor(gameState);
});
describe('execute', () => {
it('should execute a simple command successfully', () => {
const command: Command = {
id: 'test-command',
name: 'Test Command',
steps: [
{
action: CommandActionType.CreateMeeple,
params: { id: 'meeple-1', color: 'red' },
},
],
};
const result = executor.execute(command);
expect(result.success).toBe(true);
expect(result.executedSteps).toBe(1);
expect(result.totalSteps).toBe(1);
expect(gameState.getPart('meeple-1')).toBeDefined();
});
it('should execute multi-step command', () => {
const command: Command = {
id: 'setup-command',
name: 'Setup Command',
steps: [
{
action: CommandActionType.CreateRegion,
params: { id: 'board', type: RegionType.Keyed },
},
{
action: CommandActionType.CreateMeeple,
params: { id: 'meeple-1', color: 'blue' },
},
{
action: CommandActionType.CreatePlacement,
params: {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
},
},
],
};
const result = executor.execute(command);
expect(result.success).toBe(true);
expect(result.executedSteps).toBe(3);
expect(result.totalSteps).toBe(3);
expect(gameState.getRegion('board')).toBeDefined();
expect(gameState.getPart('meeple-1')).toBeDefined();
expect(gameState.getPlacement('placement-1')).toBeDefined();
});
it('should stop execution on error', () => {
const command: Command = {
id: 'failing-command',
name: 'Failing Command',
steps: [
{
action: CommandActionType.CreateMeeple,
params: { id: 'meeple-1', color: 'red' },
},
{
action: CommandActionType.CreatePlacement,
params: {
id: 'placement-1',
partId: 'non-existent',
regionId: 'non-existent',
},
},
{
action: CommandActionType.CreateMeeple,
params: { id: 'meeple-2', color: 'blue' },
},
],
};
const result = executor.execute(command);
expect(result.success).toBe(false);
expect(result.executedSteps).toBe(1);
expect(result.totalSteps).toBe(3);
expect(result.error).toBeDefined();
expect(gameState.getPart('meeple-1')).toBeDefined();
expect(gameState.getPart('meeple-2')).toBeUndefined();
});
it('should execute createCard command', () => {
const command: Command = {
id: 'create-card',
name: 'Create Card',
steps: [
{
action: CommandActionType.CreateCard,
params: { id: 'card-1', suit: 'hearts', value: 10 },
},
],
};
const result = executor.execute(command);
expect(result.success).toBe(true);
const card = gameState.getPart('card-1');
expect(card).toBeDefined();
expect(card?.type).toBe('card');
});
it('should execute createTile command', () => {
const command: Command = {
id: 'create-tile',
name: 'Create Tile',
steps: [
{
action: CommandActionType.CreateTile,
params: { id: 'tile-1', pattern: 'forest', rotation: 90 },
},
],
};
const result = executor.execute(command);
expect(result.success).toBe(true);
const tile = gameState.getPart('tile-1');
expect(tile).toBeDefined();
expect(tile?.type).toBe('tile');
});
it('should execute movePlacement command', () => {
const setupCommand: Command = {
id: 'setup',
name: 'Setup',
steps: [
{ action: CommandActionType.CreateRegion, params: { id: 'board1', type: RegionType.Unkeyed } },
{ action: CommandActionType.CreateRegion, params: { id: 'board2', type: RegionType.Unkeyed } },
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
{ action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board1' } },
],
};
executor.execute(setupCommand);
const moveCommand: Command = {
id: 'move',
name: 'Move',
steps: [
{
action: CommandActionType.MovePlacement,
params: { placementId: 'p1', targetRegionId: 'board2' },
},
],
};
const result = executor.execute(moveCommand);
expect(result.success).toBe(true);
const placement = gameState.getPlacement('p1');
expect(placement?.regionId).toBe('board2');
});
it('should execute flipPlacement command', () => {
const setupCommand: Command = {
id: 'setup',
name: 'Setup',
steps: [
{ action: CommandActionType.CreateRegion, params: { id: 'board', type: RegionType.Unkeyed } },
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
{ action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board', faceUp: true } },
],
};
executor.execute(setupCommand);
const flipCommand: Command = {
id: 'flip',
name: 'Flip',
steps: [
{ action: CommandActionType.FlipPlacement, params: { placementId: 'p1' } },
],
};
const result = executor.execute(flipCommand);
expect(result.success).toBe(true);
const placement = gameState.getPlacement('p1');
expect(placement?.faceUp).toBe(false);
});
it('should execute setPhase command', () => {
const command: Command = {
id: 'set-phase',
name: 'Set Phase',
steps: [
{ action: CommandActionType.SetPhase, params: { phase: 'midgame' } },
],
};
const result = executor.execute(command);
expect(result.success).toBe(true);
expect(gameState.data.value.phase).toBe('midgame');
});
it('should execute swapPlacements command', () => {
const setupCommand: Command = {
id: 'setup',
name: 'Setup',
steps: [
{ action: CommandActionType.CreateRegion, params: { id: 'board', type: RegionType.Unkeyed } },
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
{ action: CommandActionType.CreateMeeple, params: { id: 'm2', color: 'blue' } },
{ action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board' } },
{ action: CommandActionType.CreatePlacement, params: { id: 'p2', partId: 'm2', regionId: 'board' } },
],
};
executor.execute(setupCommand);
const region = gameState.getRegion('board');
region!.placements.value = ['p1', 'p2'];
const swapCommand: Command = {
id: 'swap',
name: 'Swap',
steps: [
{ action: CommandActionType.SwapPlacements, params: { placementId1: 'p1', placementId2: 'p2' } },
],
};
const result = executor.execute(swapCommand);
expect(result.success).toBe(true);
expect(region!.placements.value).toEqual(['p2', 'p1']);
});
it('should execute clearRegion command', () => {
const setupCommand: Command = {
id: 'setup',
name: 'Setup',
steps: [
{ action: CommandActionType.CreateRegion, params: { id: 'board', type: RegionType.Unkeyed } },
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
{ action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board' } },
{ action: CommandActionType.AddPlacementToRegion, params: { regionId: 'board', placementId: 'p1' } },
],
};
executor.execute(setupCommand);
const clearCommand: Command = {
id: 'clear',
name: 'Clear',
steps: [
{ action: CommandActionType.ClearRegion, params: { regionId: 'board' } },
],
};
const result = executor.execute(clearCommand);
expect(result.success).toBe(true);
const region = gameState.getRegion('board');
expect(region?.placements.value.length).toBe(0);
});
it('should execute updatePart command', () => {
const setupCommand: Command = {
id: 'setup',
name: 'Setup',
steps: [
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red', name: 'Original' } },
],
};
executor.execute(setupCommand);
const updateCommand: Command = {
id: 'update',
name: 'Update',
steps: [
{ action: CommandActionType.UpdatePart, params: { partId: 'm1', updates: { name: 'Updated', color: 'blue' } } },
],
};
const result = executor.execute(updateCommand);
expect(result.success).toBe(true);
const part = gameState.getPart('m1');
expect(part?.name).toBe('Updated');
expect(part?.color).toBe('blue');
});
it('should execute removePart command', () => {
const setupCommand: Command = {
id: 'setup',
name: 'Setup',
steps: [
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
],
};
executor.execute(setupCommand);
expect(gameState.getPart('m1')).toBeDefined();
const removeCommand: Command = {
id: 'remove',
name: 'Remove',
steps: [
{ action: CommandActionType.RemovePart, params: { partId: 'm1' } },
],
};
const result = executor.execute(removeCommand);
expect(result.success).toBe(true);
expect(gameState.getPart('m1')).toBeUndefined();
});
});
});
-278
View File
@@ -1,278 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { CommandLog, createCommandLog } from '../../src/commands/CommandLog';
import { Command, CommandActionType, CommandExecutionResult, StepResult } from '../../src/commands/Command';
describe('CommandLog', () => {
let log: CommandLog;
beforeEach(() => {
log = createCommandLog();
});
const sampleCommand: Command = {
id: 'test-command',
name: 'Test Command',
steps: [
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
],
};
const sampleResult: CommandExecutionResult = {
success: true,
executedSteps: 1,
totalSteps: 1,
};
const sampleStepResults: StepResult[] = [
{
stepIndex: 0,
action: CommandActionType.CreateMeeple,
success: true,
params: { id: 'm1', color: 'red' },
},
];
describe('log', () => {
it('should log a command execution', () => {
log.log(sampleCommand, sampleResult, sampleStepResults);
const entries = log.getEntries();
expect(entries.length).toBe(1);
expect(entries[0].commandId).toBe('test-command');
expect(entries[0].commandName).toBe('Test Command');
expect(entries[0].result.success).toBe(true);
});
it('should add timestamp to log entry', () => {
const beforeTime = Date.now();
log.log(sampleCommand, sampleResult, sampleStepResults);
const afterTime = Date.now();
const entry = log.getEntries()[0];
expect(entry.timestamp).toBeGreaterThanOrEqual(beforeTime);
expect(entry.timestamp).toBeLessThanOrEqual(afterTime);
});
it('should log multiple entries', () => {
log.log(sampleCommand, sampleResult, sampleStepResults);
log.log(sampleCommand, sampleResult, sampleStepResults);
log.log(sampleCommand, sampleResult, sampleStepResults);
expect(log.getEntries().length).toBe(3);
});
});
describe('getFilteredEntries', () => {
it('should filter by commandId', () => {
const command1: Command = { ...sampleCommand, id: 'cmd-1', name: 'Command 1' };
const command2: Command = { ...sampleCommand, id: 'cmd-2', name: 'Command 2' };
log.log(command1, sampleResult, sampleStepResults);
log.log(command2, sampleResult, sampleStepResults);
log.log(command1, sampleResult, sampleStepResults);
const filtered = log.getFilteredEntries({ commandId: 'cmd-1' });
expect(filtered.length).toBe(2);
});
it('should filter by success status', () => {
const successResult: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 };
const failResult: CommandExecutionResult = { success: false, executedSteps: 0, totalSteps: 1, error: 'Failed' };
log.log(sampleCommand, successResult, sampleStepResults);
log.log(sampleCommand, failResult, []);
log.log(sampleCommand, successResult, sampleStepResults);
const failed = log.getFilteredEntries({ success: false });
expect(failed.length).toBe(1);
const successful = log.getFilteredEntries({ success: true });
expect(successful.length).toBe(2);
});
it('should filter by time range', () => {
const startTime = Date.now();
log.log(sampleCommand, sampleResult, sampleStepResults);
// 模拟时间流逝
const midTime = Date.now() + 100;
// 手动创建一个带时间戳的条目来测试时间过滤
const entries = log.getEntries();
expect(entries[0].timestamp).toBeGreaterThanOrEqual(startTime);
});
});
describe('getCommandHistory', () => {
it('should return history for a specific command', () => {
const command1: Command = { ...sampleCommand, id: 'cmd-1', name: 'Command 1' };
const command2: Command = { ...sampleCommand, id: 'cmd-2', name: 'Command 2' };
log.log(command1, sampleResult, sampleStepResults);
log.log(command2, sampleResult, sampleStepResults);
log.log(command1, sampleResult, sampleStepResults);
const history = log.getCommandHistory('cmd-1');
expect(history.length).toBe(2);
});
});
describe('getFailedCommands', () => {
it('should return only failed commands', () => {
const successResult: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 };
const failResult: CommandExecutionResult = { success: false, executedSteps: 0, totalSteps: 1, error: 'Error' };
log.log(sampleCommand, successResult, sampleStepResults);
log.log(sampleCommand, failResult, []);
log.log(sampleCommand, successResult, sampleStepResults);
const failed = log.getFailedCommands();
expect(failed.length).toBe(1);
expect(failed[0].result.success).toBe(false);
});
});
describe('getSuccessfulCommands', () => {
it('should return only successful commands', () => {
const successResult: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 };
const failResult: CommandExecutionResult = { success: false, executedSteps: 0, totalSteps: 1, error: 'Error' };
log.log(sampleCommand, successResult, sampleStepResults);
log.log(sampleCommand, failResult, []);
log.log(sampleCommand, successResult, sampleStepResults);
const successful = log.getSuccessfulCommands();
expect(successful.length).toBe(2);
});
});
describe('clear', () => {
it('should clear all log entries', () => {
log.log(sampleCommand, sampleResult, sampleStepResults);
log.log(sampleCommand, sampleResult, sampleStepResults);
expect(log.getEntries().length).toBe(2);
log.clear();
expect(log.getEntries().length).toBe(0);
});
});
describe('exportToJson', () => {
it('should export logs as JSON string', () => {
log.log(sampleCommand, sampleResult, sampleStepResults);
const json = log.exportToJson();
const parsed = JSON.parse(json);
expect(Array.isArray(parsed)).toBe(true);
expect(parsed.length).toBe(1);
expect(parsed[0].commandId).toBe('test-command');
});
});
describe('getCount', () => {
it('should return the number of log entries', () => {
expect(log.getCount()).toBe(0);
log.log(sampleCommand, sampleResult, sampleStepResults);
expect(log.getCount()).toBe(1);
log.log(sampleCommand, sampleResult, sampleStepResults);
expect(log.getCount()).toBe(2);
});
});
describe('getLastEntry', () => {
it('should return the last log entry', () => {
const command1: Command = { ...sampleCommand, id: 'cmd-1', name: 'First' };
const command2: Command = { ...sampleCommand, id: 'cmd-2', name: 'Last' };
log.log(command1, sampleResult, sampleStepResults);
log.log(command2, sampleResult, sampleStepResults);
const lastEntry = log.getLastEntry();
expect(lastEntry).not.toBeNull();
expect(lastEntry?.commandId).toBe('cmd-2');
});
it('should return null when log is empty', () => {
const lastEntry = log.getLastEntry();
expect(lastEntry).toBeNull();
});
});
describe('Queue management', () => {
describe('enqueue', () => {
it('should add command to queue', () => {
const queued = log.enqueue(sampleCommand);
expect(queued.id).toBe('test-command');
expect(log.getQueueLength()).toBe(1);
});
it('should set initial status to Pending', () => {
const queued = log.enqueue(sampleCommand);
expect(queued.status).toBe('pending');
});
});
describe('dequeue', () => {
it('should remove and return the first command from queue', () => {
const command1: Command = { ...sampleCommand, id: 'cmd-1' };
const command2: Command = { ...sampleCommand, id: 'cmd-2' };
log.enqueue(command1);
log.enqueue(command2);
const dequeued = log.dequeue();
expect(dequeued?.command.id).toBe('cmd-1');
expect(log.getQueueLength()).toBe(1);
});
it('should return null when queue is empty', () => {
const dequeued = log.dequeue();
expect(dequeued).toBeNull();
});
});
describe('updateQueueStatus', () => {
it('should update command status in queue', () => {
log.enqueue(sampleCommand);
log.updateQueueStatus('test-command', 'executing');
const queue = log.getQueue();
expect(queue[0].status).toBe('executing');
});
it('should set executedAt and result when status is Completed', () => {
const result: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 };
log.enqueue(sampleCommand);
const beforeTime = Date.now();
log.updateQueueStatus('test-command', 'completed', result);
const afterTime = Date.now();
const queue = log.getQueue();
expect(queue[0].status).toBe('completed');
expect(queue[0].result).toEqual(result);
expect(queue[0].executedAt).toBeGreaterThanOrEqual(beforeTime);
expect(queue[0].executedAt).toBeLessThanOrEqual(afterTime);
});
});
describe('clearQueue', () => {
it('should clear all queued commands', () => {
log.enqueue(sampleCommand);
log.enqueue(sampleCommand);
expect(log.getQueueLength()).toBe(2);
log.clearQueue();
expect(log.getQueueLength()).toBe(0);
});
});
});
});
-160
View File
@@ -1,160 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { CommandParser, createCommandParser } from '../../src/commands/CommandParser';
import { CommandParseError } from '../../src/commands/CommandParser';
describe('CommandParser', () => {
let parser: CommandParser;
beforeEach(() => {
parser = createCommandParser();
});
describe('parse', () => {
it('should parse simple command without args', () => {
const result = parser.parse('shuffle');
expect(result.commandName).toBe('shuffle');
expect(result.args.positional).toEqual([]);
expect(result.args.flags).toEqual({});
});
it('should parse command with positional args', () => {
const result = parser.parse('move card-1 discard');
expect(result.commandName).toBe('move');
expect(result.args.positional).toEqual(['card-1', 'discard']);
expect(result.args.flags).toEqual({});
});
it('should parse command with multiple positional args', () => {
const result = parser.parse('position p1 3 5');
expect(result.commandName).toBe('position');
expect(result.args.positional).toEqual(['p1', '3', '5']);
});
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' });
});
it('should parse command with multiple flags', () => {
const result = parser.parse('create meeple m1 --color=red --name=Player1');
expect(result.commandName).toBe('create');
expect(result.args.positional).toEqual(['meeple', 'm1']);
expect(result.args.flags).toEqual({ color: 'red', name: 'Player1' });
});
it('should parse command with boolean flag', () => {
const result = parser.parse('flip p1 --faceup');
expect(result.commandName).toBe('flip');
expect(result.args.positional).toEqual(['p1']);
expect(result.args.flags).toEqual({ faceup: true });
});
it('should parse command with short flag', () => {
const result = parser.parse('shuffle d1 -s');
expect(result.commandName).toBe('shuffle');
expect(result.args.positional).toEqual(['d1']);
expect(result.args.flags).toEqual({ s: true });
});
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' });
});
it('should parse command with string number value', () => {
const result = parser.parse('rotate p1 90');
expect(result.commandName).toBe('rotate');
expect(result.args.positional).toEqual(['p1', '90']);
});
it('should parse command with negative number', () => {
const result = parser.parse('rotate p1 -45');
expect(result.commandName).toBe('rotate');
expect(result.args.positional).toEqual(['p1', '-45']);
});
it('should parse command with float number', () => {
const result = parser.parse('rotate p1 45.5');
expect(result.commandName).toBe('rotate');
expect(result.args.positional).toEqual(['p1', '45.5']);
});
it('should parse command with quoted string', () => {
const result = parser.parse('create meeple m1 --name="Red Player"');
expect(result.commandName).toBe('create');
expect(result.args.positional).toEqual(['meeple', 'm1']);
expect(result.args.flags).toEqual({ name: 'Red Player' });
});
it('should parse command with single quoted string', () => {
const result = parser.parse("create meeple m1 --name='Blue Player'");
expect(result.commandName).toBe('create');
expect(result.args.positional).toEqual(['meeple', 'm1']);
expect(result.args.flags).toEqual({ name: 'Blue Player' });
});
it('should handle extra whitespace', () => {
const result = parser.parse(' move card-1 discard ');
expect(result.commandName).toBe('move');
expect(result.args.positional).toEqual(['card-1', 'discard']);
});
it('should throw on empty command', () => {
expect(() => parser.parse('')).toThrow(CommandParseError);
expect(() => parser.parse(' ')).toThrow(CommandParseError);
});
it('should throw on unclosed quote', () => {
expect(() => parser.parse('create meeple m1 --name="Red')).toThrow(CommandParseError);
});
});
describe('formatCommand', () => {
it('should format simple command', () => {
const formatted = CommandParser.formatCommand('shuffle');
expect(formatted).toBe('shuffle');
});
it('should format command with positional args', () => {
const formatted = CommandParser.formatCommand('move', {
positional: ['card-1', 'discard'],
flags: {},
});
expect(formatted).toBe('move card-1 discard');
});
it('should format command with flags', () => {
const formatted = CommandParser.formatCommand('shuffle', {
positional: ['discard'],
flags: { seed: 2026 },
});
expect(formatted).toBe('shuffle discard --seed=2026');
});
it('should format command with boolean flag', () => {
const formatted = CommandParser.formatCommand('flip', {
positional: ['p1'],
flags: { faceup: true },
});
expect(formatted).toBe('flip p1 --faceup');
});
});
});
-254
View File
@@ -1,254 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { CommandRegistry, createCommandRegistry } from '../../src/commands/CommandRegistry';
import type { CliCommand } from '../../src/commands/CliCommand';
describe('CommandRegistry', () => {
let registry: CommandRegistry;
beforeEach(() => {
registry = createCommandRegistry();
});
const sampleCommand: CliCommand = {
name: 'test',
description: 'Test command',
usage: 'test <arg>',
args: [
{ name: 'arg', description: 'Test argument', required: true },
],
handler: (args) => {
return [
{
action: 'createMeeple',
params: { id: args.positional[0], color: 'red' },
},
];
},
};
describe('register', () => {
it('should register a command', () => {
registry.register(sampleCommand);
expect(registry.has('test')).toBe(true);
expect(registry.get('test')).toBe(sampleCommand);
});
it('should register multiple commands', () => {
const cmd1: CliCommand = {
name: 'cmd1',
description: 'Command 1',
usage: 'cmd1',
handler: () => [],
};
const cmd2: CliCommand = {
name: 'cmd2',
description: 'Command 2',
usage: 'cmd2',
handler: () => [],
};
registry.registerAll([cmd1, cmd2]);
expect(registry.has('cmd1')).toBe(true);
expect(registry.has('cmd2')).toBe(true);
expect(registry.getCount()).toBe(2);
});
});
describe('get', () => {
it('should return undefined for non-existent command', () => {
const cmd = registry.get('non-existent');
expect(cmd).toBeUndefined();
});
it('should return existing command', () => {
registry.register(sampleCommand);
const cmd = registry.get('test');
expect(cmd?.name).toBe('test');
});
});
describe('unregister', () => {
it('should remove a command', () => {
registry.register(sampleCommand);
expect(registry.has('test')).toBe(true);
registry.unregister('test');
expect(registry.has('test')).toBe(false);
});
});
describe('getAll', () => {
it('should return all registered commands', () => {
registry.register(sampleCommand);
const cmd2: CliCommand = {
name: 'cmd2',
description: 'Command 2',
usage: 'cmd2',
handler: () => [],
};
registry.register(cmd2);
const all = registry.getAll();
expect(all.length).toBe(2);
expect(all.map((c) => c.name)).toEqual(['test', 'cmd2']);
});
});
describe('execute', () => {
it('should execute a command successfully', () => {
registry.register(sampleCommand);
const result = registry.execute('test meeple-1');
expect(result.success).toBe(true);
expect(result.steps.length).toBe(1);
expect(result.steps[0].action).toBe('createMeeple');
expect(result.steps[0].params).toEqual({ id: 'meeple-1', color: 'red' });
});
it('should return error for unknown command', () => {
const result = registry.execute('unknown arg1');
expect(result.success).toBe(false);
expect(result.error).toContain('Unknown command');
expect(result.steps).toEqual([]);
});
it('should return error for missing required argument', () => {
registry.register(sampleCommand);
const result = registry.execute('test');
expect(result.success).toBe(false);
expect(result.error).toContain('Missing required argument');
});
it('should execute command with flags', () => {
const cmdWithFlags: CliCommand = {
name: 'move',
description: 'Move command',
usage: 'move <id> [--to=region]',
args: [
{ name: 'id', description: 'ID', required: true },
],
flags: [
{ name: 'to', description: 'Target', type: 'string' },
],
handler: (args) => {
return [
{
action: 'movePlacement',
params: {
placementId: args.positional[0],
targetRegionId: args.flags.to,
},
},
];
},
};
registry.register(cmdWithFlags);
const result = registry.execute('move p1 --to=board');
expect(result.success).toBe(true);
expect(result.steps[0].params).toEqual({
placementId: 'p1',
targetRegionId: 'board',
});
});
it('should handle command with optional args', () => {
const cmdOptional: CliCommand = {
name: 'draw',
description: 'Draw cards',
usage: 'draw [count]',
args: [
{ name: 'count', description: 'Count', required: false, default: '1' },
],
handler: (args) => {
return [];
},
};
registry.register(cmdOptional);
const result = registry.execute('draw');
expect(result.success).toBe(true);
});
});
describe('help', () => {
beforeEach(() => {
registry.register({
name: 'move',
description: 'Move a placement',
usage: 'move <id> <target>',
args: [
{ name: 'id', description: 'Placement ID', required: true },
{ name: 'target', description: 'Target region', required: true },
],
flags: [
{ name: 'key', description: 'Slot key', type: 'string', alias: 'k' },
],
handler: () => [],
});
registry.register({
name: 'flip',
description: 'Flip a placement',
usage: 'flip <id>',
args: [
{ name: 'id', description: 'Placement ID', required: true },
],
handler: () => [],
});
});
it('should show all commands help', () => {
const help = registry.help();
expect(help).toContain('Available commands');
expect(help).toContain('move');
expect(help).toContain('flip');
expect(help).toContain('help <command>');
});
it('should show specific command help', () => {
const help = registry.help('move');
expect(help).toContain('Command: move');
expect(help).toContain('Move a placement');
expect(help).toContain('Arguments:');
expect(help).toContain('Flags:');
});
it('should show error for unknown command help', () => {
const help = registry.help('unknown');
expect(help).toContain('Unknown command');
});
it('should show command with alias', () => {
const help = registry.help('move');
expect(help).toContain('-k,');
});
});
describe('clear', () => {
it('should clear all commands', () => {
registry.register(sampleCommand);
registry.register({
name: 'cmd2',
description: 'Command 2',
usage: 'cmd2',
handler: () => [],
});
expect(registry.getCount()).toBe(2);
registry.clear();
expect(registry.getCount()).toBe(0);
expect(registry.getAll()).toEqual([]);
});
});
});
-319
View File
@@ -1,319 +0,0 @@
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']);
});
});
});
-149
View File
@@ -1,149 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createGameState } from '../src/core/GameState';
import { PartType } from '../src/core/Part';
import {
createPartAction,
createMeepleAction,
createCardAction,
createTileAction,
updatePartAction,
removePartAction,
getPartAction,
} from '../src/actions/part.actions';
describe('Part Actions', () => {
let gameState: ReturnType<typeof createGameState>;
beforeEach(() => {
gameState = createGameState({ id: 'test-game', name: 'Test Game' });
});
describe('createPartAction', () => {
it('should create a generic part', () => {
const part = createPartAction(gameState, {
id: 'part-1',
type: PartType.Meeple,
color: 'red',
});
expect(part.id).toBe('part-1');
expect(part.type).toBe(PartType.Meeple);
expect(getPartAction(gameState, 'part-1')).toBeDefined();
});
it('should create a part with metadata', () => {
const part = createPartAction(gameState, {
id: 'part-1',
type: PartType.Tile,
pattern: 'forest',
metadata: { points: 5 },
});
expect(part.metadata).toEqual({ points: 5 });
});
});
describe('createMeepleAction', () => {
it('should create a meeple part', () => {
const meeple = createMeepleAction(gameState, 'meeple-1', 'blue');
expect(meeple.id).toBe('meeple-1');
expect(meeple.type).toBe(PartType.Meeple);
expect(meeple.color).toBe('blue');
});
it('should create a meeple with name', () => {
const meeple = createMeepleAction(gameState, 'meeple-1', 'blue', { name: 'Player 1' });
expect(meeple.name).toBe('Player 1');
});
});
describe('createCardAction', () => {
it('should create a card part', () => {
const card = createCardAction(gameState, 'card-1', { suit: 'hearts', value: 10 });
expect(card.id).toBe('card-1');
expect(card.type).toBe(PartType.Card);
expect(card.suit).toBe('hearts');
expect(card.value).toBe(10);
});
it('should create a card with string value', () => {
const card = createCardAction(gameState, 'card-2', { suit: 'spades', value: 'ace' });
expect(card.value).toBe('ace');
});
});
describe('createTileAction', () => {
it('should create a tile part', () => {
const tile = createTileAction(gameState, 'tile-1', { pattern: 'road', rotation: 90 });
expect(tile.id).toBe('tile-1');
expect(tile.type).toBe(PartType.Tile);
expect(tile.pattern).toBe('road');
expect(tile.rotation).toBe(90);
});
it('should create a tile with default rotation', () => {
const tile = createTileAction(gameState, 'tile-2', { pattern: 'city' });
expect(tile.rotation).toBeUndefined();
});
});
describe('updatePartAction', () => {
it('should update part properties', () => {
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
updatePartAction(gameState, 'meeple-1', { color: 'green' as string, name: 'Updated' });
const updated = getPartAction(gameState, 'meeple-1');
expect(updated?.color).toBe('green');
expect(updated?.name).toBe('Updated');
});
it('should update part metadata', () => {
createMeepleAction(gameState, 'meeple-1', 'red', { metadata: { score: 0 } });
updatePartAction(gameState, 'meeple-1', { metadata: { score: 10 } } as any);
const updated = getPartAction(gameState, 'meeple-1');
expect(updated?.metadata).toEqual({ score: 10 });
});
});
describe('removePartAction', () => {
it('should remove a part', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
expect(getPartAction(gameState, 'meeple-1')).toBeDefined();
removePartAction(gameState, 'meeple-1');
expect(getPartAction(gameState, 'meeple-1')).toBeUndefined();
});
it('should remove placements referencing the part', () => {
// 这个测试会在 placement 测试中更详细地验证
createMeepleAction(gameState, 'meeple-1', 'red');
removePartAction(gameState, 'meeple-1');
expect(getPartAction(gameState, 'meeple-1')).toBeUndefined();
});
});
describe('getPartAction', () => {
it('should return undefined for non-existent part', () => {
const part = getPartAction(gameState, 'non-existent');
expect(part).toBeUndefined();
});
it('should return existing part', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
const part = getPartAction(gameState, 'meeple-1');
expect(part?.id).toBe('meeple-1');
});
});
});
-422
View File
@@ -1,422 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createGameState } from '../src/core/GameState';
import { RegionType } from '../src/core/Region';
import { createMeepleAction } from '../src/actions/part.actions';
import { createRegionAction } from '../src/actions/region.actions';
import {
createPlacementAction,
getPlacementAction,
removePlacementAction,
movePlacementAction,
updatePlacementPositionAction,
updatePlacementRotationAction,
flipPlacementAction,
updatePlacementPartAction,
swapPlacementsAction,
setPlacementFaceAction,
getPlacementsInRegionAction,
getPlacementsOfPartAction,
} from '../src/actions/placement.actions';
describe('Placement Actions', () => {
let gameState: ReturnType<typeof createGameState>;
beforeEach(() => {
gameState = createGameState({ id: 'test-game', name: 'Test Game' });
});
describe('createPlacementAction', () => {
it('should create a placement', () => {
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
const placement = createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
});
expect(placement.id).toBe('placement-1');
expect(placement.partId).toBe('meeple-1');
expect(placement.regionId).toBe('board');
expect(placement.part).toBeDefined();
});
it('should create a placement with position', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
const placement = createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
position: { x: 3, y: 4 },
});
expect(placement.position).toEqual({ x: 3, y: 4 });
});
it('should throw if part does not exist', () => {
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
expect(() => {
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'non-existent',
regionId: 'board',
});
}).toThrow('Part non-existent not found');
});
it('should throw if region does not exist', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
expect(() => {
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'non-existent',
});
}).toThrow('Region non-existent not found');
});
});
describe('getPlacementAction', () => {
it('should return undefined for non-existent placement', () => {
const placement = getPlacementAction(gameState, 'non-existent');
expect(placement).toBeUndefined();
});
it('should return existing placement', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
});
const placement = getPlacementAction(gameState, 'placement-1');
expect(placement?.id).toBe('placement-1');
});
});
describe('removePlacementAction', () => {
it('should remove a placement', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
});
expect(getPlacementAction(gameState, 'placement-1')).toBeDefined();
removePlacementAction(gameState, 'placement-1');
expect(getPlacementAction(gameState, 'placement-1')).toBeUndefined();
});
it('should remove placement from region', () => {
const region = createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
});
region.placements.value = ['placement-1'];
removePlacementAction(gameState, 'placement-1');
expect(region.placements.value).not.toContain('placement-1');
});
});
describe('movePlacementAction', () => {
it('should move placement to another region', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createRegionAction(gameState, { id: 'supply', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
});
movePlacementAction(gameState, 'placement-1', 'supply');
const placement = getPlacementAction(gameState, 'placement-1');
expect(placement?.regionId).toBe('supply');
});
it('should move placement to keyed region with key', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
});
movePlacementAction(gameState, 'placement-1', 'board', 'B2');
const slotValue = gameState.regions.value.get('board')?.slots?.value.get('B2');
expect(slotValue).toBe('placement-1');
});
it('should throw if key is required but not provided', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
});
expect(() => {
movePlacementAction(gameState, 'placement-1', 'board');
}).toThrow('Key is required for keyed regions');
});
});
describe('updatePlacementPositionAction', () => {
it('should update placement position', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
position: { x: 0, y: 0 },
});
updatePlacementPositionAction(gameState, 'placement-1', { x: 5, y: 3 });
const placement = getPlacementAction(gameState, 'placement-1');
expect(placement?.position).toEqual({ x: 5, y: 3 });
});
it('should throw if placement does not exist', () => {
expect(() => {
updatePlacementPositionAction(gameState, 'non-existent', { x: 1, y: 1 });
}).toThrow('Placement non-existent not found');
});
});
describe('updatePlacementRotationAction', () => {
it('should update placement rotation', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
rotation: 0,
});
updatePlacementRotationAction(gameState, 'placement-1', 90);
const placement = getPlacementAction(gameState, 'placement-1');
expect(placement?.rotation).toBe(90);
});
});
describe('flipPlacementAction', () => {
it('should flip placement faceUp state', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
faceUp: true,
});
flipPlacementAction(gameState, 'placement-1');
const placement = getPlacementAction(gameState, 'placement-1');
expect(placement?.faceUp).toBe(false);
flipPlacementAction(gameState, 'placement-1');
expect(getPlacementAction(gameState, 'placement-1')?.faceUp).toBe(true);
});
});
describe('setPlacementFaceAction', () => {
it('should set placement face up', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
faceUp: false,
});
setPlacementFaceAction(gameState, 'placement-1', true);
expect(getPlacementAction(gameState, 'placement-1')?.faceUp).toBe(true);
});
it('should set placement face down', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
faceUp: true,
});
setPlacementFaceAction(gameState, 'placement-1', false);
expect(getPlacementAction(gameState, 'placement-1')?.faceUp).toBe(false);
});
});
describe('updatePlacementPartAction', () => {
it('should update the part reference', () => {
const meeple1 = createMeepleAction(gameState, 'meeple-1', 'red');
const meeple2 = createMeepleAction(gameState, 'meeple-2', 'blue');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
});
updatePlacementPartAction(gameState, 'placement-1', meeple2);
const placement = getPlacementAction(gameState, 'placement-1');
expect(placement?.part?.id).toBe('meeple-2');
});
it('should set part reference to null', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'board',
});
updatePlacementPartAction(gameState, 'placement-1', null);
expect(getPlacementAction(gameState, 'placement-1')?.part).toBeNull();
});
});
describe('swapPlacementsAction', () => {
it('should swap two placements in unkeyed region', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createMeepleAction(gameState, 'meeple-2', 'blue');
const region = createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'p1',
partId: 'meeple-1',
regionId: 'board',
});
createPlacementAction(gameState, {
id: 'p2',
partId: 'meeple-2',
regionId: 'board',
});
region.placements.value = ['p1', 'p2'];
swapPlacementsAction(gameState, 'p1', 'p2');
expect(region.placements.value).toEqual(['p2', 'p1']);
});
it('should swap two placements in keyed region', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createMeepleAction(gameState, 'meeple-2', 'blue');
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
createPlacementAction(gameState, {
id: 'p1',
partId: 'meeple-1',
regionId: 'board',
});
createPlacementAction(gameState, {
id: 'p2',
partId: 'meeple-2',
regionId: 'board',
});
// 设置初始槽位
const region = gameState.getRegion('board');
region?.slots?.value.set('A1', 'p1');
region?.slots?.value.set('A2', 'p2');
swapPlacementsAction(gameState, 'p1', 'p2');
expect(region?.slots?.value.get('A1')).toBe('p2');
expect(region?.slots?.value.get('A2')).toBe('p1');
});
it('should throw if placements are in different regions', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board1', type: RegionType.Unkeyed });
createRegionAction(gameState, { id: 'board2', type: RegionType.Unkeyed });
createPlacementAction(gameState, {
id: 'p1',
partId: 'meeple-1',
regionId: 'board1',
});
createPlacementAction(gameState, {
id: 'p2',
partId: 'meeple-1',
regionId: 'board2',
});
expect(() => {
swapPlacementsAction(gameState, 'p1', 'p2');
}).toThrow('Cannot swap placements in different regions directly');
});
});
describe('getPlacementsInRegionAction', () => {
it('should return all placements in a region', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board' });
createPlacementAction(gameState, { id: 'p2', partId: 'meeple-1', regionId: 'board' });
const region = gameState.getRegion('board');
region!.placements.value = ['p1', 'p2'];
const placements = getPlacementsInRegionAction(gameState, 'board');
expect(placements.length).toBe(2);
expect(placements.map((p) => p.id)).toEqual(['p1', 'p2']);
});
it('should return empty array for non-existent region', () => {
const placements = getPlacementsInRegionAction(gameState, 'non-existent');
expect(placements).toEqual([]);
});
});
describe('getPlacementsOfPartAction', () => {
it('should return all placements of a part', () => {
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createRegionAction(gameState, { id: 'board1', type: RegionType.Unkeyed });
createRegionAction(gameState, { id: 'board2', type: RegionType.Unkeyed });
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board1' });
createPlacementAction(gameState, { id: 'p2', partId: 'meeple-1', regionId: 'board2' });
const placements = getPlacementsOfPartAction(gameState, 'meeple-1');
expect(placements.length).toBe(2);
expect(placements.map((p) => p.partId)).toEqual(['meeple-1', 'meeple-1']);
});
it('should return empty array for part with no placements', () => {
createMeepleAction(gameState, 'meeple-1', 'red');
const placements = getPlacementsOfPartAction(gameState, 'meeple-1');
expect(placements).toEqual([]);
});
});
});
-302
View File
@@ -1,302 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createGameState } from '../src/core/GameState';
import { RegionType } from '../src/core/Region';
import {
createRegionAction,
getRegionAction,
removeRegionAction,
addPlacementToRegionAction,
removePlacementFromRegionAction,
setSlotAction,
getSlotAction,
clearRegionAction,
getRegionPlacementCountAction,
isRegionEmptyAction,
isRegionFullAction,
} from '../src/actions/region.actions';
import { createMeepleAction } from '../src/actions/part.actions';
import { createPlacementAction } from '../src/actions/placement.actions';
describe('Region Actions', () => {
let gameState: ReturnType<typeof createGameState>;
beforeEach(() => {
gameState = createGameState({ id: 'test-game', name: 'Test Game' });
});
describe('createRegionAction', () => {
it('should create an unkeyed region', () => {
const region = createRegionAction(gameState, {
id: 'deck',
type: RegionType.Unkeyed,
name: 'Draw Deck',
});
expect(region.id).toBe('deck');
expect(region.type).toBe(RegionType.Unkeyed);
expect(region.slots).toBeUndefined();
});
it('should create a keyed region', () => {
const region = createRegionAction(gameState, {
id: 'board',
type: RegionType.Keyed,
name: 'Game Board',
});
expect(region.id).toBe('board');
expect(region.type).toBe(RegionType.Keyed);
expect(region.slots).toBeDefined();
});
it('should create a region with capacity', () => {
const region = createRegionAction(gameState, {
id: 'hand',
type: RegionType.Unkeyed,
capacity: 5,
});
expect(region.capacity).toBe(5);
});
});
describe('getRegionAction', () => {
it('should return undefined for non-existent region', () => {
const region = getRegionAction(gameState, 'non-existent');
expect(region).toBeUndefined();
});
it('should return existing region', () => {
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
const region = getRegionAction(gameState, 'board');
expect(region?.id).toBe('board');
});
});
describe('removeRegionAction', () => {
it('should remove a region', () => {
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
expect(getRegionAction(gameState, 'board')).toBeDefined();
removeRegionAction(gameState, 'board');
expect(getRegionAction(gameState, 'board')).toBeUndefined();
});
});
describe('addPlacementToRegionAction (unkeyed)', () => {
it('should add a placement to an unkeyed region', () => {
createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
const placement = createPlacementAction(gameState, {
id: 'placement-1',
partId: 'meeple-1',
regionId: 'deck',
});
addPlacementToRegionAction(gameState, 'deck', 'placement-1');
const region = getRegionAction(gameState, 'deck');
expect(region?.placements.value).toContain('placement-1');
});
it('should throw when adding to a keyed region', () => {
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
expect(() => {
addPlacementToRegionAction(gameState, 'board', 'placement-1');
}).toThrow('Cannot use addPlacementToRegionAction on a keyed region');
});
it('should respect capacity limit', () => {
createRegionAction(gameState, { id: 'hand', type: RegionType.Unkeyed, capacity: 2 });
createMeepleAction(gameState, 'meeple-1', 'red');
createMeepleAction(gameState, 'meeple-2', 'blue');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'hand' });
createPlacementAction(gameState, { id: 'p2', partId: 'meeple-2', regionId: 'hand' });
addPlacementToRegionAction(gameState, 'hand', 'p1');
addPlacementToRegionAction(gameState, 'hand', 'p2');
expect(() => {
addPlacementToRegionAction(gameState, 'hand', 'p3');
}).toThrow('has reached its capacity');
});
});
describe('removePlacementFromRegionAction', () => {
it('should remove a placement from an unkeyed region', () => {
createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'deck' });
addPlacementToRegionAction(gameState, 'deck', 'p1');
removePlacementFromRegionAction(gameState, 'deck', 'p1');
const region = getRegionAction(gameState, 'deck');
expect(region?.placements.value).not.toContain('p1');
});
it('should clear slot in keyed region', () => {
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board' });
setSlotAction(gameState, 'board', 'A1', 'p1');
removePlacementFromRegionAction(gameState, 'board', 'p1');
const slotValue = getSlotAction(gameState, 'board', 'A1');
expect(slotValue).toBeNull();
});
});
describe('setSlotAction (keyed)', () => {
it('should set a slot in a keyed region', () => {
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board' });
setSlotAction(gameState, 'board', 'A1', 'p1');
const slotValue = getSlotAction(gameState, 'board', 'A1');
expect(slotValue).toBe('p1');
});
it('should throw when used on unkeyed region', () => {
createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed });
expect(() => {
setSlotAction(gameState, 'deck', 'slot1', 'p1');
}).toThrow('Cannot use setSlotAction on an unkeyed region');
});
it('should add placement to region list when setting slot', () => {
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
createRegionAction(gameState, { id: 'other', type: RegionType.Unkeyed });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'other' });
setSlotAction(gameState, 'board', 'A1', 'p1');
const region = getRegionAction(gameState, 'board');
expect(region?.placements.value).toContain('p1');
});
it('should clear a slot with null', () => {
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board' });
setSlotAction(gameState, 'board', 'A1', 'p1');
setSlotAction(gameState, 'board', 'A1', null);
const slotValue = getSlotAction(gameState, 'board', 'A1');
expect(slotValue).toBeNull();
});
});
describe('getSlotAction', () => {
it('should return null for empty slot', () => {
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
const slotValue = getSlotAction(gameState, 'board', 'A1');
expect(slotValue).toBeNull();
});
it('should throw when used on unkeyed region', () => {
createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed });
expect(() => {
getSlotAction(gameState, 'deck', 'slot1');
}).toThrow('Cannot use getSlotAction on an unkeyed region');
});
});
describe('clearRegionAction', () => {
it('should clear all placements from unkeyed region', () => {
createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'deck' });
createPlacementAction(gameState, { id: 'p2', partId: 'meeple-1', regionId: 'deck' });
addPlacementToRegionAction(gameState, 'deck', 'p1');
addPlacementToRegionAction(gameState, 'deck', 'p2');
clearRegionAction(gameState, 'deck');
const region = getRegionAction(gameState, 'deck');
expect(region?.placements.value.length).toBe(0);
});
it('should clear all slots in keyed region', () => {
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board' });
setSlotAction(gameState, 'board', 'A1', 'p1');
setSlotAction(gameState, 'board', 'A2', 'p1');
clearRegionAction(gameState, 'board');
const region = getRegionAction(gameState, 'board');
expect(region?.placements.value.length).toBe(0);
expect(region?.slots?.value.size).toBe(0);
});
});
describe('getRegionPlacementCountAction', () => {
it('should return the count of placements', () => {
createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'deck' });
createPlacementAction(gameState, { id: 'p2', partId: 'meeple-1', regionId: 'deck' });
addPlacementToRegionAction(gameState, 'deck', 'p1');
addPlacementToRegionAction(gameState, 'deck', 'p2');
const count = getRegionPlacementCountAction(gameState, 'deck');
expect(count).toBe(2);
});
it('should return 0 for non-existent region', () => {
const count = getRegionPlacementCountAction(gameState, 'non-existent');
expect(count).toBe(0);
});
});
describe('isRegionEmptyAction', () => {
it('should return true for empty region', () => {
createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed });
expect(isRegionEmptyAction(gameState, 'deck')).toBe(true);
});
it('should return false for non-empty region', () => {
createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'deck' });
addPlacementToRegionAction(gameState, 'deck', 'p1');
expect(isRegionEmptyAction(gameState, 'deck')).toBe(false);
});
});
describe('isRegionFullAction', () => {
it('should return false for region without capacity', () => {
createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed });
expect(isRegionFullAction(gameState, 'deck')).toBe(false);
});
it('should return true when at capacity', () => {
createRegionAction(gameState, { id: 'hand', type: RegionType.Unkeyed, capacity: 2 });
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'hand' });
createPlacementAction(gameState, { id: 'p2', partId: 'meeple-1', regionId: 'hand' });
addPlacementToRegionAction(gameState, 'hand', 'p1');
addPlacementToRegionAction(gameState, 'hand', 'p2');
expect(isRegionFullAction(gameState, 'hand')).toBe(true);
});
});
});
-595
View File
@@ -1,595 +0,0 @@
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);
});
});
});