Initial commit: boardgame-core with build fixes
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user