refactor: clean up exports

This commit is contained in:
hypercross 2026-04-05 10:22:27 +08:00
parent cc7f302677
commit c4290841e5
8 changed files with 40 additions and 227 deletions

View File

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

View File

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

View File

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

View File

@ -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];
}

View File

@ -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) {

View File

@ -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';

View File

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

View File

@ -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
}
}