Compare commits

..

5 Commits

Author SHA1 Message Date
hyper f5df1c26dc refactor: remove registry from Boop and Regicide index files 2026-04-23 15:29:14 +08:00
hyper 2014162819 refactor: update Regicide commands and improve code style 2026-04-23 15:28:45 +08:00
hyper d932c2f5e2 refactor: remove Boop command registry and simplify exports
Remove the command registry from the Boop sample and export command
functions directly. This simplifies the command structure and replaces
dynamic imports with static imports.
2026-04-23 15:21:13 +08:00
hyper 1bd49c32e0 refactor: format Onitama commands and Tic-Tac-Toe imports 2026-04-23 15:18:37 +08:00
hyper b009189b9a Extract PromptContext from GameHost into separate module
The prompt system is now managed by a dedicated PromptContext instead of
being embedded in GameHost. This simplifies the API by removing
individual prompt signals and consolidating prompt logic into a reusable
context that can be shared between components.
2026-04-23 15:09:06 +08:00
12 changed files with 1050 additions and 1156 deletions

View File

@ -1,100 +1,40 @@
import { ReadonlySignal, Signal } from "@preact/signals-core";
import { CommandSchema, CommandRegistry, PromptEvent } from "@/utils/command";
import {
createGameCommandRegistry,
createGameContext,
IGameContext,
PromptDef,
} from "./game";
import { createPromptContext } from "@/utils/command";
import { createGameContext, IGameContext } from "./game";
import { PromptContext } from "@/utils/command/command-prompt";
export type GameHostStatus = "created" | "running" | "disposed";
export class GameHost<
TState extends Record<string, unknown>,
TResult = unknown,
TModule extends GameModule<TState, TResult> = GameModule<TState, TResult>,
> {
readonly state: ReadonlySignal<TState>;
export class GameHost<TModule extends GameModule> {
readonly state: ReadonlySignal<GameState<TModule>>;
readonly status: ReadonlySignal<GameHostStatus>;
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
readonly activePromptPlayer: ReadonlySignal<string | null>;
readonly activePromptHint: ReadonlySignal<string | null>;
readonly prompts: PromptContext;
private _context: IGameContext<TState>;
private _start: (ctx: IGameContext<TState>) => Promise<TResult>;
private _context: IGameContext<GameState<TModule>>;
private _start: (
ctx: IGameContext<GameState<TModule>>,
) => Promise<GameResult<TModule>>;
private _status: Signal<GameHostStatus>;
private _activePromptSchema: Signal<CommandSchema | null>;
private _activePromptPlayer: Signal<string | null>;
private _activePromptHint: Signal<string | null>;
private _createInitialState: () => TState;
private _createInitialState: () => GameState<TModule>;
private _eventListeners: Map<"start" | "dispose", Set<() => void>>;
private _isDisposed = false;
constructor(public readonly gameModule: TModule) {
const { createInitialState, registry, start } = gameModule;
const { createInitialState, start } = gameModule as unknown as GameModule<
GameState<TModule>,
GameResult<TModule>
>;
this._createInitialState = createInitialState;
this._eventListeners = new Map();
this.prompts = createPromptContext();
const initialState = createInitialState();
this._context = createGameContext(
registry ?? createGameCommandRegistry(),
initialState,
);
this._context = createGameContext(this.prompts, initialState);
this._start = start;
this.state = this._context._state;
this._status = new Signal<GameHostStatus>("created");
this.status = this._status;
this._activePromptSchema = new Signal<CommandSchema | null>(null);
this.activePromptSchema = this._activePromptSchema;
this._activePromptPlayer = new Signal<string | null>(null);
this.activePromptPlayer = this._activePromptPlayer;
this._activePromptHint = new Signal<string | null>(null);
this.activePromptHint = this._activePromptHint;
this._setupPromptTracking();
}
private _setupPromptTracking() {
let currentPromptEvent: PromptEvent | null = null;
this._context._commands.on("prompt", (e) => {
currentPromptEvent = e as PromptEvent;
this._activePromptSchema.value = currentPromptEvent.schema;
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
this._activePromptHint.value = currentPromptEvent.hintText || null;
});
this._context._commands.on("promptEnd", () => {
currentPromptEvent = null;
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
this._activePromptHint.value = null;
});
// Initial state
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
this._activePromptHint.value = null;
}
tryInput(input: string): string | null {
if (this._isDisposed) {
return "GameHost is disposed";
}
return this._context._commands._tryCommit(input);
}
tryAnswerPrompt<TArgs extends any[]>(def: PromptDef<TArgs>, ...args: TArgs) {
return this._context._commands._tryCommit({
name: def.schema.name,
params: args,
options: {},
flags: {},
});
}
/**
@ -113,12 +53,12 @@ export class GameHost<
this._context._state.clearInterruptions();
}
start(seed?: number): Promise<TResult> {
start(seed?: number): Promise<GameResult<TModule>> {
if (this._isDisposed) {
throw new Error("GameHost is disposed");
}
this._context._commands._cancel();
this.prompts.reset();
const initialState = this._createInitialState();
this._context._state.value = initialState as any;
@ -139,7 +79,7 @@ export class GameHost<
}
this._isDisposed = true;
this._context._commands._cancel();
this.prompts.reset();
this._status.value = "disposed";
// Emit dispose event BEFORE clearing listeners
@ -169,18 +109,21 @@ export class GameHost<
}
export type GameModule<
TState extends Record<string, unknown>,
TState extends Record<string, unknown> = Record<string, unknown>,
TResult = unknown,
> = {
registry?: CommandRegistry<IGameContext<TState>>;
createInitialState: () => TState;
start: (ctx: IGameContext<TState>) => Promise<TResult>;
};
export type GameState<TModule extends GameModule> = ReturnType<
TModule["createInitialState"]
>;
export type GameResult<TModule extends GameModule> = Awaited<
ReturnType<TModule["start"]>
>;
export function createGameHost<
TState extends Record<string, unknown>,
TResult = unknown,
TModule extends GameModule<TState, TResult> = GameModule<TState, TResult>,
>(gameModule: TModule): GameHost<TState, TResult> {
export function createGameHost<TModule extends GameModule>(
gameModule: TModule,
): GameHost<TModule> {
return new GameHost(gameModule);
}

View File

@ -1,40 +1,29 @@
import { MutableSignal, mutableSignal } from "@/utils/mutable-signal";
import {
Command,
CommandRegistry,
CommandResult,
CommandRunnerContextExport,
CommandSchema,
createCommandRegistry,
createCommandRunnerContext,
parseCommandSchema,
} from "@/utils/command";
import { CommandSchema, parseCommandSchema, PromptDef } from "@/utils/command";
import { PromptValidator } from "@/utils/command/command-runner";
import { Mulberry32RNG, ReadonlyRNG, RNG } from "@/utils/rng";
import { PromptContext } from "@/utils/command/command-prompt";
export interface IGameContext<TState extends Record<string, unknown> = {}> {
get value(): TState;
get rng(): ReadonlyRNG;
produce(fn: (draft: TState) => undefined): void;
produceAsync(fn: (draft: TState) => undefined): Promise<void>;
run<T>(input: string): Promise<CommandResult<T>>;
runParsed<T>(command: Command): Promise<CommandResult<T>>;
prompt: <TResult, TArgs extends any[] = any[]>(
def: PromptDef<TArgs>,
validator: PromptValidator<TResult, TArgs>,
currentPlayer?: string | null,
player?: string,
) => Promise<TResult>;
// test only
_state: MutableSignal<TState>;
_commands: CommandRunnerContextExport<IGameContext<TState>>;
_rng: RNG;
}
export type IGameContextExport<TState extends Record<string, unknown> = {}> =
Omit<IGameContext<TState>, "_state" | "_commands" | "_rng">;
export function createGameContext<TState extends Record<string, unknown> = {}>(
commandRegistry: CommandRegistry<IGameContext<TState>>,
promptContext: PromptContext,
initialState?: TState | (() => TState),
): IGameContext<TState> {
const stateValue =
@ -42,7 +31,7 @@ export function createGameContext<TState extends Record<string, unknown> = {}>(
? initialState()
: (initialState ?? ({} as TState));
const state = mutableSignal(stateValue);
let commands: CommandRunnerContextExport<IGameContext<TState>> = null as any;
const { prompt } = promptContext;
const context: IGameContext<TState> = {
get value(): TState {
@ -57,38 +46,15 @@ export function createGameContext<TState extends Record<string, unknown> = {}>(
produceAsync(fn: (draft: TState) => undefined) {
return state.produceAsync(fn);
},
run<T>(input: string) {
return commands.run<T>(input);
},
runParsed<T>(command: Command) {
return commands.runParsed<T>(command);
},
prompt(def, validator, currentPlayer) {
return commands.prompt(
def.schema,
validator,
def.hintText,
currentPlayer,
);
},
prompt,
_state: state,
_commands: commands,
_rng: new Mulberry32RNG(),
};
context._commands = commands = createCommandRunnerContext(
commandRegistry,
context,
);
return context;
}
export type PromptDef<TArgs extends any[] = any[]> = {
schema: CommandSchema;
hintText?: string;
};
export function createPromptDef<TArgs extends any[] = any[]>(
schema: CommandSchema | string,
hintText?: string,
@ -96,9 +62,3 @@ export function createPromptDef<TArgs extends any[] = any[]>(
schema = typeof schema === "string" ? parseCommandSchema(schema) : schema;
return { schema, hintText };
}
export function createGameCommandRegistry<
TState extends Record<string, unknown> = {},
>() {
return createCommandRegistry<IGameContext<TState>>();
}

View File

@ -5,7 +5,7 @@
// Core types
export type { IGameContext } from "./core/game";
export { createGameCommandRegistry, createPromptDef } from "./core/game";
export { createPromptDef } from "./core/game";
export type { GameHost, GameHostStatus, GameModule } from "./core/game-host";
export { createGameHost } from "./core/game-host";

View File

@ -1,75 +1,11 @@
import { BoopGame } from "@/samples/boop/types-extensions";
import { PlayerType } from "@/samples/boop/types";
import { turn } from "@/samples/boop/commands/turn";
import {createGameCommandRegistry} from "@/core/game";
export const registry = createGameCommandRegistry<BoopGame['value']>();
/**
*
*/
const placeCmd = registry.register({
schema: 'place <row:number> <col:number> <player> <type>',
run: async (game, row, col, player, type) => {
const {place} = await import('./place');
return place(game, row, col, player, type);
}
});
/**
* boop -
*/
const boopCmd = registry.register({
schema: 'boop <row:number> <col:number> <type>',
run: async (game, row, col, type) => {
const {boop} = await import('./boop');
return boop(game, row, col, type);
}
});
/**
* (线)
*/
const checkWinCmd = registry.register({
schema: 'check-win',
run: async (game) => {
const {checkWin} = await import('./win');
return checkWin(game);
}
});
/**
* (线)
*/
const checkGraduatesCmd = registry.register({
schema: 'check-graduates',
run: async (game) => {
const {checkGraduates} = await import('./graduate');
return checkGraduates(game);
}
});
/**
* 8-piece
*/
const checkFullBoardCmd = registry.register({
schema: 'check-full-board <player:string>',
run: async (game, player) => {
const {checkFullBoard} = await import('./full-board');
return checkFullBoard(game, player);
}
});
/**
*
*/
const turnCmd = registry.register({
schema: 'turn <player:string>',
run: async (game, player) => {
const {turn} = await import('./turn');
return turn(game, player);
}
});
import { boop } from "./boop";
import { checkFullBoard } from "./full-board";
import { checkGraduates } from "./graduate";
import { place } from "./place";
import { checkWin } from "./win";
/**
*
@ -79,10 +15,11 @@ export async function start(game: BoopGame) {
const currentPlayer = game.value.currentPlayer;
const turnOutput = await turn(game, currentPlayer);
await game.produceAsync(state => {
await game.produceAsync((state) => {
state.winner = turnOutput.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white';
state.currentPlayer =
state.currentPlayer === "white" ? "black" : "white";
}
});
if (game.value.winner) break;
@ -91,11 +28,4 @@ export async function start(game: BoopGame) {
return game.value;
}
export {
placeCmd as place,
boopCmd as boop,
checkWinCmd as checkWin,
checkGraduatesCmd as checkGraduates,
checkFullBoardCmd as checkFullBoard,
turnCmd as turn
};
export { place, boop, checkWin, checkGraduates, checkFullBoard, turn };

View File

@ -5,38 +5,30 @@ export type {
WinnerType,
RegionType,
BoopPartMeta,
BoopPart
} from './types';
BoopPart,
} from "./types";
export type {BoopGame} from './types-extensions';
export type { BoopGame } from "./types-extensions";
// Constants
export {
BOARD_SIZE,
MAX_PIECES_PER_PLAYER,
WIN_LENGTH
} from './constants';
export { BOARD_SIZE, MAX_PIECES_PER_PLAYER, WIN_LENGTH } from "./constants";
// State
export {
createInitialState,
type BoopState
} from './state';
export { createInitialState, type BoopState } from "./state";
// Prompts
export {prompts} from './prompts';
export { prompts } from "./prompts";
// Commands
export {
registry,
start,
place as placeCmd,
boop as boopCmd,
checkWin as checkWinCmd,
checkGraduates as checkGraduatesCmd,
checkFullBoard as checkFullBoardCmd,
turn as turnCmd
} from './commands';
turn as turnCmd,
} from "./commands";
// Utils
export {
@ -45,5 +37,5 @@ export {
isCellOccupied,
getNeighborPositions,
findPartInRegion,
findPartAtPosition
} from './utils';
findPartAtPosition,
} from "./utils";

View File

@ -3,13 +3,10 @@ import {
OnitamaState,
PlayerType,
prompts,
initializeCards
initializeCards,
} from "./types";
import {createGameCommandRegistry} from "@/core/game";
import { moveToRegion } from "@/core/region";
export const registry = createGameCommandRegistry<OnitamaState>();
/**
*
*/
@ -29,8 +26,12 @@ function getPawnAtPosition(state: OnitamaState, x: number, y: number) {
/**
*
*/
function playerHasCard(state: OnitamaState, player: PlayerType, cardName: string): boolean {
const cardList = player === 'red' ? state.redCards : state.blackCards;
function playerHasCard(
state: OnitamaState,
player: PlayerType,
cardName: string,
): boolean {
const cardList = player === "red" ? state.redCards : state.blackCards;
return cardList.includes(cardName);
}
@ -38,13 +39,17 @@ function playerHasCard(state: OnitamaState, player: PlayerType, cardName: string
*
* 180
*/
export function getCardMoveCandidates(state: OnitamaState, cardName: string, player: PlayerType) {
export function getCardMoveCandidates(
state: OnitamaState,
cardName: string,
player: PlayerType,
) {
const card = state.cards[cardName];
const candidates = card.moveCandidates;
// 黑方需要将卡牌旋转180度
if (player === 'black') {
return candidates.map(m => ({ dx: -m.dx, dy: -m.dy }));
if (player === "black") {
return candidates.map((m) => ({ dx: -m.dx, dy: -m.dy }));
}
return candidates;
@ -53,7 +58,15 @@ export function getCardMoveCandidates(state: OnitamaState, cardName: string, pla
/**
*
*/
export function isValidMove(state: OnitamaState, cardName: string, fromX: number, fromY: number, toX: number, toY: number, player: PlayerType): string | null {
export function isValidMove(
state: OnitamaState,
cardName: string,
fromX: number,
fromY: number,
toX: number,
toY: number,
player: PlayerType,
): string | null {
// 检查玩家是否拥有该卡牌
if (!playerHasCard(state, player, cardName)) {
return `玩家 ${player} 不拥有卡牌 ${cardName}`;
@ -80,7 +93,7 @@ export function isValidMove(state: OnitamaState, cardName: string, fromX: number
// 检查移动是否在卡牌的移动候选项中黑方需要旋转180度
const candidates = getCardMoveCandidates(state, cardName, player);
const isValid = candidates.some(m => m.dx === dx && m.dy === dy);
const isValid = candidates.some((m) => m.dx === dx && m.dy === dy);
if (!isValid) {
return `卡牌 ${cardName} 不支持移动 (${dx}, ${dy})`;
}
@ -102,7 +115,15 @@ export function isValidMove(state: OnitamaState, cardName: string, fromX: number
/**
*
*/
async function handleMove(game: OnitamaGame, player: PlayerType, cardName: string, fromX: number, fromY: number, toX: number, toY: number) {
async function move(
game: OnitamaGame,
player: PlayerType,
cardName: string,
fromX: number,
fromY: number,
toX: number,
toY: number,
) {
const state = game.value;
// 验证移动
@ -113,7 +134,7 @@ async function handleMove(game: OnitamaGame, player: PlayerType, cardName: strin
const capturedPawnId = getPawnAtPosition(state, toX, toY)?.id || null;
await game.produceAsync(state => {
await game.produceAsync((state) => {
const pawn = state.pawns[getPawnAtPosition(state, fromX, fromY)!.id];
// 如果目标位置有敌方棋子,将其移除(吃掉)
@ -127,41 +148,40 @@ async function handleMove(game: OnitamaGame, player: PlayerType, cardName: strin
});
// 交换卡牌
await handleSwapCard(game, player, cardName);
await swapCard(game, player, cardName);
return {
from: { x: fromX, y: fromY },
to: { x: toX, y: toY },
card: cardName,
captured: capturedPawnId
captured: capturedPawnId,
};
}
const move = registry.register({
schema: 'move <player> <card:string> <fromX:number> <fromY:number> <toX:number> <toY:number>',
run: handleMove
});
/**
* 使
*/
async function handleSwapCard(game: OnitamaGame, player: PlayerType, usedCard: string) {
await game.produceAsync(state => {
async function swapCard(
game: OnitamaGame,
player: PlayerType,
usedCard: string,
) {
await game.produceAsync((state) => {
const spareCard = state.spareCard;
const usedCardData = state.cards[usedCard];
const spareCardData = state.cards[spareCard];
// 从玩家手牌中移除使用的卡牌
if (player === 'red') {
state.redCards = state.redCards.filter(c => c !== usedCard);
if (player === "red") {
state.redCards = state.redCards.filter((c) => c !== usedCard);
state.redCards.push(spareCard);
} else {
state.blackCards = state.blackCards.filter(c => c !== usedCard);
state.blackCards = state.blackCards.filter((c) => c !== usedCard);
state.blackCards.push(spareCard);
}
// 更新卡牌区域
usedCardData.regionId = 'spare';
usedCardData.regionId = "spare";
spareCardData.regionId = player;
// 更新备用卡牌
@ -169,75 +189,70 @@ async function handleSwapCard(game: OnitamaGame, player: PlayerType, usedCard: s
});
}
const swapCard = registry.register({
schema: 'swap-card <player> <card:string>',
run: handleSwapCard
});
/**
*
* (2, 4) -
* (2, 0) -
*/
async function handleCheckConquestWin(game: OnitamaGame): Promise<PlayerType | null> {
async function checkConquestWin(game: OnitamaGame): Promise<PlayerType | null> {
const state = game.value;
// 红色师父到达 (2, 4)(黑色师父的初始位置)
const redMaster = state.pawns['red-master'];
if (redMaster && redMaster.regionId === 'board' && redMaster.position[0] === 2 && redMaster.position[1] === 4) {
return 'red';
const redMaster = state.pawns["red-master"];
if (
redMaster &&
redMaster.regionId === "board" &&
redMaster.position[0] === 2 &&
redMaster.position[1] === 4
) {
return "red";
}
// 黑色师父到达 (2, 0)(红色师父的初始位置)
const blackMaster = state.pawns['black-master'];
if (blackMaster && blackMaster.regionId === 'board' && blackMaster.position[0] === 2 && blackMaster.position[1] === 0) {
return 'black';
const blackMaster = state.pawns["black-master"];
if (
blackMaster &&
blackMaster.regionId === "board" &&
blackMaster.position[0] === 2 &&
blackMaster.position[1] === 0
) {
return "black";
}
return null;
}
const checkConquestWin = registry.register({
schema: 'check-conquest-win',
run: handleCheckConquestWin
});
/**
*
*/
async function handleCheckCaptureWin(game: OnitamaGame): Promise<PlayerType | null> {
async function checkCaptureWin(game: OnitamaGame): Promise<PlayerType | null> {
const state = game.value;
// 红色师父不在棋盘上,黑色获胜
const redMaster = state.pawns['red-master'];
if (!redMaster || redMaster.regionId !== 'board') {
return 'black';
const redMaster = state.pawns["red-master"];
if (!redMaster || redMaster.regionId !== "board") {
return "black";
}
// 黑色师父不在棋盘上,红色获胜
const blackMaster = state.pawns['black-master'];
if (!blackMaster || blackMaster.regionId !== 'board') {
return 'red';
const blackMaster = state.pawns["black-master"];
if (!blackMaster || blackMaster.regionId !== "board") {
return "red";
}
return null;
}
const checkCaptureWin = registry.register({
schema: 'check-capture-win',
run: handleCheckCaptureWin
});
/**
*
*/
async function handleCheckWin(game: OnitamaGame): Promise<PlayerType | null> {
const conquestWinner = await handleCheckConquestWin(game);
async function checkWin(game: OnitamaGame): Promise<PlayerType | null> {
const conquestWinner = await checkConquestWin(game);
if (conquestWinner) {
return conquestWinner;
}
const captureWinner = await handleCheckCaptureWin(game);
const captureWinner = await checkCaptureWin(game);
if (captureWinner) {
return captureWinner;
}
@ -245,22 +260,34 @@ async function handleCheckWin(game: OnitamaGame): Promise<PlayerType | null> {
return null;
}
const checkWin = registry.register({
schema: 'check-win',
run: handleCheckWin
});
/**
*
*/
export function getAvailableMoves(state: OnitamaState, player: PlayerType): Array<{card: string, fromX: number, fromY: number, toX: number, toY: number}> {
const moves: Array<{card: string, fromX: number, fromY: number, toX: number, toY: number}> = [];
export function getAvailableMoves(
state: OnitamaState,
player: PlayerType,
): Array<{
card: string;
fromX: number;
fromY: number;
toX: number;
toY: number;
}> {
const moves: Array<{
card: string;
fromX: number;
fromY: number;
toX: number;
toY: number;
}> = [];
// 获取玩家的所有卡牌
const cardNames = player === 'red' ? state.redCards : state.blackCards;
const cardNames = player === "red" ? state.redCards : state.blackCards;
// 获取玩家的所有棋子
const playerPawns = Object.values(state.pawns).filter(p => p.owner === player && p.regionId === 'board');
const playerPawns = Object.values(state.pawns).filter(
(p) => p.owner === player && p.regionId === "board",
);
// 对于每张卡牌
for (const cardName of cardNames) {
@ -294,7 +321,7 @@ export function getAvailableMoves(state: OnitamaState, player: PlayerType): Arra
/**
*
*/
async function handleTurn(game: OnitamaGame, turnPlayer: PlayerType) {
async function turn(game: OnitamaGame, turnPlayer: PlayerType) {
const state = game.value;
const availableMoves = getAvailableMoves(state, turnPlayer);
@ -313,7 +340,7 @@ async function handleTurn(game: OnitamaGame, turnPlayer: PlayerType) {
}
return card;
},
turnPlayer
turnPlayer,
);
await swapCard(game, turnPlayer, cardToSwap);
@ -334,19 +361,27 @@ async function handleTurn(game: OnitamaGame, turnPlayer: PlayerType) {
return { player, card, fromX, fromY, toX, toY };
},
turnPlayer
turnPlayer,
);
await move(game, moveOutput.player, moveOutput.card, moveOutput.fromX, moveOutput.fromY, moveOutput.toX, moveOutput.toY);
await move(
game,
moveOutput.player,
moveOutput.card,
moveOutput.fromX,
moveOutput.fromY,
moveOutput.toX,
moveOutput.toY,
);
}
// 检查胜利
const winner = await checkWin(game);
await game.produceAsync(state => {
await game.produceAsync((state) => {
state.winner = winner;
if (!winner) {
state.currentPlayer = state.currentPlayer === 'red' ? 'black' : 'red';
state.currentPlayer = state.currentPlayer === "red" ? "black" : "red";
state.turn++;
}
});
@ -354,11 +389,6 @@ async function handleTurn(game: OnitamaGame, turnPlayer: PlayerType) {
return { winner, move: moveOutput };
}
const turn = registry.register({
schema: 'turn <player>',
run: handleTurn
});
/**
*
*/

View File

@ -1,20 +1,15 @@
import { IGameContext } from "@/core/game";
import { RegicideState } from "@/samples/regicide/state";
import {createGameCommandRegistry} from "@/core/game";
import { PlayerType, RegicideCard } from "@/samples/regicide/types";
import { CARD_VALUES, FACE_CARDS } from "@/samples/regicide/constants";
import { isEnemyDefeated } from "@/samples/regicide/utils";
export type RegicideGame = IGameContext<RegicideState>;
export const registry = createGameCommandRegistry<RegicideState>();
/**
*
*/
const playCmd = registry.register({
schema: 'play <player:string> <cardId:string>',
run: async (game: RegicideGame, player: string, cardId: string) => {
async function playCmd(game: RegicideGame, player: string, cardId: string) {
const state = game.value;
const card = state.cards[cardId];
@ -26,12 +21,15 @@ const playCmd = registry.register({
const playerKey = player as PlayerType;
const playerHand = state.playerHands[playerKey];
if (!playerHand || !playerHand.includes(cardId)) {
return {success: false, error: `卡牌 ${cardId} 不在玩家 ${player} 的手牌中`};
return {
success: false,
error: `卡牌 ${cardId} 不在玩家 ${player} 的手牌中`,
};
}
// 检查是否有当前敌人
if (!state.currentEnemy) {
return {success: false, error: '没有活跃的敌人'};
return { success: false, error: "没有活跃的敌人" };
}
// 计算伤害(基础伤害为卡牌面值)
@ -39,24 +37,27 @@ const playCmd = registry.register({
let attackReduction = 0;
// 梅花双倍伤害
if (card.suit === 'clubs') {
if (card.suit === "clubs") {
damage *= 2;
}
// 黑桃降低敌人攻击力
if (card.suit === 'spades') {
if (card.suit === "spades") {
attackReduction = card.value;
}
const enemyHpBefore = state.currentEnemy.hp;
await game.produce(state => {
await game.produceAsync((state) => {
// 对敌人造成伤害
state.currentEnemy!.hp -= damage;
// 记录黑桃的攻击力降低
if (attackReduction > 0) {
state.currentEnemy!.value = Math.max(0, state.currentEnemy!.value - attackReduction);
state.currentEnemy!.value = Math.max(
0,
state.currentEnemy!.value - attackReduction,
);
}
// 从手牌移除卡牌
@ -67,15 +68,17 @@ const playCmd = registry.register({
}
// 将卡牌移到弃牌堆
state.cards[cardId].regionId = 'discardPile';
state.cards[cardId].regionId = "discardPile";
// 红心能力:将弃牌堆洗回酒馆牌堆
if (card.suit === 'hearts') {
const discardIds = state.regions.discardPile.childIds.filter(id => id !== state.currentEnemy!.id);
if (card.suit === "hearts") {
const discardIds = state.regions.discardPile.childIds.filter(
(id) => id !== state.currentEnemy!.id,
);
if (discardIds.length > 0) {
// 将弃牌堆(除当前敌人外)移回酒馆牌堆
for (const discardId of discardIds) {
state.cards[discardId].regionId = 'tavernDeck';
state.cards[discardId].regionId = "tavernDeck";
}
state.regions.tavernDeck.childIds.push(...discardIds);
state.regions.discardPile.childIds = [state.currentEnemy!.id];
@ -83,7 +86,7 @@ const playCmd = registry.register({
}
// 方块能力:从酒馆牌堆抓牌
if (card.suit === 'diamonds') {
if (card.suit === "diamonds") {
const tavernDeckCount = state.regions.tavernDeck.childIds.length;
if (tavernDeckCount > 0) {
const drawCardId = state.regions.tavernDeck.childIds.shift()!;
@ -104,28 +107,30 @@ const playCmd = registry.register({
enemyHpBefore,
enemyHpAfter: game.value.currentEnemy!.hp,
enemyDefeated,
suitAbility: card.suit
}
suitAbility: card.suit,
},
};
}
});
/**
* A配合另一张牌
*/
const playWithACmd = registry.register({
schema: 'play-with-a <player:string> <aceCardId:string> <otherCardId:string>',
run: async (game: RegicideGame, player: string, aceCardId: string, otherCardId: string) => {
async function playWithACmd(
game: RegicideGame,
player: string,
aceCardId: string,
otherCardId: string,
) {
const state = game.value;
const aceCard = state.cards[aceCardId];
const otherCard = state.cards[otherCardId];
if (!aceCard || !otherCard) {
return {success: false, error: '卡牌不存在'};
return { success: false, error: "卡牌不存在" };
}
// 检查是否是A牌
if (aceCard.rank !== 'A') {
if (aceCard.rank !== "A") {
return { success: false, error: `第一张牌必须是A` };
}
@ -134,36 +139,39 @@ const playWithACmd = registry.register({
// 检查两张牌都在手牌中
if (!playerHand.includes(aceCardId) || !playerHand.includes(otherCardId)) {
return {success: false, error: '卡牌不在手牌中'};
return { success: false, error: "卡牌不在手牌中" };
}
if (!state.currentEnemy) {
return {success: false, error: '没有活跃的敌人'};
return { success: false, error: "没有活跃的敌人" };
}
// 计算两张牌的总伤害
let totalDamage = aceCard.value + otherCard.value;
// 如果另一张牌是梅花,双倍伤害
if (otherCard.suit === 'clubs') {
if (otherCard.suit === "clubs") {
totalDamage *= 2;
}
let attackReduction = 0;
if (aceCard.suit === 'spades') {
if (aceCard.suit === "spades") {
attackReduction += aceCard.value;
}
if (otherCard.suit === 'spades') {
if (otherCard.suit === "spades") {
attackReduction += otherCard.value;
}
await game.produce(state => {
await game.produceAsync((state) => {
// 对敌人造成伤害
state.currentEnemy!.hp -= totalDamage;
// 记录黑桃的攻击力降低
if (attackReduction > 0) {
state.currentEnemy!.value = Math.max(0, state.currentEnemy!.value - attackReduction);
state.currentEnemy!.value = Math.max(
0,
state.currentEnemy!.value - attackReduction,
);
}
// 从手牌移除两张牌
@ -174,8 +182,8 @@ const playWithACmd = registry.register({
if (otherIndex !== -1) hand.splice(otherIndex, 1);
// 将卡牌移到弃牌堆
state.cards[aceCardId].regionId = 'discardPile';
state.cards[otherCardId].regionId = 'discardPile';
state.cards[aceCardId].regionId = "discardPile";
state.cards[otherCardId].regionId = "discardPile";
});
const enemyDefeated = isEnemyDefeated(state.currentEnemy);
@ -186,33 +194,31 @@ const playWithACmd = registry.register({
damage: totalDamage,
attackReduction,
enemyHp: state.currentEnemy!.hp,
enemyDefeated
}
enemyDefeated,
},
};
}
});
/**
*
*/
const passCmd = registry.register({
schema: 'pass <player:string>',
run: async (game: RegicideGame, player: string) => {
async function passCmd(game: RegicideGame, player: string) {
// 即使让过,也会受到敌人反击(在回合结束时处理)
return { success: true, result: { message: `${player} 让过` } };
}
});
/**
* - >=
*/
const enemyCounterattackCmd = registry.register({
schema: 'counterattack <player:string> <discardCards:string[]>',
run: async (game: RegicideGame, player: string, discardCards: string[]) => {
async function enemyCounterattackCmd(
game: RegicideGame,
player: string,
discardCards: string[],
) {
const state = game.value;
if (!state.currentEnemy) {
return {success: false, error: '没有活跃的敌人'};
return { success: false, error: "没有活跃的敌人" };
}
const playerKey = player as PlayerType;
@ -240,19 +246,19 @@ const enemyCounterattackCmd = registry.register({
if (totalValue < enemyAttack) {
return {
success: false,
error: `弃牌点数和 (${totalValue}) 小于敌人攻击力 (${enemyAttack}),游戏失败`
error: `弃牌点数和 (${totalValue}) 小于敌人攻击力 (${enemyAttack}),游戏失败`,
};
}
// 执行弃牌
await game.produce(state => {
await game.produceAsync((state) => {
const hand = state.playerHands[playerKey];
for (const cardId of discardCards) {
const index = hand.indexOf(cardId);
if (index !== -1) {
hand.splice(index, 1);
}
state.cards[cardId].regionId = 'discardPile';
state.cards[cardId].regionId = "discardPile";
}
});
@ -261,22 +267,19 @@ const enemyCounterattackCmd = registry.register({
result: {
discardedCards: discardCards,
totalValue,
enemyAttack
}
enemyAttack,
},
};
}
});
/**
*
*/
const checkEnemyDefeatedCmd = registry.register({
schema: 'check-enemy',
run: async (game: RegicideGame) => {
async function checkEnemyDefeatedCmd(game: RegicideGame) {
const state = game.value;
if (!state.currentEnemy) {
return {success: false, error: '没有活跃的敌人'};
return { success: false as const, error: "没有活跃的敌人" };
}
const defeated = state.currentEnemy.hp <= 0;
@ -284,7 +287,7 @@ const checkEnemyDefeatedCmd = registry.register({
if (defeated) {
const defeatedEnemy = { ...state.currentEnemy };
await game.produce(state => {
await game.produceAsync((state) => {
// 将当前敌人移到弃牌堆
state.regions.discardPile.childIds.push(state.currentEnemy!.id);
@ -300,20 +303,20 @@ const checkEnemyDefeatedCmd = registry.register({
// 检查是否胜利(没有更多敌人)
if (!game.value.currentEnemy) {
await game.produce(state => {
state.phase = 'victory';
await game.produceAsync((state) => {
state.phase = "victory";
state.winner = true;
});
}
return {
success: true,
success: true as const,
result: {
defeated: true,
defeatedEnemy,
nextEnemy: game.value.currentEnemy,
enemiesRemaining: game.value.enemyDeck.length
}
enemiesRemaining: game.value.enemyDeck.length,
},
};
}
@ -322,26 +325,23 @@ const checkEnemyDefeatedCmd = registry.register({
result: {
defeated: false,
currentEnemy: { ...state.currentEnemy },
enemiesRemaining: state.enemyDeck.length
}
enemiesRemaining: state.enemyDeck.length,
},
};
}
});
/**
*
*/
const checkCanPlayCmd = registry.register({
schema: 'check-can-play <player:string>',
run: async (game: RegicideGame, player: string) => {
async function checkCanPlayCmd(game: RegicideGame, player: string) {
const state = game.value;
const playerKey = player as PlayerType;
const playerHand = state.playerHands[playerKey];
const canPlay = playerHand.length > 0;
const canPlayWithA = playerHand.some(cardId => {
const canPlayWithA = playerHand.some((cardId) => {
const card = state.cards[cardId];
return card && card.rank === 'A' && playerHand.length > 1;
return card && card.rank === "A" && playerHand.length > 1;
});
return {
@ -349,27 +349,26 @@ const checkCanPlayCmd = registry.register({
result: {
canPlay,
canPlayWithA,
handSize: playerHand.length
}
handSize: playerHand.length,
},
};
}
});
/**
*
*/
const checkTavernDeckCmd = registry.register({
schema: 'check-tavern-deck',
run: async (game: RegicideGame) => {
async function checkTavernDeckCmd(game: RegicideGame) {
const state = game.value;
const isEmpty = state.regions.tavernDeck.childIds.length === 0;
// 如果酒馆牌堆为空且所有玩家手牌也为空,则游戏失败
if (isEmpty) {
const allHandsEmpty = Object.values(state.playerHands).every(hand => hand.length === 0);
const allHandsEmpty = Object.values(state.playerHands).every(
(hand) => hand.length === 0,
);
if (allHandsEmpty) {
await game.produce(state => {
state.phase = 'defeat';
await game.produceAsync((state) => {
state.phase = "defeat";
state.winner = false;
});
}
@ -379,35 +378,32 @@ const checkTavernDeckCmd = registry.register({
success: true,
result: {
isEmpty,
cardsRemaining: state.regions.tavernDeck.childIds.length
}
cardsRemaining: state.regions.tavernDeck.childIds.length,
},
};
}
});
/**
*
*/
const nextTurnCmd = registry.register({
schema: 'next-turn',
run: async (game: RegicideGame) => {
async function nextTurnCmd(game: RegicideGame) {
const state = game.value;
await game.produce(state => {
state.currentPlayerIndex = (state.currentPlayerIndex + 1) % state.playerCount;
await game.produce((state) => {
state.currentPlayerIndex =
(state.currentPlayerIndex + 1) % state.playerCount;
});
const players: PlayerType[] = ['player1', 'player2', 'player3', 'player4'];
const players: PlayerType[] = ["player1", "player2", "player3", "player4"];
const currentPlayer = players[game.value.currentPlayerIndex];
return {
success: true,
result: {
currentPlayer,
currentPlayerIndex: game.value.currentPlayerIndex
}
currentPlayerIndex: game.value.currentPlayerIndex,
},
};
}
});
export {
playCmd as play,

View File

@ -1,8 +1,15 @@
import { IGameContext } from "@/core/game";
import { RegicideState } from "@/samples/regicide/state";
import {buildEnemyDeck, buildTavernDeck, createAllCards, getPlayerHandRegionId} from "@/samples/regicide/utils";
import {
buildEnemyDeck,
buildTavernDeck,
createAllCards,
getPlayerHandRegionId,
} from "@/samples/regicide/utils";
import { INITIAL_HAND_SIZE } from "@/samples/regicide/constants";
import { Enemy, PlayerType, RegicideCard } from "@/samples/regicide/types";
import { checkEnemy, nextTurn, pass, playWithA } from "./commands";
import { enemyCounterattackCmd, playCmd } from ".";
export type RegicideGame = IGameContext<RegicideState>;
@ -12,9 +19,13 @@ export type RegicideGame = IGameContext<RegicideState>;
* @param playerCount 1-4
* @param seed
*/
export async function setupGame(game: RegicideGame, playerCount: number, seed?: number) {
export async function setupGame(
game: RegicideGame,
playerCount: number,
seed?: number,
) {
if (playerCount < 1 || playerCount > 4) {
throw new Error('玩家数量必须为 1-4 人');
throw new Error("玩家数量必须为 1-4 人");
}
if (seed) {
@ -31,7 +42,7 @@ export async function setupGame(game: RegicideGame, playerCount: number, seed?:
const tavernDeck = buildTavernDeck(game._rng);
// 初始化游戏状态
await game.produceAsync(state => {
await game.produceAsync((state) => {
state.cards = allCards;
state.playerCount = playerCount;
state.currentPlayerIndex = 0;
@ -39,15 +50,15 @@ export async function setupGame(game: RegicideGame, playerCount: number, seed?:
// 设置酒馆牌堆区域
for (const card of tavernDeck) {
card.regionId = 'tavernDeck';
card.regionId = "tavernDeck";
state.regions.tavernDeck.childIds.push(card.id);
}
// 设置敌人牌堆区域只存储ID敌人是独立对象
state.regions.enemyDeck.childIds = enemyDeck.map(e => e.id);
state.regions.enemyDeck.childIds = enemyDeck.map((e) => e.id);
// 给每个玩家发牌
const players: PlayerType[] = ['player1', 'player2', 'player3', 'player4'];
const players: PlayerType[] = ["player1", "player2", "player3", "player4"];
for (let i = 0; i < playerCount; i++) {
const player = players[i];
const regionId = getPlayerHandRegionId(player);
@ -78,13 +89,13 @@ export async function start(game: RegicideGame) {
// 检查游戏是否已设置
if (!state.currentEnemy) {
throw new Error('请先调用 setupGame 初始化游戏');
throw new Error("请先调用 setupGame 初始化游戏");
}
const players: PlayerType[] = ['player1', 'player2', 'player3', 'player4'];
const players: PlayerType[] = ["player1", "player2", "player3", "player4"];
// 主游戏循环
while (state.phase === 'playing') {
while (state.phase === "playing") {
const currentPlayerIndex = state.currentPlayerIndex;
const currentPlayer = players[currentPlayerIndex];
@ -92,8 +103,9 @@ export async function start(game: RegicideGame) {
const currentHand = state.playerHands[currentPlayer];
if (currentHand.length === 0) {
// 玩家没有手牌,跳过回合
await game.produceAsync(state => {
state.currentPlayerIndex = (state.currentPlayerIndex + 1) % state.playerCount;
await game.produceAsync((state) => {
state.currentPlayerIndex =
(state.currentPlayerIndex + 1) % state.playerCount;
});
continue;
}
@ -113,31 +125,37 @@ export async function start(game: RegicideGame) {
/**
*
*/
export async function playTurn(game: RegicideGame, player: PlayerType, action: 'play' | 'pass', cardId?: string, secondCardId?: string) {
export async function playTurn(
game: RegicideGame,
player: PlayerType,
action: "play" | "pass",
cardId?: string,
secondCardId?: string,
) {
const state = game.value;
if (state.phase !== 'playing') {
return {success: false, error: '游戏已结束'};
if (state.phase !== "playing") {
return { success: false, error: "游戏已结束" };
}
if (!state.currentEnemy) {
return {success: false, error: '没有活跃的敌人'};
return { success: false, error: "没有活跃的敌人" };
}
let playResult: any;
// 执行玩家动作
if (action === 'play' && cardId) {
if (action === "play" && cardId) {
// 检查是否是A配合另一张牌
const card = state.cards[cardId];
if (card.rank === 'A' && secondCardId) {
playResult = await game.run(`play-with-a ${player} ${cardId} ${secondCardId}`);
if (card.rank === "A" && secondCardId) {
playResult = await playWithA(game, player, cardId, secondCardId);
} else {
playResult = await game.run(`play ${player} ${cardId}`);
playResult = await playCmd(game, player, cardId);
}
} else {
// 让过
playResult = await game.run(`pass ${player}`);
playResult = await pass(game, player);
}
if (!playResult.success) {
@ -145,7 +163,7 @@ export async function playTurn(game: RegicideGame, player: PlayerType, action: '
}
// 检查敌人是否被击败
const checkResult = await game.run<{defeated: boolean; currentEnemy?: any; nextEnemy?: any; defeatedEnemy?: any; enemiesRemaining?: number}>('check-enemy');
const checkResult = await checkEnemy(game);
if (!checkResult.success) {
return checkResult;
}
@ -160,15 +178,19 @@ export async function playTurn(game: RegicideGame, player: PlayerType, action: '
enemyDefeated: false,
needsDiscard: true,
enemyAttack: state.currentEnemy.value,
playerHand: state.playerHands[player]
}
playerHand: state.playerHands[player],
},
};
}
// 敌人被击败,检查是否还有更多敌人
if (state.enemyDeck.length === 0 && state.currentEnemy && state.currentEnemy.hp <= 0) {
await game.produceAsync(state => {
state.phase = 'victory';
if (
state.enemyDeck.length === 0 &&
state.currentEnemy &&
state.currentEnemy.hp <= 0
) {
await game.produceAsync((state) => {
state.phase = "victory";
state.winner = true;
});
return {
@ -176,41 +198,44 @@ export async function playTurn(game: RegicideGame, player: PlayerType, action: '
result: {
playResult: playResult.result,
enemyDefeated: true,
gameWon: true
}
gameWon: true,
},
};
}
// 切换到下一个玩家
await game.run('next-turn');
await nextTurn(game);
return {
success: true,
result: {
playResult: playResult.result,
enemyDefeated: true,
nextEnemy: state.currentEnemy
}
nextEnemy: state.currentEnemy,
},
};
}
/**
*
*/
export async function handleCounterattack(game: RegicideGame, player: PlayerType, discardCardIds: string[]) {
const result = await game.run(`counterattack ${player} ${JSON.stringify(discardCardIds)}`);
export async function handleCounterattack(
game: RegicideGame,
player: PlayerType,
discardCardIds: string[],
) {
const result = await enemyCounterattackCmd(game, player, discardCardIds);
if (!result.success) {
// 弃牌失败(点数和不足),游戏失败
await game.produceAsync(state => {
state.phase = 'defeat';
await game.produceAsync((state) => {
state.phase = "defeat";
state.winner = false;
});
return result;
}
// 弃牌成功,切换到下一个玩家
await game.run('next-turn');
await nextTurn(game);
return result;
}
@ -223,17 +248,26 @@ export function getGameStatus(game: RegicideGame) {
return {
phase: state.phase,
currentPlayer: ['player1', 'player2', 'player3', 'player4'][state.currentPlayerIndex],
currentEnemy: state.currentEnemy ? {
currentPlayer: ["player1", "player2", "player3", "player4"][
state.currentPlayerIndex
],
currentEnemy: state.currentEnemy
? {
...state.currentEnemy,
hpPercent: Math.round((state.currentEnemy.hp / state.currentEnemy.maxHp) * 100)
} : null,
hpPercent: Math.round(
(state.currentEnemy.hp / state.currentEnemy.maxHp) * 100,
),
}
: null,
enemiesRemaining: state.enemyDeck.length,
tavernDeckCount: state.regions.tavernDeck.childIds.length,
discardPileCount: state.regions.discardPile.childIds.length,
playerHands: Object.fromEntries(
Object.entries(state.playerHands).map(([player, hand]) => [player, hand.length])
Object.entries(state.playerHands).map(([player, hand]) => [
player,
hand.length,
]),
),
winner: state.winner
winner: state.winner,
};
}

View File

@ -7,8 +7,8 @@ export type {
RegicideCardMeta,
RegicideCard,
Enemy,
GamePhase
} from './types';
GamePhase,
} from "./types";
// Constants
export {
@ -18,21 +18,17 @@ export {
ALL_SUITS,
ALL_RANKS,
FACE_CARDS,
NUMBER_CARDS
} from './constants';
NUMBER_CARDS,
} from "./constants";
// State
export {
createInitialState,
type RegicideState
} from './state';
export { createInitialState, type RegicideState } from "./state";
// Prompts
export {prompts} from './prompts';
export { prompts } from "./prompts";
// Commands
export {
registry,
play as playCmd,
playWithA as playWithACmd,
pass as passCmd,
@ -41,7 +37,7 @@ export {
checkCanPlay as checkCanPlayCmd,
checkTavernDeck as checkTavernDeckCmd,
nextTurn as nextTurnCmd,
} from './commands';
} from "./commands";
// Game
export {
@ -50,8 +46,8 @@ export {
playTurn,
handleCounterattack,
getGameStatus,
type RegicideGame
} from './game';
type RegicideGame,
} from "./game";
// Utils
export {
@ -63,5 +59,5 @@ export {
buildTavernDeck,
drawFromDeck,
isEnemyDefeated,
getPlayerHandRegionId
} from './utils';
getPlayerHandRegionId,
} from "./utils";

View File

@ -1,10 +1,6 @@
import { Part } from "@/core/part";
import { createRegion } from "@/core/region";
import {
createGameCommandRegistry,
createPromptDef,
IGameContext,
} from "@/core/game";
import { createPromptDef, IGameContext } from "@/core/game";
const BOARD_SIZE = 3;
const MAX_TURNS = BOARD_SIZE * BOARD_SIZE;
@ -69,7 +65,6 @@ export function createInitialState() {
}
export type TicTacToeState = ReturnType<typeof createInitialState>;
export type TicTacToeGame = IGameContext<TicTacToeState>;
export const registry = createGameCommandRegistry<TicTacToeState>();
export const prompts = {
play: createPromptDef<[PlayerType, number, number]>(
"play <player> <row:number> <col:number>",
@ -95,9 +90,11 @@ export async function start(game: TicTacToeGame) {
return game.value;
}
const turn = registry.register({
schema: "turn <player> <turnNumber:number>",
async run(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: number) {
async function turn(
game: TicTacToeGame,
turnPlayer: PlayerType,
turnNumber: number,
) {
const { player, row, col } = await game.prompt(
prompts.play,
(player, row, col) => {
@ -121,8 +118,7 @@ const turn = registry.register({
if (turnNumber >= MAX_TURNS) return { winner: "draw" as WinnerType };
return { winner: null };
},
});
}
function isValidMove(row: number, col: number): boolean {
return (

View File

@ -4,7 +4,7 @@ import { CommandSchema } from "./types";
export interface PromptDef<TArgs extends any[]> {
schema: CommandSchema;
hint?: string;
hintText?: string;
}
export interface PromptCall<TArgs extends any[] = unknown[], TRes = unknown> {
@ -26,6 +26,7 @@ export type PromptTryResult =
ok: true;
};
export type PromptContext = ReturnType<typeof createPromptContext>;
export function createPromptContext() {
const map = new Map<string, PromptCall>();
const handleCall = createMiddlewareChain(async (call: PromptCall) => {
@ -44,6 +45,7 @@ export function createPromptContext() {
});
function tryCommit<TArgs extends any[]>(
def: PromptDef<TArgs>,
player: string,
...args: TArgs
): PromptTryResult {
@ -83,7 +85,14 @@ export function createPromptContext() {
reject: reject!,
promise,
} as PromptCall<unknown[], unknown>;
return await handleCall.execute(call);
return (await handleCall.execute(call)) as TRes;
}
function reset() {
for (const call of map.values()) {
call.reject("Prompt Reset");
}
map.clear();
}
return {
@ -91,5 +100,6 @@ export function createPromptContext() {
tryCommit,
cancel,
handleCall,
reset,
};
}

View File

@ -1,7 +1,14 @@
import { Signal, SignalOptions } from "@preact/signals-core";
import { create } from "mutative";
export class MutableSignal<T> extends Signal<T> {
export interface MutableSignal<T> extends Signal<T> {
produce(fn: (draft: T) => undefined): void;
addInterruption(promise: Promise<void>): void;
clearInterruptions(): void;
produceAsync(fn: (draft: T) => undefined): Promise<void>;
}
class MutableSignalImpl<T> extends Signal<T> {
private _interruptions: Promise<void>[] = [];
public constructor(t?: T, options?: SignalOptions<T>) {
@ -41,5 +48,5 @@ export function mutableSignal<T>(
initial?: T,
options?: SignalOptions<T>,
): MutableSignal<T> {
return new MutableSignal<T>(initial, options);
return new MutableSignalImpl<T>(initial, options);
}