|
| 1 | +import { defineGamelet, defineToolset } from "@proj-airi/plugin-sdk-tamagotchi"; |
| 2 | +import * as v from "valibot"; |
| 3 | +//#region src/tools/chessTools.ts |
| 4 | +/** Gamelet the tools forward requests to; must match the id in {@link ../index}. */ |
| 5 | +const GAMELET_ID$1 = "chess"; |
| 6 | +/** Search depth bounds keep LLM-triggered analysis responsive in the browser. */ |
| 7 | +const MIN_DEPTH = 1; |
| 8 | +const MAX_DEPTH = 20; |
| 9 | +/** MultiPV bounds prevent huge candidate-line requests from monopolising Stockfish. */ |
| 10 | +const MIN_MULTIPV = 1; |
| 11 | +const MAX_MULTIPV = 5; |
| 12 | +/** Input schema for the `analyze_position` tool. */ |
| 13 | +const analyzePositionInput = v.object({ |
| 14 | + fen: v.string(), |
| 15 | + depth: v.optional(v.number()), |
| 16 | + multipv: v.optional(v.number()) |
| 17 | +}); |
| 18 | +/** Input schema for the `explain_move` tool. */ |
| 19 | +const explainMoveInput = v.object({ |
| 20 | + fenBefore: v.string(), |
| 21 | + moveUci: v.string() |
| 22 | +}); |
| 23 | +function boundedInteger(value, min, max, label) { |
| 24 | + if (value === void 0) return void 0; |
| 25 | + if (!Number.isInteger(value)) throw new Error(`${label} must be an integer.`); |
| 26 | + if (value < min || value > max) throw new Error(`${label} must be between ${min} and ${max}.`); |
| 27 | + return value; |
| 28 | +} |
| 29 | +/** |
| 30 | +* The coach's LLM-callable chess tools. |
| 31 | +* |
| 32 | +* Each tool is a thin forwarder: the real engine work runs inside the chess |
| 33 | +* gamelet (which owns the Stockfish worker and the board), so `execute` parses |
| 34 | +* its input and relays a request to the gamelet via `ctx.gamelets.request`. |
| 35 | +* The tools are only offered while the chess gamelet is open. |
| 36 | +*/ |
| 37 | +const chessTools = [{ |
| 38 | + id: "analyze_position", |
| 39 | + title: "Analyze chess position", |
| 40 | + description: "Runs the chess engine on a position and returns the best move, evaluation, and candidate lines. Call this before commenting on a position.", |
| 41 | + inputSchema: analyzePositionInput, |
| 42 | + isAvailable: (ctx) => ctx.gamelets.isOpen(GAMELET_ID$1), |
| 43 | + execute: async (input, ctx) => { |
| 44 | + const { fen, depth, multipv } = v.parse(analyzePositionInput, input); |
| 45 | + const safeDepth = boundedInteger(depth, MIN_DEPTH, MAX_DEPTH, "depth"); |
| 46 | + const safeMultipv = boundedInteger(multipv, MIN_MULTIPV, MAX_MULTIPV, "multipv"); |
| 47 | + return ctx.gamelets.request(GAMELET_ID$1, { |
| 48 | + type: "analyze_position", |
| 49 | + fen, |
| 50 | + ...safeDepth === void 0 ? {} : { depth: safeDepth }, |
| 51 | + ...safeMultipv === void 0 ? {} : { multipv: safeMultipv } |
| 52 | + }); |
| 53 | + } |
| 54 | +}, { |
| 55 | + id: "explain_move", |
| 56 | + title: "Explain a played move", |
| 57 | + description: "Compares a played move against the engine's best move and returns its classification (brilliant, blunder, ...) and centipawn loss. Call this to evaluate a move before commenting on it.", |
| 58 | + inputSchema: explainMoveInput, |
| 59 | + isAvailable: (ctx) => ctx.gamelets.isOpen(GAMELET_ID$1), |
| 60 | + execute: async (input, ctx) => { |
| 61 | + const { fenBefore, moveUci } = v.parse(explainMoveInput, input); |
| 62 | + return ctx.gamelets.request(GAMELET_ID$1, { |
| 63 | + type: "explain_move", |
| 64 | + fenBefore, |
| 65 | + moveUci |
| 66 | + }); |
| 67 | + } |
| 68 | +}]; |
| 69 | +//#endregion |
| 70 | +//#region src/index.ts |
| 71 | +/** |
| 72 | +* Stable gamelet identifier. The board UI is registered under this id and the |
| 73 | +* coach tools address the same gamelet through it. |
| 74 | +*/ |
| 75 | +const GAMELET_ID = "chess"; |
| 76 | +/** Plugin lifecycle hook — no eager work is needed before the host APIs exist. */ |
| 77 | +async function init() {} |
| 78 | +/** Plugin lifecycle hook — the chess plugin has no host-configurable settings yet. */ |
| 79 | +async function configure() {} |
| 80 | +/** |
| 81 | +* Registers the chess gamelet UI and the coach's analysis tools, then opens the |
| 82 | +* board. |
| 83 | +* |
| 84 | +* Use when: |
| 85 | +* - The plugin host reaches the module-setup lifecycle phase |
| 86 | +* |
| 87 | +* Expects: |
| 88 | +* - `apis` exposes the stage-tamagotchi gamelet kit and the tool registry |
| 89 | +*/ |
| 90 | +async function setupModules({ apis }) { |
| 91 | + try { |
| 92 | + await defineGamelet({ apis }, { |
| 93 | + id: GAMELET_ID, |
| 94 | + title: "Chess", |
| 95 | + entrypoint: "./ui/index.html", |
| 96 | + widgets: [{ |
| 97 | + id: "main-board", |
| 98 | + kind: "primary" |
| 99 | + }] |
| 100 | + }); |
| 101 | + await defineToolset({ apis }, { tools: chessTools }); |
| 102 | + await apis.gamelets?.open(GAMELET_ID); |
| 103 | + } catch (error) { |
| 104 | + console.error("[airi-plugin-game-chess] setupModules failed:", error); |
| 105 | + throw error; |
| 106 | + } |
| 107 | +} |
| 108 | +//#endregion |
| 109 | +export { configure, init, setupModules }; |
0 commit comments