boardgame-core/tests/core/game.test.ts

181 lines
6.0 KiB
TypeScript
Raw Normal View History

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
});