Compare commits

..

No commits in common. "a0b2003c65b164d9606f0c8338ca0fa5a46dd46c" and "fb3c98b1ef93f84aa052d6869d471d296da21b05" have entirely different histories.

4 changed files with 10 additions and 180 deletions

View File

@ -1,113 +0,0 @@
import { createPromptContext, PromptDef } from "@/utils/command";
import { computed, effect, signal } from "@preact/signals-core";
import { createGameContext, IGameContext } from "./game";
export enum ClientStatus {
Idle = "idle",
Running = "running",
Disposed = "disposed",
}
export type PromptInfo =
| {
hasPrompt: false;
player: string;
}
| {
hasPrompt: true;
player: string;
def: PromptDef<unknown[]>;
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<string, unknown> = Record<string, unknown>,
R = unknown,
> {
protected _initialState: T;
protected _context: IGameContext<T>;
protected _prompts = createPromptContext();
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);
}
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<T>): Promise<R>;
assignSelector<S>(selector: (t: T) => S, callback: (s: S) => void) {
const selected = computed(() => selector(this._context.value));
return selected.subscribe(callback);
}
addInterruption() {
let resolve!: () => void;
const promise = new Promise<void>((resolver) => {
resolve = resolver;
});
this._context._state.addInterruption(promise);
return resolve;
}
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);
});
}
}

View File

@ -24,8 +24,6 @@ export {
moveToRegion,
} from "./core/region";
export * from "./core/game-client";
// Utils
export type {
Command,

View File

@ -1,8 +1,6 @@
import { Part } from "@/core/part";
import { createRegion, Region } from "@/core/region";
import { createRegion } from "@/core/region";
import { createPromptDef, IGameContext } from "@/core/game";
import { BaseGameClient } from "@/core/game-client";
import { createMiddlewareChain } from "@/utils/middleware";
const BOARD_SIZE = 3;
const MAX_TURNS = BOARD_SIZE * BOARD_SIZE;
@ -79,7 +77,7 @@ export async function start(game: TicTacToeGame) {
const turnNumber = game.value.turn + 1;
const turnOutput = await turn(game, currentPlayer, turnNumber);
await game.produceAsync((state) => {
game.produce((state) => {
state.winner = turnOutput.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === "X" ? "O" : "X";
@ -113,7 +111,7 @@ async function turn(
game.value.currentPlayer,
);
await placePiece(game, row, col, turnPlayer);
placePiece(game, row, col, turnPlayer);
const winner = checkWinner(game);
if (winner) return { winner };
@ -165,7 +163,7 @@ export function checkWinner(host: TicTacToeGame): WinnerType {
return null;
}
export async function placePiece(
export function placePiece(
host: TicTacToeGame,
row: number,
col: number,
@ -179,52 +177,9 @@ export async function placePiece(
player,
id: `piece-${player}-${moveNumber}`,
};
await host.produceAsync((state) => {
host.produce((state) => {
state.parts[piece.id] = piece;
board.childIds.push(piece.id);
board.partMap[`${row},${col}`] = piece.id;
});
}
export class Client extends BaseGameClient<TicTacToeState> {
private onTurn = createMiddlewareChain(
async (ctx: { player: PlayerType; turn: number }) => {
return await turn(this._context, ctx.player, ctx.turn);
},
);
constructor() {
super(createInitialState());
}
async start(game: IGameContext<TicTacToeState>): Promise<TicTacToeState> {
while (true) {
const currentPlayer = game.value.currentPlayer;
const turnNumber = game.value.turn + 1;
const turnOutput = await this.onTurn.execute({
player: currentPlayer,
turn: turnNumber,
});
await game.produceAsync((state) => {
state.winner = turnOutput.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === "X" ? "O" : "X";
state.turn = turnNumber;
}
});
if (game.value.winner) break;
}
return game.value;
}
selectPart(id: string, callback: (t: TicTacToePart) => void): () => void {
return this.assignSelector((state) => state.parts[id], callback);
}
selectTurn(callback: (turns: number) => void): () => void {
return this.assignSelector((s) => s.turn, callback);
}
selectCurrentPlayer(callback: (player: PlayerType) => void) {
return this.assignSelector((s) => s.currentPlayer, callback);
}
}

View File

@ -1,38 +1,28 @@
type Middleware<TContext, TReturn> = (
context: TContext,
next: () => Promise<TReturn>,
next: () => Promise<TReturn>
) => Promise<TReturn>;
export type MiddlewareChain<TContext, TReturn> = {
use: (middleware: Middleware<TContext, TReturn>) => () => void;
use: (middleware: Middleware<TContext, TReturn>) => void;
execute: (context: TContext) => Promise<TReturn>;
};
export function createMiddlewareChain<
TContext extends object,
TReturn = TContext,
>(
fallback?: (context: TContext) => Promise<TReturn>,
export function createMiddlewareChain<TContext extends object, TReturn=TContext>(
fallback?: (context: TContext) => Promise<TReturn>
): MiddlewareChain<TContext, TReturn> {
const middlewares: Middleware<TContext, TReturn>[] = [];
return {
use(middleware: Middleware<TContext, TReturn>) {
middlewares.push(middleware);
return function () {
const index = middlewares.indexOf(middleware);
if (index !== -1) {
middlewares.splice(index, 1);
}
};
},
async execute(context: TContext) {
let index = 0;
async function dispatch(ctx: TContext): Promise<TReturn> {
if (index >= middlewares.length) {
return fallback ? fallback(ctx) : (ctx as unknown as TReturn);
return fallback ? fallback(ctx) : ctx as unknown as TReturn;
}
const current = middlewares[index++];
return current(ctx, () => dispatch(ctx));