chesspal 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents.d.ts +4 -2
- package/dist/commands/agents.js +11 -6
- package/dist/commands/ai-move.js +4 -7
- package/dist/commands/coach.js +2 -2
- package/dist/commands/context.d.ts +0 -2
- package/dist/commands/ponder.d.ts +2 -1
- package/dist/commands/ponder.js +12 -10
- package/dist/commands/registry.d.ts +2 -2
- package/dist/core/facts.d.ts +13 -0
- package/dist/core/facts.js +27 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/games/index.d.ts +1 -1
- package/dist/games/index.js +1 -1
- package/dist/games/thoughts.d.ts +7 -0
- package/dist/games/thoughts.js +16 -0
- package/dist/host-files/agents.d.ts +9 -3
- package/dist/host-files/agents.js +65 -20
- package/dist/host-files/host-files.js +3 -3
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/board/board.js +9 -4
- package/dist/page/board/marks.js +50 -8
- package/dist/page/helps/helps.css +85 -0
- package/dist/page/helps/helps.js +8 -3
- package/dist/page/helps/view.js +19 -5
- package/dist/page/text.js +8 -4
- package/dist/server/caller.d.ts +6 -3
- package/dist/server/caller.js +87 -2
- package/dist/server/mcp.d.ts +6 -1
- package/dist/server/mcp.js +36 -14
- package/dist/server/page/game.js +2 -2
- package/dist/server/page/page.d.ts +1 -5
- package/dist/server/page/page.js +3 -7
- package/instructions/opponent.md +1 -1
- package/package.json +1 -1
package/dist/page/board/marks.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
|
-
// What the board draws over its squares and pieces: the kings' badges at the end, the arrows, the helps' corners and pins, the control map's counts, the
|
|
3
|
-
// bubble of a refused move, the choice of a promotion and the piece lifted off the board.
|
|
4
2
|
import { useEffect, useRef, useState } from "preact/hooks";
|
|
5
3
|
import { Piece } from "../pieces/pieces.js";
|
|
6
4
|
/** The corner folded on a piece at risk, red on the player's and green on the other side's, in ink when no side is theirs (D-91); before its line in the card. */
|
|
7
5
|
export function Notch({ tone }) {
|
|
8
6
|
return (_jsx("span", { class: tone === undefined ? "help-notch" : `help-notch ${tone}`, "aria-hidden": "true", children: _jsxs("svg", { viewBox: "0 0 10 10", children: [_jsx("path", { d: "M0.5 0.5V9.5H9.5Z" }), tone === undefined ? null : _jsx("path", { class: "edge", d: "M0.5 0.5V9.5H9.5Z" }), _jsx("circle", { cx: "2.9", cy: "7.1", r: "1.1" })] }) }));
|
|
9
7
|
}
|
|
8
|
+
/** The pushpin stuck in a pinned piece from the top left corner, its needle toward the piece, in the tone of its corner at risk (D-91); in ink in no side's. */
|
|
9
|
+
export function Pushpin({ tone }) {
|
|
10
|
+
return _jsx(Stuck, { kind: "help-pin", shape: pushpin, tone: tone });
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The skewer stuck in the skewered piece, the one in front, as the pushpin in a pinned one (D-91): a stick in ink with a big bead and a small one in the tone,
|
|
14
|
+
* as the piece in front and the cheaper one behind it, so that it reads apart from the pushpin, all in the tone (Stefano); in the card's line, in its colour.
|
|
15
|
+
*/
|
|
16
|
+
export function Skewer({ tone }) {
|
|
17
|
+
return (_jsx(Stuck, { kind: "help-skewer", shape: skewer, tone: tone, children: tone === undefined || tone === "neutral" ? null : skewerBeads.map(({ y, radius }) => _jsx("circle", { cx: "12", cy: y, r: radius }, y)) }));
|
|
18
|
+
}
|
|
10
19
|
/** A king's badge in the corner of its square. */
|
|
11
20
|
export function Badge({ kind }) {
|
|
12
21
|
return (_jsx("span", { class: `badge ${kind}`, "aria-hidden": "true", children: kind === "mate" ? "#" : kind === "half" ? "½" : (_jsx("svg", { viewBox: "0 0 24 24", children: kind === "crown" ? _jsx("path", { d: "M4 18L3 7L8.5 11L12 5L15.5 11L21 7L20 18Z" }) : _jsx("path", { d: "M6 21V4M6 4.5C10 2.5 13 6.5 18 4.5V13.5C13 15.5 10 11.5 6 13.5" }) })) }));
|
|
@@ -46,13 +55,20 @@ export function Arrows({ arrows, at, explain, occupied }) {
|
|
|
46
55
|
const starts = [...new Set(shapes.map(({ from }) => from))];
|
|
47
56
|
return (_jsx("svg", { class: explain !== undefined ? "arrows explain" : occupied !== undefined ? "arrows drawn" : "arrows", viewBox: "0 0 8 8", "aria-hidden": "true", children: starts.map((start) => _jsx(ArrowGroup, { shapes: shapes.filter(({ from }) => from === start), edged: occupied !== undefined }, start)) }));
|
|
48
57
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Each pin as a dashed line from the pinning piece through the pinned one to what stands behind it (D-78), and each skewer as a dash-dot one, beads on its stick, over
|
|
60
|
+
* a halo that reads on every board. A pin and a skewer on the same stretch of a line stand apart on its two sides, so that both read.
|
|
61
|
+
*/
|
|
62
|
+
export function PinsAndSkewers({ pins, skewers, at }) {
|
|
63
|
+
const lines = [...pins.map((line) => ({ kind: "pin", line, other: skewers })), ...skewers.map((line) => ({ kind: "skewer", line, other: pins }))];
|
|
64
|
+
return (_jsx("svg", { class: "helps-layer", viewBox: "0 0 8 8", "aria-hidden": "true", children: lines.map(({ kind, line, other }) => {
|
|
52
65
|
// The line stops short of both ends, as an explaining arrow does: it hides neither piece, nor the corner of a pinner at risk.
|
|
53
|
-
const [
|
|
54
|
-
const [
|
|
55
|
-
|
|
66
|
+
const [startX, startY] = towards(at(line.by.square), at(line.to.square), pinTrim);
|
|
67
|
+
const [endX, endY] = towards(at(line.to.square), at(line.by.square), pinTrim);
|
|
68
|
+
const shift = other.some((each) => sharedSquares(each, line) > 1) ? (kind === "pin" ? 1 : -1) * sharedLineShift : 0;
|
|
69
|
+
const [dx, dy] = across([startX, startY], [endX, endY], shift);
|
|
70
|
+
const ends = { x1: startX + dx, y1: startY + dy, x2: endX + dx, y2: endY + dy };
|
|
71
|
+
return (_jsxs("g", { class: kind, children: [_jsx("line", { class: "halo", ...ends }), _jsx("line", { ...ends })] }, `${kind} ${line.by.square}${line.piece.square}`));
|
|
56
72
|
}) }));
|
|
57
73
|
}
|
|
58
74
|
/** A square's counts on the control map, as a domino in its middle: how many of White's pieces reach it, and of Black's; none when no piece does. */
|
|
@@ -135,6 +151,24 @@ function ArrowGroup({ shapes, edged }) {
|
|
|
135
151
|
? shapes.map(({ key, shaft, head }) => (_jsxs("g", { children: [_jsx("path", { class: "edge", d: shaft }), _jsx("polygon", { class: "edge", points: head })] }, `edge${key}`)))
|
|
136
152
|
: null, shapes.map(({ key, colour, shaft, head }) => (_jsxs("g", { class: colour, children: [_jsx("path", { d: shaft }), _jsx("polygon", { points: head })] }, key)))] }));
|
|
137
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* A sign stuck in a piece from its square's top left corner, tilted toward the piece; edged in ink when coloured (Zoe). Its box holds the tilted sign and
|
|
156
|
+
* its halo only, so that it sits in the corner (Stefano).
|
|
157
|
+
*/
|
|
158
|
+
function Stuck({ kind, shape, tone, children }) {
|
|
159
|
+
return (_jsx("span", { class: tone === undefined ? kind : `${kind} ${tone}`, "aria-hidden": "true", children: _jsx("svg", { viewBox: "2 0.5 18.5 22.5", children: _jsxs("g", { transform: "rotate(-32 12 12)", children: [_jsx("path", { d: shape }), tone === undefined || tone === "neutral" ? null : _jsx("path", { class: "edge", d: shape }), children] }) }) }));
|
|
160
|
+
}
|
|
161
|
+
/** How many of their three squares a pin and a skewer share: two when they lie on the same stretch of a line. */
|
|
162
|
+
function sharedSquares(one, other) {
|
|
163
|
+
const squares = [one.by.square, one.piece.square, one.to.square];
|
|
164
|
+
return [other.by.square, other.piece.square, other.to.square].filter((square) => squares.includes(square)).length;
|
|
165
|
+
}
|
|
166
|
+
/** A step of the given length across a line, always to the same side of it whichever way the line runs. */
|
|
167
|
+
function across(start, end, distance) {
|
|
168
|
+
const length = Math.hypot(end[0] - start[0], end[1] - start[1]);
|
|
169
|
+
const way = end[0] < start[0] || (end[0] === start[0] && end[1] < start[1]) ? -1 : 1;
|
|
170
|
+
return [-(end[1] - start[1]) / length * distance * way, (end[0] - start[0]) / length * distance * way];
|
|
171
|
+
}
|
|
138
172
|
/** A point `distance` squares from `point` toward `target`. */
|
|
139
173
|
function towards(point, target, distance) {
|
|
140
174
|
const length = Math.hypot(target[0] - point[0], target[1] - point[1]);
|
|
@@ -150,10 +184,18 @@ function ExplainIcon({ kind, piece }) {
|
|
|
150
184
|
: _jsx("path", { d: "M4 8h15M15 4l4 4-4 4M20 16H5M9 12l-4 4 4 4" }) })) }));
|
|
151
185
|
}
|
|
152
186
|
const promotionPieces = ["q", "n", "r", "b"];
|
|
187
|
+
// A pushpin standing up, as the bubble's pin: its cap, its body widening into the collar, and its needle. Tilted less than a diagonal, so it never lies along a pin's line.
|
|
188
|
+
const pushpin = "M8.6 2.2H15.4Q16.6 2.2 16.6 3.4V4.6Q16.6 5.8 15.4 5.8H14.8V9.4L18.4 13.1V14.9H13V20L12 22.6L11 20V14.9H5.6V13.1L9.2 9.4V5.8H8.6Q7.4 5.8 7.4 4.6V3.4Q7.4 2.2 8.6 2.2Z";
|
|
189
|
+
// A skewer standing up, in the pushpin's box: a round end, a big bead, a small one, a stretch of stick between them, and its point.
|
|
190
|
+
const skewer = "M10.9 2.6A1.1 1.1 0 0 1 13.1 2.6V3.75A4.2 4.2 0 0 1 13.1 11.85V13A3.1 3.1 0 0 1 13.1 18.8V20.2L12 23.4L10.9 20.2V18.8A3.1 3.1 0 0 1 10.9 13V11.85A4.2 4.2 0 0 1 10.9 3.75Z";
|
|
191
|
+
// The skewer's beads, on its stick: the big one and the small one, as its shape draws them.
|
|
192
|
+
const skewerBeads = [{ y: 7.8, radius: 4.2 }, { y: 15.9, radius: 3.1 }];
|
|
153
193
|
// How much an arrow that explains is cut, in squares: off the culprit at its start, and before an occupied square at its point.
|
|
154
194
|
const explainTrim = { start: 0.36, end: 0.4 };
|
|
155
195
|
// How much a pin's line is cut at each end, in squares.
|
|
156
196
|
const pinTrim = 0.35;
|
|
197
|
+
// How far a pin and a skewer on the same stretch of a line stand off it, each on its side, in squares: a little more than a line with its halo.
|
|
198
|
+
const sharedLineShift = 0.08;
|
|
157
199
|
// An arrow's head, in squares: how long, and how far each side of its line.
|
|
158
200
|
const arrowHead = { length: 0.42, width: 0.3 };
|
|
159
201
|
// The helps' point, smaller in step with their thinner shaft (Stefano).
|
|
@@ -49,6 +49,74 @@
|
|
|
49
49
|
stroke-width: 0.5;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/* A pinned piece: a pushpin stuck in it from the top left corner, in the notch's colours and on its halo; it takes the place of the rank's number there.
|
|
53
|
+
A skewered piece, the one in front: a skewer stuck in it the same way, beside the pushpin when the piece is pinned too */
|
|
54
|
+
.help-pin,
|
|
55
|
+
.help-skewer {
|
|
56
|
+
position: absolute;
|
|
57
|
+
left: 1px;
|
|
58
|
+
top: 1px;
|
|
59
|
+
z-index: 2;
|
|
60
|
+
display: block;
|
|
61
|
+
height: clamp(15px, 3.6cqw, 26px);
|
|
62
|
+
aspect-ratio: 18.5 / 22.5;
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.help-pin svg,
|
|
67
|
+
.help-skewer svg {
|
|
68
|
+
display: block;
|
|
69
|
+
width: 100%;
|
|
70
|
+
height: 100%;
|
|
71
|
+
overflow: visible;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.help-pin path,
|
|
75
|
+
.help-skewer path {
|
|
76
|
+
fill: var(--help-ink);
|
|
77
|
+
stroke: var(--help-halo);
|
|
78
|
+
stroke-width: 2.6;
|
|
79
|
+
stroke-linejoin: round;
|
|
80
|
+
paint-order: stroke;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.help-pin.threat path {
|
|
84
|
+
fill: var(--badge-blunder);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.help-pin.chance path {
|
|
88
|
+
fill: var(--badge-best);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* The skewer's stick stays in ink and its beads take the tone, so that it reads apart from the pushpin (Stefano) */
|
|
92
|
+
.help-skewer circle {
|
|
93
|
+
stroke: var(--help-ink);
|
|
94
|
+
stroke-width: 0.9;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.help-skewer.threat circle {
|
|
98
|
+
fill: var(--badge-blunder);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.help-skewer.chance circle {
|
|
102
|
+
fill: var(--badge-best);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.help-pin path.edge,
|
|
106
|
+
.help-skewer path.edge {
|
|
107
|
+
fill: none;
|
|
108
|
+
stroke: var(--help-ink);
|
|
109
|
+
stroke-width: 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.square:has(> .help-pin) > .help-skewer {
|
|
113
|
+
left: clamp(9px, 3cqw, 22px);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.square:has(> .help-pin, > .help-skewer) > .coordinate.rank {
|
|
117
|
+
visibility: hidden;
|
|
118
|
+
}
|
|
119
|
+
|
|
52
120
|
/* A pin: a thin dashed ink line from the pinner to what stands behind, under the arrows; without a head, never an arrow */
|
|
53
121
|
.helps-layer {
|
|
54
122
|
position: absolute;
|
|
@@ -73,6 +141,11 @@
|
|
|
73
141
|
stroke-linecap: round;
|
|
74
142
|
}
|
|
75
143
|
|
|
144
|
+
/* A skewer's line: a dash and a dot, beads on its stick, so that it reads apart from a pin's even dashes */
|
|
145
|
+
.helps-layer .skewer line:not(.halo) {
|
|
146
|
+
stroke-dasharray: 0.22 0.11 0 0.11;
|
|
147
|
+
}
|
|
148
|
+
|
|
76
149
|
/* The map (Zoe, after the round): every square on one neutral ground, then light where only White reaches, dark where only Black does, half and half
|
|
77
150
|
when both do; the two counts together in the middle, as a domino. The drawings and the pieces step back */
|
|
78
151
|
.board.mapping ~ .arrows,
|
|
@@ -383,6 +456,18 @@
|
|
|
383
456
|
fill: var(--card);
|
|
384
457
|
}
|
|
385
458
|
|
|
459
|
+
/* The skewer of the board, in the line's colour and without its halo */
|
|
460
|
+
.help-rows .help-skewer {
|
|
461
|
+
left: 7px;
|
|
462
|
+
top: 4px;
|
|
463
|
+
height: 17px;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.help-rows .help-skewer path {
|
|
467
|
+
fill: var(--row);
|
|
468
|
+
stroke: none;
|
|
469
|
+
}
|
|
470
|
+
|
|
386
471
|
.help-rows .row-icon {
|
|
387
472
|
position: absolute;
|
|
388
473
|
left: 6px;
|
package/dist/page/helps/helps.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-ru
|
|
|
3
3
|
// the coach's words (D-88).
|
|
4
4
|
import { Fragment } from "preact";
|
|
5
5
|
import { useState } from "preact/hooks";
|
|
6
|
-
import { Notch } from "../board/marks.js";
|
|
6
|
+
import { Notch, Skewer } from "../board/marks.js";
|
|
7
7
|
import { BadgeIcon, NoteIcon } from "../game/badges.js";
|
|
8
8
|
import { FoldButton, useCardFold } from "../game/card-fold.js";
|
|
9
9
|
import { Figurines } from "../game/move-list.js";
|
|
@@ -70,7 +70,7 @@ function Judged({ ply, text }) {
|
|
|
70
70
|
function Tactics({ helps, opened, onOpen, onPoint, text }) {
|
|
71
71
|
const words = text.helpsCard;
|
|
72
72
|
const point = pointing(onPoint);
|
|
73
|
-
const calm = !helps.lines.some((line) => line.kind === "risk" || line.kind === "pin");
|
|
73
|
+
const calm = !helps.lines.some((line) => line.kind === "risk" || line.kind === "pin" || line.kind === "skewer");
|
|
74
74
|
return (_jsxs("ul", { class: "help-rows", children: [calm
|
|
75
75
|
? (_jsxs("li", { class: "safe", children: [_jsxs(RowIcon, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M5.2 8.2l2 2 3.8-4" })] }), words.nothingAtRisk] }))
|
|
76
76
|
: null, helps.lines.map((line) => {
|
|
@@ -86,11 +86,14 @@ function pointing(onPoint) {
|
|
|
86
86
|
onPointerLeave: (event) => event.pointerType === "mouse" && onPoint([]),
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
|
-
/** A line's sign: the corner of a piece at risk, a pin's link, a check's plus, the loose pieces' i; its colour is the line's. */
|
|
89
|
+
/** A line's sign: the corner of a piece at risk, a pin's link, the skewer of the board, a check's plus, the loose pieces' i; its colour is the line's. */
|
|
90
90
|
function LineIcon({ kind }) {
|
|
91
91
|
if (kind === "risk") {
|
|
92
92
|
return _jsx(Notch, {});
|
|
93
93
|
}
|
|
94
|
+
if (kind === "skewer") {
|
|
95
|
+
return _jsx(Skewer, {});
|
|
96
|
+
}
|
|
94
97
|
if (kind === "loose") {
|
|
95
98
|
return _jsx(InfoIcon, {});
|
|
96
99
|
}
|
|
@@ -115,6 +118,8 @@ function LineDetails({ line, helps, point, text }) {
|
|
|
115
118
|
return (_jsx("ul", { class: "line-details", children: line.items.map((risk) => (_jsxs("li", { ...point(riskSquares(risk)), children: [text.piece(risk.piece.piece, risk.piece.side), " ", _jsx(Squares, { squares: [risk.piece.square] }), " \u00B7 ", words.attackers, ": ", _jsx(Squares, { squares: risk.piece.attackers }), risk.why === "undefended" ? null : (_jsxs(_Fragment, { children: [" ", "\u00B7 ", words.defenders, ": ", _jsx(Squares, { squares: risk.piece.defenders })] })), " \u00B7 ", words[risk.why]] }, risk.piece.square))) }));
|
|
116
119
|
case "pin":
|
|
117
120
|
return (_jsx("ul", { class: "line-details", children: line.items.map((pin) => (_jsxs("li", { ...point(pinSquares(pin)), children: [text.piece(pin.piece.piece, pin.piece.side), " ", _jsx(Squares, { squares: [pin.piece.square] }), " ", words.pinnedTo(pin.to.piece), " ", _jsx(Squares, { squares: [pin.to.square] }), ", ", words.pinnedBy(pin.by.piece), " ", _jsx(Squares, { squares: [pin.by.square] })] }, pin.piece.square))) }));
|
|
121
|
+
case "skewer":
|
|
122
|
+
return (_jsx("ul", { class: "line-details", children: line.items.map((skewer) => (_jsxs("li", { ...point(pinSquares(skewer)), children: [text.piece(skewer.piece.piece, skewer.piece.side), " ", _jsx(Squares, { squares: [skewer.piece.square] }), " ", words.skeweredBefore(skewer.to.piece), " ", _jsx(Squares, { squares: [skewer.to.square] }), ", ", words.pinnedBy(skewer.by.piece), " ", _jsx(Squares, { squares: [skewer.by.square] })] }, skewer.piece.square))) }));
|
|
118
123
|
case "check":
|
|
119
124
|
return (_jsx("p", { class: "line-details", children: line.items.map((check) => (_jsx("span", { class: "help-move", children: _jsx(Figurines, { san: check, side: line.side }) }, check))) }));
|
|
120
125
|
case "loose":
|
package/dist/page/helps/view.js
CHANGED
|
@@ -7,6 +7,7 @@ export function helpsOf(game, you) {
|
|
|
7
7
|
const toMove = game.position.turn;
|
|
8
8
|
const atRisk = [...risks(facts.undefended, "undefended"), ...risks(facts.attackedByCheaper, "cheaper"), ...risks(facts.outnumbered, "outnumbered")];
|
|
9
9
|
const pins = facts.pins.map((pin) => ({ piece: on(pin.square), by: on(pin.by), to: on(pin.to) }));
|
|
10
|
+
const skewers = facts.skewers.map((skewer) => ({ piece: on(skewer.square), by: on(skewer.by), to: on(skewer.to) }));
|
|
10
11
|
const checks = facts.checks;
|
|
11
12
|
const otherChecks = facts.otherChecks ?? [];
|
|
12
13
|
const loose = facts.loose.map(on);
|
|
@@ -15,10 +16,11 @@ export function helpsOf(game, you) {
|
|
|
15
16
|
you,
|
|
16
17
|
atRisk,
|
|
17
18
|
pins,
|
|
19
|
+
skewers,
|
|
18
20
|
checks,
|
|
19
21
|
otherChecks,
|
|
20
22
|
loose,
|
|
21
|
-
lines: tacticLines({ toMove, atRisk, pins, checks, otherChecks, loose }, you),
|
|
23
|
+
lines: tacticLines({ toMove, atRisk, pins, skewers, checks, otherChecks, loose }, you),
|
|
22
24
|
strategy: game.profile,
|
|
23
25
|
sides: you === "black" ? ["black", "white"] : ["white", "black"],
|
|
24
26
|
};
|
|
@@ -28,12 +30,23 @@ export function riskOn(helps, square) {
|
|
|
28
30
|
const risk = helps.atRisk.find((risk) => risk.piece.square === square);
|
|
29
31
|
return risk === undefined ? undefined : toneOf(risk.piece.side, helps.you);
|
|
30
32
|
}
|
|
31
|
-
/**
|
|
33
|
+
/** How the helps mark a square's pinned piece: in the tone of a piece at risk, red when it is the player's, green when it is the other side's (D-91). */
|
|
34
|
+
export function pinOn(helps, square) {
|
|
35
|
+
const pin = helps.pins.find((pin) => pin.piece.square === square);
|
|
36
|
+
return pin === undefined ? undefined : toneOf(pin.piece.side, helps.you);
|
|
37
|
+
}
|
|
38
|
+
/** How the helps mark a square's skewered piece, the one in front: in the tone of a piece at risk, as a pinned one (D-91). */
|
|
39
|
+
export function skewerOn(helps, square) {
|
|
40
|
+
const skewer = helps.skewers.find((skewer) => skewer.piece.square === square);
|
|
41
|
+
return skewer === undefined ? undefined : toneOf(skewer.piece.side, helps.you);
|
|
42
|
+
}
|
|
43
|
+
/** The squares a line of the tactics lights on the board: its pieces, and those that attack, pin or skewer them; none for the checks. */
|
|
32
44
|
export function squaresOf(line) {
|
|
33
45
|
switch (line.kind) {
|
|
34
46
|
case "risk":
|
|
35
47
|
return line.items.flatMap(riskSquares);
|
|
36
48
|
case "pin":
|
|
49
|
+
case "skewer":
|
|
37
50
|
return line.items.flatMap(pinSquares);
|
|
38
51
|
case "check":
|
|
39
52
|
return [];
|
|
@@ -45,7 +58,7 @@ export function squaresOf(line) {
|
|
|
45
58
|
export function riskSquares(risk) {
|
|
46
59
|
return [risk.piece.square, ...risk.piece.attackers];
|
|
47
60
|
}
|
|
48
|
-
/** A pinned piece, the one that pins it and the one behind it. */
|
|
61
|
+
/** A pinned or skewered piece, the one that pins or skewers it and the one behind it. */
|
|
49
62
|
export function pinSquares(pin) {
|
|
50
63
|
return [pin.piece.square, pin.by.square, pin.to.square];
|
|
51
64
|
}
|
|
@@ -88,12 +101,13 @@ const cells = {
|
|
|
88
101
|
castled: castlingOf,
|
|
89
102
|
kingAttacks: (side) => side.king?.attacksNear ?? 0,
|
|
90
103
|
};
|
|
91
|
-
// The tactics' lines that have something: with the player's side, their pieces at risk and
|
|
92
|
-
// round; without it, White's then Black's; the loose pieces last.
|
|
104
|
+
// The tactics' lines that have something: with the player's side, their pieces at risk, pinned and skewered and the other side's checks, then the same the
|
|
105
|
+
// other way round; without it, White's then Black's; the loose pieces last.
|
|
93
106
|
function tacticLines(facts, you) {
|
|
94
107
|
const sideLines = (side) => [
|
|
95
108
|
{ kind: "risk", side, tone: toneOf(side, you), items: facts.atRisk.filter((risk) => risk.piece.side === side) },
|
|
96
109
|
{ kind: "pin", side, tone: toneOf(side, you), items: facts.pins.filter((pin) => pin.piece.side === side) },
|
|
110
|
+
{ kind: "skewer", side, tone: toneOf(side, you), items: facts.skewers.filter((skewer) => skewer.piece.side === side) },
|
|
97
111
|
];
|
|
98
112
|
const checkLine = (side) => ({
|
|
99
113
|
kind: "check",
|
package/dist/page/text.js
CHANGED
|
@@ -274,16 +274,18 @@ const english = {
|
|
|
274
274
|
outnumbered: "more attackers than defenders",
|
|
275
275
|
pinnedTo: (letter) => `pinned to the ${englishNames[letter]}`,
|
|
276
276
|
pinnedBy: (letter) => `by the ${englishNames[letter]}`,
|
|
277
|
+
skeweredBefore: (letter) => `ahead of the ${englishNames[letter]}`,
|
|
277
278
|
nothingAtRisk: "No piece at risk, no pins.",
|
|
278
279
|
line: (kind, side, you, toMove) => {
|
|
279
280
|
const name = side === "white" ? "White" : "Black";
|
|
281
|
+
const pieces = { risk: "pieces at risk", pin: "pinned pieces", skewer: "skewered pieces" };
|
|
280
282
|
if (you === undefined) {
|
|
281
|
-
return kind === "
|
|
283
|
+
return kind === "check" ? `${name}'s checks${toMove ? "" : `, if it were ${name}'s move`}` : `${name}'s ${pieces[kind]}`;
|
|
282
284
|
}
|
|
283
285
|
if (kind === "check") {
|
|
284
286
|
return side === you ? `Checks you ${toMove ? "can" : "could"} give` : `Checks the opponent ${toMove ? "can" : "could"} give you`;
|
|
285
287
|
}
|
|
286
|
-
return `${side === you ? "Your" : "The opponent's"} ${kind
|
|
288
|
+
return `${side === you ? "Your" : "The opponent's"} ${pieces[kind]}`;
|
|
287
289
|
},
|
|
288
290
|
loose: "Pieces neither attacked nor defended",
|
|
289
291
|
yours: "yours",
|
|
@@ -594,16 +596,18 @@ const italian = {
|
|
|
594
596
|
outnumbered: "più attaccanti che difensori",
|
|
595
597
|
pinnedTo: (letter) => `davanti ${italianWith("a", letter)}`,
|
|
596
598
|
pinnedBy: (letter) => italianWith("da", letter),
|
|
599
|
+
skeweredBefore: (letter) => `con dietro ${italianThe[letter]}`,
|
|
597
600
|
nothingAtRisk: "Nessun pezzo in pericolo, nessuna inchiodatura.",
|
|
598
601
|
line: (kind, side, you, toMove) => {
|
|
599
602
|
const name = side === "white" ? "Bianco" : "Nero";
|
|
603
|
+
const what = { risk: "in pericolo", pin: "inchiodati", skewer: "infilati" };
|
|
600
604
|
if (you === undefined) {
|
|
601
|
-
return kind === "
|
|
605
|
+
return kind === "check" ? `Scacchi del ${name}${toMove ? "" : ", se toccasse a lui"}` : `Pezzi del ${name} ${what[kind]}`;
|
|
602
606
|
}
|
|
603
607
|
if (kind === "check") {
|
|
604
608
|
return side === you ? `Scacchi che ${toMove ? "puoi" : "potresti"} dare` : `Scacchi che l'avversario ${toMove ? "può" : "potrebbe"} darti`;
|
|
605
609
|
}
|
|
606
|
-
return `${side === you ? "I tuoi pezzi" : "I pezzi avversari"} ${kind
|
|
610
|
+
return `${side === you ? "I tuoi pezzi" : "I pezzi avversari"} ${what[kind]}`;
|
|
607
611
|
},
|
|
608
612
|
loose: "Pezzi né attaccati né difesi",
|
|
609
613
|
yours: "tuoi",
|
package/dist/server/caller.d.ts
CHANGED
|
@@ -6,16 +6,19 @@ export interface Client {
|
|
|
6
6
|
}
|
|
7
7
|
/** Who made the call with these metadata: the model and effort the host ties to it, else the host's name; undefined without a host. */
|
|
8
8
|
export declare function callerOf(meta: Record<string, unknown> | undefined, client: Client | undefined, folder: string, wait?: number): Promise<Caller | undefined>;
|
|
9
|
-
/** Who in Claude Code made a tool call: the chat itself, or one of its agents by its type. */
|
|
9
|
+
/** Who in Claude Code or Codex made a tool call: the chat itself, or one of its agents by its type. */
|
|
10
10
|
export type CallingAgent = {
|
|
11
11
|
chat: true;
|
|
12
12
|
} | {
|
|
13
13
|
agent: string;
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
* Who in Claude Code made a tool call: the chat, when its line is in the chat's transcript, or the agent whose file has it, by the type Claude Code
|
|
17
|
-
* beside that file (Priya). Undefined when it cannot be told: another host,
|
|
16
|
+
* Who in Claude Code or Codex made a tool call: the chat, when its line is in the chat's transcript, or the agent whose file has it, by the type Claude Code
|
|
17
|
+
* notes beside that file (Priya); in Codex, as the call's metadata and the agent's own session say (D-98). Undefined when it cannot be told: another host,
|
|
18
|
+
* no chat noted, no line, or no type.
|
|
18
19
|
*/
|
|
19
20
|
export declare function callingAgent(meta: Record<string, unknown> | undefined, folder: string, wait?: number): Promise<CallingAgent | undefined>;
|
|
21
|
+
/** Whether a call comes from one of Codex's agents, as its metadata say: its own process starts no board page (D-98). */
|
|
22
|
+
export declare function fromCodexAgent(meta: Record<string, unknown> | undefined): boolean;
|
|
20
23
|
/** Whether an agent's tool may answer this caller: one of its agents, or a caller that cannot be told, since the tool's description is the first guard. */
|
|
21
24
|
export declare function allowedCaller(caller: CallingAgent | undefined, agents: readonly string[]): boolean;
|
package/dist/server/caller.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Who makes a tool call (D-81): only what the host ties to that call, never what the AI says of itself nor a setting the chat may have changed.
|
|
2
2
|
// Codex sends the model and the effort with each call; Claude Code writes them into the chat's transcript, on the line of the call.
|
|
3
3
|
import { closeSync, openSync, readdirSync, readFileSync, readSync, statSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { basename, dirname, join } from "node:path";
|
|
5
6
|
import { lastChatStart } from "../games/index.js";
|
|
6
7
|
/** Who made the call with these metadata: the model and effort the host ties to it, else the host's name; undefined without a host. */
|
|
@@ -20,10 +21,15 @@ export async function callerOf(meta, client, folder, wait = 50) {
|
|
|
20
21
|
return client === undefined ? undefined : { annotator: client.title ?? client.name, hostOnly: true };
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
23
|
-
* Who in Claude Code made a tool call: the chat, when its line is in the chat's transcript, or the agent whose file has it, by the type Claude Code
|
|
24
|
-
* beside that file (Priya). Undefined when it cannot be told: another host,
|
|
24
|
+
* Who in Claude Code or Codex made a tool call: the chat, when its line is in the chat's transcript, or the agent whose file has it, by the type Claude Code
|
|
25
|
+
* notes beside that file (Priya); in Codex, as the call's metadata and the agent's own session say (D-98). Undefined when it cannot be told: another host,
|
|
26
|
+
* no chat noted, no line, or no type.
|
|
25
27
|
*/
|
|
26
28
|
export async function callingAgent(meta, folder, wait = 50) {
|
|
29
|
+
const turn = objectOf(meta?.["x-codex-turn-metadata"]);
|
|
30
|
+
if (turn !== undefined) {
|
|
31
|
+
return await codexCaller(turn, wait);
|
|
32
|
+
}
|
|
27
33
|
const call = meta?.["claudecode/toolUseId"];
|
|
28
34
|
const transcript = lastChatStart(folder)?.transcript;
|
|
29
35
|
const found = typeof call === "string" && transcript !== undefined ? await foundCall(transcript, call, wait) : undefined;
|
|
@@ -36,6 +42,10 @@ export async function callingAgent(meta, folder, wait = 50) {
|
|
|
36
42
|
const type = objectOf(textOf(found.file.replace(/\.jsonl$/, ".meta.json")))?.agentType;
|
|
37
43
|
return typeof type === "string" ? { agent: type } : undefined;
|
|
38
44
|
}
|
|
45
|
+
/** Whether a call comes from one of Codex's agents, as its metadata say: its own process starts no board page (D-98). */
|
|
46
|
+
export function fromCodexAgent(meta) {
|
|
47
|
+
return objectOf(meta?.["x-codex-turn-metadata"])?.thread_source === "subagent";
|
|
48
|
+
}
|
|
39
49
|
/** Whether an agent's tool may answer this caller: one of its agents, or a caller that cannot be told, since the tool's description is the first guard. */
|
|
40
50
|
export function allowedCaller(caller, agents) {
|
|
41
51
|
return caller === undefined || ("agent" in caller && agents.includes(caller.agent));
|
|
@@ -80,6 +90,81 @@ function callerIn(text, call) {
|
|
|
80
90
|
}
|
|
81
91
|
return undefined;
|
|
82
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Who in Codex made a call: the chat, or an agent by the role its session notes first, before its first call (Priya). An agent started with the chat's
|
|
95
|
+
* own turns sees the opponent's plan, so it counts as the chat (D-88).
|
|
96
|
+
*/
|
|
97
|
+
async function codexCaller(turn, wait) {
|
|
98
|
+
if (turn.thread_source !== "subagent") {
|
|
99
|
+
return turn.thread_source === "user" ? { chat: true } : undefined;
|
|
100
|
+
}
|
|
101
|
+
const thread = turn.thread_id;
|
|
102
|
+
if (typeof thread !== "string" || !/^[0-9a-f-]{36}$/i.test(thread)) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const known = codexThreads.get(thread);
|
|
106
|
+
if (known !== undefined) {
|
|
107
|
+
return known;
|
|
108
|
+
}
|
|
109
|
+
for (let attempt = 0; attempt < codexAttempts; attempt++) {
|
|
110
|
+
const start = headOf(codexSession(thread));
|
|
111
|
+
const role = /"agent_role":"([^"]+)"/.exec(start)?.[1];
|
|
112
|
+
if (role !== undefined) {
|
|
113
|
+
const caller = start.includes('"forked_from_id":"') ? { chat: true } : { agent: role };
|
|
114
|
+
codexThreads.set(thread, caller);
|
|
115
|
+
return caller;
|
|
116
|
+
}
|
|
117
|
+
await new Promise((done) => setTimeout(done, wait));
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
// Who each agent's thread is, read once: it never changes, and an agent calls a tool many times (Federico).
|
|
122
|
+
const codexThreads = new Map();
|
|
123
|
+
// The agent's session is written as it starts: a few tries are enough.
|
|
124
|
+
const codexAttempts = 10;
|
|
125
|
+
// Its first line holds Codex's long prompt, but the role comes in its first two kilobytes (Priya).
|
|
126
|
+
const headBytes = 16 * 1024;
|
|
127
|
+
/**
|
|
128
|
+
* The file of a Codex session by its thread: in the day's folder of its start, which its UUIDv7 holds, or the days beside it around midnight; none when
|
|
129
|
+
* it is not there.
|
|
130
|
+
*/
|
|
131
|
+
function codexSession(thread) {
|
|
132
|
+
const started = Number.parseInt(thread.replaceAll("-", "").slice(0, 12), 16);
|
|
133
|
+
const sessions = join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "sessions");
|
|
134
|
+
for (const day of [0, -1, 1]) {
|
|
135
|
+
const date = new Date(started + day * 86_400_000);
|
|
136
|
+
const folder = join(sessions, String(date.getFullYear()), String(date.getMonth() + 1).padStart(2, "0"), String(date.getDate()).padStart(2, "0"));
|
|
137
|
+
try {
|
|
138
|
+
const name = readdirSync(folder).find((each) => each.endsWith(`-${thread}.jsonl`));
|
|
139
|
+
if (name !== undefined) {
|
|
140
|
+
return join(folder, name);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// No session that day.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
function headOf(file) {
|
|
150
|
+
if (file === "") {
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const descriptor = openSync(file, "r");
|
|
155
|
+
try {
|
|
156
|
+
const buffer = Buffer.alloc(headBytes);
|
|
157
|
+
const read = readSync(descriptor, buffer, 0, headBytes, 0);
|
|
158
|
+
return buffer.subarray(0, read).toString("utf8");
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
closeSync(descriptor);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
83
168
|
function withEffort(model, effort) {
|
|
84
169
|
return typeof effort === "string" && effort !== "" ? { annotator: model, effort } : { annotator: model };
|
|
85
170
|
}
|
package/dist/server/mcp.d.ts
CHANGED
|
@@ -3,11 +3,16 @@ import { type AnyCommand, type Context } from "../commands/index.ts";
|
|
|
3
3
|
/** What the host reads as the server starts, before any tool: where to begin, and the rule of every move (D-27). Short, since hosts may cut it. */
|
|
4
4
|
export declare const serverInstructions: string;
|
|
5
5
|
/** The MCP server for an AI host: every command of the registry as a tool, with its description and parameters (D-5). */
|
|
6
|
-
export declare function createMcpServer(context?: Context, list?: AnyCommand[]): McpServer;
|
|
6
|
+
export declare function createMcpServer(context?: Context, list?: AnyCommand[], beforeCall?: (meta: Record<string, unknown> | undefined) => Promise<void>): McpServer;
|
|
7
7
|
/**
|
|
8
8
|
* Serves the tools over standard input and output, as AI hosts expect: stdout carries the protocol alone, so nothing else may print there. It ends when the
|
|
9
9
|
* host goes and its input closes, once the calls still running have answered: a move half made would be lost.
|
|
10
10
|
*/
|
|
11
11
|
export declare function startMcpServer(context?: Context, list?: AnyCommand[]): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* The board page started once, before the chat's first call, the status every chat opens with, which then gives its address: the process of a Codex
|
|
14
|
+
* agent, which has one of its own, starts none, which would stand beside the chat's and could keep the doorbell from it (D-98, Priya).
|
|
15
|
+
*/
|
|
16
|
+
export declare function lazyPage(start: () => Promise<void>): (meta: Record<string, unknown> | undefined) => Promise<void>;
|
|
12
17
|
/** The opponent's name for the page, from the host's (D-97): another host than Claude's and Codex is unnamed. */
|
|
13
18
|
export declare function opponentOf(host: string | undefined): string | undefined;
|
package/dist/server/mcp.js
CHANGED
|
@@ -5,31 +5,39 @@ import { z } from "zod";
|
|
|
5
5
|
import { aiMove, commands, defaultContext, inputProblems, isBoolean, PositionChanged } from "../commands/index.js";
|
|
6
6
|
import { aiNameOf } from "../core/index.js";
|
|
7
7
|
import { endOf, loadGame, noteAiHost, noteAiModel } from "../games/index.js";
|
|
8
|
-
import { hasAgent } from "../host-files/index.js";
|
|
8
|
+
import { agentTools, hasAgent } from "../host-files/index.js";
|
|
9
9
|
import { gameFolder } from "../profiles/index.js";
|
|
10
10
|
import { version } from "../version.js";
|
|
11
11
|
import { pagePorts } from "./bell.js";
|
|
12
|
-
import { allowedCaller, callerOf, callingAgent } from "./caller.js";
|
|
12
|
+
import { allowedCaller, callerOf, callingAgent, fromCodexAgent } from "./caller.js";
|
|
13
13
|
import { chatHasRead, startPage } from "./page/page.js";
|
|
14
14
|
/** What the host reads as the server starts, before any tool: where to begin, and the rule of every move (D-27). Short, since hosts may cut it. */
|
|
15
15
|
export const serverInstructions = "Chesspal keeps the chess for you: the rules, the board, the clock, the games and the player's memory. Call `status` at the start of every "
|
|
16
16
|
+ "chat. Every move goes through Chesspal's tools: never keep the board in your head or invent a position.";
|
|
17
17
|
/** The MCP server for an AI host: every command of the registry as a tool, with its description and parameters (D-5). */
|
|
18
|
-
export function createMcpServer(context = defaultContext(), list = commands) {
|
|
18
|
+
export function createMcpServer(context = defaultContext(), list = commands, beforeCall) {
|
|
19
19
|
const server = new McpServer({ name: "chesspal", version }, { instructions: serverInstructions });
|
|
20
20
|
// The server keeps what it told the chat, the instructions a tool gives once, and the position it showed: none yet (D-71).
|
|
21
21
|
context.told ??= new Set();
|
|
22
22
|
context.positionSeen ??= null;
|
|
23
23
|
// The host the chat runs in, as it introduced itself: it names the AI on the page and in the game's PGN (D-97).
|
|
24
24
|
context.host ??= () => server.server.getClientVersion()?.name;
|
|
25
|
-
const
|
|
25
|
+
const forSomeHosts = [];
|
|
26
26
|
for (const command of list.filter((each) => each.cliOnly !== true)) {
|
|
27
27
|
const tool = server.registerTool(command.name, toolConfig(command), async (input, extra) => {
|
|
28
|
+
await beforeCall?.(extra._meta);
|
|
29
|
+
const codexAgent = fromCodexAgent(extra._meta);
|
|
30
|
+
const calling = command.byAgents !== undefined || codexAgent ? await callingAgent(extra._meta, context.folder) : undefined;
|
|
28
31
|
// An agent's tool called by the chat, or by another agent, would give what only that agent may see (Priya).
|
|
29
|
-
if (command.byAgents !== undefined && !allowedCaller(
|
|
32
|
+
if (command.byAgents !== undefined && !allowedCaller(calling, command.byAgents)) {
|
|
30
33
|
// It names no agent to start: the chat that asked would start one itself (Priya).
|
|
31
34
|
return { content: [{ type: "text", text: `${command.name} is not yours to call: the player reads what it gives on the board page. Nothing changed.` }], isError: true };
|
|
32
35
|
}
|
|
36
|
+
// Codex gives its agents every tool: one of Chesspal's keeps to its own, and never plays or talks for the opponent (D-98, Federico).
|
|
37
|
+
const own = calling !== undefined && "agent" in calling ? agentTools(calling.agent) : undefined;
|
|
38
|
+
if (codexAgent && own !== undefined && !own.includes(command.name)) {
|
|
39
|
+
return { content: [{ type: "text", text: `${command.name} is not yours to call: you work only with ${own.join(", ")}. Nothing changed.` }], isError: true };
|
|
40
|
+
}
|
|
33
41
|
// Each call knows who makes it, for the commands that write it down (D-81).
|
|
34
42
|
const caller = () => callerOf(extra._meta, server.server.getClientVersion(), context.folder);
|
|
35
43
|
const result = await track(runTool(command, input, context, caller, extra._meta));
|
|
@@ -38,15 +46,15 @@ export function createMcpServer(context = defaultContext(), list = commands) {
|
|
|
38
46
|
}
|
|
39
47
|
return result;
|
|
40
48
|
});
|
|
41
|
-
if (command.
|
|
49
|
+
if (command.hosts !== undefined) {
|
|
42
50
|
tool.disable();
|
|
43
|
-
|
|
51
|
+
forSomeHosts.push({ hosts: command.hosts, tool });
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
|
-
// A tool for
|
|
54
|
+
// A tool for some hosts is offered once the host has said its name, before it lists the tools.
|
|
47
55
|
server.server.oninitialized = () => {
|
|
48
56
|
const host = server.server.getClientVersion()?.name;
|
|
49
|
-
for (const { tool } of
|
|
57
|
+
for (const { tool } of forSomeHosts.filter((each) => host !== undefined && each.hosts.includes(host))) {
|
|
50
58
|
tool.enable();
|
|
51
59
|
}
|
|
52
60
|
};
|
|
@@ -65,10 +73,25 @@ export async function startMcpServer(context = defaultContext(), list = commands
|
|
|
65
73
|
};
|
|
66
74
|
process.stdin.on("end", leave);
|
|
67
75
|
process.stdin.on("close", leave);
|
|
68
|
-
const
|
|
69
|
-
|
|
76
|
+
const pageFirst = lazyPage(async () => {
|
|
77
|
+
context.page = await pageOf(context, () => opponentOf(context.host?.()));
|
|
78
|
+
});
|
|
79
|
+
const server = createMcpServer(context, list, pageFirst);
|
|
70
80
|
await server.connect(new StdioServerTransport());
|
|
71
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The board page started once, before the chat's first call, the status every chat opens with, which then gives its address: the process of a Codex
|
|
84
|
+
* agent, which has one of its own, starts none, which would stand beside the chat's and could keep the doorbell from it (D-98, Priya).
|
|
85
|
+
*/
|
|
86
|
+
export function lazyPage(start) {
|
|
87
|
+
let page;
|
|
88
|
+
return async (meta) => {
|
|
89
|
+
if (!fromCodexAgent(meta)) {
|
|
90
|
+
page ??= start();
|
|
91
|
+
await page;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
72
95
|
/** The opponent's name for the page, from the host's (D-97): another host than Claude's and Codex is unnamed. */
|
|
73
96
|
export function opponentOf(host) {
|
|
74
97
|
return aiNameOf(host);
|
|
@@ -76,11 +99,10 @@ export function opponentOf(host) {
|
|
|
76
99
|
/** The board page of the player's folder, served by this process for as long as the chat lasts (D-47); without it the game goes on in the chat. */
|
|
77
100
|
async function pageOf(context, opponent) {
|
|
78
101
|
try {
|
|
79
|
-
const { url, heard, chatCalls,
|
|
102
|
+
const { url, heard, chatCalls, open, askNewGame } = await startPage(context, opponent);
|
|
80
103
|
// Words the AI is given change no file: the pages hear of them from the commands.
|
|
81
104
|
context.heard = heard;
|
|
82
105
|
context.calls = chatCalls;
|
|
83
|
-
context.ponders = ponders;
|
|
84
106
|
context.pageOpen = open;
|
|
85
107
|
context.askNewGame = askNewGame;
|
|
86
108
|
if (!pagePorts(context.folder).includes(Number(new URL(url).port))) {
|
|
@@ -159,7 +181,7 @@ async function runTool(command, input, context, caller, meta) {
|
|
|
159
181
|
* goes on, so that no call of the chat's waits for it while it plays; none for the chat, or when it cannot be told.
|
|
160
182
|
*/
|
|
161
183
|
async function sharingAgent(command, meta, context) {
|
|
162
|
-
const agents = command.sharedWith?.filter((agent) => hasAgent(context.folder, agent)) ?? [];
|
|
184
|
+
const agents = command.sharedWith?.filter((agent) => hasAgent(context.folder, agent, context.host?.())) ?? [];
|
|
163
185
|
if (agents.length === 0 || gameGoingOn(context)) {
|
|
164
186
|
return undefined;
|
|
165
187
|
}
|