From 6740584fc88bc82117ac516fbe8223eb4fc2780a Mon Sep 17 00:00:00 2001 From: hypercross Date: Wed, 1 Apr 2026 13:36:16 +0800 Subject: [PATCH] refactor: mass refactoring --- src/core/GameState.ts | 312 ------------------------------------------ src/core/Part.ts | 103 -------------- src/core/Placement.ts | 88 ------------ src/core/Region.ts | 155 --------------------- src/core/context.ts | 38 +++++ src/core/part.ts | 33 +++++ src/core/region.ts | 41 ++++++ src/core/rule.ts | 32 +++++ src/utils/command.ts | 10 ++ src/utils/entity.ts | 46 +++++++ src/utils/rng.ts | 12 ++ 11 files changed, 212 insertions(+), 658 deletions(-) delete mode 100644 src/core/GameState.ts delete mode 100644 src/core/Part.ts delete mode 100644 src/core/Placement.ts delete mode 100644 src/core/Region.ts create mode 100644 src/core/context.ts create mode 100644 src/core/part.ts create mode 100644 src/core/region.ts create mode 100644 src/core/rule.ts create mode 100644 src/utils/command.ts create mode 100644 src/utils/entity.ts create mode 100644 src/utils/rng.ts diff --git a/src/core/GameState.ts b/src/core/GameState.ts deleted file mode 100644 index 28fc051..0000000 --- a/src/core/GameState.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { signal, Signal, computed } from '@preact/signals-core'; -import type { Part } from './Part'; -import type { Placement } from './Placement'; -import type { Region } from './Region'; - -/** - * 游戏状态 - */ -export interface GameStateData { - id: string; - name: string; - phase?: string; - metadata?: Record; -} - -/** - * 游戏状态类 - * 统一管理所有 Parts, Regions, Placements - */ -export class GameState { - /** 游戏基本信息 */ - data: Signal; - - /** Parts 存储 */ - parts: Signal>; - - /** Regions 存储 */ - regions: Signal>; - - /** Placements 存储 */ - placements: Signal>; - - constructor(gameData: GameStateData) { - this.data = signal(gameData); - this.parts = signal(new Map()); - this.regions = signal(new Map()); - this.placements = signal(new Map()); - } - - // ========== Part 相关方法 ========== - - /** - * 添加 Part - */ - addPart(part: Part): void { - const parts = new Map(this.parts.value); - parts.set(part.id, part); - this.parts.value = parts; - } - - /** - * 获取 Part - */ - getPart(partId: string): Part | undefined { - return this.parts.value.get(partId); - } - - /** - * 移除 Part - */ - removePart(partId: string): void { - const parts = new Map(this.parts.value); - parts.delete(partId); - this.parts.value = parts; - } - - /** - * 更新 Part - */ - updatePart(partId: string, updates: Partial): void { - const part = this.parts.value.get(partId); - if (part) { - const updated = { ...part, ...updates } as T; - const parts = new Map(this.parts.value); - parts.set(partId, updated); - this.parts.value = parts; - } - } - - // ========== Region 相关方法 ========== - - /** - * 添加 Region - */ - addRegion(region: Region): void { - const regions = new Map(this.regions.value); - regions.set(region.id, region); - this.regions.value = regions; - } - - /** - * 获取 Region - */ - getRegion(regionId: string): Region | undefined { - return this.regions.value.get(regionId); - } - - /** - * 移除 Region - */ - removeRegion(regionId: string): void { - const regions = new Map(this.regions.value); - regions.delete(regionId); - this.regions.value = regions; - } - - // ========== Placement 相关方法 ========== - - /** - * 添加 Placement - */ - addPlacement(placement: Placement): void { - const placements = new Map(this.placements.value); - placements.set(placement.id, placement); - this.placements.value = placements; - } - - /** - * 获取 Placement - */ - getPlacement(placementId: string): Placement | undefined { - return this.placements.value.get(placementId); - } - - /** - * 移除 Placement - */ - removePlacement(placementId: string): void { - const placement = this.placements.value.get(placementId); - if (placement) { - // 从 Region 中移除 - const region = this.regions.value.get(placement.regionId); - if (region) { - const current = region.placements.value; - const index = current.indexOf(placementId); - if (index !== -1) { - const updated = [...current]; - updated.splice(index, 1); - region.placements.value = updated; - } - - // 如果是 keyed region,清理 slot - if (region.type === 'keyed' && region.slots) { - const slots = new Map(region.slots.value); - for (const [key, value] of slots.entries()) { - if (value === placementId) { - slots.set(key, null); - break; - } - } - region.slots.value = slots; - } - } - } - - const placements = new Map(this.placements.value); - placements.delete(placementId); - this.placements.value = placements; - } - - /** - * 更新 Placement - */ - updatePlacement(placementId: string, updates: Partial): void { - const placement = this.placements.value.get(placementId); - if (placement) { - const updated = { ...placement, ...updates }; - const placements = new Map(this.placements.value); - placements.set(placementId, updated); - this.placements.value = placements; - } - } - - /** - * 更新 Placement 的 Part 引用 - */ - updatePlacementPart(placementId: string, part: Part | null): void { - const placement = this.placements.value.get(placementId); - if (placement) { - const updated = { ...placement, part }; - const placements = new Map(this.placements.value); - placements.set(placementId, updated); - this.placements.value = placements; - } - } - - /** - * 移动 Placement 到另一个 Region - */ - movePlacement(placementId: string, targetRegionId: string, key?: string): void { - const placement = this.placements.value.get(placementId); - if (!placement) { - throw new Error(`Placement ${placementId} not found`); - } - - const sourceRegion = this.regions.value.get(placement.regionId); - const targetRegion = this.regions.value.get(targetRegionId); - - if (!targetRegion) { - throw new Error(`Region ${targetRegionId} not found`); - } - - // 从源 Region 移除 - if (sourceRegion) { - const current = sourceRegion.placements.value; - const index = current.indexOf(placementId); - if (index !== -1) { - const updated = [...current]; - updated.splice(index, 1); - sourceRegion.placements.value = updated; - } - - // 清理源 keyed region 的 slot - if (sourceRegion.type === 'keyed' && sourceRegion.slots) { - const slots = new Map(sourceRegion.slots.value); - for (const [k, value] of slots.entries()) { - if (value === placementId) { - slots.set(k, null); - break; - } - } - sourceRegion.slots.value = slots; - } - } - - // 添加到目标 Region - if (targetRegion.type === 'keyed') { - if (key === undefined) { - throw new Error('Key is required for keyed regions'); - } - if (targetRegion.slots) { - const slots = new Map(targetRegion.slots.value); - slots.set(key, placementId); - targetRegion.slots.value = slots; - } - } - - const targetPlacements = [...targetRegion.placements.value, placementId]; - targetRegion.placements.value = targetPlacements; - - // 更新 Placement 的 regionId - const updated = { ...placement, regionId: targetRegionId }; - if (key !== undefined) { - updated.metadata = { ...updated.metadata, key }; - } - const placements = new Map(this.placements.value); - placements.set(placementId, updated); - this.placements.value = placements; - } - - // ========== 计算属性 ========== - - /** - * 获取 Region 中的所有 Placements - */ - getPlacementsInRegion(regionId: string): Placement[] { - const region = this.regions.value.get(regionId); - if (!region) { - return []; - } - - const placementIds = region.placements.value; - return placementIds - .map((id) => this.placements.value.get(id)) - .filter((p): p is Placement => p !== undefined); - } - - /** - * 获取 Part 的所有 Placements - */ - getPlacementsOfPart(partId: string): Placement[] { - const allPlacements = Array.from(this.placements.value.values()); - return allPlacements.filter((p) => p.partId === partId); - } - - /** - * 创建计算信号:获取 Region 中的 Placement 数量 - */ - createPlacementCountSignal(regionId: string): Signal { - const region = this.regions.value.get(regionId); - if (!region) { - return signal(0); - } - return computed(() => region.placements.value.length); - } - - // ========== 游戏状态管理 ========== - - /** - * 更新游戏阶段 - */ - setPhase(phase: string): void { - this.data.value = { ...this.data.value, phase }; - } - - /** - * 更新游戏元数据 - */ - updateMetadata(updates: Record): void { - this.data.value = { - ...this.data.value, - metadata: { ...this.data.value.metadata, ...updates }, - }; - } -} - -/** - * 创建游戏状态 - */ -export function createGameState(data: GameStateData): GameState { - return new GameState(data); -} diff --git a/src/core/Part.ts b/src/core/Part.ts deleted file mode 100644 index 56c4156..0000000 --- a/src/core/Part.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { signal } from '@preact/signals-core'; - -/** - * Part 类型枚举 - */ -export enum PartType { - Meeple = 'meeple', - Card = 'card', - Tile = 'tile', -} - -/** - * Part 的基础属性 - */ -export interface PartBase { - id: string; - type: PartType; - name?: string; - metadata?: Record; -} - -/** - * Meeple 特有属性 - */ -export interface MeeplePart extends PartBase { - type: PartType.Meeple; - color: string; -} - -/** - * Card 特有属性 - */ -export interface CardPart extends PartBase { - type: PartType.Card; - suit?: string; - value?: number | string; -} - -/** - * Tile 特有属性 - */ -export interface TilePart extends PartBase { - type: PartType.Tile; - pattern?: string; - rotation?: number; -} - -/** - * Part 联合类型 - */ -export type Part = MeeplePart | CardPart | TilePart; - -/** - * Part 信号类型 - */ -export type PartSignal = ReturnType>; - -/** - * 创建 Part - */ -export function createPart(part: T): T { - return part; -} - -/** - * 创建 Meeple Part - */ -export function createMeeple(id: string, color: string, options?: { name?: string; metadata?: Record }): MeeplePart { - return { - id, - type: PartType.Meeple, - color, - ...options, - }; -} - -/** - * 创建 Card Part - */ -export function createCard( - id: string, - options?: { suit?: string; value?: number | string; name?: string; metadata?: Record } -): CardPart { - return { - id, - type: PartType.Card, - ...options, - }; -} - -/** - * 创建 Tile Part - */ -export function createTile( - id: string, - options?: { pattern?: string; rotation?: number; name?: string; metadata?: Record } -): TilePart { - return { - id, - type: PartType.Tile, - ...options, - }; -} diff --git a/src/core/Placement.ts b/src/core/Placement.ts deleted file mode 100644 index 5286325..0000000 --- a/src/core/Placement.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { signal, Signal } from '@preact/signals-core'; -import type { Part } from './Part'; - -/** - * Placement 的位置信息 - */ -export interface Position { - x: number; - y: number; -} - -/** - * Placement 属性 - */ -export interface PlacementProperties { - id: string; - partId: string; - regionId: string; - position?: Position; - rotation?: number; - faceUp?: boolean; - metadata?: Record; -} - -/** - * Placement 类型 - */ -export interface Placement extends PlacementProperties { - part: Part | null; -} - -/** - * Placement 信号类型 - */ -export type PlacementSignal = Signal; - -/** - * 创建 Placement - */ -export function createPlacement(properties: { - id: string; - partId: string; - regionId: string; - part: Part; - position?: Position; - rotation?: number; - faceUp?: boolean; - metadata?: Record; -}): Placement { - return { - id: properties.id, - partId: properties.partId, - regionId: properties.regionId, - part: properties.part, - position: properties.position, - rotation: properties.rotation ?? 0, - faceUp: properties.faceUp ?? true, - metadata: properties.metadata, - }; -} - -/** - * 更新 Placement 的 Part 引用 - */ -export function updatePlacementPart(placement: Placement, part: Part | null): void { - placement.part = part; -} - -/** - * 更新 Placement 的位置 - */ -export function updatePlacementPosition(placement: Placement, position: Position): void { - placement.position = position; -} - -/** - * 更新 Placement 的旋转角度 - */ -export function updatePlacementRotation(placement: Placement, rotation: number): void { - placement.rotation = rotation; -} - -/** - * 翻转 Placement - */ -export function flipPlacement(placement: Placement): void { - placement.faceUp = !placement.faceUp; -} diff --git a/src/core/Region.ts b/src/core/Region.ts deleted file mode 100644 index c626ad8..0000000 --- a/src/core/Region.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { signal, Signal } from '@preact/signals-core'; -import type { Placement } from './Placement'; - -/** - * Region 类型 - */ -export enum RegionType { - /** - * Keyed Region - 子元素通过 key 索引 - * 适用于:玩家手牌、版图格子等有固定位置的区域 - */ - Keyed = 'keyed', - /** - * Unkeyed Region - 子元素按顺序排列 - * 适用于:牌库、弃牌堆等堆叠区域 - */ - Unkeyed = 'unkeyed', -} - -/** - * Region 属性 - */ -export interface RegionProperties { - id: string; - type: RegionType; - name?: string; - capacity?: number; - metadata?: Record; -} - -/** - * Keyed Region 的槽位 - */ -export interface Slot { - key: string; - placementId: string | null; -} - -/** - * Region 类型 - */ -export interface Region extends RegionProperties { - placements: Signal; // Placement ID 列表 - slots?: Signal>; // Keyed Region 专用:key -> placementId -} - -/** - * 创建 Region - */ -export function createRegion(properties: RegionProperties): Region { - const region: Region = { - ...properties, - placements: signal([]), - }; - - if (properties.type === RegionType.Keyed) { - region.slots = signal>(new Map()); - } - - return region; -} - -/** - * 添加 Placement ID 到 Region (unkeyed) - */ -export function addPlacementToRegion(region: Region, placementId: string): void { - if (region.type === RegionType.Keyed) { - throw new Error('Cannot use addPlacementToRegion on a keyed region. Use setSlot instead.'); - } - - const current = region.placements.value; - if (region.capacity !== undefined && current.length >= region.capacity) { - throw new Error(`Region ${region.id} has reached its capacity of ${region.capacity}`); - } - region.placements.value = [...current, placementId]; -} - -/** - * 从 Region 移除 Placement ID - */ -export function removePlacementFromRegion(region: Region, placementId: string): void { - const current = region.placements.value; - const index = current.indexOf(placementId); - if (index !== -1) { - const updated = [...current]; - updated.splice(index, 1); - region.placements.value = updated; - } -} - -/** - * 设置 Keyed Region 的槽位 - */ -export function setSlot(region: Region, key: string, placementId: string | null): void { - if (region.type !== RegionType.Keyed || !region.slots) { - throw new Error('Cannot use setSlot on an unkeyed region.'); - } - - const slots = new Map(region.slots.value); - - // 如果是放置新 placement,需要更新 placements 列表 - if (placementId !== null) { - const currentPlacements = region.placements.value; - if (!currentPlacements.includes(placementId)) { - region.placements.value = [...currentPlacements, placementId]; - } - } - - slots.set(key, placementId); - region.slots.value = slots; -} - -/** - * 获取 Keyed Region 的槽位 - */ -export function getSlot(region: Region, key: string): string | null { - if (region.type !== RegionType.Keyed || !region.slots) { - throw new Error('Cannot use getSlot on an unkeyed region.'); - } - return region.slots.value.get(key) ?? null; -} - -/** - * 清空 Region - */ -export function clearRegion(region: Region): void { - region.placements.value = []; - if (region.slots) { - region.slots.value = new Map(); - } -} - -/** - * 获取 Region 中 Placement 的数量 - */ -export function getPlacementCount(region: Region): number { - return region.placements.value.length; -} - -/** - * 检查 Region 是否为空 - */ -export function isRegionEmpty(region: Region): boolean { - return region.placements.value.length === 0; -} - -/** - * 检查 Region 是否已满 - */ -export function isRegionFull(region: Region): boolean { - if (region.capacity === undefined) { - return false; - } - return region.placements.value.length >= region.capacity; -} diff --git a/src/core/context.ts b/src/core/context.ts new file mode 100644 index 0000000..d54b3bd --- /dev/null +++ b/src/core/context.ts @@ -0,0 +1,38 @@ +import {createModel, Signal, signal} from '@preact/signals-core'; +import {createEntityCollection} from "../utils/entity"; +import {Part} from "./part"; +import {Region} from "./region"; + +export type Context = { + type: string; +} + +export const GameContext = createModel((root: Context) => { + const parts = createEntityCollection(); + const regions = createEntityCollection(); + const contexts = signal([signal(root)]); + function pushContext(context: Context) { + const ctxSignal = signal(context); + contexts.value = [...contexts.value, ctxSignal]; + return context; + } + function popContext() { + contexts.value = contexts.value.slice(0, -1); + } + function latestContext(type: T['type']){ + for(let i = contexts.value.length - 1; i >= 0; i--){ + if(contexts.value[i].value.type === type){ + return contexts.value[i] as Signal; + } + } + } + + return { + parts, + regions, + contexts, + pushContext, + popContext, + latestContext, + } +}) \ No newline at end of file diff --git a/src/core/part.ts b/src/core/part.ts new file mode 100644 index 0000000..fd7d31b --- /dev/null +++ b/src/core/part.ts @@ -0,0 +1,33 @@ +import {Entity, EntityAccessor} from "../utils/entity"; +import {Region} from "./region"; +import {RNG} from "../utils/rng"; + +export type Part = Entity & { + // cards have 2 sides, dices have multiple, tokens have 1 + sides: number; + + // mostly rotations, if relevant + alignments?: string[]; + + // current side + side: number; + // current alignment + alignment?: string; + + // current region + region: EntityAccessor; + // current position in region + position: number[]; +} + +export function flip(part: Part) { + part.side = (part.side + 1) % part.sides; +} + +export function flipTo(part: Part, side: number) { + part.side = side; +} + +export function roll(part: Part, rng: RNG) { + part.side = rng.nextInt(part.sides); +} \ No newline at end of file diff --git a/src/core/region.ts b/src/core/region.ts new file mode 100644 index 0000000..3185a86 --- /dev/null +++ b/src/core/region.ts @@ -0,0 +1,41 @@ +import {Entity, EntityAccessor} from "../utils/entity"; +import {Part} from "./part"; +import {RNG} from "../utils/rng"; + +export type Region = Entity & { + // aligning axes of the region + axes: RegionAxis[]; + + // current children; expect no overlapped positions + children: EntityAccessor[]; +} + +export type RegionAxis = { + name: string; + min?: number; + max?: number; + align?: 'start' | 'end' | 'center'; +} + +/** + * for each axis, try to remove gaps in positions. + * - if min exists and align is start, and there are parts at (for example) min+2 and min+4, then move them to min and min+1 + * - if max exists and align is end, and there are parts at (for example) max-2 and max-4, then move them to max-1 and max-3 + * - for center, move parts to the center, possibly creating parts placed at 0.5 positions + * - sort children so that they're in ascending order on each axes. + * @param region + */ +export function applyAlign(region: Region){ + for (const axis of region.axes) { + // TODO implement this + } +} + +/** + * shuffle on each axis. for each axis, try to swap position. + * @param region + * @param rng + */ +export function shuffle(region: Region, rng: RNG){ + // TODO implement this +} \ No newline at end of file diff --git a/src/core/rule.ts b/src/core/rule.ts new file mode 100644 index 0000000..a2f7bfe --- /dev/null +++ b/src/core/rule.ts @@ -0,0 +1,32 @@ +import {Context} from "./context"; +import {Command} from "../utils/command"; +import {effect} from "@preact/signals-core"; + +export type RuleContext = Context & { + actions: Command[]; + handledActions: number; + invocations: RuleContext[]; + resolution?: T; +} + +function invokeRuleContext(pushContext: (context: Context) => void, type: string, rule: Generator){ + const ctx: RuleContext = { + type, + actions: [], + handledActions: 0, + invocations: [], + resolution: undefined, + } + const dispose = effect(() => { + if(ctx.resolution) { + dispose(); + return; + } + }); + + pushContext(rule); +} + +function* rule(){ + const play: Command = yield 'play'; +} \ No newline at end of file diff --git a/src/utils/command.ts b/src/utils/command.ts new file mode 100644 index 0000000..64d2a4c --- /dev/null +++ b/src/utils/command.ts @@ -0,0 +1,10 @@ +export type Command = { + name: string; + flags: Record; + options: Record; + params: string[]; +} + +// TODO implement this +export function parseCommand(input: string): Command { +} \ No newline at end of file diff --git a/src/utils/entity.ts b/src/utils/entity.ts new file mode 100644 index 0000000..b4ab469 --- /dev/null +++ b/src/utils/entity.ts @@ -0,0 +1,46 @@ +import {Signal, signal} from "@preact/signals-core"; + +export type Entity = { + id: string; +}; + +export type EntityAccessor = { + id: string; + value: T; +} + +export function createEntityCollection() { + 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[]) => { + collection.value = { + ...collection.value, + ...Object.fromEntries(entities.map((entity) => [entity.id, signal(entity)])), + }; + }; + + const get = (id: string) => { + return { + id, + get value(){ + return collection.value[id]?.value; + }, + set value(value: T){ + const signal = collection.value[id]; + if(signal)signal.value = value; + } + } + } + + return { + collection, + remove, + add, + get + } +} \ No newline at end of file diff --git a/src/utils/rng.ts b/src/utils/rng.ts new file mode 100644 index 0000000..ff1b51a --- /dev/null +++ b/src/utils/rng.ts @@ -0,0 +1,12 @@ +export interface RNG { + /** 设置随机数种子 */ + (seed: number): void; + + /** 获取一个[0,1)随机数 */ + next(max?: number): number; + + /** 获取一个[0,max)随机整数 */ + nextInt(max: number): number; +} + +// TODO: create a RNG implementation with the alea library \ No newline at end of file