refactor: mass refactoring

This commit is contained in:
hypercross 2026-04-01 13:36:16 +08:00
parent d0d051f547
commit 6740584fc8
11 changed files with 212 additions and 658 deletions

View File

@ -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<string, unknown>;
}
/**
*
* Parts, Regions, Placements
*/
export class GameState {
/** 游戏基本信息 */
data: Signal<GameStateData>;
/** Parts 存储 */
parts: Signal<Map<string, Part>>;
/** Regions 存储 */
regions: Signal<Map<string, Region>>;
/** Placements 存储 */
placements: Signal<Map<string, Placement>>;
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<T extends Part>(partId: string, updates: Partial<T>): 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<Placement>): 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<number> {
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<string, unknown>): void {
this.data.value = {
...this.data.value,
metadata: { ...this.data.value.metadata, ...updates },
};
}
}
/**
*
*/
export function createGameState(data: GameStateData): GameState {
return new GameState(data);
}

View File

@ -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<string, unknown>;
}
/**
* 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<typeof signal<Part>>;
/**
* Part
*/
export function createPart<T extends Part>(part: T): T {
return part;
}
/**
* Meeple Part
*/
export function createMeeple(id: string, color: string, options?: { name?: string; metadata?: Record<string, unknown> }): 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<string, unknown> }
): CardPart {
return {
id,
type: PartType.Card,
...options,
};
}
/**
* Tile Part
*/
export function createTile(
id: string,
options?: { pattern?: string; rotation?: number; name?: string; metadata?: Record<string, unknown> }
): TilePart {
return {
id,
type: PartType.Tile,
...options,
};
}

View File

@ -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<string, unknown>;
}
/**
* Placement
*/
export interface Placement extends PlacementProperties {
part: Part | null;
}
/**
* Placement
*/
export type PlacementSignal = Signal<Placement>;
/**
* Placement
*/
export function createPlacement(properties: {
id: string;
partId: string;
regionId: string;
part: Part;
position?: Position;
rotation?: number;
faceUp?: boolean;
metadata?: Record<string, unknown>;
}): 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;
}

View File

@ -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<string, unknown>;
}
/**
* Keyed Region
*/
export interface Slot {
key: string;
placementId: string | null;
}
/**
* Region
*/
export interface Region extends RegionProperties {
placements: Signal<string[]>; // Placement ID 列表
slots?: Signal<Map<string, string | null>>; // Keyed Region 专用key -> placementId
}
/**
* Region
*/
export function createRegion(properties: RegionProperties): Region {
const region: Region = {
...properties,
placements: signal<string[]>([]),
};
if (properties.type === RegionType.Keyed) {
region.slots = signal<Map<string, string | null>>(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;
}

38
src/core/context.ts Normal file
View File

@ -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<Part>();
const regions = createEntityCollection<Region>();
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<T extends Context>(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<T>;
}
}
}
return {
parts,
regions,
contexts,
pushContext,
popContext,
latestContext,
}
})

33
src/core/part.ts Normal file
View File

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

41
src/core/region.ts Normal file
View File

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

32
src/core/rule.ts Normal file
View File

@ -0,0 +1,32 @@
import {Context} from "./context";
import {Command} from "../utils/command";
import {effect} from "@preact/signals-core";
export type RuleContext<T> = Context & {
actions: Command[];
handledActions: number;
invocations: RuleContext<unknown>[];
resolution?: T;
}
function invokeRuleContext<T>(pushContext: (context: Context) => void, type: string, rule: Generator<string, T, Command>){
const ctx: RuleContext<T> = {
type,
actions: [],
handledActions: 0,
invocations: [],
resolution: undefined,
}
const dispose = effect(() => {
if(ctx.resolution) {
dispose();
return;
}
});
pushContext(rule);
}
function* rule(){
const play: Command = yield 'play';
}

10
src/utils/command.ts Normal file
View File

@ -0,0 +1,10 @@
export type Command = {
name: string;
flags: Record<string, true>;
options: Record<string, string>;
params: string[];
}
// TODO implement this
export function parseCommand(input: string): Command {
}

46
src/utils/entity.ts Normal file
View File

@ -0,0 +1,46 @@
import {Signal, signal} from "@preact/signals-core";
export type Entity = {
id: string;
};
export type EntityAccessor<T extends Entity> = {
id: string;
value: T;
}
export function createEntityCollection<T extends Entity>() {
const collection = signal({} as Record<string, Signal<T>>);
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
}
}

12
src/utils/rng.ts Normal file
View File

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