chess-tactics 0.0.12 → 0.0.13
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 +15 -5
- package/dist/tactics/BaseTactic.js +2 -0
- package/dist/tactics/Fork.js +4 -1
- package/dist/tactics/Pin.js +7 -1
- package/dist/tactics/Skewer.js +6 -0
- package/dist/tactics/Trap.js +3 -0
- package/dist/utils/SequenceInterpreter.js +9 -5
- package/dist/utils/TacticHeuristics.d.ts +1 -0
- package/dist/utils/TacticHeuristics.js +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# chess-tactics
|
|
2
2
|
|
|
3
|
-
chess-tactics is
|
|
3
|
+
chess-tactics is an opinionated tactic detection library that finds a tactic, the pieces involved, and the tactical sequence given a position and its engine evaluation.
|
|
4
4
|
|
|
5
5
|
Supported Tactics:
|
|
6
6
|
|
|
@@ -37,7 +37,6 @@ const tactics = chessTactics.classify(context); // [{type:"fork", ... }]
|
|
|
37
37
|
Creates a new instance of the tactics classifier.
|
|
38
38
|
|
|
39
39
|
- **`tacticKeys`** (optional): An array of which tactics to include. Defaults to all available tactics
|
|
40
|
-
- **Returns**: An instance of `ChessTactics`.
|
|
41
40
|
|
|
42
41
|
---
|
|
43
42
|
|
|
@@ -46,8 +45,8 @@ Creates a new instance of the tactics classifier.
|
|
|
46
45
|
Analyzes a board position and the sequence of moves to determine if a specific tactic has occurred.
|
|
47
46
|
|
|
48
47
|
- **`context`** (TacticContext): An object containing the position & evaluation
|
|
49
|
-
- **`options`** (
|
|
50
|
-
- **Returns**: [`Tactic[]`](#tactic).
|
|
48
|
+
- **`options`** [`TacticOptions`](#tactic-options): An object to modify class behavior
|
|
49
|
+
- **Returns**: [`Tactic[]`](#tactic). An array of tactics found in the position
|
|
51
50
|
|
|
52
51
|
---
|
|
53
52
|
|
|
@@ -61,7 +60,7 @@ Supported tactical patterns:
|
|
|
61
60
|
|
|
62
61
|
### `TacticContext`
|
|
63
62
|
|
|
64
|
-
Context required for the tactic algorithms
|
|
63
|
+
Context required for the tactic algorithms
|
|
65
64
|
|
|
66
65
|
| Type | Supported Tactic Keys | Description |
|
|
67
66
|
| :-------------------------------- | :------------------------------------------------------ | :-------------------------------------------------------------------- |
|
|
@@ -85,6 +84,17 @@ Context required for the tactic algorithms. Provide the type that supports all p
|
|
|
85
84
|
|
|
86
85
|
---
|
|
87
86
|
|
|
87
|
+
### `TacticOptions`
|
|
88
|
+
|
|
89
|
+
Options to modify behavior
|
|
90
|
+
|
|
91
|
+
<a id="tactic-options"></a>
|
|
92
|
+
|
|
93
|
+
| Type | Type | Description |
|
|
94
|
+
| :------------------ | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
95
|
+
| `trimEndSequence` | `boolean` (default:`True`) | Preprocessing step that if true, trims the end of the evaluation sequence while it contains captures or checks. If you are providing a raw engine string, this should be True to avoid miscalculation of sequence material change. (Ex. Set to false if you provide puzzle evaluations that end at the puzzle's completion). |
|
|
96
|
+
| `maxLookaheadMoves` | `number` (default: 5) | Specifies the maximum number of half-moves (ply) that can contain checks or captures before the tactical move is played. (Ex. If set to zero, a trade preceeding a fork will not be found because the first move is not a forking move) |
|
|
97
|
+
|
|
88
98
|
### `Evaluation`
|
|
89
99
|
|
|
90
100
|
<a id="evaluation"></a>
|
|
@@ -19,6 +19,8 @@ class BaseTactic {
|
|
|
19
19
|
this.sequenceInterpreter.setContext(newContext);
|
|
20
20
|
const tactic = this.isTactic(newContext);
|
|
21
21
|
if (tactic) {
|
|
22
|
+
// TODO
|
|
23
|
+
// Why the hardcode? Could we find even some subset of behavior that allows for "two attackers vs one defender" free pieces?
|
|
22
24
|
if (tactic.type === "hanging" && i > 0) {
|
|
23
25
|
return null;
|
|
24
26
|
}
|
package/dist/tactics/Fork.js
CHANGED
|
@@ -10,7 +10,10 @@ class ForkTactics extends _tactics_1.BaseTactic {
|
|
|
10
10
|
const chess = new chess_js_1.Chess(position);
|
|
11
11
|
const currentMove = chess.move(evaluation.sequence[0]);
|
|
12
12
|
const cosmeticForks = this.getCosmeticForks(position, currentMove);
|
|
13
|
-
|
|
13
|
+
let attackedSquares = cosmeticForks.map((m) => m.to);
|
|
14
|
+
attackedSquares = (0, _utils_1.filterOutInitiallyAttackedSquares)(position, currentMove, attackedSquares);
|
|
15
|
+
if (attackedSquares.length < 2)
|
|
16
|
+
return null;
|
|
14
17
|
const tacticalSequence = this.sequenceInterpreter.identifyWinningSequence([currentMove.to], attackedSquares);
|
|
15
18
|
if (tacticalSequence) {
|
|
16
19
|
return {
|
package/dist/tactics/Pin.js
CHANGED
|
@@ -9,8 +9,14 @@ class PinTactics extends _tactics_1.BaseTactic {
|
|
|
9
9
|
const { position, evaluation } = context;
|
|
10
10
|
const chess = new chess_js_1.Chess(position);
|
|
11
11
|
const currentMove = evaluation.sequence[0];
|
|
12
|
-
|
|
12
|
+
let cosmeticPins = this.getCosmeticPins(position, currentMove);
|
|
13
13
|
for (const [nextMoveWithPiece, nextMoveWithoutPiece] of cosmeticPins) {
|
|
14
|
+
if ((0, _utils_1.filterOutInitiallyAttackedSquares)(position, currentMove, [
|
|
15
|
+
nextMoveWithPiece.to,
|
|
16
|
+
nextMoveWithoutPiece.to,
|
|
17
|
+
]).length < 2) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
14
20
|
const tacticalSequence = this.sequenceInterpreter.identifyWinningSequence([currentMove.to], [nextMoveWithPiece.to, nextMoveWithoutPiece.to]);
|
|
15
21
|
if (tacticalSequence) {
|
|
16
22
|
return {
|
package/dist/tactics/Skewer.js
CHANGED
|
@@ -11,6 +11,12 @@ class SkewerTactics extends _tactics_1.BaseTactic {
|
|
|
11
11
|
const currentMove = chess.move(evaluation.sequence[0]);
|
|
12
12
|
const cosmeticSkewers = this.getCosmeticSkewers(context);
|
|
13
13
|
for (const [nextMoveWithPiece, nextMoveWithoutPiece] of cosmeticSkewers) {
|
|
14
|
+
if ((0, _utils_1.filterOutInitiallyAttackedSquares)(position, currentMove, [
|
|
15
|
+
nextMoveWithPiece.to,
|
|
16
|
+
nextMoveWithoutPiece.to,
|
|
17
|
+
]).length < 2) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
14
20
|
const tacticalSequence = this.sequenceInterpreter.identifyWinningSequence([currentMove.to], [nextMoveWithPiece.to, nextMoveWithoutPiece.to]);
|
|
15
21
|
if (tacticalSequence) {
|
|
16
22
|
return {
|
package/dist/tactics/Trap.js
CHANGED
|
@@ -36,7 +36,10 @@ class TrapTactics extends _tactics_1.BaseTactic {
|
|
|
36
36
|
chess.move(currentMove);
|
|
37
37
|
const m = capturingMoves[i];
|
|
38
38
|
const fen = chess.fen();
|
|
39
|
+
// There are cases where previously trapped pieces are classified on the next move.
|
|
40
|
+
// The current move should reveal or directly attack the piece of interest
|
|
39
41
|
// TODO
|
|
42
|
+
// Add testcases where piece is trapped, and moving away from an opponent's one check threat triggers the 'trap' on the unrelated piece
|
|
40
43
|
if (this.pieceIsTrapped(fen, m)) {
|
|
41
44
|
if (m.captured && _utils_1.PIECE_VALUES[m.piece] < _utils_1.PIECE_VALUES[m.captured]) {
|
|
42
45
|
return {
|
|
@@ -26,11 +26,10 @@ class SequenceInterpreter {
|
|
|
26
26
|
}
|
|
27
27
|
return matches;
|
|
28
28
|
}
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// still allow captures on attackedSquares to be found
|
|
29
|
+
// Tactic is returned if the pieces on attackerSquares capture any of the pieces on attackedSquares
|
|
30
|
+
// AND the resulting position after checks and captures won material
|
|
31
|
+
// TODO
|
|
32
|
+
// The isDesparado case handles 6 of 170 tests. It's probably too broad, try only desparados
|
|
34
33
|
identifyWinningSequence(attackerSquares, attackedSquares) {
|
|
35
34
|
if (attackedSquares.length === 0 || attackerSquares.length === 0)
|
|
36
35
|
return null;
|
|
@@ -60,6 +59,11 @@ class SequenceInterpreter {
|
|
|
60
59
|
return null;
|
|
61
60
|
}
|
|
62
61
|
}
|
|
62
|
+
// Track the attackers movements through the sequence
|
|
63
|
+
const attackerIdx = attackerSquares.indexOf(move.from);
|
|
64
|
+
if (attackerIdx !== -1) {
|
|
65
|
+
attackerSquares[attackerIdx] = move.to;
|
|
66
|
+
}
|
|
63
67
|
}
|
|
64
68
|
return null;
|
|
65
69
|
}
|
|
@@ -7,3 +7,4 @@ export declare function getEscapeSquares(fen: Fen, square: Square): any[];
|
|
|
7
7
|
export declare function getBlockingMoves(fen: Fen, attackingSquare: Square, threatenedSquare: Square): any[];
|
|
8
8
|
export declare function getMovesToSquare(fen: Fen, square: Square): Move[];
|
|
9
9
|
export declare function getThreateningMoves(position: Fen, currentMove: Move): Move[];
|
|
10
|
+
export declare function filterOutInitiallyAttackedSquares(position: Fen, currentMove: Move, attackedSquares: Square[]): Square[];
|
|
@@ -7,6 +7,7 @@ exports.getEscapeSquares = getEscapeSquares;
|
|
|
7
7
|
exports.getBlockingMoves = getBlockingMoves;
|
|
8
8
|
exports.getMovesToSquare = getMovesToSquare;
|
|
9
9
|
exports.getThreateningMoves = getThreateningMoves;
|
|
10
|
+
exports.filterOutInitiallyAttackedSquares = filterOutInitiallyAttackedSquares;
|
|
10
11
|
const chess_js_1 = require("chess.js");
|
|
11
12
|
const _utils_1 = require("./index");
|
|
12
13
|
function attackingSquareIsGood(fen, square, startingMove = null) {
|
|
@@ -163,7 +164,8 @@ function getThreateningMoves(position, currentMove) {
|
|
|
163
164
|
const threateningMoves = [];
|
|
164
165
|
for (const m of possibleMoves) {
|
|
165
166
|
for (const n of possibleMoves) {
|
|
166
|
-
|
|
167
|
+
// we remove the pieces to avoid the scenario where one of the forked pieces 'defends' the other through the attacking piece and nullifies the tactic
|
|
168
|
+
if (n.captured !== "k" && n.captured !== "p" && n.to !== m.to)
|
|
167
169
|
chess.remove(n.to);
|
|
168
170
|
}
|
|
169
171
|
if (m.captured && attackingSquareIsGood(chess.fen(), m.to, m) && m.captured !== m.piece) {
|
|
@@ -175,3 +177,10 @@ function getThreateningMoves(position, currentMove) {
|
|
|
175
177
|
}
|
|
176
178
|
return threateningMoves;
|
|
177
179
|
}
|
|
180
|
+
function filterOutInitiallyAttackedSquares(position, currentMove, attackedSquares) {
|
|
181
|
+
const chess = new chess_js_1.Chess(position);
|
|
182
|
+
const attackingPieceInitialMoves = chess
|
|
183
|
+
.moves({ square: currentMove.from, verbose: true })
|
|
184
|
+
.map((m) => m.to);
|
|
185
|
+
return attackedSquares.filter((s) => !attackingPieceInitialMoves.includes(s));
|
|
186
|
+
}
|