diff --git a/src/core/game-host.ts b/src/core/game-host.ts index a3c1133..6a95614 100644 --- a/src/core/game-host.ts +++ b/src/core/game-host.ts @@ -11,11 +11,6 @@ import {createGameContext, IGameContext} from './game'; export type GameHostStatus = 'created' | 'running' | 'disposed'; -export interface GameModule> { - registry: CommandRegistry>; - createInitialState: () => TState; -} - export class GameHost> { readonly context: IGameContext; readonly status: ReadonlySignal; @@ -152,11 +147,16 @@ export class GameHost> { } } +export type GameModule> = { + registry: CommandRegistry>; + createInitialState: () => TState; +} + export function createGameHost>( - module: GameModule, + gameModule: GameModule ): GameHost { return new GameHost( - module.registry, - module.createInitialState, + gameModule.registry, + gameModule.createInitialState, ); } diff --git a/src/core/game.ts b/src/core/game.ts index 207321c..1818fab 100644 --- a/src/core/game.ts +++ b/src/core/game.ts @@ -2,14 +2,11 @@ import {MutableSignal, mutableSignal} from "@/utils/mutable-signal"; import { Command, CommandRegistry, CommandResult, - CommandRunnerContext, CommandRunnerContextExport, + CommandRunnerContextExport, CommandSchema, createCommandRegistry, createCommandRunnerContext, - parseCommandSchema, - registerCommand } from "@/utils/command"; -import type { GameModule } from './game-host'; import {PromptValidator} from "@/utils/command/command-runner"; export interface IGameContext = {} > { @@ -66,28 +63,6 @@ export function createGameContext = {} >( return context; } -/** - * so that we can do `import * as tictactoe from './tic-tac-toe.ts';\n\n createGameContextFromModule(tictactoe);` - * @param module - */ -export function createGameContextFromModule = {} >( - module: { - registry: CommandRegistry>, - createInitialState: () => TState - }, -): IGameContext { - return createGameContext(module.registry, module.createInitialState); -} - export function createGameCommandRegistry = {} >() { return createCommandRegistry>(); -} - -export { GameHost, createGameHost } from './game-host'; -export type { GameHostStatus, GameModule } from './game-host'; - -export function createGameModule>( - module: GameModule -): GameModule { - return module; } \ No newline at end of file diff --git a/src/core/part-factory.ts b/src/core/part-factory.ts index e78111e..0fbb77b 100644 --- a/src/core/part-factory.ts +++ b/src/core/part-factory.ts @@ -1,116 +1,6 @@ import {Part} from "./part"; +import {Immutable} from "mutative"; -export type PartTemplate = Omit>, 'id'> & TMeta; - -export type PartPool = { - parts: Record>; - template: PartTemplate; - draw(): Part | undefined; - return(part: Part): void; - remaining(): number; -}; - -export function createPart( - template: PartTemplate, - id: string -): Part { - const part: Part = { - id, - regionId: template.regionId ?? '', - position: template.position ?? [], - ...template, - }; - return part; -} - -export function createParts( - template: PartTemplate, - count: number, - idPrefix: string -): Part[] { - const parts: Part[] = []; - for (let i = 0; i < count; i++) { - parts.push(createPart(template, `${idPrefix}-${i + 1}`)); - } - return parts; -} - -export function createPartPool( - template: PartTemplate, - count: number, - idPrefix: string -): PartPool { - const partsArray = createParts(template, count, idPrefix); - const parts: Record> = {}; - for (const part of partsArray) { - parts[part.id] = part; - } - const available = [...partsArray]; - - return { - parts, - template, - draw() { - return available.pop(); - }, - return(part: Part) { - part.regionId = ''; - part.position = []; - available.push(part); - }, - remaining() { - return available.length; - }, - }; -} - -export function mergePartPools( - ...pools: PartPool[] -): PartPool { - if (pools.length === 0) { - return createEmptyPartPool(); - } - - const allPartsArray = pools.flatMap(p => Object.values(p.parts)); - const allParts: Record> = {}; - 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) { - part.regionId = ''; - part.position = []; - available.push(part); - }, - remaining() { - return available.length; - }, - }; -} - -function createEmptyPartPool(): PartPool { - return { - parts: {}, - template: {} as PartTemplate, - draw() { - return undefined; - }, - return(_part: Part) { - // no-op for empty pool - }, - remaining() { - return 0; - }, - }; -} export function createPartsFromTable(items: T[], getId: (item: T, index: number) => string, getCount?: ((item: T) => number) | number){ const pool: Record> = {}; for (const entry of items) { @@ -121,9 +11,13 @@ export function createPartsFromTable(items: T[], getId: (item: T, index: numb id, regionId: '', position: [], - ...entry + ...entry as Immutable }; } } return pool; } + +export function createParts(item: T, getId: (index: number) => string, count?: number){ + return createPartsFromTable([item], (_,index) => getId(index), count); +} \ No newline at end of file diff --git a/src/core/part.ts b/src/core/part.ts index 415b51c..33307b5 100644 --- a/src/core/part.ts +++ b/src/core/part.ts @@ -1,5 +1,5 @@ import {RNG} from '@/utils/rng'; -import {Region} from '@/core/region'; +import {Immutable} from "mutative"; export type Part = { id: string; @@ -11,7 +11,7 @@ export type Part = { alignment?: string; regionId: string; position: number[]; -} & TMeta; +} & Immutable; export function flip(part: Part) { if(!part.sides) return; @@ -27,34 +27,3 @@ export function roll(part: Part, rng: RNG) { if(!part.sides) return; part.side = rng.nextInt(part.sides); } - -export function findPartById(parts: Record>, id: string): Part | undefined { - return parts[id]; -} - -export function isCellOccupied(parts: Record>, 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(parts: Record>, regionId: string, position: number[]): Part | 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(region: Region, parts: Record>, position: number[]): Part | undefined { - const posKey = position.join(','); - const partId = region.partMap[posKey]; - if (!partId) return undefined; - return parts[partId]; -} diff --git a/src/core/region.ts b/src/core/region.ts index e7fbdd6..37dbee4 100644 --- a/src/core/region.ts +++ b/src/core/region.ts @@ -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(region: Region, parts: Record>) { const map: Record = {}; for (const childId of region.childIds) { diff --git a/src/index.ts b/src/index.ts index 0d88be0..2557fd6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,19 +5,18 @@ // Core types 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 { createGameHost, createGameModule } from './core/game'; +export type { GameHost, GameHostStatus, GameModule } from './core/game-host'; +export { createGameHost } from './core/game-host'; 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 { createPart, createParts, createPartPool, mergePartPools, createPartsFromTable } from './core/part-factory'; +export { createParts, createPartsFromTable } from './core/part-factory'; 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 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 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 { createRNG, Mulberry32RNG } from './utils/rng'; diff --git a/src/samples/tic-tac-toe.ts b/src/samples/tic-tac-toe.ts index 28ac7f6..cff6288 100644 --- a/src/samples/tic-tac-toe.ts +++ b/src/samples/tic-tac-toe.ts @@ -1,5 +1,5 @@ import { - createGameCommandRegistry, Part, createRegion, createPart, isCellOccupied as isCellOccupiedUtil, + createGameCommandRegistry, Part, createRegion, IGameContext } from '@/index'; @@ -18,7 +18,6 @@ const WINNING_LINES: number[][][] = [ export type PlayerType = 'X' | 'O'; export type WinnerType = PlayerType | 'draw' | null; - type TicTacToePart = Part<{ player: PlayerType }>; export function createInitialState() { @@ -91,7 +90,7 @@ function isValidMove(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 { @@ -119,10 +118,10 @@ export function checkWinner(host: TicTacToeGame): WinnerType { export function placePiece(host: TicTacToeGame, row: number, col: number, player: PlayerType) { const board = host.value.board; const moveNumber = Object.keys(host.value.parts).length + 1; - const piece = createPart<{ player: PlayerType }>( - { regionId: 'board', position: [row, col], player }, - `piece-${player}-${moveNumber}` - ); + const piece: TicTacToePart = { + regionId: 'board', position: [row, col], player, + id: `piece-${player}-${moveNumber}` + } host.produce(state => { state.parts[piece.id] = piece; board.childIds.push(piece.id); diff --git a/src/utils/mutable-signal.ts b/src/utils/mutable-signal.ts index aaf33d2..aefe76c 100644 --- a/src/utils/mutable-signal.ts +++ b/src/utils/mutable-signal.ts @@ -1,4 +1,4 @@ -import {Signal, signal, SignalOptions} from '@preact/signals-core'; +import {Signal, SignalOptions} from '@preact/signals-core'; import {create} from 'mutative'; export class MutableSignal extends Signal { @@ -40,35 +40,3 @@ export class MutableSignal extends Signal { export function mutableSignal(initial?: T, options?: SignalOptions): MutableSignal { return new MutableSignal(initial, options); } - -export type EntityCollection = { - collection: Signal>>; - remove(...ids: string[]): void; - add(...entities: (T & {id: string})[]): void; - get(id: string): MutableSignal; -} - -export function createEntityCollection(): EntityCollection { - const collection = signal({} as Record>); - 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 - } -}