@connectorvol/chess-widgets 1.0.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/dist/button-variants.d.ts +73 -0
- package/dist/button-variants.js +31 -0
- package/dist/constants/editable-board-settings.d.ts +26 -0
- package/dist/constants/editable-board-settings.js +27 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/position-editor/EditFen.svelte +164 -0
- package/dist/position-editor/EditFen.svelte.d.ts +16 -0
- package/dist/position-editor/EditMove.svelte +180 -0
- package/dist/position-editor/EditMove.svelte.d.ts +9 -0
- package/dist/position-editor/EditPanel.svelte +164 -0
- package/dist/position-editor/EditPanel.svelte.d.ts +8 -0
- package/dist/position-editor/fen.svelte.d.ts +26 -0
- package/dist/position-editor/fen.svelte.js +177 -0
- package/dist/puzzle/puzzleCreatedPayload.d.ts +17 -0
- package/dist/puzzle/puzzleCreatedPayload.js +24 -0
- package/dist/puzzle/puzzleData.d.ts +32 -0
- package/dist/puzzle/puzzleData.js +51 -0
- package/dist/puzzle/puzzleSolverForkAnnotations.d.ts +14 -0
- package/dist/puzzle/puzzleSolverForkAnnotations.js +94 -0
- package/dist/puzzle/puzzleStepPreviewSolver.d.ts +39 -0
- package/dist/puzzle/puzzleStepPreviewSolver.js +87 -0
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte +247 -0
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte.d.ts +5 -0
- package/dist/puzzle-creation/StepMoves.svelte +225 -0
- package/dist/puzzle-creation/StepMoves.svelte.d.ts +15 -0
- package/dist/puzzle-creation/StepPosition.svelte +210 -0
- package/dist/puzzle-creation/StepPosition.svelte.d.ts +11 -0
- package/dist/puzzle-creation/StepPreview.svelte +589 -0
- package/dist/puzzle-creation/StepPreview.svelte.d.ts +23 -0
- package/dist/puzzle-creation/types.d.ts +27 -0
- package/dist/puzzle-creation/types.js +1 -0
- package/dist/utils.d.ts +15 -0
- package/dist/utils.js +8 -0
- package/package.json +76 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BoardApi } from "@connectorvol/chessboard";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
color: "w" | "b";
|
|
6
|
+
api: BoardApi;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { color, api }: Props = $props();
|
|
10
|
+
|
|
11
|
+
function onSelect(
|
|
12
|
+
promotion: "q" | "r" | "b" | "n" | "k" | "p" | "delete" | "drag"
|
|
13
|
+
) {
|
|
14
|
+
if (promotion === "delete") {
|
|
15
|
+
api.editState = {
|
|
16
|
+
mode: {
|
|
17
|
+
type: "delete",
|
|
18
|
+
piece: null,
|
|
19
|
+
color: null,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
} else if (promotion === "drag") {
|
|
23
|
+
api.editState = {
|
|
24
|
+
mode: {
|
|
25
|
+
type: "drag",
|
|
26
|
+
piece: null,
|
|
27
|
+
color: null,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
} else {
|
|
31
|
+
api.editState = {
|
|
32
|
+
mode: {
|
|
33
|
+
type: "insert",
|
|
34
|
+
piece: promotion,
|
|
35
|
+
color,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
{#if api.editState?.mode}
|
|
43
|
+
<div style="max-width: {api.getBoardSize()}rem; " class="flex">
|
|
44
|
+
<div
|
|
45
|
+
class="flex h-full w-full border"
|
|
46
|
+
style=" border-color: {api.theme.b}; "
|
|
47
|
+
>
|
|
48
|
+
<button
|
|
49
|
+
data-testid="edit-panel-drag-{color}"
|
|
50
|
+
aria-label="Edit mode"
|
|
51
|
+
style="background-color: {api.editState?.mode?.type === 'drag'
|
|
52
|
+
? 'pink'
|
|
53
|
+
: api.theme.b}"
|
|
54
|
+
class=" flex aspect-square h-full w-1/8 hover:contrast-125"
|
|
55
|
+
onclick={() => onSelect("drag")}
|
|
56
|
+
>
|
|
57
|
+
<svg
|
|
58
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
59
|
+
fill="none"
|
|
60
|
+
viewBox="0 0 24 24"
|
|
61
|
+
stroke-width="1.5"
|
|
62
|
+
stroke="currentColor"
|
|
63
|
+
class="p-2 md:p-3 text-white"
|
|
64
|
+
>
|
|
65
|
+
<path
|
|
66
|
+
stroke-linecap="round"
|
|
67
|
+
stroke-linejoin="round"
|
|
68
|
+
d="M10.05 4.575a1.575 1.575 0 1 0-3.15 0v3m3.15-3v-1.5a1.575 1.575 0 0 1 3.15 0v1.5m-3.15 0 .075 5.925m3.075.75V4.575m0 0a1.575 1.575 0 0 1 3.15 0V15M6.9 7.575a1.575 1.575 0 1 0-3.15 0v8.175a6.75 6.75 0 0 0 6.75 6.75h2.018a5.25 5.25 0 0 0 3.712-1.538l1.732-1.732a5.25 5.25 0 0 0 1.538-3.712l.003-2.024a.668.668 0 0 1 .198-.471 1.575 1.575 0 1 0-2.228-2.228 3.818 3.818 0 0 0-1.12 2.687M6.9 7.575V12m6.27 4.318A4.49 4.49 0 0 1 16.35 15m.002 0h-.002"
|
|
69
|
+
/>
|
|
70
|
+
</svg>
|
|
71
|
+
</button>
|
|
72
|
+
<button
|
|
73
|
+
data-testid="edit-panel-pawn-{color}"
|
|
74
|
+
style="background-color: {api.editState?.mode?.piece === 'p' &&
|
|
75
|
+
api.editState?.mode?.color === color
|
|
76
|
+
? 'pink'
|
|
77
|
+
: api.theme.w}"
|
|
78
|
+
class="flex aspect-square w-1/8"
|
|
79
|
+
onclick={() => onSelect("p")}
|
|
80
|
+
>
|
|
81
|
+
{@render api.chessSet?.pawn(color === "w" ? "w" : "b")}
|
|
82
|
+
</button>
|
|
83
|
+
<button
|
|
84
|
+
data-testid="edit-panel-knight-{color}"
|
|
85
|
+
style="background-color: {api.editState?.mode?.piece === 'n' &&
|
|
86
|
+
api.editState?.mode?.color === color
|
|
87
|
+
? 'pink'
|
|
88
|
+
: api.theme.b}"
|
|
89
|
+
class="flex aspect-square w-1/8"
|
|
90
|
+
onclick={() => onSelect("n")}
|
|
91
|
+
>
|
|
92
|
+
{@render api.chessSet?.knight(color)}
|
|
93
|
+
</button>
|
|
94
|
+
<button
|
|
95
|
+
data-testid="edit-panel-bishop-{color}"
|
|
96
|
+
style="background-color: {api.editState?.mode?.piece === 'b' &&
|
|
97
|
+
api.editState?.mode?.color === color
|
|
98
|
+
? 'pink'
|
|
99
|
+
: api.theme.w}"
|
|
100
|
+
class="flex aspect-square w-1/8"
|
|
101
|
+
onclick={() => onSelect("b")}
|
|
102
|
+
>
|
|
103
|
+
{@render api.chessSet?.bishop(color)}
|
|
104
|
+
</button>
|
|
105
|
+
<button
|
|
106
|
+
data-testid="edit-panel-rook-{color}"
|
|
107
|
+
style="background-color: {api.editState?.mode?.piece === 'r' &&
|
|
108
|
+
api.editState?.mode?.color === color
|
|
109
|
+
? 'pink'
|
|
110
|
+
: api.theme.b}"
|
|
111
|
+
class="flex aspect-square w-1/8"
|
|
112
|
+
onclick={() => onSelect("r")}
|
|
113
|
+
>
|
|
114
|
+
{@render api.chessSet?.rook(color)}
|
|
115
|
+
</button>
|
|
116
|
+
<button
|
|
117
|
+
data-testid="edit-panel-queen-{color}"
|
|
118
|
+
style="background-color: {api.editState?.mode?.piece === 'q' &&
|
|
119
|
+
api.editState?.mode?.color === color
|
|
120
|
+
? 'pink'
|
|
121
|
+
: api.theme.w}"
|
|
122
|
+
class="flex aspect-square w-1/8"
|
|
123
|
+
onclick={() => onSelect("q")}
|
|
124
|
+
>
|
|
125
|
+
{@render api.chessSet?.queen(color)}
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
data-testid="edit-panel-king-{color}"
|
|
129
|
+
style="background-color: {api.editState?.mode?.piece === 'k' &&
|
|
130
|
+
api.editState?.mode?.color === color
|
|
131
|
+
? 'pink'
|
|
132
|
+
: api.theme.b}"
|
|
133
|
+
class="flex aspect-square w-1/8"
|
|
134
|
+
onclick={() => onSelect("k")}
|
|
135
|
+
>
|
|
136
|
+
{@render api.chessSet?.king(color)}
|
|
137
|
+
</button>
|
|
138
|
+
<button
|
|
139
|
+
aria-label="Delete piece"
|
|
140
|
+
data-testid="edit-panel-delete-{color}"
|
|
141
|
+
style="background-color: {api.editState?.mode?.type === 'delete'
|
|
142
|
+
? 'pink'
|
|
143
|
+
: api.theme.w}"
|
|
144
|
+
class="flex aspect-square w-1/8"
|
|
145
|
+
onclick={() => onSelect("delete")}
|
|
146
|
+
>
|
|
147
|
+
<svg
|
|
148
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
149
|
+
fill="none"
|
|
150
|
+
viewBox="0 0 24 24"
|
|
151
|
+
stroke-width="1.5"
|
|
152
|
+
stroke="currentColor"
|
|
153
|
+
class="p-2 md:p-3 text-white"
|
|
154
|
+
>
|
|
155
|
+
<path
|
|
156
|
+
stroke-linecap="round"
|
|
157
|
+
stroke-linejoin="round"
|
|
158
|
+
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
|
159
|
+
/>
|
|
160
|
+
</svg>
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
{/if}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BoardApi } from "@connectorvol/chessboard";
|
|
2
|
+
export type Castling = `${"K" | "Q" | "k" | "q" | ""}${"Q" | "k" | "q" | ""}${"k" | "q" | ""}${"q" | ""}` | "-";
|
|
3
|
+
export declare class Fen {
|
|
4
|
+
private api;
|
|
5
|
+
position: string;
|
|
6
|
+
move: "w" | "b";
|
|
7
|
+
castling: Castling;
|
|
8
|
+
enPassant: string;
|
|
9
|
+
halfmove: number;
|
|
10
|
+
fullmove: number;
|
|
11
|
+
constructor(api: BoardApi);
|
|
12
|
+
get fullFen(): string;
|
|
13
|
+
set fullFen(fen: string);
|
|
14
|
+
validateFullFen(fen: string): boolean;
|
|
15
|
+
validateMove(move: string): boolean;
|
|
16
|
+
validateEnPassant(enPassant: string): boolean;
|
|
17
|
+
validateHalfmove(halfmove: string): boolean;
|
|
18
|
+
validateFullmove(fullmove: string): boolean;
|
|
19
|
+
validatePosition(position: string): boolean;
|
|
20
|
+
validateCastling(castling: Castling): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Представляет генерацию возможных ходов en passant.
|
|
23
|
+
* Возвращает коллекцию возможных полей для взятия на проходе.
|
|
24
|
+
*/
|
|
25
|
+
genPossibleEnPassantMoves(): string[];
|
|
26
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
export class Fen {
|
|
2
|
+
api;
|
|
3
|
+
position;
|
|
4
|
+
move = $state("w");
|
|
5
|
+
castling = $state("KQkq");
|
|
6
|
+
enPassant = $state("-");
|
|
7
|
+
halfmove = $state(0);
|
|
8
|
+
fullmove = $state(1);
|
|
9
|
+
constructor(api) {
|
|
10
|
+
this.api = api;
|
|
11
|
+
this.position = $derived(this.api.fen);
|
|
12
|
+
}
|
|
13
|
+
get fullFen() {
|
|
14
|
+
return `${this.position} ${this.move} ${this.castling} ${this.enPassant} ${this.halfmove} ${this.fullmove}`;
|
|
15
|
+
}
|
|
16
|
+
set fullFen(fen) {
|
|
17
|
+
const [position, move, castling, enPassant, halfmove, fullmove] = fen.split(" ");
|
|
18
|
+
if (!this.validateFullFen(fen)) {
|
|
19
|
+
throw new Error("Invalid FEN");
|
|
20
|
+
}
|
|
21
|
+
this.api.fen = position;
|
|
22
|
+
this.move = move;
|
|
23
|
+
this.castling = castling;
|
|
24
|
+
this.enPassant = enPassant ? enPassant : "-";
|
|
25
|
+
this.halfmove = Number(halfmove) || 0;
|
|
26
|
+
this.fullmove = Number(fullmove) || 1;
|
|
27
|
+
}
|
|
28
|
+
validateFullFen(fen) {
|
|
29
|
+
const [position, move, castling, enPassant, halfmove, fullmove] = fen.split(" ");
|
|
30
|
+
return (this.validatePosition(position) &&
|
|
31
|
+
this.validateCastling(castling) &&
|
|
32
|
+
this.validateMove(move) &&
|
|
33
|
+
this.validateEnPassant(enPassant) &&
|
|
34
|
+
this.validateHalfmove(halfmove) &&
|
|
35
|
+
this.validateFullmove(fullmove));
|
|
36
|
+
}
|
|
37
|
+
validateMove(move) {
|
|
38
|
+
return move === "w" || move === "b";
|
|
39
|
+
}
|
|
40
|
+
validateEnPassant(enPassant) {
|
|
41
|
+
return enPassant === "-" || enPassant.length === 2;
|
|
42
|
+
}
|
|
43
|
+
validateHalfmove(halfmove) {
|
|
44
|
+
return Number(halfmove) >= 0;
|
|
45
|
+
}
|
|
46
|
+
validateFullmove(fullmove) {
|
|
47
|
+
return Number(fullmove) >= 1;
|
|
48
|
+
}
|
|
49
|
+
validatePosition(position) {
|
|
50
|
+
const rows = position.split("/");
|
|
51
|
+
if (rows.length !== 8)
|
|
52
|
+
return false;
|
|
53
|
+
for (const row of rows) {
|
|
54
|
+
let counter = 0;
|
|
55
|
+
for (const cell of row) {
|
|
56
|
+
if (cell !== "P" &&
|
|
57
|
+
cell !== "p" &&
|
|
58
|
+
cell !== "N" &&
|
|
59
|
+
cell !== "n" &&
|
|
60
|
+
cell !== "B" &&
|
|
61
|
+
cell !== "b" &&
|
|
62
|
+
cell !== "R" &&
|
|
63
|
+
cell !== "r" &&
|
|
64
|
+
cell !== "Q" &&
|
|
65
|
+
cell !== "q" &&
|
|
66
|
+
cell !== "K" &&
|
|
67
|
+
cell !== "k" &&
|
|
68
|
+
cell !== "1" &&
|
|
69
|
+
cell !== "2" &&
|
|
70
|
+
cell !== "3" &&
|
|
71
|
+
cell !== "4" &&
|
|
72
|
+
cell !== "5" &&
|
|
73
|
+
cell !== "6" &&
|
|
74
|
+
cell !== "7" &&
|
|
75
|
+
cell !== "8") {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (cell === "1" ||
|
|
79
|
+
cell === "2" ||
|
|
80
|
+
cell === "3" ||
|
|
81
|
+
cell === "4" ||
|
|
82
|
+
cell === "5" ||
|
|
83
|
+
cell === "6" ||
|
|
84
|
+
cell === "7" ||
|
|
85
|
+
cell === "8") {
|
|
86
|
+
counter += Number(cell);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
counter++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (counter > 8)
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
validateCastling(castling) {
|
|
98
|
+
if (castling === "-")
|
|
99
|
+
return true;
|
|
100
|
+
if (castling.length > 4)
|
|
101
|
+
return false;
|
|
102
|
+
const castlingArray = castling.split("");
|
|
103
|
+
for (const c of castlingArray) {
|
|
104
|
+
if (c !== "K" && c !== "Q" && c !== "k" && c !== "q")
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Представляет генерацию возможных ходов en passant.
|
|
111
|
+
* Возвращает коллекцию возможных полей для взятия на проходе.
|
|
112
|
+
*/
|
|
113
|
+
genPossibleEnPassantMoves() {
|
|
114
|
+
const fenPosition = this.position.split(" ")[0];
|
|
115
|
+
const rows = fenPosition.split("/");
|
|
116
|
+
function toSquare(file, rank) {
|
|
117
|
+
return String.fromCharCode("a".charCodeAt(0) + file) + (8 - rank);
|
|
118
|
+
}
|
|
119
|
+
function parseFenRow(row) {
|
|
120
|
+
const result = [];
|
|
121
|
+
for (const c of row) {
|
|
122
|
+
if (/\d/.test(c)) {
|
|
123
|
+
for (let i = 0; i < Number(c); i++)
|
|
124
|
+
result.push(null);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
result.push(c);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
const result = [];
|
|
133
|
+
const row6 = parseFenRow(rows[6] || "");
|
|
134
|
+
const row5 = parseFenRow(rows[5] || "");
|
|
135
|
+
const row4 = parseFenRow(rows[4] || "");
|
|
136
|
+
const row3 = parseFenRow(rows[3] || "");
|
|
137
|
+
const row2 = parseFenRow(rows[2] || "");
|
|
138
|
+
const row1 = parseFenRow(rows[1] || "");
|
|
139
|
+
if (this.move === "b") {
|
|
140
|
+
for (let file = 0; file < 8; file++) {
|
|
141
|
+
if (row4[file] === "P" && !row5[file] && !row6[file]) {
|
|
142
|
+
if (file > 0 && row4[file - 1] === "p") {
|
|
143
|
+
const square = toSquare(file, 5);
|
|
144
|
+
if (!result.includes(square)) {
|
|
145
|
+
result.push(square);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (file < 7 && row4[file + 1] === "p") {
|
|
149
|
+
const square = toSquare(file, 5);
|
|
150
|
+
if (!result.includes(square)) {
|
|
151
|
+
result.push(square);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
for (let file = 0; file < 8; file++) {
|
|
159
|
+
if (row3[file] === "p" && !row2[file] && !row1[file]) {
|
|
160
|
+
if (file > 0 && row3[file - 1] === "P") {
|
|
161
|
+
const square = toSquare(file, 2);
|
|
162
|
+
if (!result.includes(square)) {
|
|
163
|
+
result.push(square);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (file < 7 && row3[file + 1] === "P") {
|
|
167
|
+
const square = toSquare(file, 2);
|
|
168
|
+
if (!result.includes(square)) {
|
|
169
|
+
result.push(square);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Представляет данные задачи, передаваемые колбэку после завершения мастера.
|
|
3
|
+
*/
|
|
4
|
+
export interface TPuzzleCreatedPayload {
|
|
5
|
+
/**
|
|
6
|
+
* Возвращает строку только с ходами и результатом (без тегов заголовка PGN).
|
|
7
|
+
*/
|
|
8
|
+
puzzlePgn: string;
|
|
9
|
+
/**
|
|
10
|
+
* Возвращает начальный FEN задачи (из тега FEN или начальная позиция по умолчанию).
|
|
11
|
+
*/
|
|
12
|
+
fen: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Представляет разбор полного PGN из дерева задачи на FEN и чистую строку ходов.
|
|
16
|
+
*/
|
|
17
|
+
export declare function puzzlePartsFromFullPgn(fullPgn: string): TPuzzleCreatedPayload;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { INITIAL_FEN } from "@connectorvol/chessops/fen";
|
|
2
|
+
import { makePgn, parsePgn, } from "@connectorvol/chessops/pgn";
|
|
3
|
+
/**
|
|
4
|
+
* Представляет разбор полного PGN из дерева задачи на FEN и чистую строку ходов.
|
|
5
|
+
*/
|
|
6
|
+
export function puzzlePartsFromFullPgn(fullPgn) {
|
|
7
|
+
const trimmed = fullPgn.trim();
|
|
8
|
+
if (!trimmed) {
|
|
9
|
+
return { puzzlePgn: "", fen: INITIAL_FEN };
|
|
10
|
+
}
|
|
11
|
+
const games = parsePgn(trimmed);
|
|
12
|
+
const game = games[0];
|
|
13
|
+
if (!game) {
|
|
14
|
+
return { puzzlePgn: "", fen: INITIAL_FEN };
|
|
15
|
+
}
|
|
16
|
+
const fen = game.headers.get("FEN") ?? INITIAL_FEN;
|
|
17
|
+
const movesOnly = {
|
|
18
|
+
headers: new Map(),
|
|
19
|
+
comments: game.comments,
|
|
20
|
+
moves: game.moves,
|
|
21
|
+
};
|
|
22
|
+
const puzzlePgn = makePgn(movesOnly).trim();
|
|
23
|
+
return { puzzlePgn, fen };
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TChessTree } from "@connectorvol/tree";
|
|
2
|
+
/** Полный FEN пустой доски (используется как начальное состояние мастера без входного FEN). */
|
|
3
|
+
export declare const PUZZLE_EMPTY_BOARD_FEN = "8/8/8/8/8/8/8/8 w - - 0 1";
|
|
4
|
+
/**
|
|
5
|
+
* Представляет данные шахматного puzzle.
|
|
6
|
+
*/
|
|
7
|
+
export interface PuzzleData {
|
|
8
|
+
/**
|
|
9
|
+
* Возвращает начальную позицию в формате FEN.
|
|
10
|
+
*/
|
|
11
|
+
initialFen: string;
|
|
12
|
+
/**
|
|
13
|
+
* Возвращает массив ходов решения в нотации SAN.
|
|
14
|
+
*/
|
|
15
|
+
moves: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Возвращает начальные данные для создания puzzle.
|
|
19
|
+
*
|
|
20
|
+
* @param initialFen полный FEN; если не передан — пустая доска (`PUZZLE_EMPTY_BOARD_FEN`).
|
|
21
|
+
*/
|
|
22
|
+
export declare function createInitialPuzzleData(initialFen?: string): PuzzleData;
|
|
23
|
+
/**
|
|
24
|
+
* Представляет создание пустого дерева ходов из начальной позиции FEN.
|
|
25
|
+
* Возвращает корень дерева (TChessTree) для использования в ChessTree.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createEmptyTreeFromFen(initialFen: string): TChessTree;
|
|
28
|
+
/**
|
|
29
|
+
* Представляет извлечение главной линии (первого варианта) из дерева ходов.
|
|
30
|
+
* Возвращает массив ходов в нотации SAN.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getMainLineFromTree(rootNode: TChessTree): string[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defaultHeaders } from "@connectorvol/chessops/pgn";
|
|
2
|
+
import { generateId } from "@connectorvol/shared";
|
|
3
|
+
/** Полный FEN пустой доски (используется как начальное состояние мастера без входного FEN). */
|
|
4
|
+
export const PUZZLE_EMPTY_BOARD_FEN = "8/8/8/8/8/8/8/8 w - - 0 1";
|
|
5
|
+
/**
|
|
6
|
+
* Возвращает начальные данные для создания puzzle.
|
|
7
|
+
*
|
|
8
|
+
* @param initialFen полный FEN; если не передан — пустая доска (`PUZZLE_EMPTY_BOARD_FEN`).
|
|
9
|
+
*/
|
|
10
|
+
export function createInitialPuzzleData(initialFen) {
|
|
11
|
+
return {
|
|
12
|
+
initialFen: initialFen ?? PUZZLE_EMPTY_BOARD_FEN,
|
|
13
|
+
moves: [],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Представляет создание пустого дерева ходов из начальной позиции FEN.
|
|
18
|
+
* Возвращает корень дерева (TChessTree) для использования в ChessTree.
|
|
19
|
+
*/
|
|
20
|
+
export function createEmptyTreeFromFen(initialFen) {
|
|
21
|
+
const headers = defaultHeaders();
|
|
22
|
+
headers.set("FEN", initialFen);
|
|
23
|
+
const rootNode = {
|
|
24
|
+
id: generateId(),
|
|
25
|
+
children: [],
|
|
26
|
+
data: {
|
|
27
|
+
fen: initialFen,
|
|
28
|
+
san: "",
|
|
29
|
+
ply: 0,
|
|
30
|
+
fullMoves: 0,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
moves: rootNode,
|
|
35
|
+
headers,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Представляет извлечение главной линии (первого варианта) из дерева ходов.
|
|
40
|
+
* Возвращает массив ходов в нотации SAN.
|
|
41
|
+
*/
|
|
42
|
+
export function getMainLineFromTree(rootNode) {
|
|
43
|
+
const line = [];
|
|
44
|
+
let node = rootNode.moves;
|
|
45
|
+
while (node.children.length > 0) {
|
|
46
|
+
node = node.children[0];
|
|
47
|
+
if (node.data.san)
|
|
48
|
+
line.push(node.data.san);
|
|
49
|
+
}
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChessTree } from "@connectorvol/tree";
|
|
2
|
+
import { Color } from "@connectorvol/shared";
|
|
3
|
+
/**
|
|
4
|
+
* Представляет проверку линии решения задачи: метки ✓/✗ только на ходах из развилки,
|
|
5
|
+
* запрет ✓/✗ ниже по дереву после ✗ на ходе решателя, разметка развилок со стороны
|
|
6
|
+
* решателя и окончание вариантов на развилке соперника ходом решателя вне поддерева
|
|
7
|
+
* после хода решателя с меткой «неверное решение».
|
|
8
|
+
*/
|
|
9
|
+
export declare function validatePuzzleSolverForkAnnotations(tree: ChessTree, solverColor: Color): {
|
|
10
|
+
ok: true;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Color, PUZZLE_BRANCH_CORRECT_NAG_ID, PUZZLE_BRANCH_WRONG_NAG_ID, } from "@connectorvol/shared";
|
|
2
|
+
/**
|
|
3
|
+
* Представляет извлечение стороны, имеющей ход, из полной строки FEN.
|
|
4
|
+
*/
|
|
5
|
+
function sideToMoveFromFullFen(fullFen) {
|
|
6
|
+
const token = fullFen.trim().split(/\s+/)[1];
|
|
7
|
+
return token === "b" ? Color.BLACK : Color.WHITE;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Представляет узел конца главной линии от указанного узла (цепочка первых детей до листа).
|
|
11
|
+
*/
|
|
12
|
+
function tailAlongFirstVariation(node) {
|
|
13
|
+
let n = node;
|
|
14
|
+
while (n.children.length > 0) {
|
|
15
|
+
n = n.children[0];
|
|
16
|
+
}
|
|
17
|
+
return n;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Представляет проверку: в позиции после последнего хода на линии последним ходил решатель (теперь ходит соперник).
|
|
21
|
+
*/
|
|
22
|
+
function solverJustPlayedLastOnLine(fullFen, solverColor) {
|
|
23
|
+
return sideToMoveFromFullFen(fullFen) !== solverColor;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Представляет проверку линии решения задачи: метки ✓/✗ только на ходах из развилки,
|
|
27
|
+
* запрет ✓/✗ ниже по дереву после ✗ на ходе решателя, разметка развилок со стороны
|
|
28
|
+
* решателя и окончание вариантов на развилке соперника ходом решателя вне поддерева
|
|
29
|
+
* после хода решателя с меткой «неверное решение».
|
|
30
|
+
*/
|
|
31
|
+
export function validatePuzzleSolverForkAnnotations(tree, solverColor) {
|
|
32
|
+
function walk(node, parent, ancestorsContainWrongMarkedSolver) {
|
|
33
|
+
const puzzleForkNags = node.data.nags ?? [];
|
|
34
|
+
const hasPuzzleForkMarker = puzzleForkNags.includes(PUZZLE_BRANCH_CORRECT_NAG_ID) ||
|
|
35
|
+
puzzleForkNags.includes(PUZZLE_BRANCH_WRONG_NAG_ID);
|
|
36
|
+
if (hasPuzzleForkMarker && (!parent || parent.children.length <= 1)) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: "Метки верного или неверного хода задачи (✓ / ✗) можно ставить только на ход из развилки: у родительской позиции должно быть несколько вариантов хода.",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (ancestorsContainWrongMarkedSolver && hasPuzzleForkMarker) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
reason: "Внутри продолжения после хода решателя с меткой ✗ «неверное решение» не ставьте метки ✓ и ✗ на последующих развилках.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (node.children.length > 1) {
|
|
49
|
+
const mover = sideToMoveFromFullFen(node.data.fen);
|
|
50
|
+
if (mover === solverColor && !ancestorsContainWrongMarkedSolver) {
|
|
51
|
+
for (const child of node.children) {
|
|
52
|
+
const nags = child.data.nags ?? [];
|
|
53
|
+
const markedCorrect = nags.includes(PUZZLE_BRANCH_CORRECT_NAG_ID);
|
|
54
|
+
const markedWrong = nags.includes(PUZZLE_BRANCH_WRONG_NAG_ID);
|
|
55
|
+
if (markedCorrect === markedWrong) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: "На каждой развилке со стороны решателя отметьте каждый вариант: ✓ верное решение или ✗ неверное.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (markedCorrect) {
|
|
62
|
+
const leaf = tailAlongFirstVariation(child);
|
|
63
|
+
if (!solverJustPlayedLastOnLine(leaf.data.fen, solverColor)) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
reason: "Ветка с ✓ должна заканчиваться ходом решателя: продолжите главный вариант после верного хода, пока полуход решателя не станет последним на этой линии.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (mover !== solverColor && !ancestorsContainWrongMarkedSolver) {
|
|
73
|
+
for (const child of node.children) {
|
|
74
|
+
const leaf = tailAlongFirstVariation(child);
|
|
75
|
+
if (!solverJustPlayedLastOnLine(leaf.data.fen, solverColor)) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
reason: "На развилке со стороны соперника каждый вариант должен заканчиваться ходом решателя: продолжите линию после ответа соперника, пока последним не походит решатель.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const wrongMarkedAmongAncestorsForChildren = ancestorsContainWrongMarkedSolver ||
|
|
85
|
+
(node.data.nags ?? []).includes(PUZZLE_BRANCH_WRONG_NAG_ID);
|
|
86
|
+
for (const child of node.children) {
|
|
87
|
+
const sub = walk(child, node, wrongMarkedAmongAncestorsForChildren);
|
|
88
|
+
if (!sub.ok)
|
|
89
|
+
return sub;
|
|
90
|
+
}
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
return walk(tree.rootNode.moves, null, false);
|
|
94
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ChessTree, ChessTreeNode } from "@connectorvol/tree";
|
|
2
|
+
import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
|
|
3
|
+
import { Color } from "@connectorvol/shared";
|
|
4
|
+
/**
|
|
5
|
+
* Представляет результат разбора хода решателя относительно вариантов в узле дерева задачи.
|
|
6
|
+
*/
|
|
7
|
+
export type TSolverMoveOutcome = {
|
|
8
|
+
kind: "no_matching_variant";
|
|
9
|
+
} | {
|
|
10
|
+
kind: "wrong_marked_variant";
|
|
11
|
+
wrongBranchRoot: ChessTreeNode;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "correct";
|
|
14
|
+
childIndex: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Представляет извлечение стороны, имеющей ход, из полной строки FEN.
|
|
18
|
+
*/
|
|
19
|
+
export declare function puzzlePreviewSideToMoveFromFen(fullFen: string): Color;
|
|
20
|
+
/**
|
|
21
|
+
* Представляет получение узла дерева по пути индексов детей от корня партии.
|
|
22
|
+
*/
|
|
23
|
+
export declare function puzzlePreviewNodeAtPath(rootMoves: ChessTreeNode, path: number[]): ChessTreeNode;
|
|
24
|
+
/**
|
|
25
|
+
* Представляет поиск индекса ребёнка, чей SAN даёт ту же результирующую позицию, что и сыгранный SAN.
|
|
26
|
+
*/
|
|
27
|
+
export declare function puzzlePreviewFindChildIndexForPlayedSan(cursorFen: string, children: ChessTreeNode[], playedSan: string): number;
|
|
28
|
+
/**
|
|
29
|
+
* Представляет классификацию хода решателя на развилке (или на единственном продолжении).
|
|
30
|
+
*/
|
|
31
|
+
export declare function puzzlePreviewClassifySolverMove(cursorNode: ChessTreeNode, cursorFen: string, playedSan: string, solverColor: Color): TSolverMoveOutcome;
|
|
32
|
+
/**
|
|
33
|
+
* Представляет проверку: линия закончилась ключевым ходом решателя (ход соперника, лист дерева).
|
|
34
|
+
*/
|
|
35
|
+
export declare function puzzlePreviewIsSolvedPosition(previewChess: PgnOps, leafNode: ChessTreeNode, solverColor: Color): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Представляет рекурсивное клонирование поддерева варианта под текущим узлом студенческого дерева (новые id обеспечивает ChessTree.addNodeToCurrent).
|
|
38
|
+
*/
|
|
39
|
+
export declare function puzzlePreviewDupSubtreeUnderStudentCursor(studentTree: ChessTree, sourceNode: ChessTreeNode): void;
|