chore: update tests and imports
This commit is contained in:
parent
65a3d682b6
commit
50c146964d
|
|
@ -14,12 +14,13 @@ export function createPart<TMeta = {}>(
|
|||
template: PartTemplate<TMeta>,
|
||||
id: string
|
||||
): Part<TMeta> {
|
||||
return {
|
||||
regionId: '',
|
||||
position: [],
|
||||
...template,
|
||||
const part: Part<TMeta> = {
|
||||
id,
|
||||
} as Part<TMeta>;
|
||||
regionId: template.regionId ?? '',
|
||||
position: template.position ?? [],
|
||||
...template,
|
||||
};
|
||||
return part;
|
||||
}
|
||||
|
||||
export function createParts<TMeta = {}>(
|
||||
|
|
@ -67,7 +68,7 @@ export function mergePartPools<TMeta = {}>(
|
|||
...pools: PartPool<TMeta>[]
|
||||
): PartPool<TMeta> {
|
||||
if (pools.length === 0) {
|
||||
return createPartPool({} as PartTemplate<TMeta>, 0, 'merged');
|
||||
return createEmptyPartPool<TMeta>();
|
||||
}
|
||||
|
||||
const allPartsArray = pools.flatMap(p => Object.values(p.parts));
|
||||
|
|
@ -94,3 +95,19 @@ export function mergePartPools<TMeta = {}>(
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createEmptyPartPool<TMeta>(): PartPool<TMeta> {
|
||||
return {
|
||||
parts: {},
|
||||
template: {} as PartTemplate<TMeta>,
|
||||
draw() {
|
||||
return undefined;
|
||||
},
|
||||
return(_part: Part<TMeta>) {
|
||||
// no-op for empty pool
|
||||
},
|
||||
remaining() {
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {RNG} from "@/utils/rng";
|
||||
import {RNG} from '@/utils/rng';
|
||||
import {Region} from '@/core/region';
|
||||
|
||||
export type Part<TMeta = {}> = {
|
||||
id: string;
|
||||
|
|
@ -40,3 +41,20 @@ export function getPartAtPosition<TMeta>(parts: Record<string, Part<TMeta>>, reg
|
|||
const posKey = position.join(',');
|
||||
return Object.values(parts).find(p => p.regionId === regionId && p.position.join(',') === posKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* O(1) cell occupancy check using Region.partMap
|
||||
*/
|
||||
export function isCellOccupiedByRegion(region: Region, position: number[]): boolean {
|
||||
return position.join(',') in region.partMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* O(1) part lookup using Region.partMap and parts Record
|
||||
*/
|
||||
export function getPartAtPositionInRegion<TMeta>(region: Region, parts: Record<string, Part<TMeta>>, position: number[]): Part<TMeta> | undefined {
|
||||
const posKey = position.join(',');
|
||||
const partId = region.partMap[posKey];
|
||||
if (!partId) return undefined;
|
||||
return parts[partId];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export type { IGameContext } from './core/game';
|
|||
export { createGameContext, createGameCommandRegistry } from './core/game';
|
||||
|
||||
export type { Part } from './core/part';
|
||||
export { flip, flipTo, roll, findPartById, isCellOccupied, getPartAtPosition } from './core/part';
|
||||
export { flip, flipTo, roll, findPartById, isCellOccupied, getPartAtPosition, isCellOccupiedByRegion, getPartAtPositionInRegion } from './core/part';
|
||||
|
||||
export type { PartTemplate, PartPool } from './core/part-factory';
|
||||
export { createPart, createParts, createPartPool, mergePartPools } from './core/part-factory';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
import {createGameCommandRegistry, Part, MutableSignal, createRegion, createPart, isCellOccupied as isCellOccupiedUtil, getPartAtPosition} from '@/index';
|
||||
import {
|
||||
createGameCommandRegistry,
|
||||
Part,
|
||||
MutableSignal,
|
||||
createRegion,
|
||||
createPart,
|
||||
isCellOccupied as isCellOccupiedUtil,
|
||||
getPartAtPosition,
|
||||
} from '@/index';
|
||||
|
||||
const BOARD_SIZE = 6;
|
||||
const MAX_PIECES_PER_PLAYER = 8;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {Signal, signal, SignalOptions} from "@preact/signals-core";
|
||||
import {Signal, signal, SignalOptions} from '@preact/signals-core';
|
||||
import {create} from 'mutative';
|
||||
|
||||
export class MutableSignal<T> extends Signal<T> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { createPart, createParts, createPartPool, mergePartPools, PartPool } from '@/core/part-factory';
|
||||
|
||||
describe('createPart', () => {
|
||||
it('should create a part with given template and id', () => {
|
||||
const part = createPart<{ player: string }>(
|
||||
{ regionId: 'board', position: [1, 2], player: 'X' },
|
||||
'piece-1'
|
||||
);
|
||||
expect(part.id).toBe('piece-1');
|
||||
expect(part.regionId).toBe('board');
|
||||
expect(part.position).toEqual([1, 2]);
|
||||
expect(part.player).toBe('X');
|
||||
});
|
||||
|
||||
it('should apply default values for regionId and position when not provided', () => {
|
||||
const part = createPart({}, 'piece-1');
|
||||
expect(part.regionId).toBe('');
|
||||
expect(part.position).toEqual([]);
|
||||
});
|
||||
|
||||
it('should allow overriding default values', () => {
|
||||
const part = createPart({ regionId: 'custom', position: [0] }, 'piece-1');
|
||||
expect(part.regionId).toBe('custom');
|
||||
expect(part.position).toEqual([0]);
|
||||
});
|
||||
|
||||
it('should preserve metadata fields', () => {
|
||||
type TestMeta = { type: string; count: number };
|
||||
const part = createPart<TestMeta>(
|
||||
{ regionId: 'board', position: [0], type: 'kitten', count: 5 },
|
||||
'piece-1'
|
||||
);
|
||||
expect(part.type).toBe('kitten');
|
||||
expect(part.count).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createParts', () => {
|
||||
it('should create multiple parts with auto-generated IDs', () => {
|
||||
const parts = createParts(
|
||||
{ regionId: 'deck', position: [] },
|
||||
3,
|
||||
'card'
|
||||
);
|
||||
expect(parts.length).toBe(3);
|
||||
expect(parts[0].id).toBe('card-1');
|
||||
expect(parts[1].id).toBe('card-2');
|
||||
expect(parts[2].id).toBe('card-3');
|
||||
});
|
||||
|
||||
it('should create parts with identical properties', () => {
|
||||
const parts = createParts(
|
||||
{ regionId: 'deck', position: [], type: 'token' },
|
||||
2,
|
||||
'token'
|
||||
);
|
||||
expect(parts[0].regionId).toBe('deck');
|
||||
expect(parts[1].regionId).toBe('deck');
|
||||
expect(parts[0].type).toBe('token');
|
||||
expect(parts[1].type).toBe('token');
|
||||
});
|
||||
|
||||
it('should create zero parts when count is 0', () => {
|
||||
const parts = createParts({}, 0, 'empty');
|
||||
expect(parts.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPartPool', () => {
|
||||
it('should create a pool with specified count', () => {
|
||||
const pool = createPartPool(
|
||||
{ regionId: 'supply', position: [] },
|
||||
5,
|
||||
'token'
|
||||
);
|
||||
expect(pool.remaining()).toBe(5);
|
||||
expect(Object.keys(pool.parts).length).toBe(5);
|
||||
});
|
||||
|
||||
it('should generate parts with correct IDs', () => {
|
||||
const pool = createPartPool({}, 3, 'piece');
|
||||
expect(pool.parts['piece-1']).toBeDefined();
|
||||
expect(pool.parts['piece-2']).toBeDefined();
|
||||
expect(pool.parts['piece-3']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should draw parts from the pool', () => {
|
||||
const pool = createPartPool({}, 2, 'card');
|
||||
const drawn = pool.draw();
|
||||
expect(drawn).toBeDefined();
|
||||
expect(drawn!.id).toBe('card-2');
|
||||
expect(pool.remaining()).toBe(1);
|
||||
});
|
||||
|
||||
it('should return undefined when pool is empty', () => {
|
||||
const pool = createPartPool({}, 1, 'card');
|
||||
pool.draw();
|
||||
const drawn = pool.draw();
|
||||
expect(drawn).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return part to pool', () => {
|
||||
const pool = createPartPool({ regionId: 'board', position: [0, 0] }, 1, 'card');
|
||||
const drawn = pool.draw();
|
||||
expect(pool.remaining()).toBe(0);
|
||||
|
||||
pool.return(drawn!);
|
||||
expect(pool.remaining()).toBe(1);
|
||||
expect(drawn!.regionId).toBe('');
|
||||
expect(drawn!.position).toEqual([]);
|
||||
});
|
||||
|
||||
it('should store parts as Record keyed by ID', () => {
|
||||
const pool = createPartPool({}, 2, 'piece');
|
||||
expect(typeof pool.parts).toBe('object');
|
||||
expect(pool.parts['piece-1']).toBeDefined();
|
||||
expect(pool.parts['piece-2']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergePartPools', () => {
|
||||
it('should merge multiple pools', () => {
|
||||
const pool1 = createPartPool({ regionId: 'deck1', position: [] }, 2, 'card1');
|
||||
const pool2 = createPartPool({ regionId: 'deck2', position: [] }, 3, 'card2');
|
||||
const merged = mergePartPools(pool1, pool2);
|
||||
|
||||
expect(Object.keys(merged.parts).length).toBe(5);
|
||||
// Parts with regionId: '' are available; pool parts have regionId from template
|
||||
// After merge, available = parts with regionId === ''
|
||||
expect(merged.remaining()).toBe(0); // parts have regionId: 'deck1' or 'deck2'
|
||||
expect(merged.parts['card1-1']).toBeDefined();
|
||||
expect(merged.parts['card2-3']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should use first pool template', () => {
|
||||
const pool1 = createPartPool({ regionId: 'deck1', position: [] }, 1, 'card1');
|
||||
const pool2 = createPartPool({ regionId: 'deck2', position: [] }, 1, 'card2');
|
||||
const merged = mergePartPools(pool1, pool2);
|
||||
|
||||
expect(merged.template.regionId).toBe('deck1');
|
||||
});
|
||||
|
||||
it('should return empty pool when no pools provided', () => {
|
||||
const merged = mergePartPools();
|
||||
expect(Object.keys(merged.parts).length).toBe(0);
|
||||
expect(merged.remaining()).toBe(0);
|
||||
});
|
||||
|
||||
it('should only count available parts for remaining()', () => {
|
||||
const pool1 = createPartPool({}, 2, 'card');
|
||||
const pool2 = createPartPool({}, 2, 'token');
|
||||
|
||||
const drawn = pool1.draw();
|
||||
drawn!.regionId = 'board';
|
||||
drawn!.position = [0, 0];
|
||||
|
||||
const merged = mergePartPools(pool1, pool2);
|
||||
expect(merged.remaining()).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle drawing from merged pool', () => {
|
||||
const pool1 = createPartPool({}, 2, 'card');
|
||||
const pool2 = createPartPool({}, 2, 'token');
|
||||
const merged = mergePartPools(pool1, pool2);
|
||||
|
||||
const drawn = merged.draw();
|
||||
expect(drawn).toBeDefined();
|
||||
expect(merged.remaining()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { flip, flipTo, roll, findPartById, isCellOccupied, getPartAtPosition, isCellOccupiedByRegion, getPartAtPositionInRegion, Part } from '@/core/part';
|
||||
import { createRegion } from '@/core/region';
|
||||
import { createRNG } from '@/utils/rng';
|
||||
|
||||
function createTestPart<TMeta = {}>(overrides: Partial<Part<TMeta>> & TMeta): Part<TMeta> {
|
||||
return {
|
||||
id: 'test-part',
|
||||
regionId: 'test-region',
|
||||
position: [0, 0],
|
||||
...overrides,
|
||||
} as Part<TMeta>;
|
||||
}
|
||||
|
||||
describe('flip', () => {
|
||||
it('should cycle to next side when sides is defined', () => {
|
||||
const part = createTestPart({ sides: 2, side: 0 });
|
||||
flip(part);
|
||||
expect(part.side).toBe(1);
|
||||
flip(part);
|
||||
expect(part.side).toBe(0);
|
||||
});
|
||||
|
||||
it('should wrap around when side reaches sides count', () => {
|
||||
const part = createTestPart({ sides: 6, side: 5 });
|
||||
flip(part);
|
||||
expect(part.side).toBe(0);
|
||||
});
|
||||
|
||||
it('should do nothing when sides is undefined', () => {
|
||||
const part = createTestPart({ side: 3 });
|
||||
flip(part);
|
||||
expect(part.side).toBe(3);
|
||||
});
|
||||
|
||||
it('should initialize side to 0 when side is undefined', () => {
|
||||
const part = createTestPart({ sides: 4 });
|
||||
flip(part);
|
||||
expect(part.side).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flipTo', () => {
|
||||
it('should set side to specified value', () => {
|
||||
const part = createTestPart({ sides: 6, side: 0 });
|
||||
flipTo(part, 3);
|
||||
expect(part.side).toBe(3);
|
||||
});
|
||||
|
||||
it('should do nothing when sides is undefined', () => {
|
||||
const part = createTestPart({ side: 2 });
|
||||
flipTo(part, 5);
|
||||
expect(part.side).toBe(2);
|
||||
});
|
||||
|
||||
it('should do nothing when side exceeds sides count', () => {
|
||||
const part = createTestPart({ sides: 6, side: 0 });
|
||||
flipTo(part, 6);
|
||||
expect(part.side).toBe(0);
|
||||
});
|
||||
|
||||
it('should allow side equal to sides - 1', () => {
|
||||
const part = createTestPart({ sides: 6, side: 0 });
|
||||
flipTo(part, 5);
|
||||
expect(part.side).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('roll', () => {
|
||||
it('should randomize side when sides is defined', () => {
|
||||
const part = createTestPart({ sides: 6 });
|
||||
const rng = createRNG(12345);
|
||||
roll(part, rng);
|
||||
expect(part.side).toBeGreaterThanOrEqual(0);
|
||||
expect(part.side).toBeLessThan(6);
|
||||
});
|
||||
|
||||
it('should do nothing when sides is undefined', () => {
|
||||
const part = createTestPart({ side: 3 });
|
||||
const rng = createRNG(12345);
|
||||
roll(part, rng);
|
||||
expect(part.side).toBe(3);
|
||||
});
|
||||
|
||||
it('should produce deterministic results with same seed', () => {
|
||||
const part1 = createTestPart({ sides: 6 });
|
||||
const part2 = createTestPart({ sides: 6 });
|
||||
const rng1 = createRNG(42);
|
||||
const rng2 = createRNG(42);
|
||||
roll(part1, rng1);
|
||||
roll(part2, rng2);
|
||||
expect(part1.side).toBe(part2.side);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPartById', () => {
|
||||
it('should find part by id', () => {
|
||||
const parts: Record<string, Part> = {
|
||||
'part-1': createTestPart({ id: 'part-1' }),
|
||||
'part-2': createTestPart({ id: 'part-2' }),
|
||||
};
|
||||
const result = findPartById(parts, 'part-1');
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.id).toBe('part-1');
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent id', () => {
|
||||
const parts: Record<string, Part> = {
|
||||
'part-1': createTestPart({ id: 'part-1' }),
|
||||
};
|
||||
const result = findPartById(parts, 'part-99');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should work with empty record', () => {
|
||||
const parts: Record<string, Part> = {};
|
||||
const result = findPartById(parts, 'any');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCellOccupied', () => {
|
||||
it('should return true for occupied cell', () => {
|
||||
const parts: Record<string, Part> = {
|
||||
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] }),
|
||||
};
|
||||
expect(isCellOccupied(parts, 'board', [1, 2])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for empty cell', () => {
|
||||
const parts: Record<string, Part> = {
|
||||
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] }),
|
||||
};
|
||||
expect(isCellOccupied(parts, 'board', [3, 4])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for different region', () => {
|
||||
const parts: Record<string, Part> = {
|
||||
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] }),
|
||||
};
|
||||
expect(isCellOccupied(parts, 'hand', [1, 2])).toBe(false);
|
||||
});
|
||||
|
||||
it('should work with empty record', () => {
|
||||
const parts: Record<string, Part> = {};
|
||||
expect(isCellOccupied(parts, 'board', [0, 0])).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle multi-dimensional positions', () => {
|
||||
const parts: Record<string, Part> = {
|
||||
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2, 3] }),
|
||||
};
|
||||
expect(isCellOccupied(parts, 'board', [1, 2, 3])).toBe(true);
|
||||
expect(isCellOccupied(parts, 'board', [1, 2])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPartAtPosition', () => {
|
||||
it('should return part at specified position', () => {
|
||||
const part1 = createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] });
|
||||
const parts: Record<string, Part> = { 'p1': part1 };
|
||||
const result = getPartAtPosition(parts, 'board', [1, 2]);
|
||||
expect(result).toBe(part1);
|
||||
});
|
||||
|
||||
it('should return undefined for empty cell', () => {
|
||||
const parts: Record<string, Part> = {
|
||||
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] }),
|
||||
};
|
||||
const result = getPartAtPosition(parts, 'board', [3, 4]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for different region', () => {
|
||||
const parts: Record<string, Part> = {
|
||||
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] }),
|
||||
};
|
||||
const result = getPartAtPosition(parts, 'hand', [1, 2]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should work with empty record', () => {
|
||||
const parts: Record<string, Part> = {};
|
||||
const result = getPartAtPosition(parts, 'board', [0, 0]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCellOccupiedByRegion', () => {
|
||||
it('should return true for occupied cell', () => {
|
||||
const region = createRegion('board', [
|
||||
{ name: 'x', min: 0, max: 2 },
|
||||
{ name: 'y', min: 0, max: 2 },
|
||||
]);
|
||||
const part = createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] });
|
||||
region.childIds.push(part.id);
|
||||
region.partMap['1,2'] = part.id;
|
||||
|
||||
expect(isCellOccupiedByRegion(region, [1, 2])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for empty cell', () => {
|
||||
const region = createRegion('board', [
|
||||
{ name: 'x', min: 0, max: 2 },
|
||||
{ name: 'y', min: 0, max: 2 },
|
||||
]);
|
||||
const part = createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] });
|
||||
region.childIds.push(part.id);
|
||||
region.partMap['1,2'] = part.id;
|
||||
|
||||
expect(isCellOccupiedByRegion(region, [0, 0])).toBe(false);
|
||||
});
|
||||
|
||||
it('should work with empty region', () => {
|
||||
const region = createRegion('board', []);
|
||||
expect(isCellOccupiedByRegion(region, [0, 0])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPartAtPositionInRegion', () => {
|
||||
it('should return part at specified position', () => {
|
||||
const region = createRegion('board', []);
|
||||
const part1 = createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] });
|
||||
const parts: Record<string, Part> = { 'p1': part1 };
|
||||
region.childIds.push(part1.id);
|
||||
region.partMap['1,2'] = part1.id;
|
||||
|
||||
const result = getPartAtPositionInRegion(region, parts, [1, 2]);
|
||||
expect(result).toBe(part1);
|
||||
});
|
||||
|
||||
it('should return undefined for empty cell', () => {
|
||||
const region = createRegion('board', []);
|
||||
const part1 = createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] });
|
||||
const parts: Record<string, Part> = { 'p1': part1 };
|
||||
region.childIds.push(part1.id);
|
||||
region.partMap['1,2'] = part1.id;
|
||||
|
||||
const result = getPartAtPositionInRegion(region, parts, [0, 0]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should work with empty region', () => {
|
||||
const region = createRegion('board', []);
|
||||
const parts: Record<string, Part> = {};
|
||||
const result = getPartAtPositionInRegion(region, parts, [0, 0]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -16,7 +16,7 @@ import {
|
|||
PlayerType,
|
||||
getBoardRegion,
|
||||
} from '@/samples/boop';
|
||||
import {MutableSignal} from "@/utils/entity";
|
||||
import {MutableSignal} from "@/utils/mutable-signal";
|
||||
import {createGameContext} from "@/";
|
||||
import type { PromptEvent } from '@/utils/command';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue