chesspal 0.3.5 → 0.4.0
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/README.md +1 -0
- package/dist/cli/cli.js +17 -4
- package/dist/cli/command-line.js +22 -12
- package/dist/cli/icon.d.ts +2 -0
- package/dist/cli/icon.js +11 -1
- package/dist/cli/prompt-hook.d.ts +17 -0
- package/dist/cli/prompt-hook.js +100 -0
- package/dist/commands/analysis/analyse.d.ts +2 -2
- package/dist/commands/analysis/analyse.js +10 -16
- package/dist/commands/analysis/annotate.d.ts +8 -0
- package/dist/commands/analysis/annotate.js +19 -3
- package/dist/commands/answers/agents.d.ts +17 -7
- package/dist/commands/answers/agents.js +46 -16
- package/dist/commands/answers/clock.js +2 -1
- package/dist/commands/answers/drawings.js +6 -2
- package/dist/commands/answers/index.d.ts +4 -3
- package/dist/commands/answers/index.js +4 -3
- package/dist/commands/answers/lines.d.ts +5 -3
- package/dist/commands/answers/lines.js +17 -9
- package/dist/commands/answers/opening-book.d.ts +13 -7
- package/dist/commands/answers/opening-book.js +34 -38
- package/dist/commands/answers/opening-tags.d.ts +4 -0
- package/dist/commands/answers/opening-tags.js +11 -0
- package/dist/commands/answers/play-move.d.ts +3 -1
- package/dist/commands/answers/play-move.js +9 -6
- package/dist/commands/answers/say.js +2 -1
- package/dist/commands/answers/summary.d.ts +7 -3
- package/dist/commands/answers/summary.js +16 -5
- package/dist/commands/answers/view.d.ts +1 -1
- package/dist/commands/answers/view.js +2 -2
- package/dist/commands/game/ai-move.js +4 -4
- package/dist/commands/game/coach.js +7 -6
- package/dist/commands/game/end.js +2 -2
- package/dist/commands/game/export.d.ts +1 -1
- package/dist/commands/game/index.d.ts +1 -1
- package/dist/commands/game/index.js +1 -1
- package/dist/commands/game/look.d.ts +7 -3
- package/dist/commands/game/look.js +9 -4
- package/dist/commands/game/new.js +42 -12
- package/dist/commands/game/player-move.d.ts +16 -0
- package/dist/commands/game/player-move.js +69 -19
- package/dist/commands/game/ponder.js +7 -4
- package/dist/commands/game/position.d.ts +2 -1
- package/dist/commands/game/position.js +9 -4
- package/dist/commands/game/status.d.ts +4 -2
- package/dist/commands/game/status.js +24 -14
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/index.js +3 -3
- package/dist/commands/profiles/past-games.d.ts +7 -2
- package/dist/commands/profiles/past-games.js +45 -18
- package/dist/commands/registry/context.d.ts +2 -0
- package/dist/commands/registry/context.js +5 -0
- package/dist/commands/registry/index.d.ts +1 -1
- package/dist/commands/registry/index.js +1 -1
- package/dist/commands/talk/ai-say.d.ts +1 -1
- package/dist/commands/talk/ai-say.js +7 -6
- package/dist/core/game.d.ts +13 -0
- package/dist/core/game.js +29 -3
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +3 -3
- package/dist/core/openings.d.ts +20 -6
- package/dist/core/openings.js +45 -22
- package/dist/core/pgn.d.ts +4 -0
- package/dist/core/pgn.js +35 -6
- package/dist/games/analysis.d.ts +7 -4
- package/dist/games/analysis.js +5 -3
- package/dist/games/archive.d.ts +25 -5
- package/dist/games/archive.js +52 -19
- package/dist/games/chat-start.d.ts +2 -2
- package/dist/games/chat-start.js +6 -1
- package/dist/games/current-game.d.ts +2 -2
- package/dist/games/current-game.js +3 -2
- package/dist/games/index.d.ts +3 -2
- package/dist/games/index.js +3 -2
- package/dist/games/position-shown.d.ts +24 -0
- package/dist/games/position-shown.js +51 -0
- package/dist/games/thoughts.d.ts +12 -4
- package/dist/games/thoughts.js +27 -10
- package/dist/home/index.d.ts +1 -0
- package/dist/home/index.js +1 -0
- package/dist/home/same-path.d.ts +7 -0
- package/dist/home/same-path.js +24 -0
- package/dist/host-files/host-files.d.ts +5 -0
- package/dist/host-files/host-files.js +21 -7
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/app/anchor.js +15 -11
- package/dist/page/app/app.js +49 -14
- package/dist/page/app/served.js +30 -38
- package/dist/page/app/watch.js +97 -0
- package/dist/page/avatars/torretta.css +108 -0
- package/dist/page/base.css +1 -1
- package/dist/page/board/board.css +11 -8
- package/dist/page/board/marks.js +2 -2
- package/dist/page/board/pointer.js +2 -2
- package/dist/page/dialogs/analyses.js +5 -3
- package/dist/page/dialogs/dialogs.css +3 -1
- package/dist/page/dialogs/rows.js +72 -0
- package/dist/page/dialogs/site-games.js +3 -1
- package/dist/page/game/column.js +10 -8
- package/dist/page/game/game.css +137 -16
- package/dist/page/game/move-list.js +7 -5
- package/dist/page/game/plies.js +4 -0
- package/dist/page/game/review-progress.js +11 -0
- package/dist/page/guide/drawings/drawings.css +0 -1
- package/dist/page/guide/en.md +2 -2
- package/dist/page/guide/it.md +2 -2
- package/dist/page/helps/helps.css +5 -5
- package/dist/page/helps/helps.js +3 -181
- package/dist/page/helps/rows.js +16 -0
- package/dist/page/helps/strategy.js +105 -0
- package/dist/page/helps/tactics.js +69 -0
- package/dist/page/layout/header.js +49 -6
- package/dist/page/layout/icon.ico +0 -0
- package/dist/page/layout/layout.css +49 -17
- package/dist/page/layout/window.js +45 -6
- package/dist/page/lead/lead.js +41 -4
- package/dist/page/legend/legend.css +4 -16
- package/dist/page/legend/legend.js +28 -15
- package/dist/page/profile/settings.js +1 -0
- package/dist/page/sounds/sounds.js +1 -1
- package/dist/page/style.css +1 -0
- package/dist/page/table/action.js +42 -0
- package/dist/page/table/table.css +57 -39
- package/dist/page/table/window.js +67 -67
- package/dist/page/talk/talk.css +52 -118
- package/dist/page/talk/talk.js +65 -20
- package/dist/page/talk/voice.js +92 -21
- package/dist/page/text.js +34 -8
- package/dist/page/theme.css +13 -0
- package/dist/profiles/profiles.js +5 -1
- package/dist/server/bell/bell.d.ts +24 -29
- package/dist/server/bell/bell.js +79 -133
- package/dist/server/bell/index.d.ts +1 -1
- package/dist/server/bell/index.js +2 -2
- package/dist/server/mcp/mcp.js +49 -7
- package/dist/server/page/analyses.js +15 -4
- package/dist/server/page/chat.d.ts +5 -1
- package/dist/server/page/chat.js +5 -1
- package/dist/server/page/files.js +3 -1
- package/dist/server/page/game.d.ts +5 -0
- package/dist/server/page/game.js +95 -15
- package/dist/server/page/http.d.ts +1 -1
- package/dist/server/page/page.d.ts +14 -9
- package/dist/server/page/page.js +111 -26
- package/dist/server/page/table.d.ts +40 -5
- package/dist/server/page/table.js +65 -25
- package/dist/server/page/update.d.ts +1 -1
- package/dist/server/page/update.js +4 -4
- package/dist/server/page/windows.d.ts +22 -0
- package/dist/server/page/windows.js +45 -0
- package/dist/server/ports/index.d.ts +1 -0
- package/dist/server/ports/index.js +2 -0
- package/dist/server/ports/ports.d.ts +78 -0
- package/dist/server/ports/ports.js +222 -0
- package/dist/server/table/browser.d.ts +22 -6
- package/dist/server/table/browser.js +98 -20
- package/dist/server/table/hosts/chats.d.ts +5 -0
- package/dist/server/table/hosts/chats.js +11 -4
- package/dist/server/table/hosts/hooks.d.ts +6 -0
- package/dist/server/table/hosts/hooks.js +38 -0
- package/dist/server/table/hosts/hosts.d.ts +13 -5
- package/dist/server/table/hosts/hosts.js +16 -10
- package/dist/server/table/hosts/index.d.ts +1 -0
- package/dist/server/table/hosts/index.js +1 -0
- package/dist/server/table/hosts/play.d.ts +7 -3
- package/dist/server/table/hosts/play.js +63 -9
- package/dist/server/table/icon/index.d.ts +2 -0
- package/dist/server/table/icon/index.js +16 -2
- package/dist/server/table/index.d.ts +2 -2
- package/dist/server/table/index.js +1 -1
- package/dist/server/table/launch.d.ts +5 -4
- package/dist/server/table/launch.js +18 -5
- package/dist/server/table/link.js +4 -1
- package/dist/server/table/table.d.ts +7 -0
- package/dist/server/table/table.js +40 -12
- package/dist/sites/index.d.ts +1 -1
- package/dist/sites/index.js +1 -1
- package/dist/sites/{chesscom.d.ts → ratings/chesscom.d.ts} +2 -2
- package/dist/sites/{chesscom.js → ratings/chesscom.js} +1 -1
- package/dist/sites/{choice.d.ts → ratings/choice.d.ts} +1 -1
- package/dist/sites/{choice.js → ratings/choice.js} +1 -1
- package/dist/sites/ratings/index.d.ts +1 -0
- package/dist/sites/ratings/index.js +2 -0
- package/dist/sites/{lichess.d.ts → ratings/lichess.d.ts} +2 -2
- package/dist/sites/{lichess.js → ratings/lichess.js} +1 -1
- package/dist/sites/{sites.d.ts → ratings/ratings.d.ts} +2 -2
- package/dist/sites/{sites.js → ratings/ratings.js} +1 -1
- package/instructions/opponent.md +1 -1
- package/instructions/start.md +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join, relative } from "node:path";
|
|
4
4
|
import { makeFolder, writeAtomically } from "../games/index.js";
|
|
5
|
-
import { agentFiles, instructionsFile } from "./agents.js";
|
|
5
|
+
import { agentFiles, agentHosts, instructionsFile } from "./agents.js";
|
|
6
6
|
/**
|
|
7
7
|
* Sets up the player's folder for Claude Code and Codex (D-31): .mcp.json with the chesspal server, AGENTS.md with the lines that start a chat between
|
|
8
8
|
* Chesspal's marks (D-102), CLAUDE.md importing it, the hosts' settings with the server approved, and their hooks. What the player wrote there stays; it returns
|
|
@@ -31,6 +31,13 @@ export function outdatedHostFiles(folder, platform) {
|
|
|
31
31
|
export function codexHooksPath(folder) {
|
|
32
32
|
return join(folder, ".codex", "hooks.json");
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether the host runs the doorbell's hook init writes, Claude Code and Codex by the names they give the server, so the player's act on the page wakes
|
|
36
|
+
* the chat; the command line, which names no host, is taken as one. Elsewhere the player tells the chat in its window.
|
|
37
|
+
*/
|
|
38
|
+
export function hasDoorbell(host) {
|
|
39
|
+
return host === undefined || agentHosts.includes(host);
|
|
40
|
+
}
|
|
34
41
|
/** The instructions of the AI opponent, read from the package wherever it is installed: the MCP server's `instructions` tool gives them (D-102). */
|
|
35
42
|
export function opponentInstructions() {
|
|
36
43
|
return instructionsFile("opponent");
|
|
@@ -88,8 +95,8 @@ function claudeFile(folder) {
|
|
|
88
95
|
return before.split(/\r?\n/).some((line) => line.trim() === "@AGENTS.md") ? undefined : { path, text: `@AGENTS.md\n${before}` };
|
|
89
96
|
}
|
|
90
97
|
/**
|
|
91
|
-
* Claude Code's settings: the chesspal server of .mcp.json approved and its tools allowed, so that the first chat asks for neither,
|
|
92
|
-
* moves on the page (D-47),
|
|
98
|
+
* Claude Code's settings: the chesspal server of .mcp.json approved and its tools allowed, so that the first chat asks for neither, the doorbell of the
|
|
99
|
+
* moves on the page (D-47), the note of how each chat starts (D-57), and the move the player writes alone in the chat (D-36); the player's own stay.
|
|
93
100
|
*/
|
|
94
101
|
function claudeSettingsFile(folder) {
|
|
95
102
|
const path = join(folder, ".claude", "settings.json");
|
|
@@ -100,7 +107,7 @@ function claudeSettingsFile(folder) {
|
|
|
100
107
|
...settings,
|
|
101
108
|
enabledMcpjsonServers: withItem(settings.enabledMcpjsonServers, "chesspal"),
|
|
102
109
|
permissions: { ...permissions, allow: withItem(permissions.allow, "mcp__chesspal") },
|
|
103
|
-
hooks: withHook(withHook(hooks, "Stop", bellHook), "SessionStart", chatHook),
|
|
110
|
+
hooks: withHook(withHook(withHook(hooks, "Stop", bellHook), "SessionStart", chatHook), "UserPromptSubmit", promptHook),
|
|
104
111
|
});
|
|
105
112
|
}
|
|
106
113
|
/** The hooks with one of Chesspal's put back beside the player's own whenever it is missing; an event of theirs that is no list stays theirs. */
|
|
@@ -118,9 +125,12 @@ function withHook(hooks, event, ours) {
|
|
|
118
125
|
const bellHook = { type: "command", command: "chesspal listen", asyncRewake: true, timeout: 86_400 };
|
|
119
126
|
// Claude Code's SessionStart hook (D-57): it notes how each chat starts, so that a new game waits for a clean one.
|
|
120
127
|
const chatHook = { type: "command", command: "chesspal chat" };
|
|
128
|
+
// Claude Code's UserPromptSubmit hook (D-36): a message that is one move and nothing else is played before the AI reads it, which then only replies.
|
|
129
|
+
const promptHook = { type: "command", command: "chesspal prompt", timeout: 10 };
|
|
121
130
|
/**
|
|
122
131
|
* Codex's settings, read once the player trusts the folder: the chesspal server with its tools approved, a table of Chesspal's rewritten but for the
|
|
123
|
-
* environment the player gave it
|
|
132
|
+
* environment the player gave it; CHESSPAL_HOME and CHESSPAL_SEAT always passed on to the server, which Codex otherwise starts without them, so that a
|
|
133
|
+
* playtest's data folder is the one the server reads, and the chat `chesspal play` holds knows it. The rest stays.
|
|
124
134
|
*/
|
|
125
135
|
function codexConfigFile(folder, platform) {
|
|
126
136
|
const path = join(folder, ".codex", "config.toml");
|
|
@@ -128,8 +138,12 @@ function codexConfigFile(folder, platform) {
|
|
|
128
138
|
const { command, args } = chesspalServer(platform);
|
|
129
139
|
// The table runs up to the next one, or to the end; a header with a comment after it is the same table.
|
|
130
140
|
const ours = /^\s*\[mcp_servers\.chesspal\]\s*(#.*)?\r?\n[\s\S]*?(?=^\s*\[|(?![\s\S]))/m;
|
|
131
|
-
const
|
|
132
|
-
const
|
|
141
|
+
const given = ours.exec(before)?.[0].match(/^\s*env(?:_vars)?\s*=.*$/gm) ?? [];
|
|
142
|
+
const environment = given.filter((line) => !/^\s*env_vars\s*=/.test(line)).map((line) => `${line.trim()}\n`).join("");
|
|
143
|
+
const passed = given.filter((line) => /^\s*env_vars\s*=/.test(line)).flatMap((line) => [...line.matchAll(/["']([^"']+)["']/g)].map((name) => name[1] ?? ""));
|
|
144
|
+
const names = [...new Set(["CHESSPAL_HOME", "CHESSPAL_SEAT", ...passed])].map((name) => JSON.stringify(name)).join(", ");
|
|
145
|
+
const table = `[mcp_servers.chesspal]\ncommand = ${JSON.stringify(command)}\nargs = [${args.map((arg) => JSON.stringify(arg)).join(", ")}]\n`
|
|
146
|
+
+ `default_tools_approval_mode = "approve"\nenv_vars = [${names}]\n${environment}`;
|
|
133
147
|
const rest = before.replace(ours, "").trim();
|
|
134
148
|
const text = rest === "" ? table : `${rest}\n\n${table}`;
|
|
135
149
|
return text === before ? undefined : { path, text };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructionsFile, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.ts";
|
|
2
|
-
export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts, startInstructions } from "./host-files.ts";
|
|
2
|
+
export { codexHooksPath, hasDoorbell, opponentInstructions, outdatedHostFiles, setUpHosts, startInstructions } from "./host-files.ts";
|
package/dist/host-files/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The files through which an AI host finds Chesspal in the player's folder: the server, the opponent's instructions, the hosts' settings.
|
|
2
2
|
export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructionsFile, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.js";
|
|
3
|
-
export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts, startInstructions } from "./host-files.js";
|
|
3
|
+
export { codexHooksPath, hasDoorbell, opponentInstructions, outdatedHostFiles, setUpHosts, startInstructions } from "./host-files.js";
|
package/dist/page/app/anchor.js
CHANGED
|
@@ -17,16 +17,16 @@ export function anchorName(name) {
|
|
|
17
17
|
// A game's name without its folder and .pgn, the time it started, as games/archive.ts writes it: the page cannot import that module.
|
|
18
18
|
const stem = /\d{4}-\d{2}-\d{2}-\d{6}/.source;
|
|
19
19
|
/**
|
|
20
|
-
* What the page does with the place the address names: goes to its move on the game shown;
|
|
21
|
-
* analysis, `asked` already; else opens its game in analysis, but the game just played, which opens on the player's yes alone (D-89): the link waits
|
|
22
|
-
* it while the table is set (D-77), and goes once another game shows (Zoe).
|
|
20
|
+
* What the page does with the place the address names: goes to its move on the game shown; refuses it while a game goes on; waits while its game opens
|
|
21
|
+
* in analysis, `asked` already; else opens its game in analysis, but the game just played, which opens on the player's yes alone (D-89): the link waits
|
|
22
|
+
* for it while the table is set (D-77), and goes once another game shows (Zoe).
|
|
23
23
|
*/
|
|
24
24
|
export function anchorAction(wanted, games, asked) {
|
|
25
25
|
if (wanted.game === games.shown) {
|
|
26
26
|
return "view";
|
|
27
27
|
}
|
|
28
28
|
if (games.going) {
|
|
29
|
-
return "
|
|
29
|
+
return "refuse";
|
|
30
30
|
}
|
|
31
31
|
if (wanted.game === games.played) {
|
|
32
32
|
return games.shown === undefined ? "wait" : "drop";
|
|
@@ -39,16 +39,16 @@ export function shownHash(games, viewing) {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Keeps the move shown in the address, and goes to the place the address names as the page opens or as the player writes it (anchorAction); `open` opens a
|
|
42
|
-
* game in analysis and says whether it did. A link never shows its move on another game.
|
|
42
|
+
* game in analysis and says whether it did, and `ignored` says why a link was not followed. A link never shows its move on another game.
|
|
43
43
|
*/
|
|
44
|
-
export function useAnchor(games, viewing, view, open) {
|
|
44
|
+
export function useAnchor(games, viewing, view, open, ignored) {
|
|
45
45
|
const { ready, shown, played, going } = games;
|
|
46
|
-
const [wanted, setWanted] = useState(() => anchorOf(location.hash));
|
|
46
|
+
const [wanted, setWanted] = useState(() => anchorOf(window.location.hash));
|
|
47
47
|
const asked = useRef(undefined);
|
|
48
48
|
useEffect(() => {
|
|
49
49
|
const heard = () => {
|
|
50
50
|
asked.current = undefined;
|
|
51
|
-
setWanted(anchorOf(location.hash));
|
|
51
|
+
setWanted(anchorOf(window.location.hash));
|
|
52
52
|
};
|
|
53
53
|
window.addEventListener("hashchange", heard);
|
|
54
54
|
return () => window.removeEventListener("hashchange", heard);
|
|
@@ -61,7 +61,10 @@ export function useAnchor(games, viewing, view, open) {
|
|
|
61
61
|
if (action === "view") {
|
|
62
62
|
view(wanted.plies);
|
|
63
63
|
}
|
|
64
|
-
if (action === "
|
|
64
|
+
if (action === "refuse") {
|
|
65
|
+
ignored("going");
|
|
66
|
+
}
|
|
67
|
+
if (action === "view" || action === "refuse" || action === "drop") {
|
|
65
68
|
setWanted(undefined);
|
|
66
69
|
}
|
|
67
70
|
if (action !== "open") {
|
|
@@ -72,6 +75,7 @@ export function useAnchor(games, viewing, view, open) {
|
|
|
72
75
|
void open(game).then((opened) => {
|
|
73
76
|
// A place written since stays wanted (Federico).
|
|
74
77
|
if (!opened && asked.current === game) {
|
|
78
|
+
ignored("gone");
|
|
75
79
|
setWanted(undefined);
|
|
76
80
|
}
|
|
77
81
|
});
|
|
@@ -81,8 +85,8 @@ export function useAnchor(games, viewing, view, open) {
|
|
|
81
85
|
return;
|
|
82
86
|
}
|
|
83
87
|
const hash = shownHash(games, viewing);
|
|
84
|
-
if (hash !== location.hash) {
|
|
85
|
-
history.replaceState(history.state, "", hash === "" ? location.pathname + location.search : hash);
|
|
88
|
+
if (hash !== window.location.hash) {
|
|
89
|
+
window.history.replaceState(window.history.state, "", hash === "" ? window.location.pathname + window.location.search : hash);
|
|
86
90
|
}
|
|
87
91
|
}, [ready, shown, going, viewing, wanted]);
|
|
88
92
|
}
|
package/dist/page/app/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
3
|
-
import { aiHostIn, aiModelIn, aiNameOf, annotatorOf, closingOf, Game, gameReviewOf, materialOf, moodAt, movesOnly, openingOf, readPgn, suggestedLevel, timeControlText, writePgn, } from "../../core/index.js";
|
|
3
|
+
import { aiHostIn, aiModelIn, aiNameOf, annotatorOf, closingOf, Game, gameReviewOf, materialOf, moodAt, movesOnly, openingOf, outOfBookMove, readPgn, suggestedLevel, timeControlText, writePgn, } from "../../core/index.js";
|
|
4
4
|
import { PlayerPicture } from "../avatars/avatars.js";
|
|
5
5
|
import { Thinker, Thought } from "../avatars/thought.js";
|
|
6
6
|
import { Torretta } from "../avatars/torretta.js";
|
|
@@ -19,7 +19,7 @@ import { GameColumn } from "../game/column.js";
|
|
|
19
19
|
import { howEnded, useDeclined, useJustEnded, useSawEnd } from "../game/ending.js";
|
|
20
20
|
import { Steps } from "../game/move-list.js";
|
|
21
21
|
import { useOpeningBook } from "../game/opening-book.js";
|
|
22
|
-
import { aiSpoke, aiWords, analysisTalk, judgmentsOf, playerSpent, playerWords, pliesOf, withBook, withJudgments, withoutReviews } from "../game/plies.js";
|
|
22
|
+
import { aiSpoke, aiWords, analysisTalk, judgmentsOf, playerSpent, playerWords, pliesOf, reviewCount, withBook, withJudgments, withoutReviews } from "../game/plies.js";
|
|
23
23
|
import { GuideDialog } from "../guide/guide.js";
|
|
24
24
|
import { BareButton, HelpsCard, HintLine, MapButton } from "../helps/helps.js";
|
|
25
25
|
import { helpsOf } from "../helps/view.js";
|
|
@@ -27,6 +27,7 @@ import { FoldTab } from "../layout/fold-tab.js";
|
|
|
27
27
|
import { nextFold, rememberFold, startFolds } from "../layout/folds.js";
|
|
28
28
|
import { PageFoot } from "../layout/footer.js";
|
|
29
29
|
import { Header } from "../layout/header.js";
|
|
30
|
+
import { LeadChart } from "../lead/lead.js";
|
|
30
31
|
import { standingsOf } from "../lead/view.js";
|
|
31
32
|
import { MarksLegend } from "../legend/legend.js";
|
|
32
33
|
import { legendOf } from "../legend/view.js";
|
|
@@ -43,7 +44,7 @@ import { textFor } from "../text.js";
|
|
|
43
44
|
import { anchorName, useAnchor } from "./anchor.js";
|
|
44
45
|
import { useBrowseKeys, useFoldKeys, useLegendKey, useMapKey, useWheelSteps } from "./keys.js";
|
|
45
46
|
import { phaseOf, verdictOf } from "./phase.js";
|
|
46
|
-
import { ask, post, sendMessage, sendMove, useServed } from "./served.js";
|
|
47
|
+
import { ask, post, refusedText, sendMessage, sendMove, useServed } from "./served.js";
|
|
47
48
|
import { tabTitle, useTabTitle } from "./title.js";
|
|
48
49
|
/**
|
|
49
50
|
* The page: the board of the game in the player's folder, where the player moves on their turn, the talk with the AI beside it, and the game's column; in the
|
|
@@ -54,6 +55,8 @@ export function App({ start }) {
|
|
|
54
55
|
const [setting, setSetting] = useState(false);
|
|
55
56
|
const text = useMemo(() => textFor(languagesOf(settings)), [settings.language]);
|
|
56
57
|
const sound = settings.sound;
|
|
58
|
+
// The sounds off hush the voice too: it is one of them.
|
|
59
|
+
const voiced = settings.voice && sound;
|
|
57
60
|
useEffect(() => void (document.documentElement.lang = text.language), [text]);
|
|
58
61
|
const apply = (next) => {
|
|
59
62
|
setSettings(next);
|
|
@@ -94,6 +97,20 @@ export function App({ start }) {
|
|
|
94
97
|
};
|
|
95
98
|
const [selected, setSelected] = useState(undefined);
|
|
96
99
|
const [refusal, setRefusal] = useState(undefined);
|
|
100
|
+
// A link the page did not follow says why on the board, in the language of the settings the page has by then, until the player's next click or key.
|
|
101
|
+
const [linkIgnored, setLinkIgnored] = useState(undefined);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (linkIgnored === undefined) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const dismiss = () => setLinkIgnored(undefined);
|
|
107
|
+
window.addEventListener("pointerdown", dismiss, { once: true });
|
|
108
|
+
window.addEventListener("keydown", dismiss, { once: true });
|
|
109
|
+
return () => {
|
|
110
|
+
window.removeEventListener("pointerdown", dismiss);
|
|
111
|
+
window.removeEventListener("keydown", dismiss);
|
|
112
|
+
};
|
|
113
|
+
}, [linkIgnored]);
|
|
97
114
|
// A pawn taken to the last rank waits for the player to choose what it becomes.
|
|
98
115
|
const [promoting, setPromoting] = useState(undefined);
|
|
99
116
|
const [flipped, setFlipped] = useState(false);
|
|
@@ -117,6 +134,20 @@ export function App({ start }) {
|
|
|
117
134
|
const name = served?.opponent ?? text.ai;
|
|
118
135
|
// Under it, the model and effort the AI played its last move with (D-82).
|
|
119
136
|
const signed = served?.aiModel === undefined ? undefined : signature(served.aiModel.model, served.aiModel.effort);
|
|
137
|
+
// A new game that waited for the chat and could not start says why on the board, as one refused at once (D-107): once, and never one from before the
|
|
138
|
+
// page loaded.
|
|
139
|
+
const waitRefused = served?.table?.refused;
|
|
140
|
+
const waitRefusedSeen = useRef(undefined);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (served === undefined) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const number = waitRefused?.number ?? 0;
|
|
146
|
+
if (waitRefused !== undefined && waitRefusedSeen.current !== undefined && number > waitRefusedSeen.current) {
|
|
147
|
+
setRefusal({ text: refusedText(waitRefused, text, name) ?? "" });
|
|
148
|
+
}
|
|
149
|
+
waitRefusedSeen.current = number;
|
|
150
|
+
}, [served === undefined, waitRefused?.number]);
|
|
120
151
|
// The game is replayed from its PGN once for each one the server sends, not at every click. Once no game goes on, the game in analysis shows in its place
|
|
121
152
|
// (D-69), with the AI's badges and notes: replayed again only when its review changed.
|
|
122
153
|
const game = served?.game;
|
|
@@ -138,8 +169,10 @@ export function App({ start }) {
|
|
|
138
169
|
const analyst = annotator === undefined ? name : aiNameOf(annotator.annotator, annotator.annotator) ?? text.ai;
|
|
139
170
|
const material = useMemo(() => materialOf(live.position.fen), [live]);
|
|
140
171
|
const pieces = useMemo(() => piecesOf(live.position.fen), [live]);
|
|
172
|
+
// How many of the game's first plies a line of the book reaches, as the server counts them (D-68).
|
|
173
|
+
const bookPlies = studied !== undefined ? studied.book : game?.book ?? 0;
|
|
141
174
|
// While the game goes on, the book's first moves carry its badge, when the player wants it, as the AI's review gives it after (D-69).
|
|
142
|
-
const inBook = studied === undefined && settings.book ?
|
|
175
|
+
const inBook = studied === undefined && settings.book ? bookPlies : 0;
|
|
143
176
|
// The judgments of the player's moves, and the AI's drawings, show during a game only with the helps on, always once it is over (D-78); during it the
|
|
144
177
|
// coach's come from its own file, never in the PGN the opponent may read (D-88).
|
|
145
178
|
const judged = !going || settings.helps;
|
|
@@ -215,6 +248,8 @@ export function App({ start }) {
|
|
|
215
248
|
// In analysis, the opening the move shown has reached, while the book knows the moves (D-69).
|
|
216
249
|
const bookAt = studied === undefined || book === undefined || at === 0 || at > studied.book ? undefined : { tags: live.record.tags, moves: live.record.moves.slice(0, at) };
|
|
217
250
|
const shownOpening = useMemo(() => bookAt === undefined || book === undefined ? undefined : openingOf(bookAt, book), [live, book, bookAt === undefined ? 0 : at]);
|
|
251
|
+
// Whether the move shown is still the book's, whatever the badges show: the card says the move the game left it at, once it is past it.
|
|
252
|
+
const leftBook = outOfBookMove(at, bookPlies);
|
|
218
253
|
// A jump of more than one move shows the position at once: only a step slides its pieces.
|
|
219
254
|
const shownBefore = useRef(at);
|
|
220
255
|
const jumped = Math.abs(shownBefore.current - at) > 1;
|
|
@@ -229,7 +264,7 @@ export function App({ start }) {
|
|
|
229
264
|
// The move shown stays in the page's address, for a reload and a link to it; after the reset on a new game, which would undo the move it goes to. A
|
|
230
265
|
// game a link opens is there to look at: the analyst starts on the player's yes alone (D-89; Zoe and Federico).
|
|
231
266
|
const playedName = game === undefined || game === null ? undefined : anchorName(game.name);
|
|
232
|
-
useAnchor({ ready: served !== undefined, shown: studied !== undefined ? anchorName(studied.name) : idle ? undefined : playedName, played: playedName, going }, viewing, (plies) => goTo(plies ?? total), async (wanted) => await post("/api/study", { game: wanted, look: true }, text, name) === undefined);
|
|
267
|
+
useAnchor({ ready: served !== undefined, shown: studied !== undefined ? anchorName(studied.name) : idle ? undefined : playedName, played: playedName, going }, viewing, (plies) => goTo(plies ?? total), async (wanted) => await post("/api/study", { game: wanted, look: true }, text, name) === undefined, setLinkIgnored);
|
|
233
268
|
// The tab names the game and the move shown, as the cards name its players; between games, Chesspal alone.
|
|
234
269
|
const titled = idle || served === undefined ? undefined : studiedSides(live.record.tags, served.profile.name, playedAs, text.sides, served.profile.accounts, othersOf(served.profiles, served.profile.id));
|
|
235
270
|
useTabTitle(titled === undefined ? undefined : tabTitle(titled.white.name, titled.black.name, at === 0 ? undefined : plies[at - 1]));
|
|
@@ -255,11 +290,8 @@ export function App({ start }) {
|
|
|
255
290
|
canOffer: offer === undefined && pending === undefined && live.position.turn !== played.ai && total >= 2,
|
|
256
291
|
onOffer: () => act("/api/draw", { action: "offer" }),
|
|
257
292
|
onAnswer: (accept) => act("/api/draw", { action: accept ? "accept" : "decline" }),
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
act("/api/resign", {});
|
|
261
|
-
}
|
|
262
|
-
},
|
|
293
|
+
// The header asks first, in its own words (D-65).
|
|
294
|
+
onResign: () => act("/api/resign", {}),
|
|
263
295
|
};
|
|
264
296
|
const { kind, verdict } = aborted && phase === "over" ? { kind: "over", verdict: text.aborted } : verdictOf(phase, how, winner, offer, declined, text);
|
|
265
297
|
const header = (_jsx(Header, { text: text, kind: kind, verdict: verdict, ...(!ended && (offer === "ai" || declined) ? { who: name } : {}), actions: actions, onNewGame: served === undefined ? undefined : askNewGame,
|
|
@@ -316,6 +348,8 @@ export function App({ start }) {
|
|
|
316
348
|
// With nobody at it, a review not finished says so, and the player brings the game back to the AI (D-109).
|
|
317
349
|
const completeIt = summary === undefined && studied?.incomplete === true && !offline ? () => void studyGame({ game: studied.name }) : undefined;
|
|
318
350
|
const closing = studied === undefined ? undefined : closingOf(live.record);
|
|
351
|
+
// How far the review has got, counted as status counts it for the AI: who is ahead after how many of the game's moves.
|
|
352
|
+
const count = reviewing || completeIt !== undefined ? reviewCount(plies) : undefined;
|
|
319
353
|
// In analysis the AI's card says who played that game, as its PGN keeps it (D-82), not who plays now.
|
|
320
354
|
const playedBy = studied === undefined ? undefined : aiModelIn(live.record);
|
|
321
355
|
const talk = game?.talk ?? served.talk ?? [];
|
|
@@ -436,11 +470,11 @@ export function App({ start }) {
|
|
|
436
470
|
: null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
|
|
437
471
|
? (_jsx(TalkFaces, { name: name, aiPicture: thinker(40), you: profile.name, picture: picture, ...(idle ? {} : { ai, player }), aiMoves: going && !playerToMove, playerMoves: playerToMove, unread: unread || asleep, text: text }))
|
|
438
472
|
: (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, served.profiles, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, served.profiles, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined && studied !== undefined
|
|
439
|
-
? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[ai].name, children: [studiedCard(ai, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(reviewing ? { reviewing: true } : {}), ...(completeIt === undefined ? {} : { onComplete: completeIt }), name: analyst, by: annotator, mood: mood, ...(
|
|
473
|
+
? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[ai].name, children: [studiedCard(ai, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(reviewing ? { reviewing: true } : {}), ...(count === undefined ? {} : { count }), ...(completeIt === undefined ? {} : { onComplete: completeIt }), name: analyst, by: annotator, mood: mood, ...(voiced ? { voice: text } : {}), text: text })] }), _jsxs("section", { class: "you-block", "aria-label": sides[player].name, children: [_jsx("div", { class: "chat", children: _jsx(Compose, { lines: analysisTalk(talk, studied.name), newestFirst: false, spent: 0, draft: draft, onDraft: (words) => {
|
|
440
474
|
setDraft(words);
|
|
441
475
|
setUnsent(undefined);
|
|
442
476
|
}, onSend: () => sendWords({ game: studied.name, at }), canMove: false, name: name, problem: unsent, about: { ...(at === 0 ? {} : { ply: plies[at - 1] }), onShow: goTo }, text: text }) }), studiedCard(player, "you")] })] }))
|
|
443
|
-
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: table === undefined ? name : _jsx(AiNameButton, { name: name, table: table, onOpen: askChat, text: text }), signed: signed, side: idle ? undefined : ai, ...(idle ? {} : { level: text.start.levels[level] }), taken: material.taken[ai], ahead: ahead(ai), clock: clockOf(ai), text: text }), asleepNote, _jsx(AiWords, { words: aiWords(plies, ai, talk), newestFirst: !flipped, ...(
|
|
477
|
+
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: table === undefined ? name : _jsx(AiNameButton, { name: name, table: table, onOpen: askChat, text: text }), signed: signed, side: idle ? undefined : ai, ...(idle ? {} : { level: text.start.levels[level] }), taken: material.taken[ai], ahead: ahead(ai), clock: clockOf(ai), text: text }), asleepNote, _jsx(AiWords, { words: aiWords(plies, ai, talk), newestFirst: !flipped, ...(voiced ? { voice: text } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
|
|
444
478
|
// Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
|
|
445
479
|
spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
|
|
446
480
|
setDraft(words);
|
|
@@ -449,13 +483,14 @@ export function App({ start }) {
|
|
|
449
483
|
// Between games the board goes off (D-77): a game the page saw end once its effects are over, its final position less off than the table set.
|
|
450
484
|
class: between && !justEnded ? "board-wrap off" : "board-wrap", onWheel: wheel,
|
|
451
485
|
// A press on the board while the player looks back brings the game as it is: the next one plays.
|
|
452
|
-
onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end: boardEnd, player: playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined || viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing
|
|
486
|
+
onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end: boardEnd, player: playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined || viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing !== undefined ? undefined : refusal ?? (linkIgnored === undefined ? undefined : { text: text.link[linkIgnored] }), promotion: promoting === undefined || viewing !== undefined || !playerToMove ? undefined : { ...promoting, side: player }, onPromote: (piece) => piece === undefined ? setPromoting(undefined) : play(promoting.from, promoting.to, piece), onSelect: select, onMove: play, onRefuse: refuse, text: text }), folds.talk !== "open" && (asleepNote !== null || clockWaits)
|
|
453
487
|
? (_jsxs("div", { class: "over-board", children: [asleepNote, startClocks] }))
|
|
454
|
-
: null] }), studied === undefined ? null : (
|
|
488
|
+
: null] }), studied === undefined ? null : (_jsxs("div", { class: "board-steps", children: [_jsx(Steps, { shown: at, total: total, onStep: step, text: text }), standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: at, onShow: goTo, text: text })] })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [legendAt(true), _jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, ...(leftBook === undefined ? {} : { outOfBook: leftBook }), helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, legend: legend, text: text })), ...(helps === undefined && legendRows.length > 0 ? { legend } : {}), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : {
|
|
455
489
|
analysis: {
|
|
456
490
|
by: analyst,
|
|
457
491
|
...(summary === undefined ? {} : { summary }),
|
|
458
492
|
...(reviewing ? { reviewing: true } : {}),
|
|
493
|
+
...(count === undefined ? {} : { count }),
|
|
459
494
|
...(completeIt === undefined ? {} : { onComplete: completeIt }),
|
|
460
495
|
},
|
|
461
496
|
}), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null && replayed.record.moves.length > 0 ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text }), _jsx(PageFoot, { version: served.version, text: text })] })] }) }));
|
package/dist/page/app/served.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// What the page asks the server (D-47): the game of the player's folder, read again each time it changes, and the player's moves, words and choices sent
|
|
2
2
|
// there, refused in the player's language.
|
|
3
3
|
import { useEffect, useRef, useState } from "preact/hooks";
|
|
4
|
+
import { closesForNewer, eventsAddress } from "../layout/window.js";
|
|
5
|
+
import { watchServed } from "./watch.js";
|
|
4
6
|
/**
|
|
5
7
|
* The game of the player's folder, read from the server, and read again each time the server says it changed, whoever changed it; `onNewGame` when the chat
|
|
6
8
|
* asks for a new game, which the player chooses on the page.
|
|
@@ -11,43 +13,29 @@ export function useServed(onNewGame) {
|
|
|
11
13
|
const [offline, setOffline] = useState(false);
|
|
12
14
|
// Reads the game again: when the server says it changed, and when a clock runs out on the page.
|
|
13
15
|
const reload = useRef(() => { });
|
|
16
|
+
// The server says when the game changes, whoever changed it: the page reads it again, and as it connects, the first time or to a new chat's server.
|
|
17
|
+
// The server lives in the chat's MCP server, or the table: when it is gone the page waits for the next one and says so.
|
|
14
18
|
useEffect(() => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
})
|
|
37
|
-
.catch((error) => setProblem(String(error)));
|
|
38
|
-
};
|
|
39
|
-
// The server says when the game changes, whoever changed it: the page reads it again, and as it connects, the first time or to a new chat's server.
|
|
40
|
-
// The server lives in the chat's MCP server: when the chat ends, after /clear in Claude's app too, the page waits for the next one and says so.
|
|
41
|
-
reload.current = load;
|
|
42
|
-
const events = new EventSource("/api/events");
|
|
43
|
-
events.onopen = () => {
|
|
44
|
-
setOffline(false);
|
|
45
|
-
load();
|
|
46
|
-
};
|
|
47
|
-
// The chat asks for a new game: the player chooses it in the New game window, which opens here.
|
|
48
|
-
events.onmessage = (event) => event.data === "new-game" ? onNewGame() : load();
|
|
49
|
-
events.onerror = () => setOffline(events.readyState !== EventSource.OPEN);
|
|
50
|
-
return () => events.close();
|
|
19
|
+
const watch = watchServed({
|
|
20
|
+
read: () => fetch("/api/game").then((response) => response.json()),
|
|
21
|
+
connect: (handlers) => {
|
|
22
|
+
const events = new EventSource(eventsAddress());
|
|
23
|
+
events.onopen = handlers.open;
|
|
24
|
+
events.onmessage = (event) => closesForNewer(event.data) || handlers.message(event.data);
|
|
25
|
+
events.onerror = handlers.error;
|
|
26
|
+
return events;
|
|
27
|
+
},
|
|
28
|
+
later: (run, wait) => window.setTimeout(run, wait),
|
|
29
|
+
cancel: (timer) => window.clearTimeout(timer),
|
|
30
|
+
}, {
|
|
31
|
+
read: setData,
|
|
32
|
+
problem: setProblem,
|
|
33
|
+
offline: setOffline,
|
|
34
|
+
newGame: onNewGame,
|
|
35
|
+
otherVersion: () => window.location.reload(),
|
|
36
|
+
});
|
|
37
|
+
reload.current = watch.reload;
|
|
38
|
+
return watch.stop;
|
|
51
39
|
}, []);
|
|
52
40
|
return { data, problem, offline, reload: () => reload.current() };
|
|
53
41
|
}
|
|
@@ -67,13 +55,17 @@ export async function ask(path, body, text, name) {
|
|
|
67
55
|
return { answer: await response.json().catch(() => ({})) };
|
|
68
56
|
}
|
|
69
57
|
const refused = (await response.json());
|
|
70
|
-
|
|
71
|
-
return { refused: { text: known?.(refused.most ?? 0, name) ?? refused.error ?? response.statusText, words: refused.refusal?.startsWith("words-") === true } };
|
|
58
|
+
return { refused: { text: refusedText(refused, text, name) ?? response.statusText, words: refused.refusal?.startsWith("words-") === true } };
|
|
72
59
|
}
|
|
73
60
|
catch (error) {
|
|
74
61
|
return { refused: { text: String(error), words: false } };
|
|
75
62
|
}
|
|
76
63
|
}
|
|
64
|
+
/** A refusal of the server's in the player's language when the page knows its code, with the host's name where it sends the player; else its words. */
|
|
65
|
+
export function refusedText(refused, text, name) {
|
|
66
|
+
const known = refused.refusal === undefined ? undefined : text.refused[refused.refusal];
|
|
67
|
+
return known?.(refused.most ?? 0, name) ?? refused.error;
|
|
68
|
+
}
|
|
77
69
|
/** Sends the player's move to the server, which plays it as the chat would, with the words written for the AI: the reason it was refused, or undefined. */
|
|
78
70
|
export function sendMove(move, words, text, name) {
|
|
79
71
|
const message = words.trim();
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// How the page follows the server's game: it reads it when the server says it changed, tries a read lost on the way again, and connects again when the
|
|
2
|
+
// browser gives the server up for good, as when an old table hands the page to the new one. Without Preact, so that a test drives it with fake timers.
|
|
3
|
+
/** How many times, and how far apart in milliseconds, a read lost on the way is tried again; how long before a stream given up is opened again. */
|
|
4
|
+
export const readTries = 5;
|
|
5
|
+
export const readTryWait = 1000;
|
|
6
|
+
export const reconnectWait = 3000;
|
|
7
|
+
/** Follows the server's game: the first read when the events connect, another at each change, until `stop`. */
|
|
8
|
+
export function watchServed(tools, listener) {
|
|
9
|
+
let latest = 0;
|
|
10
|
+
// The Chesspal version of the first answer: a server of another, after an update, sends data this page may not read.
|
|
11
|
+
let pageVersion;
|
|
12
|
+
// The reads of the game asked last that failed, and the next try.
|
|
13
|
+
let failures = 0;
|
|
14
|
+
let retry;
|
|
15
|
+
let reconnect;
|
|
16
|
+
let stopped = false;
|
|
17
|
+
// Whether the page shows a game: then a read that fails leaves it shown, offline, rather than replacing it with the error.
|
|
18
|
+
let shown = false;
|
|
19
|
+
let events = open();
|
|
20
|
+
function load(again = false) {
|
|
21
|
+
tools.cancel(retry);
|
|
22
|
+
failures = again ? failures : 0;
|
|
23
|
+
// Two reads in flight may answer out of order: only the last one asked counts.
|
|
24
|
+
const asked = ++latest;
|
|
25
|
+
tools.read()
|
|
26
|
+
.then((body) => {
|
|
27
|
+
if (asked !== latest || stopped) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
pageVersion ??= body.version;
|
|
31
|
+
if (body.version !== undefined && body.version !== pageVersion) {
|
|
32
|
+
listener.otherVersion();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
listener.problem(body.error);
|
|
36
|
+
if (body.error === undefined) {
|
|
37
|
+
shown = true;
|
|
38
|
+
listener.offline(false);
|
|
39
|
+
listener.read(body);
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
.catch((error) => {
|
|
43
|
+
if (asked !== latest || stopped) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// With the server gone the page waits for it and says so: connecting again reads the game.
|
|
47
|
+
if (events.readyState !== streamOpen) {
|
|
48
|
+
listener.offline(true);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// A read lost on the way while the server is there is tried again shortly.
|
|
52
|
+
if (++failures <= readTries) {
|
|
53
|
+
retry = tools.later(() => load(true), readTryWait);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (shown) {
|
|
57
|
+
listener.offline(true);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
listener.problem(String(error));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function open() {
|
|
65
|
+
const stream = tools.connect({
|
|
66
|
+
open: () => {
|
|
67
|
+
listener.offline(false);
|
|
68
|
+
load();
|
|
69
|
+
},
|
|
70
|
+
// The chat asks for a new game: the player chooses it in the New game window.
|
|
71
|
+
message: (data) => data === "new-game" ? listener.newGame() : load(),
|
|
72
|
+
// The browser connects again by itself, but not after a server that answered with an error: then the page does, a little later.
|
|
73
|
+
error: () => {
|
|
74
|
+
listener.offline(stream.readyState !== streamOpen);
|
|
75
|
+
if (stream.readyState === streamClosed && !stopped) {
|
|
76
|
+
tools.cancel(reconnect);
|
|
77
|
+
reconnect = tools.later(() => {
|
|
78
|
+
events = open();
|
|
79
|
+
}, reconnectWait);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return stream;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
reload: () => load(),
|
|
87
|
+
stop: () => {
|
|
88
|
+
stopped = true;
|
|
89
|
+
tools.cancel(retry);
|
|
90
|
+
tools.cancel(reconnect);
|
|
91
|
+
events.close();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// EventSource's states, which a test's fake stream has no class for.
|
|
96
|
+
const streamOpen = 1;
|
|
97
|
+
const streamClosed = 2;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* The Torretta, whose eyes blink now and then, and its moods (D-70): a new face that pops in, never fading out, a red light when worried, out when sad, a light that breathes when
|
|
2
|
+
thoughtful */
|
|
3
|
+
.torretta .antenna {
|
|
4
|
+
fill: none;
|
|
5
|
+
stroke: var(--torretta-shade);
|
|
6
|
+
stroke-width: 4;
|
|
7
|
+
stroke-linecap: round;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.torretta .light {
|
|
11
|
+
fill: var(--torretta-light);
|
|
12
|
+
transition: fill 200ms;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.torretta.mood-worried .light {
|
|
16
|
+
fill: var(--torretta-alarm);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.torretta.mood-thoughtful .light {
|
|
20
|
+
animation: ponder 2400ms ease-in-out infinite;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.torretta.mood-sad .light {
|
|
24
|
+
fill: var(--torretta-shade);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@keyframes ponder {
|
|
28
|
+
50% {
|
|
29
|
+
fill: var(--torretta-ponder);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.torretta .expression.changed {
|
|
34
|
+
transform-box: fill-box;
|
|
35
|
+
transform-origin: center;
|
|
36
|
+
animation: face-in 160ms ease-out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@keyframes face-in {
|
|
40
|
+
from {
|
|
41
|
+
transform: scale(0.8);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Its bust on a player's card: a disc of the player's picture's colour, which cuts what falls out of it */
|
|
46
|
+
.torretta.bust {
|
|
47
|
+
clip-path: circle(50%);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.torretta .back {
|
|
51
|
+
fill: var(--say);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.torretta .shade {
|
|
55
|
+
fill: var(--torretta-shade);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.torretta .body {
|
|
59
|
+
fill: var(--torretta-body);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.torretta .screen {
|
|
63
|
+
fill: var(--torretta-screen);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.torretta .eyes,
|
|
67
|
+
.torretta .mouth {
|
|
68
|
+
fill: var(--torretta-face);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.torretta .expression .line,
|
|
72
|
+
.torretta .brows {
|
|
73
|
+
fill: none;
|
|
74
|
+
stroke: var(--torretta-face);
|
|
75
|
+
stroke-width: 3.4;
|
|
76
|
+
stroke-linecap: round;
|
|
77
|
+
stroke-linejoin: round;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.torretta .eyes {
|
|
81
|
+
transform-box: fill-box;
|
|
82
|
+
transform-origin: center;
|
|
83
|
+
animation: blink 6000ms infinite;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@keyframes blink {
|
|
87
|
+
0%,
|
|
88
|
+
95%,
|
|
89
|
+
100% {
|
|
90
|
+
transform: scaleY(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
97% {
|
|
94
|
+
transform: scaleY(0.1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@media (prefers-reduced-motion: reduce) {
|
|
99
|
+
.torretta .eyes,
|
|
100
|
+
.torretta .expression.changed,
|
|
101
|
+
.torretta.mood-thoughtful .light {
|
|
102
|
+
animation: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.torretta .light {
|
|
106
|
+
transition: none;
|
|
107
|
+
}
|
|
108
|
+
}
|