Initial commit: boardgame-core with build fixes

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
hyper
2026-03-31 18:01:57 +08:00
co-authored by Qwen-Coder
commit d27948fbfc
39 changed files with 8545 additions and 0 deletions
+312
View File
@@ -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);
}
+103
View File
@@ -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,
};
}
+88
View File
@@ -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;
}
+155
View File
@@ -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;
}