2026-04-02 11:21:57 +08:00
|
|
|
import { describe, it, expect } from 'vitest';
|
2026-04-02 12:48:29 +08:00
|
|
|
import { createGameContext, createGameCommand, IGameContext } from '../../src/core/game';
|
2026-04-02 11:21:57 +08:00
|
|
|
import { createCommandRegistry, parseCommandSchema, type CommandRegistry } from '../../src/utils/command';
|
2026-04-02 12:48:29 +08:00
|
|
|
|
|
|
|
|
type MyState = {
|
|
|
|
|
score: number;
|
|
|
|
|
round: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type MyContext = IGameContext & {
|
|
|
|
|
state: MyState;
|
|
|
|
|
};
|
2026-04-02 11:21:57 +08:00
|
|
|
|
|
|
|
|
describe('createGameContext', () => {
|
|
|
|
|
it('should create a game context with empty parts and regions', () => {
|
|
|
|
|
const registry = createCommandRegistry<IGameContext>();
|
|
|
|
|
const ctx = createGameContext(registry);
|
|
|
|
|
|
|
|
|
|
expect(ctx.parts.collection.value).toEqual({});
|
|
|
|
|
expect(ctx.regions.collection.value).toEqual({});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should wire commands to the context', () => {
|
|
|
|
|
const registry = createCommandRegistry<IGameContext>();
|
|
|
|
|
const ctx = createGameContext(registry);
|
|
|
|
|
|
|
|
|
|
expect(ctx.commands).not.toBeNull();
|
|
|
|
|
expect(ctx.commands.registry).toBe(registry);
|
|
|
|
|
expect(ctx.commands.context).toBe(ctx);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-02 12:48:29 +08:00
|
|
|
it('should accept initial state as an object', () => {
|
|
|
|
|
const registry = createCommandRegistry<MyContext>();
|
|
|
|
|
const ctx = createGameContext<MyContext>(registry, {
|
|
|
|
|
state: { score: 0, round: 1 },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(ctx.state.score).toBe(0);
|
|
|
|
|
expect(ctx.state.round).toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should accept initial state as a factory function', () => {
|
|
|
|
|
const registry = createCommandRegistry<MyContext>();
|
|
|
|
|
const ctx = createGameContext<MyContext>(registry, () => ({
|
|
|
|
|
state: { score: 10, round: 3 },
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
expect(ctx.state.score).toBe(10);
|
|
|
|
|
expect(ctx.state.round).toBe(3);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-02 11:21:57 +08:00
|
|
|
it('should forward prompt events to the prompts queue', async () => {
|
|
|
|
|
const registry = createCommandRegistry<IGameContext>();
|
|
|
|
|
const ctx = createGameContext(registry);
|
|
|
|
|
|
|
|
|
|
const schema = parseCommandSchema('test <value>');
|
|
|
|
|
registry.set('test', {
|
|
|
|
|
schema,
|
|
|
|
|
run: async function () {
|
|
|
|
|
return this.prompt('prompt <answer>');
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const runPromise = ctx.commands.run('test hello');
|
|
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
|
|
|
|
|
|
const promptEvent = await ctx.prompts.pop();
|
|
|
|
|
expect(promptEvent).not.toBeNull();
|
|
|
|
|
expect(promptEvent.schema.name).toBe('prompt');
|
|
|
|
|
|
|
|
|
|
promptEvent.resolve({ name: 'prompt', params: ['yes'], options: {}, flags: {} });
|
|
|
|
|
|
|
|
|
|
const result = await runPromise;
|
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
expect((result.result as any).params[0]).toBe('yes');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('createGameCommand', () => {
|
|
|
|
|
it('should create a command from a string schema', () => {
|
|
|
|
|
const cmd = createGameCommand('test <a>', async function () {
|
|
|
|
|
return 1;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(cmd.schema.name).toBe('test');
|
|
|
|
|
expect(cmd.schema.params[0].name).toBe('a');
|
|
|
|
|
expect(cmd.schema.params[0].required).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should create a command from a CommandSchema object', () => {
|
|
|
|
|
const schema = parseCommandSchema('foo <x> [y]');
|
|
|
|
|
const cmd = createGameCommand(schema, async function () {
|
|
|
|
|
return 2;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(cmd.schema.name).toBe('foo');
|
|
|
|
|
expect(cmd.schema.params[0].name).toBe('x');
|
|
|
|
|
expect(cmd.schema.params[0].required).toBe(true);
|
|
|
|
|
expect(cmd.schema.params[1].name).toBe('y');
|
|
|
|
|
expect(cmd.schema.params[1].required).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should run a command with access to game context', async () => {
|
|
|
|
|
const registry = createCommandRegistry<IGameContext>();
|
|
|
|
|
const ctx = createGameContext(registry);
|
|
|
|
|
|
|
|
|
|
const addRegion = createGameCommand('add-region <id>', async function (cmd) {
|
|
|
|
|
const id = cmd.params[0] as string;
|
|
|
|
|
this.context.regions.add({ id, axes: [], children: [] });
|
|
|
|
|
return id;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
registry.set('add-region', addRegion);
|
|
|
|
|
|
|
|
|
|
const result = await ctx.commands.run('add-region board');
|
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
expect(result.result).toBe('board');
|
|
|
|
|
}
|
|
|
|
|
expect(ctx.regions.get('board')).not.toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should run a command that adds parts', async () => {
|
|
|
|
|
const registry = createCommandRegistry<IGameContext>();
|
|
|
|
|
const ctx = createGameContext(registry);
|
|
|
|
|
|
|
|
|
|
ctx.regions.add({ id: 'zone', axes: [], children: [] });
|
|
|
|
|
|
|
|
|
|
const addPart = createGameCommand('add-part <id>', async function (cmd) {
|
|
|
|
|
const id = cmd.params[0] as string;
|
|
|
|
|
const part = {
|
|
|
|
|
id,
|
|
|
|
|
sides: 1,
|
|
|
|
|
side: 0,
|
|
|
|
|
region: this.context.regions.get('zone'),
|
|
|
|
|
position: [0],
|
|
|
|
|
};
|
|
|
|
|
this.context.parts.add(part);
|
|
|
|
|
return id;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
registry.set('add-part', addPart);
|
|
|
|
|
|
|
|
|
|
const result = await ctx.commands.run('add-part piece-1');
|
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
expect(result.result).toBe('piece-1');
|
|
|
|
|
}
|
|
|
|
|
expect(ctx.parts.get('piece-1')).not.toBeNull();
|
|
|
|
|
});
|
2026-04-02 12:48:29 +08:00
|
|
|
|
|
|
|
|
it('should run a typed command with extended context', async () => {
|
|
|
|
|
const registry = createCommandRegistry<MyContext>();
|
|
|
|
|
|
|
|
|
|
const addScore = createGameCommand<MyContext, number>(
|
|
|
|
|
'add-score <amount:number>',
|
|
|
|
|
async function (cmd) {
|
|
|
|
|
const amount = cmd.params[0] as number;
|
|
|
|
|
this.context.state.score += amount;
|
|
|
|
|
return this.context.state.score;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
registry.set('add-score', addScore);
|
|
|
|
|
|
|
|
|
|
const ctx = createGameContext<MyContext>(registry, () => ({
|
|
|
|
|
state: { score: 0, round: 1 },
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const result = await ctx.commands.run('add-score 5');
|
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
expect(result.result).toBe(5);
|
|
|
|
|
}
|
|
|
|
|
expect(ctx.state.score).toBe(5);
|
|
|
|
|
});
|
2026-04-02 11:21:57 +08:00
|
|
|
});
|