Compare commits
2 Commits
cc7f302677
...
8c2c6dc94c
| Author | SHA1 | Date |
|---|---|---|
|
|
8c2c6dc94c | |
|
|
c4290841e5 |
|
|
@ -11,11 +11,6 @@ import {createGameContext, IGameContext} from './game';
|
||||||
|
|
||||||
export type GameHostStatus = 'created' | 'running' | 'disposed';
|
export type GameHostStatus = 'created' | 'running' | 'disposed';
|
||||||
|
|
||||||
export interface GameModule<TState extends Record<string, unknown>> {
|
|
||||||
registry: CommandRegistry<IGameContext<TState>>;
|
|
||||||
createInitialState: () => TState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GameHost<TState extends Record<string, unknown>> {
|
export class GameHost<TState extends Record<string, unknown>> {
|
||||||
readonly context: IGameContext<TState>;
|
readonly context: IGameContext<TState>;
|
||||||
readonly status: ReadonlySignal<GameHostStatus>;
|
readonly status: ReadonlySignal<GameHostStatus>;
|
||||||
|
|
@ -152,11 +147,16 @@ export class GameHost<TState extends Record<string, unknown>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GameModule<TState extends Record<string, unknown>> = {
|
||||||
|
registry: CommandRegistry<IGameContext<TState>>;
|
||||||
|
createInitialState: () => TState;
|
||||||
|
}
|
||||||
|
|
||||||
export function createGameHost<TState extends Record<string, unknown>>(
|
export function createGameHost<TState extends Record<string, unknown>>(
|
||||||
module: GameModule<TState>,
|
gameModule: GameModule<TState>
|
||||||
): GameHost<TState> {
|
): GameHost<TState> {
|
||||||
return new GameHost(
|
return new GameHost(
|
||||||
module.registry,
|
gameModule.registry,
|
||||||
module.createInitialState,
|
gameModule.createInitialState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,11 @@ import {MutableSignal, mutableSignal} from "@/utils/mutable-signal";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandRegistry, CommandResult,
|
CommandRegistry, CommandResult,
|
||||||
CommandRunnerContext, CommandRunnerContextExport,
|
CommandRunnerContextExport,
|
||||||
CommandSchema,
|
CommandSchema,
|
||||||
createCommandRegistry,
|
createCommandRegistry,
|
||||||
createCommandRunnerContext,
|
createCommandRunnerContext,
|
||||||
parseCommandSchema,
|
|
||||||
registerCommand
|
|
||||||
} from "@/utils/command";
|
} from "@/utils/command";
|
||||||
import type { GameModule } from './game-host';
|
|
||||||
import {PromptValidator} from "@/utils/command/command-runner";
|
import {PromptValidator} from "@/utils/command/command-runner";
|
||||||
|
|
||||||
export interface IGameContext<TState extends Record<string, unknown> = {} > {
|
export interface IGameContext<TState extends Record<string, unknown> = {} > {
|
||||||
|
|
@ -66,28 +63,6 @@ export function createGameContext<TState extends Record<string, unknown> = {} >(
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* so that we can do `import * as tictactoe from './tic-tac-toe.ts';\n\n createGameContextFromModule(tictactoe);`
|
|
||||||
* @param module
|
|
||||||
*/
|
|
||||||
export function createGameContextFromModule<TState extends Record<string, unknown> = {} >(
|
|
||||||
module: {
|
|
||||||
registry: CommandRegistry<IGameContext<TState>>,
|
|
||||||
createInitialState: () => TState
|
|
||||||
},
|
|
||||||
): IGameContext<TState> {
|
|
||||||
return createGameContext(module.registry, module.createInitialState);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createGameCommandRegistry<TState extends Record<string, unknown> = {} >() {
|
export function createGameCommandRegistry<TState extends Record<string, unknown> = {} >() {
|
||||||
return createCommandRegistry<IGameContext<TState>>();
|
return createCommandRegistry<IGameContext<TState>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
export { GameHost, createGameHost } from './game-host';
|
|
||||||
export type { GameHostStatus, GameModule } from './game-host';
|
|
||||||
|
|
||||||
export function createGameModule<TState extends Record<string, unknown>>(
|
|
||||||
module: GameModule<TState>
|
|
||||||
): GameModule<TState> {
|
|
||||||
return module;
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +1,6 @@
|
||||||
import {Part} from "./part";
|
import {Part} from "./part";
|
||||||
|
import {Immutable} from "mutative";
|
||||||
|
|
||||||
export type PartTemplate<TMeta = {}> = Omit<Partial<Part<TMeta>>, 'id'> & TMeta;
|
|
||||||
|
|
||||||
export type PartPool<TMeta = {}> = {
|
|
||||||
parts: Record<string, Part<TMeta>>;
|
|
||||||
template: PartTemplate<TMeta>;
|
|
||||||
draw(): Part<TMeta> | undefined;
|
|
||||||
return(part: Part<TMeta>): void;
|
|
||||||
remaining(): number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createPart<TMeta = {}>(
|
|
||||||
template: PartTemplate<TMeta>,
|
|
||||||
id: string
|
|
||||||
): Part<TMeta> {
|
|
||||||
const part: Part<TMeta> = {
|
|
||||||
id,
|
|
||||||
regionId: template.regionId ?? '',
|
|
||||||
position: template.position ?? [],
|
|
||||||
...template,
|
|
||||||
};
|
|
||||||
return part;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createParts<TMeta = {}>(
|
|
||||||
template: PartTemplate<TMeta>,
|
|
||||||
count: number,
|
|
||||||
idPrefix: string
|
|
||||||
): Part<TMeta>[] {
|
|
||||||
const parts: Part<TMeta>[] = [];
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
parts.push(createPart(template, `${idPrefix}-${i + 1}`));
|
|
||||||
}
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPartPool<TMeta = {}>(
|
|
||||||
template: PartTemplate<TMeta>,
|
|
||||||
count: number,
|
|
||||||
idPrefix: string
|
|
||||||
): PartPool<TMeta> {
|
|
||||||
const partsArray = createParts(template, count, idPrefix);
|
|
||||||
const parts: Record<string, Part<TMeta>> = {};
|
|
||||||
for (const part of partsArray) {
|
|
||||||
parts[part.id] = part;
|
|
||||||
}
|
|
||||||
const available = [...partsArray];
|
|
||||||
|
|
||||||
return {
|
|
||||||
parts,
|
|
||||||
template,
|
|
||||||
draw() {
|
|
||||||
return available.pop();
|
|
||||||
},
|
|
||||||
return(part: Part<TMeta>) {
|
|
||||||
part.regionId = '';
|
|
||||||
part.position = [];
|
|
||||||
available.push(part);
|
|
||||||
},
|
|
||||||
remaining() {
|
|
||||||
return available.length;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergePartPools<TMeta = {}>(
|
|
||||||
...pools: PartPool<TMeta>[]
|
|
||||||
): PartPool<TMeta> {
|
|
||||||
if (pools.length === 0) {
|
|
||||||
return createEmptyPartPool<TMeta>();
|
|
||||||
}
|
|
||||||
|
|
||||||
const allPartsArray = pools.flatMap(p => Object.values(p.parts));
|
|
||||||
const allParts: Record<string, Part<TMeta>> = {};
|
|
||||||
for (const part of allPartsArray) {
|
|
||||||
allParts[part.id] = part;
|
|
||||||
}
|
|
||||||
const template = pools[0].template;
|
|
||||||
const available = allPartsArray.filter(p => p.regionId === '');
|
|
||||||
|
|
||||||
return {
|
|
||||||
parts: allParts,
|
|
||||||
template,
|
|
||||||
draw() {
|
|
||||||
return available.pop();
|
|
||||||
},
|
|
||||||
return(part: Part<TMeta>) {
|
|
||||||
part.regionId = '';
|
|
||||||
part.position = [];
|
|
||||||
available.push(part);
|
|
||||||
},
|
|
||||||
remaining() {
|
|
||||||
return available.length;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export function createPartsFromTable<T>(items: T[], getId: (item: T, index: number) => string, getCount?: ((item: T) => number) | number){
|
export function createPartsFromTable<T>(items: T[], getId: (item: T, index: number) => string, getCount?: ((item: T) => number) | number){
|
||||||
const pool: Record<string, Part<T>> = {};
|
const pool: Record<string, Part<T>> = {};
|
||||||
for (const entry of items) {
|
for (const entry of items) {
|
||||||
|
|
@ -121,9 +11,13 @@ export function createPartsFromTable<T>(items: T[], getId: (item: T, index: numb
|
||||||
id,
|
id,
|
||||||
regionId: '',
|
regionId: '',
|
||||||
position: [],
|
position: [],
|
||||||
...entry
|
...entry as Immutable<T>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createParts<T>(item: T, getId: (index: number) => string, count?: number){
|
||||||
|
return createPartsFromTable([item], (_,index) => getId(index), count);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {RNG} from '@/utils/rng';
|
import {RNG} from '@/utils/rng';
|
||||||
import {Region} from '@/core/region';
|
import {Immutable} from "mutative";
|
||||||
|
|
||||||
export type Part<TMeta = {}> = {
|
export type Part<TMeta = {}> = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -11,7 +11,7 @@ export type Part<TMeta = {}> = {
|
||||||
alignment?: string;
|
alignment?: string;
|
||||||
regionId: string;
|
regionId: string;
|
||||||
position: number[];
|
position: number[];
|
||||||
} & TMeta;
|
} & Immutable<TMeta>;
|
||||||
|
|
||||||
export function flip<TMeta>(part: Part<TMeta>) {
|
export function flip<TMeta>(part: Part<TMeta>) {
|
||||||
if(!part.sides) return;
|
if(!part.sides) return;
|
||||||
|
|
@ -27,34 +27,3 @@ export function roll<TMeta>(part: Part<TMeta>, rng: RNG) {
|
||||||
if(!part.sides) return;
|
if(!part.sides) return;
|
||||||
part.side = rng.nextInt(part.sides);
|
part.side = rng.nextInt(part.sides);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findPartById<TMeta>(parts: Record<string, Part<TMeta>>, id: string): Part<TMeta> | undefined {
|
|
||||||
return parts[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCellOccupied<TMeta>(parts: Record<string, Part<TMeta>>, regionId: string, position: number[]): boolean {
|
|
||||||
const posKey = position.join(',');
|
|
||||||
return Object.values(parts).some(p => p.regionId === regionId && p.position.join(',') === posKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPartAtPosition<TMeta>(parts: Record<string, Part<TMeta>>, regionId: string, position: number[]): Part<TMeta> | undefined {
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,15 @@ export function createRegion(id: string, axes: RegionAxis[]): Region {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createRegionAxis(name: string, min?: number, max?: number, align?: 'start' | 'end' | 'center'): RegionAxis {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
align,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildPartMap<TMeta>(region: Region, parts: Record<string, Part<TMeta>>) {
|
function buildPartMap<TMeta>(region: Region, parts: Record<string, Part<TMeta>>) {
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, string> = {};
|
||||||
for (const childId of region.childIds) {
|
for (const childId of region.childIds) {
|
||||||
|
|
|
||||||
15
src/index.ts
15
src/index.ts
|
|
@ -5,19 +5,18 @@
|
||||||
|
|
||||||
// Core types
|
// Core types
|
||||||
export type { IGameContext } from './core/game';
|
export type { IGameContext } from './core/game';
|
||||||
export { createGameContext, createGameCommandRegistry } from './core/game';
|
export { createGameCommandRegistry } from './core/game';
|
||||||
|
|
||||||
export type { GameHost, GameHostStatus, GameModule } from './core/game';
|
export type { GameHost, GameHostStatus, GameModule } from './core/game-host';
|
||||||
export { createGameHost, createGameModule } from './core/game';
|
export { createGameHost } from './core/game-host';
|
||||||
|
|
||||||
export type { Part } from './core/part';
|
export type { Part } from './core/part';
|
||||||
export { flip, flipTo, roll, findPartById, isCellOccupied, getPartAtPosition, isCellOccupiedByRegion, getPartAtPositionInRegion } from './core/part';
|
export { flip, flipTo, roll } from './core/part';
|
||||||
|
|
||||||
export type { PartTemplate, PartPool } from './core/part-factory';
|
export { createParts, createPartsFromTable } from './core/part-factory';
|
||||||
export { createPart, createParts, createPartPool, mergePartPools, createPartsFromTable } from './core/part-factory';
|
|
||||||
|
|
||||||
export type { Region, RegionAxis } from './core/region';
|
export type { Region, RegionAxis } from './core/region';
|
||||||
export { createRegion, applyAlign, shuffle, moveToRegion } from './core/region';
|
export { createRegion, createRegionAxis, applyAlign, shuffle, moveToRegion } from './core/region';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
export type { Command, CommandResult, CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema } from './utils/command';
|
export type { Command, CommandResult, CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema } from './utils/command';
|
||||||
|
|
@ -27,7 +26,7 @@ export type { CommandRunner, CommandRunnerHandler, CommandRunnerContext, PromptE
|
||||||
export { createCommandRegistry, registerCommand, unregisterCommand, hasCommand, getCommand, runCommand, runCommandParsed, createCommandRunnerContext, type CommandRegistry, type CommandRunnerContextExport } from './utils/command';
|
export { createCommandRegistry, registerCommand, unregisterCommand, hasCommand, getCommand, runCommand, runCommandParsed, createCommandRunnerContext, type CommandRegistry, type CommandRunnerContextExport } from './utils/command';
|
||||||
|
|
||||||
export type { MutableSignal } from './utils/mutable-signal';
|
export type { MutableSignal } from './utils/mutable-signal';
|
||||||
export { mutableSignal, createEntityCollection } from './utils/mutable-signal';
|
export { mutableSignal } from './utils/mutable-signal';
|
||||||
|
|
||||||
export type { RNG } from './utils/rng';
|
export type { RNG } from './utils/rng';
|
||||||
export { createRNG, Mulberry32RNG } from './utils/rng';
|
export { createRNG, Mulberry32RNG } from './utils/rng';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {
|
import {
|
||||||
createGameCommandRegistry, Part, createRegion, createPart, isCellOccupied as isCellOccupiedUtil,
|
createGameCommandRegistry, Part, createRegion,
|
||||||
IGameContext
|
IGameContext
|
||||||
} from '@/index';
|
} from '@/index';
|
||||||
|
|
||||||
|
|
@ -18,7 +18,6 @@ const WINNING_LINES: number[][][] = [
|
||||||
|
|
||||||
export type PlayerType = 'X' | 'O';
|
export type PlayerType = 'X' | 'O';
|
||||||
export type WinnerType = PlayerType | 'draw' | null;
|
export type WinnerType = PlayerType | 'draw' | null;
|
||||||
|
|
||||||
type TicTacToePart = Part<{ player: PlayerType }>;
|
type TicTacToePart = Part<{ player: PlayerType }>;
|
||||||
|
|
||||||
export function createInitialState() {
|
export function createInitialState() {
|
||||||
|
|
@ -91,7 +90,7 @@ function isValidMove(row: number, col: number): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCellOccupied(host: TicTacToeGame, row: number, col: number): boolean {
|
export function isCellOccupied(host: TicTacToeGame, row: number, col: number): boolean {
|
||||||
return isCellOccupiedUtil(host.value.parts, 'board', [row, col]);
|
return !!host.value.board.partMap[`${row},${col}`];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasWinningLine(positions: number[][]): boolean {
|
export function hasWinningLine(positions: number[][]): boolean {
|
||||||
|
|
@ -119,10 +118,10 @@ export function checkWinner(host: TicTacToeGame): WinnerType {
|
||||||
export function placePiece(host: TicTacToeGame, row: number, col: number, player: PlayerType) {
|
export function placePiece(host: TicTacToeGame, row: number, col: number, player: PlayerType) {
|
||||||
const board = host.value.board;
|
const board = host.value.board;
|
||||||
const moveNumber = Object.keys(host.value.parts).length + 1;
|
const moveNumber = Object.keys(host.value.parts).length + 1;
|
||||||
const piece = createPart<{ player: PlayerType }>(
|
const piece: TicTacToePart = {
|
||||||
{ regionId: 'board', position: [row, col], player },
|
regionId: 'board', position: [row, col], player,
|
||||||
`piece-${player}-${moveNumber}`
|
id: `piece-${player}-${moveNumber}`
|
||||||
);
|
}
|
||||||
host.produce(state => {
|
host.produce(state => {
|
||||||
state.parts[piece.id] = piece;
|
state.parts[piece.id] = piece;
|
||||||
board.childIds.push(piece.id);
|
board.childIds.push(piece.id);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {Signal, signal, SignalOptions} from '@preact/signals-core';
|
import {Signal, SignalOptions} from '@preact/signals-core';
|
||||||
import {create} from 'mutative';
|
import {create} from 'mutative';
|
||||||
|
|
||||||
export class MutableSignal<T> extends Signal<T> {
|
export class MutableSignal<T> extends Signal<T> {
|
||||||
|
|
@ -40,35 +40,3 @@ export class MutableSignal<T> extends Signal<T> {
|
||||||
export function mutableSignal<T>(initial?: T, options?: SignalOptions<T>): MutableSignal<T> {
|
export function mutableSignal<T>(initial?: T, options?: SignalOptions<T>): MutableSignal<T> {
|
||||||
return new MutableSignal<T>(initial, options);
|
return new MutableSignal<T>(initial, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EntityCollection<T> = {
|
|
||||||
collection: Signal<Record<string, MutableSignal<T>>>;
|
|
||||||
remove(...ids: string[]): void;
|
|
||||||
add(...entities: (T & {id: string})[]): void;
|
|
||||||
get(id: string): MutableSignal<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createEntityCollection<T>(): EntityCollection<T> {
|
|
||||||
const collection = signal({} as Record<string, MutableSignal<T>>);
|
|
||||||
const remove = (...ids: string[]) => {
|
|
||||||
collection.value = Object.fromEntries(
|
|
||||||
Object.entries(collection.value).filter(([id]) => !ids.includes(id)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const add = (...entities: (T & {id: string})[]) => {
|
|
||||||
collection.value = {
|
|
||||||
...collection.value,
|
|
||||||
...Object.fromEntries(entities.map((e) => [e.id, mutableSignal(e)])),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const get = (id: string) => collection.value[id];
|
|
||||||
|
|
||||||
return {
|
|
||||||
collection,
|
|
||||||
remove,
|
|
||||||
add,
|
|
||||||
get
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,171 +1,71 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { createPart, createParts, createPartPool, mergePartPools, PartPool } from '@/core/part-factory';
|
import { createParts, createPartsFromTable } 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', () => {
|
describe('createParts', () => {
|
||||||
it('should create multiple parts with auto-generated IDs', () => {
|
it('should create multiple parts with auto-generated IDs', () => {
|
||||||
const parts = createParts(
|
const parts = createParts(
|
||||||
{ regionId: 'deck', position: [] },
|
{ regionId: 'deck', position: [] },
|
||||||
3,
|
(i) => `card-${i + 1}`,
|
||||||
'card'
|
3
|
||||||
);
|
);
|
||||||
expect(parts.length).toBe(3);
|
expect(Object.keys(parts).length).toBe(3);
|
||||||
expect(parts[0].id).toBe('card-1');
|
expect(parts['card-1']).toBeDefined();
|
||||||
expect(parts[1].id).toBe('card-2');
|
expect(parts['card-2']).toBeDefined();
|
||||||
expect(parts[2].id).toBe('card-3');
|
expect(parts['card-3']).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create parts with identical properties', () => {
|
it('should create parts with identical properties', () => {
|
||||||
const parts = createParts(
|
const parts = createParts(
|
||||||
{ regionId: 'deck', position: [], type: 'token' },
|
{ regionId: 'deck', position: [], type: 'token' as string },
|
||||||
2,
|
(i) => `token-${i + 1}`,
|
||||||
'token'
|
2
|
||||||
);
|
);
|
||||||
expect(parts[0].regionId).toBe('deck');
|
expect(parts['token-1'].regionId).toBe('deck');
|
||||||
expect(parts[1].regionId).toBe('deck');
|
expect(parts['token-2'].regionId).toBe('deck');
|
||||||
expect(parts[0].type).toBe('token');
|
expect(parts['token-1'].type).toBe('token');
|
||||||
expect(parts[1].type).toBe('token');
|
expect(parts['token-2'].type).toBe('token');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create zero parts when count is 0', () => {
|
it('should create one part when count is not provided', () => {
|
||||||
const parts = createParts({}, 0, 'empty');
|
const parts = createParts({}, (i) => `item-${i}`);
|
||||||
expect(parts.length).toBe(0);
|
expect(Object.keys(parts).length).toBe(1);
|
||||||
|
expect(parts['item-0']).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createPartPool', () => {
|
describe('createPartsFromTable', () => {
|
||||||
it('should create a pool with specified count', () => {
|
it('should create parts from table entries', () => {
|
||||||
const pool = createPartPool(
|
const items = [
|
||||||
{ regionId: 'supply', position: [] },
|
{ type: 'kitten', count: 13 },
|
||||||
5,
|
{ type: 'cat', count: 6 },
|
||||||
'token'
|
];
|
||||||
|
const parts = createPartsFromTable(
|
||||||
|
items,
|
||||||
|
(item, index) => `${item.type}-${index + 1}`,
|
||||||
|
(item) => item.count
|
||||||
);
|
);
|
||||||
expect(pool.remaining()).toBe(5);
|
expect(Object.keys(parts).length).toBe(19);
|
||||||
expect(Object.keys(pool.parts).length).toBe(5);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate parts with correct IDs', () => {
|
it('should create one part per item when count is not specified', () => {
|
||||||
const pool = createPartPool({}, 3, 'piece');
|
const items = [{ name: 'A' }, { name: 'B' }];
|
||||||
expect(pool.parts['piece-1']).toBeDefined();
|
const parts = createPartsFromTable(
|
||||||
expect(pool.parts['piece-2']).toBeDefined();
|
items,
|
||||||
expect(pool.parts['piece-3']).toBeDefined();
|
(item, index) => `${item.name}-${index}`
|
||||||
|
);
|
||||||
|
expect(Object.keys(parts).length).toBe(2);
|
||||||
|
// index is 0 for each item since count defaults to 1
|
||||||
|
expect(parts['A-0']).toBeDefined();
|
||||||
|
expect(parts['B-0']).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should draw parts from the pool', () => {
|
it('should use fixed count when provided as number', () => {
|
||||||
const pool = createPartPool({}, 2, 'card');
|
const items = [{ name: 'A' }, { name: 'B' }];
|
||||||
const drawn = pool.draw();
|
const parts = createPartsFromTable(
|
||||||
expect(drawn).toBeDefined();
|
items,
|
||||||
expect(drawn!.id).toBe('card-2');
|
(item, index) => `${item.name}-${index}`,
|
||||||
expect(pool.remaining()).toBe(1);
|
2
|
||||||
});
|
);
|
||||||
|
expect(Object.keys(parts).length).toBe(4);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { flip, flipTo, roll, findPartById, isCellOccupied, getPartAtPosition, isCellOccupiedByRegion, getPartAtPositionInRegion, Part } from '@/core/part';
|
import { flip, flipTo, roll, Part } from '@/core/part';
|
||||||
import { createRegion } from '@/core/region';
|
|
||||||
import { createRNG } from '@/utils/rng';
|
import { createRNG } from '@/utils/rng';
|
||||||
|
|
||||||
function createTestPart<TMeta = {}>(overrides: Partial<Part<TMeta>> & TMeta): Part<TMeta> {
|
function createTestPart<TMeta = {}>(overrides: Partial<Part<TMeta>> & TMeta): Part<TMeta> {
|
||||||
|
|
@ -92,158 +91,3 @@ describe('roll', () => {
|
||||||
expect(part1.side).toBe(part2.side);
|
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { registry, createInitialState, BoopState } from '@/samples/boop';
|
import { registry, createInitialState, BoopState } from '@/samples/boop';
|
||||||
import { createGameContext } from '@/index';
|
import { createGameContext } from '@/core/game';
|
||||||
import type { PromptEvent } from '@/utils/command';
|
import type { PromptEvent } from '@/utils/command';
|
||||||
|
|
||||||
function createTestContext() {
|
function createTestContext() {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
WinnerType,
|
WinnerType,
|
||||||
PlayerType
|
PlayerType
|
||||||
} from '@/samples/tic-tac-toe';
|
} from '@/samples/tic-tac-toe';
|
||||||
import { createGameContext } from '@/index';
|
import { createGameContext } from '@/core/game';
|
||||||
import type { PromptEvent } from '@/utils/command';
|
import type { PromptEvent } from '@/utils/command';
|
||||||
|
|
||||||
function createTestContext() {
|
function createTestContext() {
|
||||||
|
|
|
||||||
|
|
@ -1,110 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { createEntityCollection, MutableSignal, mutableSignal } from '@/utils/mutable-signal';
|
import { MutableSignal, mutableSignal } from '@/utils/mutable-signal';
|
||||||
|
|
||||||
type TestEntity = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
value: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('createEntityCollection', () => {
|
|
||||||
it('should create empty collection', () => {
|
|
||||||
const collection = createEntityCollection<TestEntity>();
|
|
||||||
expect(collection.collection.value).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add single entity', () => {
|
|
||||||
const collection = createEntityCollection<TestEntity>();
|
|
||||||
const testEntity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 };
|
|
||||||
|
|
||||||
collection.add(testEntity);
|
|
||||||
|
|
||||||
expect(collection.collection.value).toHaveProperty('e1');
|
|
||||||
expect(collection.get('e1').value).toEqual(testEntity);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add multiple entities', () => {
|
|
||||||
const collection = createEntityCollection<TestEntity>();
|
|
||||||
const entity1: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 };
|
|
||||||
const entity2: TestEntity = { id: 'e2', name: 'Entity 2', value: 20 };
|
|
||||||
const entity3: TestEntity = { id: 'e3', name: 'Entity 3', value: 30 };
|
|
||||||
|
|
||||||
collection.add(entity1, entity2, entity3);
|
|
||||||
|
|
||||||
expect(Object.keys(collection.collection.value)).toHaveLength(3);
|
|
||||||
expect(collection.get('e1').value.name).toBe('Entity 1');
|
|
||||||
expect(collection.get('e2').value.name).toBe('Entity 2');
|
|
||||||
expect(collection.get('e3').value.name).toBe('Entity 3');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove single entity', () => {
|
|
||||||
const collection = createEntityCollection<TestEntity>();
|
|
||||||
const entity1: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 };
|
|
||||||
const entity2: TestEntity = { id: 'e2', name: 'Entity 2', value: 20 };
|
|
||||||
|
|
||||||
collection.add(entity1, entity2);
|
|
||||||
collection.remove('e1');
|
|
||||||
|
|
||||||
expect(Object.keys(collection.collection.value)).toHaveLength(1);
|
|
||||||
expect(collection.collection.value).not.toHaveProperty('e1');
|
|
||||||
expect(collection.collection.value).toHaveProperty('e2');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove multiple entities', () => {
|
|
||||||
const collection = createEntityCollection<TestEntity>();
|
|
||||||
const entity1: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 };
|
|
||||||
const entity2: TestEntity = { id: 'e2', name: 'Entity 2', value: 20 };
|
|
||||||
const entity3: TestEntity = { id: 'e3', name: 'Entity 3', value: 30 };
|
|
||||||
|
|
||||||
collection.add(entity1, entity2, entity3);
|
|
||||||
collection.remove('e1', 'e3');
|
|
||||||
|
|
||||||
expect(Object.keys(collection.collection.value)).toHaveLength(1);
|
|
||||||
expect(collection.collection.value).toHaveProperty('e2');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update entity via accessor', () => {
|
|
||||||
const collection = createEntityCollection<TestEntity>();
|
|
||||||
const testEntity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 };
|
|
||||||
|
|
||||||
collection.add(testEntity);
|
|
||||||
|
|
||||||
const accessor = collection.get('e1');
|
|
||||||
accessor.value = { ...testEntity, value: 100, name: 'Updated' };
|
|
||||||
|
|
||||||
expect(collection.get('e1').value.value).toBe(100);
|
|
||||||
expect(collection.get('e1').value.name).toBe('Updated');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined for non-existent entity', () => {
|
|
||||||
const collection = createEntityCollection<TestEntity>();
|
|
||||||
|
|
||||||
expect(collection.get('nonexistent')).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle removing non-existent entity', () => {
|
|
||||||
const collection = createEntityCollection<TestEntity>();
|
|
||||||
const testEntity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 };
|
|
||||||
|
|
||||||
collection.add(testEntity);
|
|
||||||
collection.remove('nonexistent');
|
|
||||||
|
|
||||||
expect(Object.keys(collection.collection.value)).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with reactive updates', () => {
|
|
||||||
const collection = createEntityCollection<TestEntity>();
|
|
||||||
const testEntity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 };
|
|
||||||
|
|
||||||
collection.add(testEntity);
|
|
||||||
|
|
||||||
const accessor = collection.get('e1');
|
|
||||||
expect(accessor.value.value).toBe(10);
|
|
||||||
|
|
||||||
accessor.value = { ...testEntity, value: 50 };
|
|
||||||
expect(accessor.value.value).toBe(50);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('MutableSignal', () => {
|
describe('MutableSignal', () => {
|
||||||
it('should create signal with initial value', () => {
|
it('should create signal with initial value', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue