diff --git a/src/core/game-client.ts b/src/core/game-client.ts index 0c487c4..ba7e71c 100644 --- a/src/core/game-client.ts +++ b/src/core/game-client.ts @@ -1,35 +1,31 @@ -import { createPromptContext, PromptCall } from "@/utils/command"; -import { createMiddlewareChain } from "@/utils/middleware"; -import { computed } from "@preact/signals-core"; +import { createPromptContext, PromptDef } from "@/utils/command"; +import { computed, effect, signal } from "@preact/signals-core"; import { createGameContext, IGameContext } from "./game"; -export interface GameClient { - // c# calls this to generate a ReactiveProperty - select( - type: K, - id: string, - callback: (t: E[K]) => void, - ): () => void; - - // c# calls this to interrupt and capture async events - use( - trigger: K, - callback: (ctx: T[K], next: () => Promise) => Promise, - ): () => void; -} - -export type EntityMap = {}; - -export type ClientTriggerMap = { - status: { status: ClientStatus; result?: unknown }; - prompt: PromptCall; -}; - export enum ClientStatus { Idle = "idle", Running = "running", Disposed = "disposed", } + +export type PromptInfo = + | { + hasPrompt: false; + player: string; + } + | { + hasPrompt: true; + player: string; + def: PromptDef; + tryAnswer(...args: unknown[]): string | null; + }; + +/** + * 1. create a client instance + * 2. setStatus("running") + * 3. select("selector", t => {}) for c# to capture game state + * 4. useClient("status" | "prompt") for c# to handle prompt / game status + */ export abstract class BaseGameClient< T extends Record = Record, R = unknown, @@ -37,33 +33,32 @@ export abstract class BaseGameClient< protected _initialState: T; protected _context: IGameContext; protected _prompts = createPromptContext(); - protected _status = ClientStatus.Idle; - - protected _setStatus = createMiddlewareChain( - async (ctx: ClientTriggerMap["status"]) => { - this._status = ctx.status; - if (this._status === ClientStatus.Disposed) { - this._prompts.reset(); - } else if (this._status === ClientStatus.Running) { - this._prompts.reset(); - this._context._state.value = this._initialState; - const r = await this.start(this._context); - this._setStatus.execute({ status: ClientStatus.Idle, result: r }); - } - }, - ); + protected _status = signal(ClientStatus.Idle); + protected _result = signal(null as null | R); + public get state() { + return this._context.value; + } constructor(is: T) { this._initialState = is; this._context = createGameContext(this._prompts, is); } - setStatus(status: ClientStatus) { - this._setStatus.execute({ status }); + async setStatus(status: ClientStatus) { + this._status.value = status; + if (status === ClientStatus.Disposed) { + this._prompts.reset(); + } else if (status === ClientStatus.Running) { + this._prompts.reset(); + this._context._state.value = this._initialState; + this._result.value = null; + this._result.value = await this.start(this._context); + this.setStatus(ClientStatus.Idle); + } } abstract start(ctx: IGameContext): Promise; - protected assignSelector(selector: (t: T) => S, callback: (s: S) => void) { + assignSelector(selector: (t: T) => S, callback: (s: S) => void) { const selected = computed(() => selector(this._context.value)); return selected.subscribe(callback); } @@ -77,27 +72,42 @@ export abstract class BaseGameClient< return resolve; } - useClient( - trigger: K, - callback: ( - ctx: ClientTriggerMap[K], - next: () => Promise, - ) => Promise, - ) { - if (trigger === "prompt") { - return this._prompts.handleCall.use( - callback as ( - ctx: ClientTriggerMap["prompt"], - next: () => Promise, - ) => Promise, - ); - } else if (trigger === "status") { - return this._setStatus.use( - callback as ( - ctx: ClientTriggerMap["status"], - next: () => Promise, - ) => Promise, - ); - } + selectPrompt(callback: (prompt: PromptInfo) => void) { + return this._prompts.handleCall.use(async (ctx, next) => { + function tryAnswer(...args: any[]) { + try { + const res = ctx.validator(...args); + ctx.resolve(res); + return null; + } catch (reason) { + if (typeof reason === "string") { + return reason; + } + throw reason; + } + } + + callback({ + hasPrompt: true, + player: ctx.player || "global", + def: ctx.def, + tryAnswer, + }); + await next(); + callback({ + hasPrompt: false, + player: ctx.player || "global", + }); + }); + } + selectStatus(callback: (status: ClientStatus) => void) { + return effect(() => { + callback(this._status.value); + }); + } + selectResult(callback: (status: R | null) => void) { + return effect(() => { + callback(this._result.value); + }); } } diff --git a/src/samples/tic-tac-toe.ts b/src/samples/tic-tac-toe.ts index 2176cde..ba13d68 100644 --- a/src/samples/tic-tac-toe.ts +++ b/src/samples/tic-tac-toe.ts @@ -1,5 +1,5 @@ import { Part } from "@/core/part"; -import { createRegion } from "@/core/region"; +import { createRegion, Region } from "@/core/region"; import { createPromptDef, IGameContext } from "@/core/game"; import { BaseGameClient, GameClient } from "@/core/game-client"; import { createMiddlewareChain } from "@/utils/middleware"; @@ -186,7 +186,7 @@ export async function placePiece( }); } -type EntityMap = { parts: TicTacToePart }; +type EntityMap = { parts: TicTacToePart; board: Region }; type TriggerMap = { turn: { player: PlayerType; turn: number }; };