chesspal 0.0.1 → 0.2.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 +99 -2
- package/dist/cli/chat-hook.d.ts +2 -0
- package/dist/cli/chat-hook.js +33 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +28 -0
- package/dist/cli/command-line.d.ts +7 -0
- package/dist/cli/command-line.js +177 -0
- package/dist/commands/agents.d.ts +26 -0
- package/dist/commands/agents.js +72 -0
- package/dist/commands/ai-move.d.ts +28 -0
- package/dist/commands/ai-move.js +85 -0
- package/dist/commands/ai-say.d.ts +22 -0
- package/dist/commands/ai-say.js +58 -0
- package/dist/commands/analyse-game.d.ts +65 -0
- package/dist/commands/analyse-game.js +265 -0
- package/dist/commands/annotate-game.d.ts +39 -0
- package/dist/commands/annotate-game.js +131 -0
- package/dist/commands/answer-lines.d.ts +28 -0
- package/dist/commands/answer-lines.js +144 -0
- package/dist/commands/clock.d.ts +15 -0
- package/dist/commands/clock.js +45 -0
- package/dist/commands/coach.d.ts +45 -0
- package/dist/commands/coach.js +344 -0
- package/dist/commands/context.d.ts +67 -0
- package/dist/commands/context.js +37 -0
- package/dist/commands/delete-game.d.ts +7 -0
- package/dist/commands/delete-game.js +30 -0
- package/dist/commands/delete-review.d.ts +8 -0
- package/dist/commands/delete-review.js +34 -0
- package/dist/commands/draw-offer.d.ts +10 -0
- package/dist/commands/draw-offer.js +43 -0
- package/dist/commands/drawings.d.ts +13 -0
- package/dist/commands/drawings.js +25 -0
- package/dist/commands/end-game.d.ts +23 -0
- package/dist/commands/end-game.js +67 -0
- package/dist/commands/export-game.d.ts +14 -0
- package/dist/commands/export-game.js +27 -0
- package/dist/commands/game-summary.d.ts +78 -0
- package/dist/commands/game-summary.js +102 -0
- package/dist/commands/index.d.ts +18 -0
- package/dist/commands/index.js +64 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/lead-input.d.ts +8 -0
- package/dist/commands/lead-input.js +21 -0
- package/dist/commands/levels.d.ts +9 -0
- package/dist/commands/levels.js +25 -0
- package/dist/commands/look.d.ts +31 -0
- package/dist/commands/look.js +64 -0
- package/dist/commands/new-game.d.ts +68 -0
- package/dist/commands/new-game.js +218 -0
- package/dist/commands/opening-book.d.ts +13 -0
- package/dist/commands/opening-book.js +84 -0
- package/dist/commands/page-link.d.ts +11 -0
- package/dist/commands/page-link.js +28 -0
- package/dist/commands/past-games.d.ts +11 -0
- package/dist/commands/past-games.js +47 -0
- package/dist/commands/play-move.d.ts +37 -0
- package/dist/commands/play-move.js +94 -0
- package/dist/commands/player-move.d.ts +13 -0
- package/dist/commands/player-move.js +51 -0
- package/dist/commands/player-say.d.ts +5 -0
- package/dist/commands/player-say.js +16 -0
- package/dist/commands/ponder.d.ts +52 -0
- package/dist/commands/ponder.js +179 -0
- package/dist/commands/position.d.ts +14 -0
- package/dist/commands/position.js +130 -0
- package/dist/commands/profiles.d.ts +69 -0
- package/dist/commands/profiles.js +163 -0
- package/dist/commands/registry.d.ts +59 -0
- package/dist/commands/registry.js +62 -0
- package/dist/commands/say.d.ts +16 -0
- package/dist/commands/say.js +84 -0
- package/dist/commands/speech.d.ts +8 -0
- package/dist/commands/speech.js +20 -0
- package/dist/commands/status.d.ts +35 -0
- package/dist/commands/status.js +128 -0
- package/dist/commands/study-games.d.ts +5 -0
- package/dist/commands/study-games.js +40 -0
- package/dist/commands/study.d.ts +38 -0
- package/dist/commands/study.js +106 -0
- package/dist/commands/view.d.ts +14 -0
- package/dist/commands/view.js +125 -0
- package/dist/core/board-text.d.ts +7 -0
- package/dist/core/board-text.js +18 -0
- package/dist/core/book.json +1 -0
- package/dist/core/clock.d.ts +45 -0
- package/dist/core/clock.js +109 -0
- package/dist/core/facts.d.ts +55 -0
- package/dist/core/facts.js +175 -0
- package/dist/core/game.d.ts +95 -0
- package/dist/core/game.js +352 -0
- package/dist/core/illegal.d.ts +19 -0
- package/dist/core/illegal.js +141 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +12 -0
- package/dist/core/lead.d.ts +15 -0
- package/dist/core/lead.js +40 -0
- package/dist/core/levels.d.ts +20 -0
- package/dist/core/levels.js +33 -0
- package/dist/core/material.d.ts +9 -0
- package/dist/core/material.js +12 -0
- package/dist/core/moods.d.ts +21 -0
- package/dist/core/moods.js +45 -0
- package/dist/core/openings.d.ts +35 -0
- package/dist/core/openings.js +98 -0
- package/dist/core/openings.json +1 -0
- package/dist/core/pgn.d.ts +10 -0
- package/dist/core/pgn.js +213 -0
- package/dist/core/profile.d.ts +117 -0
- package/dist/core/profile.js +232 -0
- package/dist/core/record.d.ts +21 -0
- package/dist/core/record.js +25 -0
- package/dist/core/review.d.ts +80 -0
- package/dist/core/review.js +146 -0
- package/dist/games/ai-model.d.ts +5 -0
- package/dist/games/ai-model.js +15 -0
- package/dist/games/analysis.d.ts +12 -0
- package/dist/games/analysis.js +26 -0
- package/dist/games/archive.d.ts +63 -0
- package/dist/games/archive.js +242 -0
- package/dist/games/chat-start.d.ts +16 -0
- package/dist/games/chat-start.js +26 -0
- package/dist/games/coach.d.ts +57 -0
- package/dist/games/coach.js +144 -0
- package/dist/games/current-game.d.ts +56 -0
- package/dist/games/current-game.js +134 -0
- package/dist/games/files.d.ts +9 -0
- package/dist/games/files.js +53 -0
- package/dist/games/index.d.ts +11 -0
- package/dist/games/index.js +12 -0
- package/dist/games/plans.d.ts +9 -0
- package/dist/games/plans.js +23 -0
- package/dist/games/table-talk.d.ts +12 -0
- package/dist/games/table-talk.js +25 -0
- package/dist/games/talk.d.ts +36 -0
- package/dist/games/talk.js +131 -0
- package/dist/games/thoughts.d.ts +15 -0
- package/dist/games/thoughts.js +60 -0
- package/dist/home/data-folder.d.ts +12 -0
- package/dist/home/data-folder.js +55 -0
- package/dist/home/index.d.ts +2 -0
- package/dist/home/index.js +3 -0
- package/dist/home/open-folder.d.ts +2 -0
- package/dist/home/open-folder.js +14 -0
- package/dist/host-files/agents.d.ts +20 -0
- package/dist/host-files/agents.js +53 -0
- package/dist/host-files/host-files.d.ts +15 -0
- package/dist/host-files/host-files.js +182 -0
- package/dist/host-files/index.d.ts +2 -0
- package/dist/host-files/index.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/page/app/anchor.js +88 -0
- package/dist/page/app/app.js +458 -0
- package/dist/page/app/keys.js +94 -0
- package/dist/page/app/phase.js +30 -0
- package/dist/page/app/served.js +73 -0
- package/dist/page/avatars/avatars.js +19 -0
- package/dist/page/avatars/beanie.webp +0 -0
- package/dist/page/avatars/beard.webp +0 -0
- package/dist/page/avatars/bun.webp +0 -0
- package/dist/page/avatars/cat.webp +0 -0
- package/dist/page/avatars/curls.webp +0 -0
- package/dist/page/avatars/glasses.webp +0 -0
- package/dist/page/avatars/moustache.webp +0 -0
- package/dist/page/avatars/owl.webp +0 -0
- package/dist/page/avatars/short-hair.webp +0 -0
- package/dist/page/avatars/silver-bob.webp +0 -0
- package/dist/page/avatars/thought.js +10 -0
- package/dist/page/avatars/torretta.js +38 -0
- package/dist/page/base.css +160 -0
- package/dist/page/board/board.css +959 -0
- package/dist/page/board/board.js +125 -0
- package/dist/page/board/explain.js +58 -0
- package/dist/page/board/marks.js +160 -0
- package/dist/page/board/motion.js +71 -0
- package/dist/page/board/pointer.js +239 -0
- package/dist/page/board/squares.js +26 -0
- package/dist/page/dialogs/analyses.js +195 -0
- package/dist/page/dialogs/dialog.js +26 -0
- package/dist/page/dialogs/dialogs.css +1228 -0
- package/dist/page/dialogs/new-game.js +42 -0
- package/dist/page/dialogs/profiles.js +152 -0
- package/dist/page/dialogs/search.js +31 -0
- package/dist/page/dialogs/settings.js +42 -0
- package/dist/page/dialogs/studied.js +39 -0
- package/dist/page/dialogs/study.js +51 -0
- package/dist/page/game/badges.js +50 -0
- package/dist/page/game/browse.js +17 -0
- package/dist/page/game/card-fold.js +40 -0
- package/dist/page/game/column.js +51 -0
- package/dist/page/game/ending.js +73 -0
- package/dist/page/game/game.css +577 -0
- package/dist/page/game/move-list.js +72 -0
- package/dist/page/game/opening-book.js +14 -0
- package/dist/page/game/plies.js +82 -0
- package/dist/page/game/result-tile.js +14 -0
- package/dist/page/helps/helps.css +689 -0
- package/dist/page/helps/helps.js +220 -0
- package/dist/page/helps/view.js +127 -0
- package/dist/page/layout/fold-tab.js +11 -0
- package/dist/page/layout/folds.js +43 -0
- package/dist/page/layout/header.js +28 -0
- package/dist/page/layout/layout.css +834 -0
- package/dist/page/lead/lead.js +82 -0
- package/dist/page/lead/view.js +38 -0
- package/dist/page/main.js +8 -0
- package/dist/page/pieces/CREDITS.md +19 -0
- package/dist/page/pieces/capablanca-small.svg +20 -0
- package/dist/page/pieces/capablanca.svg +22 -0
- package/dist/page/pieces/nitida-small.svg +20 -0
- package/dist/page/pieces/nitida.svg +21 -0
- package/dist/page/pieces/pieces.js +28 -0
- package/dist/page/pieces/torretta-small.svg +20 -0
- package/dist/page/pieces/torretta.svg +21 -0
- package/dist/page/players/card.js +56 -0
- package/dist/page/players/clock-time.js +12 -0
- package/dist/page/players/model-name.js +20 -0
- package/dist/page/profile/profile.js +99 -0
- package/dist/page/profile/settings.js +77 -0
- package/dist/page/sounds/capture.ogg +0 -0
- package/dist/page/sounds/check.ogg +0 -0
- package/dist/page/sounds/draw.ogg +0 -0
- package/dist/page/sounds/flag.ogg +0 -0
- package/dist/page/sounds/mate.ogg +0 -0
- package/dist/page/sounds/move.ogg +0 -0
- package/dist/page/sounds/offer.ogg +0 -0
- package/dist/page/sounds/resign.ogg +0 -0
- package/dist/page/sounds/sounds.js +105 -0
- package/dist/page/style.css +9 -0
- package/dist/page/talk/talk.css +958 -0
- package/dist/page/talk/talk.js +165 -0
- package/dist/page/talk/voice.js +38 -0
- package/dist/page/text.js +711 -0
- package/dist/page/theme.css +292 -0
- package/dist/profiles/avatar.d.ts +23 -0
- package/dist/profiles/avatar.js +84 -0
- package/dist/profiles/front-matter.d.ts +9 -0
- package/dist/profiles/front-matter.js +33 -0
- package/dist/profiles/index.d.ts +3 -0
- package/dist/profiles/index.js +5 -0
- package/dist/profiles/profiles.d.ts +69 -0
- package/dist/profiles/profiles.js +254 -0
- package/dist/profiles/settings.d.ts +12 -0
- package/dist/profiles/settings.js +46 -0
- package/dist/server/bell.d.ts +68 -0
- package/dist/server/bell.js +225 -0
- package/dist/server/caller.d.ts +21 -0
- package/dist/server/caller.js +132 -0
- package/dist/server/mcp.d.ts +13 -0
- package/dist/server/mcp.js +207 -0
- package/dist/server/page/analyses.d.ts +23 -0
- package/dist/server/page/analyses.js +65 -0
- package/dist/server/page/files.d.ts +5 -0
- package/dist/server/page/files.js +92 -0
- package/dist/server/page/game.d.ts +6 -0
- package/dist/server/page/game.js +381 -0
- package/dist/server/page/http.d.ts +29 -0
- package/dist/server/page/http.js +68 -0
- package/dist/server/page/page.d.ts +41 -0
- package/dist/server/page/page.js +229 -0
- package/dist/server/page/profiles.d.ts +19 -0
- package/dist/server/page/profiles.js +215 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/instructions/analysis.md +4 -0
- package/instructions/analyst-agent.md +8 -0
- package/instructions/analyst.md +4 -0
- package/instructions/coach.md +11 -0
- package/instructions/opponent.md +15 -0
- package/instructions/ponder.md +9 -0
- package/package.json +66 -5
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The page's keys and wheel: [ and ] fold the side columns, M holds the helps' map (D-78), the arrows, Home and End step through the moves and f
|
|
2
|
+
// and x turn the board, and the wheel over the board steps too.
|
|
3
|
+
import { useEffect, useRef } from "preact/hooks";
|
|
4
|
+
import { flipKeys, stepKeys } from "../game/browse.js";
|
|
5
|
+
import { foldKeys, gameUnderBoard, oneColumn } from "../layout/folds.js";
|
|
6
|
+
/**
|
|
7
|
+
* `[` and `]` fold the talk and the game, as in editors with two side bars; never while the player types or drags a piece, nor in one column, nor the game
|
|
8
|
+
* under the board.
|
|
9
|
+
*/
|
|
10
|
+
export function useFoldKeys(fold) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const press = (event) => {
|
|
13
|
+
if (!forThePage(event) || oneColumn()) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const column = Object.keys(foldKeys).find((key) => foldKeys[key] === event.key);
|
|
17
|
+
if (column !== undefined && !(column === "game" && gameUnderBoard())) {
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
fold(column);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
window.addEventListener("keydown", press);
|
|
23
|
+
return () => window.removeEventListener("keydown", press);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** M held shows the control map while the helps are on (D-78); it goes off when M is let go or the page loses the focus. */
|
|
27
|
+
export function useMapKey(helping, map) {
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!helping) {
|
|
30
|
+
map(false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const down = (event) => {
|
|
34
|
+
if (forThePage(event) && event.key.toLowerCase() === "m") {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
map(true);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const up = (event) => {
|
|
40
|
+
if (event.key.toLowerCase() === "m") {
|
|
41
|
+
map(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const away = () => map(false);
|
|
45
|
+
window.addEventListener("keydown", down);
|
|
46
|
+
window.addEventListener("keyup", up);
|
|
47
|
+
window.addEventListener("blur", away);
|
|
48
|
+
return () => {
|
|
49
|
+
window.removeEventListener("keydown", down);
|
|
50
|
+
window.removeEventListener("keyup", up);
|
|
51
|
+
window.removeEventListener("blur", away);
|
|
52
|
+
};
|
|
53
|
+
}, [helping]);
|
|
54
|
+
}
|
|
55
|
+
/** The keys of Lichess and chess.com: the arrows, Home and End step through the moves, f and x turn the board round. */
|
|
56
|
+
export function useBrowseKeys(step, flip) {
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const press = (event) => {
|
|
59
|
+
const to = stepKeys[event.key];
|
|
60
|
+
const flips = flipKeys.includes(event.key.toLowerCase());
|
|
61
|
+
if (!forThePage(event) || (to === undefined && !flips)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
if (to === undefined) {
|
|
66
|
+
flip();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
step(to);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
window.addEventListener("keydown", press);
|
|
73
|
+
return () => window.removeEventListener("keydown", press);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** The mouse wheel over the board steps through the moves, as on Lichess: down forward, a step a notch, however finely a touchpad turns it. */
|
|
77
|
+
export function useWheelSteps(step) {
|
|
78
|
+
const turned = useRef(0);
|
|
79
|
+
return (event) => {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
turned.current += event.deltaMode === WheelEvent.DOM_DELTA_PIXEL ? event.deltaY : event.deltaY * notch;
|
|
82
|
+
while (Math.abs(turned.current) >= notch) {
|
|
83
|
+
step(turned.current > 0 ? "forward" : "back");
|
|
84
|
+
turned.current -= Math.sign(turned.current) * notch;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// How far a wheel turns in one notch, in pixels.
|
|
89
|
+
const notch = 100;
|
|
90
|
+
/** Whether a key is the page's: not one the player types into a box or the settings, nor with a modifier, nor while a piece is in the air. */
|
|
91
|
+
function forThePage(event) {
|
|
92
|
+
const typing = event.target.closest("input, textarea, select, [contenteditable], dialog") !== null;
|
|
93
|
+
return !(event.defaultPrevented || event.ctrlKey || event.altKey || event.metaKey || typing || document.documentElement.classList.contains("dragging"));
|
|
94
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page's phase: a game `analysing`, else the game the server sent, null before the first, whether it is `over`, and whether the page saw it end, which
|
|
3
|
+
* keeps its final position until the next starts.
|
|
4
|
+
*/
|
|
5
|
+
export function phaseOf(analysing, game, over, sawEnd) {
|
|
6
|
+
if (analysing) {
|
|
7
|
+
return "analysis";
|
|
8
|
+
}
|
|
9
|
+
if (game === undefined) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (game !== null && !over) {
|
|
13
|
+
return "going";
|
|
14
|
+
}
|
|
15
|
+
return sawEnd ? "over" : "idle";
|
|
16
|
+
}
|
|
17
|
+
/** What the header says of the game, with the kind that gives it a colour and an icon: in analysis, none going on, how it ended, or the draw offered. */
|
|
18
|
+
export function verdictOf(phase, how, winner, offer, declined, text) {
|
|
19
|
+
if (phase === "analysis") {
|
|
20
|
+
return { kind: "analysis", verdict: text.analysing };
|
|
21
|
+
}
|
|
22
|
+
if (phase === "idle") {
|
|
23
|
+
return { kind: "idle", verdict: text.noGameNow };
|
|
24
|
+
}
|
|
25
|
+
if (phase === "over") {
|
|
26
|
+
return { kind: "over", verdict: text.verdict(how, winner) };
|
|
27
|
+
}
|
|
28
|
+
const verdict = offer === "ai" ? text.offers : offer === "player" ? text.drawOffered : declined ? text.declines : undefined;
|
|
29
|
+
return verdict === undefined ? {} : { kind: "offer", verdict };
|
|
30
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
// there, refused in the player's language.
|
|
3
|
+
import { useEffect, useRef, useState } from "preact/hooks";
|
|
4
|
+
/**
|
|
5
|
+
* 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
|
+
* asks for a new game, which the player chooses on the page.
|
|
7
|
+
*/
|
|
8
|
+
export function useServed(onNewGame) {
|
|
9
|
+
const [data, setData] = useState(undefined);
|
|
10
|
+
const [problem, setProblem] = useState(undefined);
|
|
11
|
+
const [offline, setOffline] = useState(false);
|
|
12
|
+
// Reads the game again: when the server says it changed, and when a clock runs out on the page.
|
|
13
|
+
const reload = useRef(() => { });
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let latest = 0;
|
|
16
|
+
const load = () => {
|
|
17
|
+
// Two reads in flight may answer out of order: only the last one asked counts.
|
|
18
|
+
const asked = ++latest;
|
|
19
|
+
fetch("/api/game")
|
|
20
|
+
.then((response) => response.json())
|
|
21
|
+
.then((body) => {
|
|
22
|
+
if (asked !== latest) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
setProblem(body.error);
|
|
26
|
+
if (body.error === undefined) {
|
|
27
|
+
setData(body);
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.catch((error) => setProblem(String(error)));
|
|
31
|
+
};
|
|
32
|
+
// 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.
|
|
33
|
+
// 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.
|
|
34
|
+
reload.current = load;
|
|
35
|
+
const events = new EventSource("/api/events");
|
|
36
|
+
events.onopen = () => {
|
|
37
|
+
setOffline(false);
|
|
38
|
+
load();
|
|
39
|
+
};
|
|
40
|
+
// The chat asks for a new game: the player chooses it in the New game window, which opens here.
|
|
41
|
+
events.onmessage = (event) => event.data === "new-game" ? onNewGame() : load();
|
|
42
|
+
events.onerror = () => setOffline(events.readyState !== EventSource.OPEN);
|
|
43
|
+
return () => events.close();
|
|
44
|
+
}, []);
|
|
45
|
+
return { data, problem, offline, reload: () => reload.current() };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Sends a request to the server: the reason it was refused, in the player's language when the page knows the refusal (D-57), with the host's name where
|
|
49
|
+
* it sends the player, or undefined.
|
|
50
|
+
*/
|
|
51
|
+
export async function post(path, body, text, name) {
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
54
|
+
if (response.ok) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const refused = (await response.json());
|
|
58
|
+
const known = refused.refusal === undefined ? undefined : text.refused[refused.refusal];
|
|
59
|
+
return { text: known?.(refused.most ?? 0, name) ?? refused.error ?? response.statusText, words: refused.refusal?.startsWith("words-") === true };
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return { text: String(error), words: false };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** 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. */
|
|
66
|
+
export function sendMove(move, words, text, name) {
|
|
67
|
+
const message = words.trim();
|
|
68
|
+
return post("/api/move", message === "" ? { move } : { move, message }, text, name);
|
|
69
|
+
}
|
|
70
|
+
/** Sends the player's words alone: the reason they were refused, or undefined. */
|
|
71
|
+
export function sendMessage(words, text, name) {
|
|
72
|
+
return post("/api/message", { text: words.trim() }, text, name);
|
|
73
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
// The player's picture (D-45): their own image when the profile has one, else one of the drawn avatars, else the plain face of a page without profiles.
|
|
3
|
+
import { imageAddress } from "../profile/profile.js";
|
|
4
|
+
/** The player's picture, `size` pixels wide: their image, their drawn avatar, or the plain face. */
|
|
5
|
+
export function PlayerPicture({ profile, size = 48 }) {
|
|
6
|
+
const image = profile === undefined ? undefined : imageAddress(profile);
|
|
7
|
+
if (image !== undefined) {
|
|
8
|
+
return _jsx("img", { class: "avatar photo", src: image, width: size, height: size, alt: "" });
|
|
9
|
+
}
|
|
10
|
+
return profile?.avatar === undefined ? _jsx(PlainFace, {}) : _jsx(DrawnAvatar, { id: profile.avatar, size: size });
|
|
11
|
+
}
|
|
12
|
+
/** One of the drawn avatars, its colour behind it in the picture; named for a screen reader when `label` is given. */
|
|
13
|
+
export function DrawnAvatar({ id, size, label }) {
|
|
14
|
+
return _jsx("img", { class: "avatar drawn", src: `/page/avatars/${id}.webp`, width: size, height: size, alt: label ?? "", decoding: "async" });
|
|
15
|
+
}
|
|
16
|
+
/** The player's picture on a page without profiles: a simple face, in a circle. */
|
|
17
|
+
function PlainFace() {
|
|
18
|
+
return (_jsxs("svg", { class: "avatar", viewBox: "0 0 64 64", width: "48", height: "48", "aria-hidden": "true", children: [_jsx("circle", { class: "back", cx: "32", cy: "32", r: "32" }), _jsx("path", { class: "shirt", d: "M10 66C11 52 20 45 32 45C44 45 53 52 54 66Z" }), _jsx("path", { class: "skin", d: "M27 37H37V47C37 50 27 50 27 47Z" }), _jsx("circle", { class: "skin", cx: "32", cy: "28", r: "12" }), _jsx("path", { class: "hair", d: "M19.5 29C18 18 24 12.5 32.5 12.5C41 12.5 46.5 18 44.5 29C43 23.5 39 20.5 33 21.5C28 22.5 23 24 19.5 29Z" }), _jsx("circle", { class: "eye", cx: "27.5", cy: "29.5", r: "1.7" }), _jsx("circle", { class: "eye", cx: "36.5", cy: "29.5", r: "1.7" }), _jsx("path", { class: "smile", d: "M28.5 34.5Q32 37.5 35.5 34.5" })] }));
|
|
19
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
/** The AI's picture with its thought cloud while it thinks; the picture alone else. */
|
|
3
|
+
export function Thinker({ thinking, words, children }) {
|
|
4
|
+
return (_jsxs("span", { class: "thinker", children: [children, thinking === undefined ? null : _jsx(Thought, { thinking: thinking, words: words })] }));
|
|
5
|
+
}
|
|
6
|
+
/** The cloud alone, as on the folded talk's tab: named for a screen reader that explores it, never announced, since the AI's move says it is done. */
|
|
7
|
+
export function Thought({ thinking, words }) {
|
|
8
|
+
const dots = thinking === "move" ? { radius: 1.8, at: [6.8, 11.5, 16.2] } : { radius: 1.4, at: [7.3, 11.5, 15.7] };
|
|
9
|
+
return (_jsxs("svg", { class: `thought ${thinking}`, viewBox: "0 0 24 20", role: "img", "aria-label": words[thinking], children: [_jsx("title", { children: words[thinking] }), _jsx("path", { class: "cloud", d: "M7 1.5h9a5.5 5.5 0 0 1 0 11H7a5.5 5.5 0 0 1 0-11Z" }), _jsx("circle", { class: "cloud", cx: "19", cy: "16.5", r: "2.1" }), dots.at.map((x) => _jsx("circle", { class: "dot", cx: x, cy: "7", r: dots.radius }, x))] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
|
+
// The Torretta (D-45, D-46): the opponent and the logo, a rook that is a robot. Its screen shows the AI's mood (D-14, D-70): only the eyes, the mouth and the light change.
|
|
3
|
+
import { useRef } from "preact/hooks";
|
|
4
|
+
/** The Torretta at a size in pixels, with the face of a mood (neutral by default); with a label it is an image a screen reader names, else a decoration. */
|
|
5
|
+
export function Torretta({ size, label, mood = "neutral" }) {
|
|
6
|
+
// The face pops in when the mood changes, never when the Torretta first shows (the logo, a card, a dialog).
|
|
7
|
+
const changed = useChanged(mood);
|
|
8
|
+
return (_jsxs("svg", { class: `torretta mood-${mood}`, viewBox: "0 0 128 128", width: size, height: size, role: label === undefined ? undefined : "img", "aria-label": label, "aria-hidden": label === undefined ? "true" : undefined, children: [_jsx("path", { class: "antenna", d: "M64 31V19" }), _jsx("circle", { class: "light", cx: "64", cy: "16", r: "5" }), _jsx("rect", { class: "shade", x: "29", y: "64", width: "12", height: "26", rx: "2" }), _jsx("rect", { class: "shade", x: "87", y: "64", width: "12", height: "26", rx: "2" }), _jsx("path", { class: "body", d: "M34 30H47V40H57V30H71V40H81V30H94V54L89 58L92 106H36L39 58L34 54Z" }), _jsx("rect", { class: "shade", x: "30", y: "104", width: "68", height: "12", rx: "4" }), _jsx("rect", { class: "screen", x: "44", y: "63", width: "40", height: "32", rx: "7" }), _jsx("g", { class: changed ? "expression changed" : "expression", children: _jsx(Face, { mood: mood }) }, mood)] }));
|
|
9
|
+
}
|
|
10
|
+
/** Whether the mood has changed since the Torretta first showed: true from its first change on, so a face already shown never starts its entrance again. */
|
|
11
|
+
function useChanged(mood) {
|
|
12
|
+
const first = useRef(mood);
|
|
13
|
+
const changed = useRef(false);
|
|
14
|
+
changed.current ||= mood !== first.current;
|
|
15
|
+
return changed.current;
|
|
16
|
+
}
|
|
17
|
+
/** The eyes and the mouth of a mood, on the screen (x 44–84, y 63–95); strokes of 3.4 at least, to read at 40 px. */
|
|
18
|
+
function Face({ mood }) {
|
|
19
|
+
switch (mood) {
|
|
20
|
+
case "happy":
|
|
21
|
+
// Eyes closed in two arcs, a wide open smile.
|
|
22
|
+
return (_jsxs(_Fragment, { children: [_jsx("path", { class: "eyes line", d: "M50.5 78.5a4.5 4.5 0 0 1 9 0M68.5 78.5a4.5 4.5 0 0 1 9 0" }), _jsx("path", { class: "mouth", d: "M57 84h14a7 7 0 0 1-14 0Z" })] }));
|
|
23
|
+
case "surprised":
|
|
24
|
+
// Wide eyes and a round mouth.
|
|
25
|
+
return (_jsxs(_Fragment, { children: [_jsxs("g", { class: "eyes", children: [_jsx("circle", { cx: "54.5", cy: "75.5", r: "5.8" }), _jsx("circle", { cx: "73.5", cy: "75.5", r: "5.8" })] }), _jsx("ellipse", { class: "mouth", cx: "64", cy: "88.5", rx: "3.4", ry: "4" })] }));
|
|
26
|
+
case "worried":
|
|
27
|
+
// Brows raised in the middle, smaller eyes, a frown; the light turns red.
|
|
28
|
+
return (_jsxs(_Fragment, { children: [_jsx("path", { class: "brows", d: "M50 70L58 67M70 67L78 70" }), _jsxs("g", { class: "eyes", children: [_jsx("circle", { cx: "55", cy: "78.5", r: "3.8" }), _jsx("circle", { cx: "73", cy: "78.5", r: "3.8" })] }), _jsx("path", { class: "mouth line", d: "M56.5 91q7.5-6 15 0" })] }));
|
|
29
|
+
case "thoughtful":
|
|
30
|
+
// A glance to the side under one raised brow, the mouth pushed to the other side; the light breathes.
|
|
31
|
+
return (_jsxs(_Fragment, { children: [_jsx("path", { class: "brows", d: "M71 68L80 66" }), _jsxs("g", { class: "eyes", children: [_jsx("circle", { cx: "58", cy: "76.5", r: "3.9" }), _jsx("circle", { cx: "76", cy: "76.5", r: "3.9" })] }), _jsx("path", { class: "mouth line", d: "M52.5 89L63 87" })] }));
|
|
32
|
+
case "sad":
|
|
33
|
+
// No brows, the face sunk low: eyes half shut under lids that droop outwards, a deep frown; the light goes out.
|
|
34
|
+
return (_jsxs(_Fragment, { children: [_jsx("path", { class: "eyes", d: "M60.3 77.1A5.6 5.6 0 0 1 49.7 80.9ZM78.3 80.9A5.6 5.6 0 0 1 67.7 77.1Z" }), _jsx("path", { class: "mouth line", d: "M57.5 92.5q6.5-6.5 13 0" })] }));
|
|
35
|
+
default:
|
|
36
|
+
return (_jsxs(_Fragment, { children: [_jsxs("g", { class: "eyes", children: [_jsx("circle", { cx: "55", cy: "76", r: "4.6" }), _jsx("circle", { cx: "73", cy: "76", r: "4.6" })] }), _jsx("rect", { class: "mouth", x: "58", y: "85.4", width: "12", height: "3.2", rx: "1.6" })] }));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/* What every part shares: the page's type, its buttons and icons, the focus ring, the spinner */
|
|
2
|
+
|
|
3
|
+
* {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
margin: 0;
|
|
9
|
+
min-height: 100vh;
|
|
10
|
+
background: var(--page);
|
|
11
|
+
color: var(--ink);
|
|
12
|
+
font-family: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;
|
|
13
|
+
font-size: 15px;
|
|
14
|
+
-webkit-font-smoothing: antialiased;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
button,
|
|
18
|
+
textarea {
|
|
19
|
+
font: inherit;
|
|
20
|
+
color: inherit;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
button {
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.icon {
|
|
28
|
+
width: 17px;
|
|
29
|
+
height: 17px;
|
|
30
|
+
fill: none;
|
|
31
|
+
stroke: currentColor;
|
|
32
|
+
stroke-width: 1.7;
|
|
33
|
+
stroke-linecap: round;
|
|
34
|
+
stroke-linejoin: round;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:focus-visible {
|
|
38
|
+
outline: 2px solid var(--accent);
|
|
39
|
+
outline-offset: 2px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.command {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 7px;
|
|
46
|
+
background: none;
|
|
47
|
+
border: 1px solid transparent;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
padding: 7px 11px;
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
color: var(--soft);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.command:hover,
|
|
55
|
+
.command[aria-pressed="true"] {
|
|
56
|
+
color: var(--ink);
|
|
57
|
+
border-color: var(--line);
|
|
58
|
+
background: var(--card);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.command.primary {
|
|
62
|
+
background: var(--accent);
|
|
63
|
+
border-color: var(--accent);
|
|
64
|
+
color: var(--on-accent);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.command.primary:hover {
|
|
68
|
+
background: color-mix(in oklab, var(--accent) 85%, var(--ink));
|
|
69
|
+
border-color: transparent;
|
|
70
|
+
color: var(--on-accent);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* A draw the player cannot offer now: before a move each, on the AI's turn, or with an offer standing (D-65); their own offer stands pressed, at full strength */
|
|
74
|
+
.command[aria-disabled="true"]:not([aria-pressed="true"]) {
|
|
75
|
+
cursor: default;
|
|
76
|
+
opacity: 0.5;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.command[aria-disabled="true"]:not([aria-pressed="true"]):hover {
|
|
80
|
+
color: var(--soft);
|
|
81
|
+
border-color: transparent;
|
|
82
|
+
background: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.icon-button {
|
|
86
|
+
display: grid;
|
|
87
|
+
place-items: center;
|
|
88
|
+
width: 36px;
|
|
89
|
+
height: 36px;
|
|
90
|
+
background: none;
|
|
91
|
+
border: 1px solid var(--line);
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
color: var(--soft);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.icon-button:hover {
|
|
97
|
+
color: var(--ink);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
:root[data-theme="dark"] .icon-button .sun,
|
|
101
|
+
:root:not([data-theme="dark"]) .icon-button .moon {
|
|
102
|
+
display: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.spinner {
|
|
106
|
+
flex: none;
|
|
107
|
+
width: 14px;
|
|
108
|
+
height: 14px;
|
|
109
|
+
border: 2px solid color-mix(in srgb, var(--accent) 25%, transparent);
|
|
110
|
+
border-top-color: var(--accent);
|
|
111
|
+
border-radius: 50%;
|
|
112
|
+
animation: spin 900ms linear infinite;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@keyframes spin {
|
|
116
|
+
to {
|
|
117
|
+
transform: rotate(360deg);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@media (prefers-reduced-motion: reduce) {
|
|
122
|
+
.spinner {
|
|
123
|
+
animation-duration: 3s;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.visually-hidden {
|
|
128
|
+
position: absolute;
|
|
129
|
+
width: 1px;
|
|
130
|
+
height: 1px;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
clip-path: inset(50%);
|
|
133
|
+
white-space: nowrap;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* The cards, in every column */
|
|
137
|
+
.card {
|
|
138
|
+
background: var(--card);
|
|
139
|
+
border: 1px solid var(--line);
|
|
140
|
+
border-radius: var(--radius);
|
|
141
|
+
padding: 12px 14px;
|
|
142
|
+
box-shadow: var(--shadow);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.note {
|
|
146
|
+
color: var(--soft);
|
|
147
|
+
font-size: 13px;
|
|
148
|
+
font-weight: 400;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.empty {
|
|
152
|
+
margin: 0;
|
|
153
|
+
color: var(--soft);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.problem {
|
|
157
|
+
max-width: 40em;
|
|
158
|
+
margin: 24px auto;
|
|
159
|
+
padding: 0 16px;
|
|
160
|
+
}
|