chesspal 0.2.3 → 0.2.4

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.
Files changed (75) hide show
  1. package/dist/commands/agents.js +2 -1
  2. package/dist/commands/ai-say.d.ts +2 -1
  3. package/dist/commands/analyse-game.d.ts +2 -0
  4. package/dist/commands/analyse-game.js +7 -4
  5. package/dist/commands/annotate-game.d.ts +2 -0
  6. package/dist/commands/annotate-game.js +26 -8
  7. package/dist/commands/answer-lines.d.ts +8 -4
  8. package/dist/commands/answer-lines.js +19 -3
  9. package/dist/commands/coach.js +2 -2
  10. package/dist/commands/context.d.ts +2 -0
  11. package/dist/commands/delete-review.js +3 -2
  12. package/dist/commands/game-summary.d.ts +7 -2
  13. package/dist/commands/game-summary.js +7 -0
  14. package/dist/commands/index.d.ts +1 -1
  15. package/dist/commands/index.js +1 -1
  16. package/dist/commands/look.d.ts +9 -5
  17. package/dist/commands/look.js +19 -9
  18. package/dist/commands/ponder.js +1 -1
  19. package/dist/commands/position.js +2 -2
  20. package/dist/commands/say.d.ts +11 -6
  21. package/dist/commands/say.js +21 -13
  22. package/dist/commands/status.d.ts +3 -1
  23. package/dist/commands/status.js +31 -6
  24. package/dist/commands/study.d.ts +11 -1
  25. package/dist/commands/study.js +31 -2
  26. package/dist/commands/view.d.ts +17 -5
  27. package/dist/commands/view.js +67 -10
  28. package/dist/core/facts.d.ts +14 -2
  29. package/dist/core/facts.js +32 -7
  30. package/dist/core/game.d.ts +5 -0
  31. package/dist/core/game.js +9 -0
  32. package/dist/core/index.d.ts +1 -0
  33. package/dist/core/placement.d.ts +63 -0
  34. package/dist/core/placement.js +259 -0
  35. package/dist/core/possibilities.d.ts +38 -0
  36. package/dist/core/possibilities.js +254 -0
  37. package/dist/core/review.d.ts +4 -1
  38. package/dist/core/review.js +17 -6
  39. package/dist/games/analysis.d.ts +8 -4
  40. package/dist/games/analysis.js +9 -5
  41. package/dist/games/current-game.d.ts +16 -1
  42. package/dist/games/current-game.js +10 -8
  43. package/dist/games/files.d.ts +2 -0
  44. package/dist/games/files.js +7 -1
  45. package/dist/games/index.d.ts +2 -2
  46. package/dist/games/index.js +1 -1
  47. package/dist/games/table-talk.d.ts +2 -0
  48. package/dist/games/table-talk.js +5 -1
  49. package/dist/games/talk.d.ts +4 -3
  50. package/dist/games/talk.js +3 -3
  51. package/dist/page/app/app.js +36 -12
  52. package/dist/page/app/keys.js +16 -2
  53. package/dist/page/app/served.js +2 -2
  54. package/dist/page/board/board.js +2 -1
  55. package/dist/page/board/marks.js +4 -24
  56. package/dist/page/board/signs.js +23 -0
  57. package/dist/page/game/column.js +2 -2
  58. package/dist/page/game/game.css +12 -0
  59. package/dist/page/game/move-list.js +2 -2
  60. package/dist/page/game/plies.js +8 -0
  61. package/dist/page/helps/helps.css +3 -1
  62. package/dist/page/helps/helps.js +15 -15
  63. package/dist/page/layout/layout.css +29 -5
  64. package/dist/page/legend/legend.css +203 -0
  65. package/dist/page/legend/legend.js +169 -0
  66. package/dist/page/legend/view.js +54 -0
  67. package/dist/page/style.css +2 -0
  68. package/dist/page/talk/chat.css +90 -0
  69. package/dist/page/talk/talk.js +27 -5
  70. package/dist/page/text.js +38 -2
  71. package/dist/server/mcp.js +8 -6
  72. package/dist/server/page/game.js +20 -7
  73. package/dist/server/page/page.js +5 -3
  74. package/instructions/opponent.md +1 -1
  75. package/package.json +1 -1
@@ -456,13 +456,15 @@
456
456
  fill: var(--card);
457
457
  }
458
458
 
459
- /* The skewer of the board, in the line's colour and without its halo */
459
+ /* The pushpin and the skewer of the board, in the line's colour and without their halo */
460
+ .help-rows .help-pin,
460
461
  .help-rows .help-skewer {
461
462
  left: 7px;
462
463
  top: 4px;
463
464
  height: 17px;
464
465
  }
465
466
 
467
+ .help-rows .help-pin path,
466
468
  .help-rows .help-skewer path {
467
469
  fill: var(--row);
468
470
  stroke: none;
@@ -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, Skewer } from "../board/marks.js";
6
+ import { Notch, Pushpin, 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";
@@ -15,7 +15,7 @@ import { groupOf, pinSquares, riskSquares, squaresOf, strategyOf } from "./view.
15
15
  * its button or M is held, and the board without its drawings while their button is. A line under the pointer lights its squares on the board. Its title
16
16
  * folds it to the title and opens it again (Stefano).
17
17
  */
18
- export function HelpsCard({ helps, judged, hint, hintOpen, onHint, onPoint, onMap, mapping, onBare, bare, text }) {
18
+ export function HelpsCard({ helps, judged, hint, hintOpen, onHint, onPoint, onMap, mapping, onBare, bare, legend, text }) {
19
19
  const [tab, setTab] = useState("tactics");
20
20
  // The tactics' lines the player opened stay open from move to move, so that a pin is followed as the game goes on.
21
21
  const [opened, setOpened] = useState(new Set());
@@ -29,7 +29,7 @@ export function HelpsCard({ helps, judged, hint, hintOpen, onHint, onPoint, onMa
29
29
  }
30
30
  toggleFold();
31
31
  };
32
- return (_jsxs("section", { class: `card helps-card${folded ? " folded" : ""}`, "aria-label": words.title, children: [_jsxs("div", { class: "helps-head", children: [_jsx("h3", { class: "card-title", children: _jsxs(FoldButton, { folded: folded, onFold: fold, text: text, children: [_jsx(HelpsIcon, {}), words.title] }) }), _jsxs("span", { class: "helps-tools", children: [_jsx(MapButton, { mapping: mapping, onMap: onMap, text: text }), _jsx(BareButton, { bare: bare, onBare: onBare, text: text })] })] }), folded ? null : (_jsxs(_Fragment, { children: [judged.length === 0 && hint === undefined ? null : (_jsxs("section", { class: `coach${judged.length === 0 ? " hint-only" : ""}`, "aria-label": words.coach, children: [judged.map((ply) => _jsx(Judged, { ply: ply, text: text }, `${ply.number} ${ply.side}`)), hint === undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: onHint, text: text })] })), _jsxs("div", { class: "tabs", role: "tablist", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": tab === "tactics", onClick: () => setTab("tactics"), children: words.tactics }), _jsx("button", { type: "button", role: "tab", "aria-selected": tab === "strategy", onClick: () => setTab("strategy"), children: words.strategy })] }), tab === "tactics"
32
+ return (_jsxs("section", { class: `card helps-card${folded ? " folded" : ""}`, "aria-label": words.title, children: [_jsxs("div", { class: "helps-head", children: [_jsx("h3", { class: "card-title", children: _jsxs(FoldButton, { folded: folded, onFold: fold, text: text, children: [_jsx(HelpsIcon, {}), words.title] }) }), _jsxs("span", { class: "helps-tools", children: [_jsx(MapButton, { mapping: mapping, onMap: onMap, text: text }), _jsx(BareButton, { bare: bare, onBare: onBare, text: text }), legend] })] }), folded ? null : (_jsxs(_Fragment, { children: [judged.length === 0 && hint === undefined ? null : (_jsxs("section", { class: `coach${judged.length === 0 ? " hint-only" : ""}`, "aria-label": words.coach, children: [judged.map((ply) => _jsx(Judged, { ply: ply, text: text }, `${ply.number} ${ply.side}`)), hint === undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: onHint, text: text })] })), _jsxs("div", { class: "tabs", role: "tablist", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": tab === "tactics", onClick: () => setTab("tactics"), children: words.tactics }), _jsx("button", { type: "button", role: "tab", "aria-selected": tab === "strategy", onClick: () => setTab("strategy"), children: words.strategy })] }), tab === "tactics"
33
33
  ? _jsx(Tactics, { helps: helps, opened: opened, onOpen: openLine, onPoint: onPoint, text: text })
34
34
  : _jsx(Strategy, { helps: helps, onPoint: onPoint, text: text })] }))] }));
35
35
  }
@@ -86,20 +86,20 @@ 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, the skewer of the board, a check's plus, the loose pieces' i; its colour is the line's. */
89
+ /** A line's sign, as the board draws it: the corner of a piece at risk, the pushpin, the skewer; a check's plus, the loose pieces' i; its colour is the line's. */
90
90
  function LineIcon({ kind }) {
91
- if (kind === "risk") {
92
- return _jsx(Notch, {});
93
- }
94
- if (kind === "skewer") {
95
- return _jsx(Skewer, {});
96
- }
97
- if (kind === "loose") {
98
- return _jsx(InfoIcon, {});
91
+ switch (kind) {
92
+ case "risk":
93
+ return _jsx(Notch, {});
94
+ case "pin":
95
+ return _jsx(Pushpin, {});
96
+ case "skewer":
97
+ return _jsx(Skewer, {});
98
+ case "loose":
99
+ return _jsx(InfoIcon, {});
100
+ case "check":
101
+ return (_jsxs(RowIcon, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M8 5v6M5 8h6" })] }));
99
102
  }
100
- return (_jsx(RowIcon, { children: kind === "pin"
101
- ? _jsx("path", { d: "M6.5 9.5l3-3M7.5 4.5l1-1a2.5 2.5 0 0 1 3.5 3.5l-1 1M8.5 11.5l-1 1a2.5 2.5 0 0 1-3.5-3.5l1-1" })
102
- : (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M8 5v6M5 8h6" })] })) }));
103
103
  }
104
104
  /** How many: the loose pieces by side, each after its pawn, the player's first (Zoe); the others in one number. */
105
105
  function LineCount({ line, helps, words }) {
@@ -751,10 +751,31 @@
751
751
  display: none;
752
752
  }
753
753
 
754
- /* A game in analysis, turned round or not: the card on top, the board, its steps under the thumb, the Torretta's note, the card below, the moves (Zoe) */
755
- .app:has(.talk.studied),
754
+ /* A game in analysis: the card on top, the board, its steps under the thumb, the Torretta's note, the talk, the card below, the moves; turned round,
755
+ the player's card on top (Zoe) */
756
+ .app:has(.talk.studied) {
757
+ grid-template-areas: "head" "ai" "board" "steps" "note" "chat" "you" "side";
758
+ }
759
+
756
760
  .app.flipped:has(.talk.studied) {
757
- grid-template-areas: "head" "ai" "board" "steps" "note" "you" "side";
761
+ grid-template-areas: "head" "you" "board" "steps" "note" "chat" "ai" "side";
762
+ }
763
+
764
+ .talk.studied > .you-block {
765
+ display: contents;
766
+ }
767
+
768
+ .talk.studied > .you-block > :not(.chat) {
769
+ grid-area: you;
770
+ }
771
+
772
+ /* The talk, under the note: half the screen of it at most */
773
+ .talk.studied .chat {
774
+ grid-area: chat;
775
+ }
776
+
777
+ .talk.studied .sent {
778
+ max-height: 50vh;
758
779
  }
759
780
 
760
781
  .talk.studied > .ai-block {
@@ -771,9 +792,12 @@
771
792
  }
772
793
 
773
794
  /* With no chat listening, its note comes before the Torretta's (Zoe) */
774
- .app:has(.talk.studied > .ai-block > .asleep),
795
+ .app:has(.talk.studied > .ai-block > .asleep) {
796
+ grid-template-areas: "head" "ai" "board" "steps" "asleep" "note" "chat" "you" "side";
797
+ }
798
+
775
799
  .app.flipped:has(.talk.studied > .ai-block > .asleep) {
776
- grid-template-areas: "head" "ai" "board" "steps" "asleep" "note" "you" "side";
800
+ grid-template-areas: "head" "you" "board" "steps" "asleep" "note" "chat" "ai" "side";
777
801
  }
778
802
 
779
803
  .talk.studied > .ai-block > .asleep {
@@ -0,0 +1,203 @@
1
+ /* The legend of the marks on the board (D-99): a ? beside the map's button, and under it the marks the board shows now, each as the board draws it */
2
+ .marks-legend {
3
+ position: relative;
4
+ display: flex;
5
+ }
6
+
7
+ /* The ? in a ring, drawn in lines as the eye shut; lit while the legend is open */
8
+ .legend-button svg {
9
+ width: 15px;
10
+ height: 15px;
11
+ fill: none;
12
+ stroke: currentColor;
13
+ stroke-width: 1.4;
14
+ stroke-linecap: round;
15
+ stroke-linejoin: round;
16
+ }
17
+
18
+ .legend-button[aria-expanded="true"] {
19
+ background: var(--accent);
20
+ color: var(--on-accent);
21
+ border-color: var(--accent);
22
+ }
23
+
24
+ /* Over the cards under it, the board in sight beside it; placed under its button by the page, since the cards clip what overflows them */
25
+ .legend-panel {
26
+ position: fixed;
27
+ z-index: 20;
28
+ width: 300px;
29
+ max-width: calc(100vw - 32px);
30
+ overflow-y: auto;
31
+ padding: 10px 10px 8px;
32
+ border: 1px solid var(--line);
33
+ border-radius: 10px;
34
+ background: var(--card);
35
+ box-shadow: 0 8px 28px rgba(16, 28, 44, 0.22);
36
+ color: var(--ink);
37
+ font-size: 13.5px;
38
+ font-weight: normal;
39
+ text-align: left;
40
+ }
41
+
42
+ .legend-title {
43
+ margin: 0 0 6px 6px;
44
+ color: var(--soft);
45
+ font-size: 12px;
46
+ font-weight: 600;
47
+ }
48
+
49
+ .legend-none {
50
+ margin: 0 6px 4px;
51
+ color: var(--soft);
52
+ }
53
+
54
+ .legend-panel ul {
55
+ display: grid;
56
+ gap: 2px;
57
+ margin: 0;
58
+ padding: 0;
59
+ list-style: none;
60
+ }
61
+
62
+ .legend-panel li {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 10px;
66
+ min-height: 30px;
67
+ padding: 2px 6px;
68
+ border-radius: 6px;
69
+ }
70
+
71
+ .legend-panel li:hover {
72
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
73
+ }
74
+
75
+ /* The marks in a column of their own, so that the names line up */
76
+ .legend-mark {
77
+ flex: none;
78
+ display: flex;
79
+ justify-content: center;
80
+ width: 48px;
81
+ }
82
+
83
+ /* Names start with a capital, whoever's words they are */
84
+ .legend-name::first-letter {
85
+ text-transform: uppercase;
86
+ }
87
+
88
+ .legend-name small {
89
+ margin-left: 6px;
90
+ color: var(--soft);
91
+ font-size: 12px;
92
+ }
93
+
94
+ /* A square of the board's dark colour, or two, under the marks the board draws on its squares */
95
+ .legend-tile {
96
+ position: relative;
97
+ display: block;
98
+ width: 24px;
99
+ height: 24px;
100
+ border-radius: 3px;
101
+ background: var(--dark-square);
102
+ }
103
+
104
+ .legend-tile.wide {
105
+ width: 48px;
106
+ }
107
+
108
+ /* A square rung as the AI rings one: the arrival square when an arrow goes with it */
109
+ .legend-ring {
110
+ position: absolute;
111
+ top: 0;
112
+ right: 0;
113
+ width: 24px;
114
+ height: 24px;
115
+ border-radius: 3px;
116
+ box-shadow: inset 0 0 0 3px var(--ring), inset 0 0 0 4.5px rgba(255, 255, 255, 0.8);
117
+ }
118
+
119
+ .legend-ring.green {
120
+ --ring: var(--drawn-green);
121
+ }
122
+
123
+ .legend-ring.red {
124
+ --ring: var(--drawn-red);
125
+ }
126
+
127
+ .legend-ring.yellow {
128
+ --ring: var(--drawn-yellow);
129
+ }
130
+
131
+ .legend-ring.blue {
132
+ --ring: var(--drawn-blue);
133
+ }
134
+
135
+ /* The board's own marks, sized for a square of 24 pixels */
136
+ .legend-tile .helps-layer,
137
+ .legend-tile .arrows {
138
+ position: absolute;
139
+ inset: 0;
140
+ width: 100%;
141
+ height: 100%;
142
+ aspect-ratio: auto;
143
+ }
144
+
145
+ .legend-tile .help-notch {
146
+ width: 14px;
147
+ height: 14px;
148
+ }
149
+
150
+ .legend-tile .help-pin,
151
+ .legend-tile .help-skewer {
152
+ height: 17px;
153
+ }
154
+
155
+ .legend-mark .move-badge {
156
+ width: 20px;
157
+ height: 20px;
158
+ }
159
+
160
+ .legend-mark .badge {
161
+ position: static;
162
+ width: 20px;
163
+ height: 20px;
164
+ font-size: 13px;
165
+ box-shadow: 0 0 0 1.5px #fff, 0 1px 3px rgba(0, 0, 0, 0.4);
166
+ }
167
+
168
+ .legend-mark .control-count {
169
+ position: static;
170
+ translate: none;
171
+ font-size: 12px;
172
+ }
173
+
174
+ /* On a phone the ? is the last on the right under the board, and the legend opens below the board's row, as wide as it, never over the board */
175
+ .board-map .marks-legend {
176
+ position: static;
177
+ float: right;
178
+ margin-left: 8px;
179
+ }
180
+
181
+ .board-map .marks-legend .hold-button {
182
+ float: none;
183
+ margin-left: 0;
184
+ }
185
+
186
+ .legend-panel.under {
187
+ position: absolute;
188
+ top: calc(100% + 6px);
189
+ right: 0;
190
+ left: 0;
191
+ width: auto;
192
+ max-width: none;
193
+ }
194
+
195
+ @media (max-width: 840px) {
196
+ .board-map {
197
+ position: relative;
198
+ }
199
+
200
+ .helps-card .marks-legend {
201
+ display: none;
202
+ }
203
+ }
@@ -0,0 +1,169 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from "preact/hooks";
3
+ import { Badge, Notch, Pushpin, Skewer } from "../board/marks.js";
4
+ import { BadgeIcon } from "../game/badges.js";
5
+ /**
6
+ * The ? beside the map's button, and the legend it opens: a click, the ? key, Esc or a click elsewhere closes it, and puts out the squares it lit; while
7
+ * open it follows the board. The pointer on a line lights the squares that bear its mark, as a square in the AI's words. Under the board on a phone,
8
+ * `under` opens it below the board's row, never over the board.
9
+ */
10
+ export function MarksLegend({ rows, open, onOpen, drawn, onPoint, under = false, text }) {
11
+ const button = useRef(null);
12
+ const panel = useId();
13
+ useClosing(open, () => onOpen(false));
14
+ usePutOut(open, onPoint);
15
+ const place = usePlace(button, open && !under);
16
+ const words = text.legend;
17
+ const shown = open && (under || place !== undefined);
18
+ return (_jsxs("span", { class: "marks-legend", children: [_jsx("button", { ref: button, type: "button", class: "hold-button legend-button", "aria-expanded": open, "aria-controls": shown ? panel : undefined, "aria-label": words.title, title: words.hint, onClick: () => onOpen(!open), children: _jsxs("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M6.1 6.3a1.95 1.95 0 1 1 2.7 1.8c-.5.2-.8.6-.8 1.1v.5M8 11.4v.1" })] }) }), shown
19
+ ? (_jsxs("section", { id: panel, class: `legend-panel${under ? " under" : ""}`, "aria-label": words.title, style: under ? undefined : place, children: [_jsx("p", { class: "legend-title", children: words.title }), rows.length === 0 ? _jsx("p", { class: "legend-none", children: words.none }) : (_jsx("ul", { children: rows.map((row) => (_jsxs("li", { onPointerEnter: () => onPoint(row.squares), onPointerLeave: (event) => event.pointerType === "mouse" && onPoint([]), children: [_jsx("span", { class: "legend-mark", children: _jsx(Mark, { row: row }) }), _jsx(Name, { row: row, drawn: drawn, text: text })] }, keyOf(row)))) }))] }))
20
+ : null] }));
21
+ }
22
+ /** A mark as the board draws it, on a square of the board's colour where the board draws it on one. */
23
+ function Mark({ row }) {
24
+ switch (row.kind) {
25
+ case "badge":
26
+ return _jsx(BadgeIcon, { badge: row.badge });
27
+ case "risk":
28
+ return (_jsx(Tile, { children: _jsx(Notch, { tone: row.tone }) }));
29
+ case "pin":
30
+ return (_jsx(Tile, { children: _jsx(Pushpin, { tone: row.tone }) }));
31
+ case "skewer":
32
+ return (_jsx(Tile, { children: _jsx(Skewer, { tone: row.tone }) }));
33
+ case "pinLine":
34
+ case "skewerLine":
35
+ return (_jsx(Tile, { wide: true, children: _jsx("svg", { class: "helps-layer", viewBox: "0 0 2 1", children: _jsxs("g", { class: row.kind === "pinLine" ? "pin" : "skewer", children: [_jsx("line", { class: "halo", x1: "0.2", y1: "0.5", x2: "1.8", y2: "0.5" }), _jsx("line", { x1: "0.2", y1: "0.5", x2: "1.8", y2: "0.5" })] }) }) }));
36
+ case "drawn":
37
+ return (_jsxs(Tile, { wide: row.arrow, children: [row.ring ? _jsx("span", { class: `legend-ring ${row.colour}` }) : null, row.arrow
38
+ ? (_jsxs("svg", { class: "arrows drawn", viewBox: "0 0 2 1", children: [_jsx("path", { class: "edge", d: arrowShaft }), _jsx("polygon", { class: "edge", points: arrowHead }), _jsxs("g", { class: row.colour, children: [_jsx("path", { d: arrowShaft }), _jsx("polygon", { points: arrowHead })] })] }))
39
+ : null] }));
40
+ case "control":
41
+ return (_jsxs("span", { class: "control-count", "aria-hidden": "true", children: [_jsx("b", { class: "white", children: "2" }), _jsx("b", { class: "black", children: "1" })] }));
42
+ case "end":
43
+ return _jsx(Badge, { kind: row.sign });
44
+ }
45
+ }
46
+ function Name({ row, drawn, text }) {
47
+ const words = text.legend;
48
+ switch (row.kind) {
49
+ case "badge":
50
+ return _jsx(Named, { name: text.badges[row.badge], detail: words.badge });
51
+ case "risk":
52
+ case "pin":
53
+ case "skewer":
54
+ return _jsx(Named, { name: words.sign(row.kind, row.tone) });
55
+ case "pinLine":
56
+ return _jsx(Named, { name: words.pinLine });
57
+ case "skewerLine":
58
+ return _jsx(Named, { name: words.skewerLine });
59
+ case "drawn":
60
+ return _jsx(Named, { name: drawn.words[row.colour], detail: drawn.by });
61
+ case "control":
62
+ return _jsx(Named, { name: words.control });
63
+ case "end":
64
+ return _jsx(Named, { name: words.ends[row.sign] });
65
+ }
66
+ }
67
+ function Named({ name, detail }) {
68
+ return (_jsxs("span", { class: "legend-name", children: [name, detail === undefined ? null : _jsx("small", { children: detail })] }));
69
+ }
70
+ /** A square, or two side by side, of the board's dark colour, for the marks the board draws on its squares. */
71
+ function Tile({ wide = false, children }) {
72
+ return _jsx("span", { class: `legend-tile${wide ? " wide" : ""}`, "aria-hidden": "true", children: children });
73
+ }
74
+ /** Puts out the squares the legend lit when it closes, however it closes. */
75
+ function usePutOut(open, onPoint) {
76
+ const wasOpen = useRef(open);
77
+ useEffect(() => {
78
+ if (wasOpen.current && !open) {
79
+ onPoint([]);
80
+ }
81
+ wasOpen.current = open;
82
+ }, [open]);
83
+ }
84
+ /**
85
+ * Closes the legend at Esc and at a press outside it. The page has a legend in the card and one under the board for a phone, one of them hidden: a press on
86
+ * either is no press outside.
87
+ */
88
+ function useClosing(open, close) {
89
+ useEffect(() => {
90
+ if (!open) {
91
+ return;
92
+ }
93
+ const key = (event) => {
94
+ if (event.key === "Escape") {
95
+ close();
96
+ }
97
+ };
98
+ const press = (event) => {
99
+ if (!(event.target instanceof Element && event.target.closest(".marks-legend") !== null)) {
100
+ close();
101
+ }
102
+ };
103
+ window.addEventListener("keydown", key);
104
+ window.addEventListener("pointerdown", press);
105
+ return () => {
106
+ window.removeEventListener("keydown", key);
107
+ window.removeEventListener("pointerdown", press);
108
+ };
109
+ }, [open]);
110
+ }
111
+ /**
112
+ * Where the legend opens: under its button, or over it when the window has more room there, its right edge on the button's and no wider than the card it
113
+ * sits in, so that it leaves the board in sight; over the page, since the cards clip what overflows them. It follows the button as the window changes or
114
+ * scrolls.
115
+ */
116
+ function usePlace(button, open) {
117
+ const [place, setPlace] = useState(undefined);
118
+ useLayoutEffect(() => {
119
+ if (!open) {
120
+ setPlace(undefined);
121
+ return;
122
+ }
123
+ const follow = () => {
124
+ const box = button.current?.getBoundingClientRect();
125
+ const card = button.current?.closest(".card")?.getBoundingClientRect();
126
+ if (box !== undefined) {
127
+ const { clientWidth: width, clientHeight: height } = document.documentElement;
128
+ const below = height - box.bottom - panelGap - edge;
129
+ const above = box.top - panelGap - edge;
130
+ const side = { right: `${Math.max(edge, width - box.right)}px`, width: `${Math.min(panelWidth, box.right - (card?.left ?? box.right - panelWidth))}px` };
131
+ setPlace(below >= roomEnough || below >= above
132
+ ? { top: `${box.bottom + panelGap}px`, ...side, maxHeight: `${below}px` }
133
+ : { bottom: `${height - box.top + panelGap}px`, ...side, maxHeight: `${above}px` });
134
+ }
135
+ };
136
+ follow();
137
+ window.addEventListener("resize", follow);
138
+ window.addEventListener("scroll", follow, true);
139
+ return () => {
140
+ window.removeEventListener("resize", follow);
141
+ window.removeEventListener("scroll", follow, true);
142
+ };
143
+ }, [open]);
144
+ return place;
145
+ }
146
+ // How far the legend stands from its button, and from the window's edges; its width at most; the room under the button that is enough for it.
147
+ const panelGap = 6;
148
+ const edge = 16;
149
+ const panelWidth = 300;
150
+ const roomEnough = 320;
151
+ function keyOf(row) {
152
+ switch (row.kind) {
153
+ case "badge":
154
+ return row.kind;
155
+ case "risk":
156
+ case "pin":
157
+ case "skewer":
158
+ return `${row.kind} ${row.tone}`;
159
+ case "drawn":
160
+ return `${row.kind} ${row.colour}`;
161
+ case "end":
162
+ return `${row.kind} ${row.sign}`;
163
+ default:
164
+ return row.kind;
165
+ }
166
+ }
167
+ // An arrow across two squares, as the AI's: its shaft into its point, the point as long and wide as the board's.
168
+ const arrowShaft = "M0.3 0.5 L1.475 0.5";
169
+ const arrowHead = "1.4,0.71 1.7,0.5 1.4,0.29";
@@ -0,0 +1,54 @@
1
+ import { pinSquares } from "../helps/view.js";
2
+ /** The legend's lines, in the order the eye meets the marks: the move's judgment, the helps' signs, the drawings, the map, the end. */
3
+ export function legendOf({ badge, helps, drawn, control = false, ends }) {
4
+ const rows = badge === undefined ? [] : [{ kind: "badge", badge: badge.badge, squares: [badge.square] }];
5
+ return [...rows, ...helpsRows(helps, control), ...drawnRows(drawn), ...(control ? [{ kind: "control", squares: [] }] : []), ...endRows(ends)];
6
+ }
7
+ // The helps' signs by kind, then by tone, the player's threats first, each lighting the pieces that bear it; the pins' and skewers' lines after them,
8
+ // lighting the whole line. Over the control map the board shows the corners alone.
9
+ function helpsRows(helps, control) {
10
+ if (helps === undefined) {
11
+ return [];
12
+ }
13
+ const kinds = control ? ["risk"] : ["risk", "pin", "skewer"];
14
+ const signs = kinds.flatMap((kind) => tones.flatMap((tone) => {
15
+ const squares = helps.lines.filter((line) => line.kind === kind && line.tone === tone).flatMap(markedSquares);
16
+ return squares.length === 0 ? [] : [{ kind, tone, squares }];
17
+ }));
18
+ const lines = control ? [] : [
19
+ ...(helps.pins.length === 0 ? [] : [{ kind: "pinLine", squares: helps.pins.flatMap(pinSquares) }]),
20
+ ...(helps.skewers.length === 0 ? [] : [{ kind: "skewerLine", squares: helps.skewers.flatMap(pinSquares) }]),
21
+ ];
22
+ return [...signs, ...lines];
23
+ }
24
+ // The pieces a line of the tactics marks on the board.
25
+ function markedSquares(line) {
26
+ switch (line.kind) {
27
+ case "risk":
28
+ return line.items.map((risk) => risk.piece.square);
29
+ case "pin":
30
+ case "skewer":
31
+ return line.items.map((pin) => pin.piece.square);
32
+ default:
33
+ return [];
34
+ }
35
+ }
36
+ // One line a colour, with an arrow, a ring or both as the board has them.
37
+ function drawnRows(drawn) {
38
+ return colours.flatMap((colour) => {
39
+ const arrows = (drawn?.arrows ?? []).filter((arrow) => arrow.colour === colour);
40
+ const rings = (drawn?.squares ?? []).filter((square) => square.colour === colour);
41
+ const squares = [...arrows.flatMap((arrow) => [arrow.from, arrow.to]), ...rings.map((ring) => ring.square)];
42
+ return squares.length === 0 ? [] : [{ kind: "drawn", colour, arrow: arrows.length > 0, ring: rings.length > 0, squares }];
43
+ });
44
+ }
45
+ function endRows(ends) {
46
+ const signs = [...(ends ?? [])];
47
+ return endSigns.flatMap((sign) => {
48
+ const squares = signs.filter(([, each]) => each.badge === sign).map(([square]) => square);
49
+ return squares.length === 0 ? [] : [{ kind: "end", sign, squares }];
50
+ });
51
+ }
52
+ const tones = ["threat", "chance", "neutral"];
53
+ const colours = ["green", "red", "yellow", "blue"];
54
+ const endSigns = ["crown", "mate", "flag", "half"];
@@ -4,6 +4,8 @@
4
4
  @import "layout/layout.css";
5
5
  @import "board/board.css";
6
6
  @import "talk/talk.css";
7
+ @import "talk/chat.css";
7
8
  @import "game/game.css";
8
9
  @import "helps/helps.css";
10
+ @import "legend/legend.css";
9
11
  @import "dialogs/dialogs.css";
@@ -0,0 +1,90 @@
1
+ /* The talk over a game in analysis (D-101): the player's words and the AI's answers in one thread over the box, by the player's card; the AI's note stays
2
+ with the other side's card, and the thread gives way before the cards do (Zoe) */
3
+ .talk.studied > .you-block {
4
+ flex: 1 1 auto;
5
+ max-height: none;
6
+ }
7
+
8
+ /* A line apart from the AI's note, which another voice signed (Zoe) */
9
+ .talk.studied > section > .chat {
10
+ display: flex;
11
+ flex-direction: column;
12
+ flex: 1 1 auto;
13
+ min-height: 0;
14
+ margin-top: 12px;
15
+ padding-top: 10px;
16
+ border-top: 1px solid var(--line);
17
+ }
18
+
19
+ .chat > .compose {
20
+ flex: none;
21
+ }
22
+
23
+ /* The thread reads in time's order whichever way the board is turned: the newest words just over the box (Zoe) */
24
+ .flipped .talk.studied .sent {
25
+ justify-content: flex-end;
26
+ margin: 0 0 8px;
27
+ }
28
+
29
+ .flipped .talk.studied .compose {
30
+ margin: 0;
31
+ }
32
+
33
+ /* Beside the board the talk takes the room the AI's notes leave, not half the column as in a game; turned round, the player's card goes on top and the
34
+ AI's note below the box; on a phone the talk has an area of its own (layout.css) */
35
+ @media (min-width: 841px) {
36
+ .chat > .sent {
37
+ flex: 1 1 0;
38
+ }
39
+
40
+ .flipped .talk.studied,
41
+ .flipped .talk.studied > section {
42
+ flex-direction: column-reverse;
43
+ }
44
+
45
+ .flipped .talk.studied > section > .chat {
46
+ margin: 0 0 12px;
47
+ padding: 0 0 10px;
48
+ border-top: 0;
49
+ border-bottom: 1px solid var(--line);
50
+ }
51
+ }
52
+
53
+ /* The AI's answers on the left, in its bubbles, without the tail that points at the Torretta: it is not by them */
54
+ .sent li.from-ai {
55
+ align-self: flex-start;
56
+ align-items: flex-start;
57
+ }
58
+
59
+ .talk.studied .sent .say::before,
60
+ .talk.studied .sent .say::after {
61
+ content: none;
62
+ }
63
+
64
+ /* The box has no tail either: the card it would point at may be someone else's */
65
+ .compose.asking::after {
66
+ content: none;
67
+ }
68
+
69
+ /* The move a line was written on, small at its head: a press shows it again */
70
+ .shown-move {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 1px;
74
+ margin: 0 0 2px;
75
+ padding: 0;
76
+ border: 0;
77
+ background: none;
78
+ color: inherit;
79
+ font: inherit;
80
+ font-size: 12px;
81
+ font-weight: 600;
82
+ opacity: 0.85;
83
+ cursor: pointer;
84
+ }
85
+
86
+ .shown-move:hover,
87
+ .shown-move:focus-visible {
88
+ opacity: 1;
89
+ text-decoration: underline;
90
+ }