@connectorvol/chessops 0.15.1
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/LICENSE.txt +674 -0
- package/README.md +61 -0
- package/dist/cjs/attacks.js +152 -0
- package/dist/cjs/attacks.js.map +1 -0
- package/dist/cjs/board.js +143 -0
- package/dist/cjs/board.js.map +1 -0
- package/dist/cjs/chess.js +638 -0
- package/dist/cjs/chess.js.map +1 -0
- package/dist/cjs/compat.js +89 -0
- package/dist/cjs/compat.js.map +1 -0
- package/dist/cjs/debug.js +103 -0
- package/dist/cjs/debug.js.map +1 -0
- package/dist/cjs/fen.js +325 -0
- package/dist/cjs/fen.js.map +1 -0
- package/dist/cjs/index.js +94 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/pgn.js +796 -0
- package/dist/cjs/pgn.js.map +1 -0
- package/dist/cjs/san.js +174 -0
- package/dist/cjs/san.js.map +1 -0
- package/dist/cjs/setup.js +167 -0
- package/dist/cjs/setup.js.map +1 -0
- package/dist/cjs/squareSet.js +206 -0
- package/dist/cjs/squareSet.js.map +1 -0
- package/dist/cjs/transform.js +57 -0
- package/dist/cjs/transform.js.map +1 -0
- package/dist/cjs/types.js +24 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/util.js +104 -0
- package/dist/cjs/util.js.map +1 -0
- package/dist/cjs/variant.js +833 -0
- package/dist/cjs/variant.js.map +1 -0
- package/dist/esm/attacks.js +140 -0
- package/dist/esm/attacks.js.map +1 -0
- package/dist/esm/board.js +138 -0
- package/dist/esm/board.js.map +1 -0
- package/dist/esm/chess.js +624 -0
- package/dist/esm/chess.js.map +1 -0
- package/dist/esm/compat.js +81 -0
- package/dist/esm/compat.js.map +1 -0
- package/dist/esm/debug.js +94 -0
- package/dist/esm/debug.js.map +1 -0
- package/dist/esm/fen.js +308 -0
- package/dist/esm/fen.js.map +1 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/pgn.js +769 -0
- package/dist/esm/pgn.js.map +1 -0
- package/dist/esm/san.js +167 -0
- package/dist/esm/san.js.map +1 -0
- package/dist/esm/setup.js +157 -0
- package/dist/esm/setup.js.map +1 -0
- package/dist/esm/squareSet.js +202 -0
- package/dist/esm/squareSet.js.map +1 -0
- package/dist/esm/transform.js +48 -0
- package/dist/esm/transform.js.map +1 -0
- package/dist/esm/types.js +19 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/util.js +87 -0
- package/dist/esm/util.js.map +1 -0
- package/dist/esm/variant.js +812 -0
- package/dist/esm/variant.js.map +1 -0
- package/dist/types/attacks.d.ts +58 -0
- package/dist/types/board.d.ts +62 -0
- package/dist/types/chess.d.ts +82 -0
- package/dist/types/compat.d.ts +26 -0
- package/dist/types/debug.d.ts +10 -0
- package/dist/types/fen.d.ts +40 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/pgn.d.ts +203 -0
- package/dist/types/san.d.ts +6 -0
- package/dist/types/setup.d.ts +65 -0
- package/dist/types/squareSet.d.ts +50 -0
- package/dist/types/transform.d.ts +9 -0
- package/dist/types/types.d.ts +58 -0
- package/dist/types/util.d.ts +21 -0
- package/dist/types/variant.d.ts +92 -0
- package/package.json +86 -0
- package/src/attacks.ts +160 -0
- package/src/board.ts +168 -0
- package/src/chess.ts +687 -0
- package/src/compat.ts +120 -0
- package/src/debug.ts +100 -0
- package/src/fen.ts +328 -0
- package/src/index.ts +85 -0
- package/src/pgn.ts +876 -0
- package/src/san.ts +190 -0
- package/src/setup.ts +203 -0
- package/src/squareSet.ts +243 -0
- package/src/transform.ts +49 -0
- package/src/types.ts +93 -0
- package/src/util.ts +116 -0
- package/src/variant.ts +939 -0
package/src/variant.ts
ADDED
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
import { Result } from "@badrap/result";
|
|
2
|
+
import { between, kingAttacks, pawnAttacks } from "./attacks.js";
|
|
3
|
+
import { Board } from "./board.js";
|
|
4
|
+
import {
|
|
5
|
+
Castles,
|
|
6
|
+
castlingSide,
|
|
7
|
+
Chess,
|
|
8
|
+
Context,
|
|
9
|
+
equalsIgnoreMoves,
|
|
10
|
+
IllegalSetup,
|
|
11
|
+
isImpossibleCheck,
|
|
12
|
+
isStandardMaterialSide,
|
|
13
|
+
normalizeMove,
|
|
14
|
+
Position,
|
|
15
|
+
PositionError,
|
|
16
|
+
pseudoDests,
|
|
17
|
+
} from "./chess.js";
|
|
18
|
+
import { Material, MaterialSide, RemainingChecks, Setup } from "./setup.js";
|
|
19
|
+
import { SquareSet } from "./squareSet.js";
|
|
20
|
+
import { Color, COLORS, Outcome, Piece, Rules, Square } from "./types.js";
|
|
21
|
+
import { defined, opposite } from "./util.js";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
Castles,
|
|
25
|
+
castlingSide,
|
|
26
|
+
Chess,
|
|
27
|
+
Context,
|
|
28
|
+
equalsIgnoreMoves,
|
|
29
|
+
IllegalSetup,
|
|
30
|
+
isImpossibleCheck,
|
|
31
|
+
normalizeMove,
|
|
32
|
+
Position,
|
|
33
|
+
PositionError,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export class Crazyhouse extends Position {
|
|
37
|
+
private constructor() {
|
|
38
|
+
super("crazyhouse");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
reset() {
|
|
42
|
+
super.reset();
|
|
43
|
+
this.pockets = Material.empty();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected setupUnchecked(setup: Setup) {
|
|
47
|
+
super.setupUnchecked(setup);
|
|
48
|
+
this.board.promoted = setup.board.promoted
|
|
49
|
+
.intersect(setup.board.occupied)
|
|
50
|
+
.diff(setup.board.king)
|
|
51
|
+
.diff(setup.board.pawn);
|
|
52
|
+
this.pockets = setup.pockets ? setup.pockets.clone() : Material.empty();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static default(): Crazyhouse {
|
|
56
|
+
const pos = new this();
|
|
57
|
+
pos.reset();
|
|
58
|
+
return pos;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static fromSetup(setup: Setup): Result<Crazyhouse, PositionError> {
|
|
62
|
+
const pos = new this();
|
|
63
|
+
pos.setupUnchecked(setup);
|
|
64
|
+
return pos.validate().map((_) => pos);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
clone(): Crazyhouse {
|
|
68
|
+
return super.clone() as Crazyhouse;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected validate(): Result<undefined, PositionError> {
|
|
72
|
+
return super.validate().chain((_) => {
|
|
73
|
+
if (this.pockets?.count("king")) {
|
|
74
|
+
return Result.err(new PositionError(IllegalSetup.Kings));
|
|
75
|
+
}
|
|
76
|
+
if ((this.pockets?.size() || 0) + this.board.occupied.size() > 64) {
|
|
77
|
+
return Result.err(new PositionError(IllegalSetup.Variant));
|
|
78
|
+
}
|
|
79
|
+
return Result.ok(undefined);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
hasInsufficientMaterial(color: Color): boolean {
|
|
84
|
+
// No material can leave the game, but we can easily check this for
|
|
85
|
+
// custom positions.
|
|
86
|
+
if (!this.pockets) return super.hasInsufficientMaterial(color);
|
|
87
|
+
return (
|
|
88
|
+
this.board.occupied.size() + this.pockets.size() <= 3 &&
|
|
89
|
+
this.board.pawn.isEmpty() &&
|
|
90
|
+
this.board.promoted.isEmpty() &&
|
|
91
|
+
this.board.rooksAndQueens().isEmpty() &&
|
|
92
|
+
this.pockets.count("pawn") <= 0 &&
|
|
93
|
+
this.pockets.count("rook") <= 0 &&
|
|
94
|
+
this.pockets.count("queen") <= 0
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
dropDests(ctx?: Context): SquareSet {
|
|
99
|
+
const mask = this.board.occupied
|
|
100
|
+
.complement()
|
|
101
|
+
.intersect(
|
|
102
|
+
this.pockets?.[this.turn].hasNonPawns()
|
|
103
|
+
? SquareSet.full()
|
|
104
|
+
: this.pockets?.[this.turn].hasPawns()
|
|
105
|
+
? SquareSet.backranks().complement()
|
|
106
|
+
: SquareSet.empty(),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
ctx = ctx || this.ctx();
|
|
110
|
+
if (defined(ctx.king) && ctx.checkers.nonEmpty()) {
|
|
111
|
+
const checker = ctx.checkers.singleSquare();
|
|
112
|
+
if (!defined(checker)) return SquareSet.empty();
|
|
113
|
+
return mask.intersect(between(checker, ctx.king));
|
|
114
|
+
} else return mask;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class Atomic extends Position {
|
|
119
|
+
private constructor() {
|
|
120
|
+
super("atomic");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static default(): Atomic {
|
|
124
|
+
const pos = new this();
|
|
125
|
+
pos.reset();
|
|
126
|
+
return pos;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static fromSetup(setup: Setup): Result<Atomic, PositionError> {
|
|
130
|
+
const pos = new this();
|
|
131
|
+
pos.setupUnchecked(setup);
|
|
132
|
+
return pos.validate().map((_) => pos);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
clone(): Atomic {
|
|
136
|
+
return super.clone() as Atomic;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
protected validate(): Result<undefined, PositionError> {
|
|
140
|
+
// Like chess, but allow our king to be missing.
|
|
141
|
+
if (this.board.occupied.isEmpty()) return Result.err(new PositionError(IllegalSetup.Empty));
|
|
142
|
+
if (this.board.king.size() > 2) return Result.err(new PositionError(IllegalSetup.Kings));
|
|
143
|
+
const otherKing = this.board.kingOf(opposite(this.turn));
|
|
144
|
+
if (!defined(otherKing)) return Result.err(new PositionError(IllegalSetup.Kings));
|
|
145
|
+
if (this.kingAttackers(otherKing, this.turn, this.board.occupied).nonEmpty()) {
|
|
146
|
+
return Result.err(new PositionError(IllegalSetup.OppositeCheck));
|
|
147
|
+
}
|
|
148
|
+
if (SquareSet.backranks().intersects(this.board.pawn)) {
|
|
149
|
+
return Result.err(new PositionError(IllegalSetup.PawnsOnBackrank));
|
|
150
|
+
}
|
|
151
|
+
return Result.ok(undefined);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
kingAttackers(square: Square, attacker: Color, occupied: SquareSet): SquareSet {
|
|
155
|
+
const attackerKings = this.board.pieces(attacker, "king");
|
|
156
|
+
if (attackerKings.isEmpty() || kingAttacks(square).intersects(attackerKings)) {
|
|
157
|
+
return SquareSet.empty();
|
|
158
|
+
}
|
|
159
|
+
return super.kingAttackers(square, attacker, occupied);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
protected playCaptureAt(square: Square, captured: Piece): void {
|
|
163
|
+
super.playCaptureAt(square, captured);
|
|
164
|
+
this.board.take(square);
|
|
165
|
+
for (const explode of kingAttacks(square)
|
|
166
|
+
.intersect(this.board.occupied)
|
|
167
|
+
.diff(this.board.pawn)) {
|
|
168
|
+
const piece = this.board.take(explode);
|
|
169
|
+
if (piece?.role === "rook") this.castles.discardRook(explode);
|
|
170
|
+
if (piece?.role === "king") this.castles.discardColor(piece.color);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
hasInsufficientMaterial(color: Color): boolean {
|
|
175
|
+
// Remaining material does not matter if the enemy king is already
|
|
176
|
+
// exploded.
|
|
177
|
+
if (this.board.pieces(opposite(color), "king").isEmpty()) return false;
|
|
178
|
+
|
|
179
|
+
// Bare king cannot mate.
|
|
180
|
+
if (this.board[color].diff(this.board.king).isEmpty()) return true;
|
|
181
|
+
|
|
182
|
+
// As long as the enemy king is not alone, there is always a chance their
|
|
183
|
+
// own pieces explode next to it.
|
|
184
|
+
if (this.board[opposite(color)].diff(this.board.king).nonEmpty()) {
|
|
185
|
+
// Unless there are only bishops that cannot explode each other.
|
|
186
|
+
if (this.board.occupied.equals(this.board.bishop.union(this.board.king))) {
|
|
187
|
+
if (!this.board.bishop.intersect(this.board.white).intersects(SquareSet.darkSquares())) {
|
|
188
|
+
return !this.board.bishop
|
|
189
|
+
.intersect(this.board.black)
|
|
190
|
+
.intersects(SquareSet.lightSquares());
|
|
191
|
+
}
|
|
192
|
+
if (!this.board.bishop.intersect(this.board.white).intersects(SquareSet.lightSquares())) {
|
|
193
|
+
return !this.board.bishop.intersect(this.board.black).intersects(SquareSet.darkSquares());
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Queen or pawn (future queen) can give mate against bare king.
|
|
200
|
+
if (this.board.queen.nonEmpty() || this.board.pawn.nonEmpty()) return false;
|
|
201
|
+
|
|
202
|
+
// Single knight, bishop or rook cannot mate against bare king.
|
|
203
|
+
if (this.board.knight.union(this.board.bishop).union(this.board.rook).size() === 1) return true;
|
|
204
|
+
|
|
205
|
+
// If only knights, more than two are required to mate bare king.
|
|
206
|
+
if (this.board.occupied.equals(this.board.knight.union(this.board.king))) {
|
|
207
|
+
return this.board.knight.size() <= 2;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
dests(square: Square, ctx?: Context): SquareSet {
|
|
214
|
+
ctx = ctx || this.ctx();
|
|
215
|
+
let dests = SquareSet.empty();
|
|
216
|
+
for (const to of pseudoDests(this, square, ctx)) {
|
|
217
|
+
const after = this.clone();
|
|
218
|
+
after.play({ from: square, to });
|
|
219
|
+
const ourKing = after.board.kingOf(this.turn);
|
|
220
|
+
if (
|
|
221
|
+
defined(ourKing) &&
|
|
222
|
+
(!defined(after.board.kingOf(after.turn)) ||
|
|
223
|
+
after.kingAttackers(ourKing, after.turn, after.board.occupied).isEmpty())
|
|
224
|
+
) {
|
|
225
|
+
dests = dests.with(to);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return dests;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
isVariantEnd(): boolean {
|
|
232
|
+
return !!this.variantOutcome();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
variantOutcome(_ctx?: Context): Outcome | undefined {
|
|
236
|
+
for (const color of COLORS) {
|
|
237
|
+
if (this.board.pieces(color, "king").isEmpty()) return { winner: opposite(color) };
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export class Antichess extends Position {
|
|
244
|
+
private constructor() {
|
|
245
|
+
super("antichess");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
reset() {
|
|
249
|
+
super.reset();
|
|
250
|
+
this.castles = Castles.empty();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
protected setupUnchecked(setup: Setup) {
|
|
254
|
+
super.setupUnchecked(setup);
|
|
255
|
+
this.castles = Castles.empty();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
static default(): Antichess {
|
|
259
|
+
const pos = new this();
|
|
260
|
+
pos.reset();
|
|
261
|
+
return pos;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
static fromSetup(setup: Setup): Result<Antichess, PositionError> {
|
|
265
|
+
const pos = new this();
|
|
266
|
+
pos.setupUnchecked(setup);
|
|
267
|
+
return pos.validate().map((_) => pos);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
clone(): Antichess {
|
|
271
|
+
return super.clone() as Antichess;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
protected validate(): Result<undefined, PositionError> {
|
|
275
|
+
if (this.board.occupied.isEmpty()) return Result.err(new PositionError(IllegalSetup.Empty));
|
|
276
|
+
if (SquareSet.backranks().intersects(this.board.pawn)) {
|
|
277
|
+
return Result.err(new PositionError(IllegalSetup.PawnsOnBackrank));
|
|
278
|
+
}
|
|
279
|
+
return Result.ok(undefined);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
kingAttackers(_square: Square, _attacker: Color, _occupied: SquareSet): SquareSet {
|
|
283
|
+
return SquareSet.empty();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
ctx(): Context {
|
|
287
|
+
const ctx = super.ctx();
|
|
288
|
+
if (
|
|
289
|
+
defined(this.epSquare) &&
|
|
290
|
+
pawnAttacks(opposite(this.turn), this.epSquare).intersects(
|
|
291
|
+
this.board.pieces(this.turn, "pawn"),
|
|
292
|
+
)
|
|
293
|
+
) {
|
|
294
|
+
ctx.mustCapture = true;
|
|
295
|
+
return ctx;
|
|
296
|
+
}
|
|
297
|
+
const enemy = this.board[opposite(this.turn)];
|
|
298
|
+
for (const from of this.board[this.turn]) {
|
|
299
|
+
if (pseudoDests(this, from, ctx).intersects(enemy)) {
|
|
300
|
+
ctx.mustCapture = true;
|
|
301
|
+
return ctx;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return ctx;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
dests(square: Square, ctx?: Context): SquareSet {
|
|
308
|
+
ctx = ctx || this.ctx();
|
|
309
|
+
const dests = pseudoDests(this, square, ctx);
|
|
310
|
+
const enemy = this.board[opposite(this.turn)];
|
|
311
|
+
return dests.intersect(
|
|
312
|
+
ctx.mustCapture
|
|
313
|
+
? defined(this.epSquare) && this.board.getRole(square) === "pawn"
|
|
314
|
+
? enemy.with(this.epSquare)
|
|
315
|
+
: enemy
|
|
316
|
+
: SquareSet.full(),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
hasInsufficientMaterial(color: Color): boolean {
|
|
321
|
+
if (this.board[color].isEmpty()) return false;
|
|
322
|
+
if (this.board[opposite(color)].isEmpty()) return true;
|
|
323
|
+
if (this.board.occupied.equals(this.board.bishop)) {
|
|
324
|
+
const weSomeOnLight = this.board[color].intersects(SquareSet.lightSquares());
|
|
325
|
+
const weSomeOnDark = this.board[color].intersects(SquareSet.darkSquares());
|
|
326
|
+
const theyAllOnDark = this.board[opposite(color)].isDisjoint(SquareSet.lightSquares());
|
|
327
|
+
const theyAllOnLight = this.board[opposite(color)].isDisjoint(SquareSet.darkSquares());
|
|
328
|
+
return (weSomeOnLight && theyAllOnDark) || (weSomeOnDark && theyAllOnLight);
|
|
329
|
+
}
|
|
330
|
+
if (this.board.occupied.equals(this.board.knight) && this.board.occupied.size() === 2) {
|
|
331
|
+
return (
|
|
332
|
+
(this.board.white.intersects(SquareSet.lightSquares()) !==
|
|
333
|
+
this.board.black.intersects(SquareSet.darkSquares())) !==
|
|
334
|
+
(this.turn === color)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
isVariantEnd(): boolean {
|
|
341
|
+
return this.board[this.turn].isEmpty();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
variantOutcome(ctx?: Context): Outcome | undefined {
|
|
345
|
+
ctx = ctx || this.ctx();
|
|
346
|
+
if (ctx.variantEnd || this.isStalemate(ctx)) {
|
|
347
|
+
return { winner: this.turn };
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export class KingOfTheHill extends Position {
|
|
354
|
+
private constructor() {
|
|
355
|
+
super("kingofthehill");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
static default(): KingOfTheHill {
|
|
359
|
+
const pos = new this();
|
|
360
|
+
pos.reset();
|
|
361
|
+
return pos;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
static fromSetup(setup: Setup): Result<KingOfTheHill, PositionError> {
|
|
365
|
+
const pos = new this();
|
|
366
|
+
pos.setupUnchecked(setup);
|
|
367
|
+
return pos.validate().map((_) => pos);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
clone(): KingOfTheHill {
|
|
371
|
+
return super.clone() as KingOfTheHill;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
hasInsufficientMaterial(_color: Color): boolean {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
isVariantEnd(): boolean {
|
|
379
|
+
return this.board.king.intersects(SquareSet.center());
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
variantOutcome(_ctx?: Context): Outcome | undefined {
|
|
383
|
+
for (const color of COLORS) {
|
|
384
|
+
if (this.board.pieces(color, "king").intersects(SquareSet.center())) return { winner: color };
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export class ThreeCheck extends Position {
|
|
391
|
+
private constructor() {
|
|
392
|
+
super("3check");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
reset() {
|
|
396
|
+
super.reset();
|
|
397
|
+
this.remainingChecks = RemainingChecks.default();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
protected setupUnchecked(setup: Setup) {
|
|
401
|
+
super.setupUnchecked(setup);
|
|
402
|
+
this.remainingChecks = setup.remainingChecks?.clone() || RemainingChecks.default();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
static default(): ThreeCheck {
|
|
406
|
+
const pos = new this();
|
|
407
|
+
pos.reset();
|
|
408
|
+
return pos;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
static fromSetup(setup: Setup): Result<ThreeCheck, PositionError> {
|
|
412
|
+
const pos = new this();
|
|
413
|
+
pos.setupUnchecked(setup);
|
|
414
|
+
return pos.validate().map((_) => pos);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
clone(): ThreeCheck {
|
|
418
|
+
return super.clone() as ThreeCheck;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
hasInsufficientMaterial(color: Color): boolean {
|
|
422
|
+
return this.board.pieces(color, "king").equals(this.board[color]);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
isVariantEnd(): boolean {
|
|
426
|
+
return (
|
|
427
|
+
!!this.remainingChecks && (this.remainingChecks.white <= 0 || this.remainingChecks.black <= 0)
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
variantOutcome(_ctx?: Context): Outcome | undefined {
|
|
432
|
+
if (this.remainingChecks) {
|
|
433
|
+
for (const color of COLORS) {
|
|
434
|
+
if (this.remainingChecks[color] <= 0) return { winner: color };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const racingKingsBoard = (): Board => {
|
|
442
|
+
const board = Board.empty();
|
|
443
|
+
board.occupied = new SquareSet(0xffff, 0);
|
|
444
|
+
board.promoted = SquareSet.empty();
|
|
445
|
+
board.white = new SquareSet(0xf0f0, 0);
|
|
446
|
+
board.black = new SquareSet(0x0f0f, 0);
|
|
447
|
+
board.pawn = SquareSet.empty();
|
|
448
|
+
board.knight = new SquareSet(0x1818, 0);
|
|
449
|
+
board.bishop = new SquareSet(0x2424, 0);
|
|
450
|
+
board.rook = new SquareSet(0x4242, 0);
|
|
451
|
+
board.queen = new SquareSet(0x0081, 0);
|
|
452
|
+
board.king = new SquareSet(0x8100, 0);
|
|
453
|
+
return board;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
export class RacingKings extends Position {
|
|
457
|
+
private constructor() {
|
|
458
|
+
super("racingkings");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
reset() {
|
|
462
|
+
this.board = racingKingsBoard();
|
|
463
|
+
this.pockets = undefined;
|
|
464
|
+
this.turn = "white";
|
|
465
|
+
this.castles = Castles.empty();
|
|
466
|
+
this.epSquare = undefined;
|
|
467
|
+
this.remainingChecks = undefined;
|
|
468
|
+
this.halfmoves = 0;
|
|
469
|
+
this.fullmoves = 1;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
setupUnchecked(setup: Setup) {
|
|
473
|
+
super.setupUnchecked(setup);
|
|
474
|
+
this.castles = Castles.empty();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
static default(): RacingKings {
|
|
478
|
+
const pos = new this();
|
|
479
|
+
pos.reset();
|
|
480
|
+
return pos;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
static fromSetup(setup: Setup): Result<RacingKings, PositionError> {
|
|
484
|
+
const pos = new this();
|
|
485
|
+
pos.setupUnchecked(setup);
|
|
486
|
+
return pos.validate().map((_) => pos);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
clone(): RacingKings {
|
|
490
|
+
return super.clone() as RacingKings;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
protected validate(): Result<undefined, PositionError> {
|
|
494
|
+
if (this.isCheck() || this.board.pawn.nonEmpty())
|
|
495
|
+
return Result.err(new PositionError(IllegalSetup.Variant));
|
|
496
|
+
return super.validate();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
dests(square: Square, ctx?: Context): SquareSet {
|
|
500
|
+
ctx = ctx || this.ctx();
|
|
501
|
+
|
|
502
|
+
// Kings cannot give check.
|
|
503
|
+
if (square === ctx.king) return super.dests(square, ctx);
|
|
504
|
+
|
|
505
|
+
// Do not allow giving check.
|
|
506
|
+
let dests = SquareSet.empty();
|
|
507
|
+
for (const to of super.dests(square, ctx)) {
|
|
508
|
+
// Valid, because there are no promotions (or even pawns).
|
|
509
|
+
const move = { from: square, to };
|
|
510
|
+
const after = this.clone();
|
|
511
|
+
after.play(move);
|
|
512
|
+
if (!after.isCheck()) dests = dests.with(to);
|
|
513
|
+
}
|
|
514
|
+
return dests;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
hasInsufficientMaterial(_color: Color): boolean {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
isVariantEnd(): boolean {
|
|
522
|
+
const goal = SquareSet.fromRank(7);
|
|
523
|
+
const inGoal = this.board.king.intersect(goal);
|
|
524
|
+
if (inGoal.isEmpty()) return false;
|
|
525
|
+
if (this.turn === "white" || inGoal.intersects(this.board.black)) return true;
|
|
526
|
+
|
|
527
|
+
// White has reached the backrank. Check if black can catch up.
|
|
528
|
+
const blackKing = this.board.kingOf("black");
|
|
529
|
+
if (defined(blackKing)) {
|
|
530
|
+
const occ = this.board.occupied.without(blackKing);
|
|
531
|
+
for (const target of kingAttacks(blackKing).intersect(goal).diff(this.board.black)) {
|
|
532
|
+
if (this.kingAttackers(target, "white", occ).isEmpty()) return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
variantOutcome(ctx?: Context): Outcome | undefined {
|
|
539
|
+
if (ctx ? !ctx.variantEnd : !this.isVariantEnd()) return;
|
|
540
|
+
const goal = SquareSet.fromRank(7);
|
|
541
|
+
const blackInGoal = this.board.pieces("black", "king").intersects(goal);
|
|
542
|
+
const whiteInGoal = this.board.pieces("white", "king").intersects(goal);
|
|
543
|
+
if (blackInGoal && !whiteInGoal) return { winner: "black" };
|
|
544
|
+
if (whiteInGoal && !blackInGoal) return { winner: "white" };
|
|
545
|
+
return { winner: undefined };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const hordeBoard = (): Board => {
|
|
550
|
+
const board = Board.empty();
|
|
551
|
+
board.occupied = new SquareSet(0xffff_ffff, 0xffff_0066);
|
|
552
|
+
board.promoted = SquareSet.empty();
|
|
553
|
+
board.white = new SquareSet(0xffff_ffff, 0x0000_0066);
|
|
554
|
+
board.black = new SquareSet(0, 0xffff_0000);
|
|
555
|
+
board.pawn = new SquareSet(0xffff_ffff, 0x00ff_0066);
|
|
556
|
+
board.knight = new SquareSet(0, 0x4200_0000);
|
|
557
|
+
board.bishop = new SquareSet(0, 0x2400_0000);
|
|
558
|
+
board.rook = new SquareSet(0, 0x8100_0000);
|
|
559
|
+
board.queen = new SquareSet(0, 0x0800_0000);
|
|
560
|
+
board.king = new SquareSet(0, 0x1000_0000);
|
|
561
|
+
return board;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
export class Horde extends Position {
|
|
565
|
+
private constructor() {
|
|
566
|
+
super("horde");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
reset() {
|
|
570
|
+
this.board = hordeBoard();
|
|
571
|
+
this.pockets = undefined;
|
|
572
|
+
this.turn = "white";
|
|
573
|
+
this.castles = Castles.default();
|
|
574
|
+
this.castles.discardColor("white");
|
|
575
|
+
this.epSquare = undefined;
|
|
576
|
+
this.remainingChecks = undefined;
|
|
577
|
+
this.halfmoves = 0;
|
|
578
|
+
this.fullmoves = 1;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
static default(): Horde {
|
|
582
|
+
const pos = new this();
|
|
583
|
+
pos.reset();
|
|
584
|
+
return pos;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
static fromSetup(setup: Setup): Result<Horde, PositionError> {
|
|
588
|
+
const pos = new this();
|
|
589
|
+
pos.setupUnchecked(setup);
|
|
590
|
+
return pos.validate().map((_) => pos);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
clone(): Horde {
|
|
594
|
+
return super.clone() as Horde;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
protected validate(): Result<undefined, PositionError> {
|
|
598
|
+
if (this.board.occupied.isEmpty()) return Result.err(new PositionError(IllegalSetup.Empty));
|
|
599
|
+
if (this.board.king.size() !== 1) return Result.err(new PositionError(IllegalSetup.Kings));
|
|
600
|
+
|
|
601
|
+
const otherKing = this.board.kingOf(opposite(this.turn));
|
|
602
|
+
if (
|
|
603
|
+
defined(otherKing) &&
|
|
604
|
+
this.kingAttackers(otherKing, this.turn, this.board.occupied).nonEmpty()
|
|
605
|
+
) {
|
|
606
|
+
return Result.err(new PositionError(IllegalSetup.OppositeCheck));
|
|
607
|
+
}
|
|
608
|
+
for (const color of COLORS) {
|
|
609
|
+
const backranks = this.board.pieces(color, "king").isEmpty()
|
|
610
|
+
? SquareSet.backrank(opposite(color))
|
|
611
|
+
: SquareSet.backranks();
|
|
612
|
+
if (this.board.pieces(color, "pawn").intersects(backranks)) {
|
|
613
|
+
return Result.err(new PositionError(IllegalSetup.PawnsOnBackrank));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return Result.ok(undefined);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
hasInsufficientMaterial(color: Color): boolean {
|
|
620
|
+
// The side with the king can always win by capturing the horde.
|
|
621
|
+
if (this.board.pieces(color, "king").nonEmpty()) return false;
|
|
622
|
+
|
|
623
|
+
type SquareColor = "light" | "dark";
|
|
624
|
+
const oppositeSquareColor = (squareColor: SquareColor): SquareColor =>
|
|
625
|
+
squareColor === "light" ? "dark" : "light";
|
|
626
|
+
const coloredSquares = (squareColor: SquareColor): SquareSet =>
|
|
627
|
+
squareColor === "light" ? SquareSet.lightSquares() : SquareSet.darkSquares();
|
|
628
|
+
|
|
629
|
+
const hasBishopPair = (side: Color) => {
|
|
630
|
+
const bishops = this.board.pieces(side, "bishop");
|
|
631
|
+
return (
|
|
632
|
+
bishops.intersects(SquareSet.darkSquares()) && bishops.intersects(SquareSet.lightSquares())
|
|
633
|
+
);
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// By this point: color is the horde.
|
|
637
|
+
// Based on
|
|
638
|
+
// https://github.com/stevepapazis/horde-insufficient-material-tests.
|
|
639
|
+
const horde = MaterialSide.fromBoard(this.board, color);
|
|
640
|
+
const hordeBishops = (squareColor: SquareColor) =>
|
|
641
|
+
coloredSquares(squareColor).intersect(this.board.pieces(color, "bishop")).size();
|
|
642
|
+
const hordeBishopColor: SquareColor = hordeBishops("light") >= 1 ? "light" : "dark";
|
|
643
|
+
const hordeNum =
|
|
644
|
+
horde.pawn +
|
|
645
|
+
horde.knight +
|
|
646
|
+
horde.rook +
|
|
647
|
+
horde.queen +
|
|
648
|
+
Math.min(hordeBishops("dark"), 2) +
|
|
649
|
+
Math.min(hordeBishops("light"), 2);
|
|
650
|
+
|
|
651
|
+
const pieces = MaterialSide.fromBoard(this.board, opposite(color));
|
|
652
|
+
const piecesBishops = (squareColor: SquareColor) =>
|
|
653
|
+
coloredSquares(squareColor)
|
|
654
|
+
.intersect(this.board.pieces(opposite(color), "bishop"))
|
|
655
|
+
.size();
|
|
656
|
+
const piecesNum = pieces.size();
|
|
657
|
+
const piecesOfRoleNot = (piece: number) => piecesNum - piece;
|
|
658
|
+
|
|
659
|
+
if (hordeNum === 0) return true;
|
|
660
|
+
if (hordeNum >= 4) {
|
|
661
|
+
// Four or more pieces can always deliver mate.
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
if ((horde.pawn >= 1 || horde.queen >= 1) && hordeNum >= 2) {
|
|
665
|
+
// Pawns/queens are never insufficient material when paired with any other
|
|
666
|
+
// piece (a pawn promotes to a queen and delivers mate).
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
if (horde.rook >= 1 && hordeNum >= 2) {
|
|
670
|
+
// A rook is insufficient material only when it is paired with a bishop
|
|
671
|
+
// against a lone king. The horde can mate in any other case.
|
|
672
|
+
// A rook on A1 and a bishop on C3 mate a king on B1 when there is a
|
|
673
|
+
// friendly pawn/opposite-color-bishop/rook/queen on C2.
|
|
674
|
+
// A rook on B8 and a bishop C3 mate a king on A1 when there is a friendly
|
|
675
|
+
// knight on A2.
|
|
676
|
+
if (
|
|
677
|
+
!(
|
|
678
|
+
hordeNum === 2 &&
|
|
679
|
+
horde.rook === 1 &&
|
|
680
|
+
horde.bishop === 1 &&
|
|
681
|
+
piecesOfRoleNot(piecesBishops(hordeBishopColor)) === 1
|
|
682
|
+
)
|
|
683
|
+
) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (hordeNum === 1) {
|
|
689
|
+
if (piecesNum === 1) {
|
|
690
|
+
// A lone piece cannot mate a lone king.
|
|
691
|
+
return true;
|
|
692
|
+
} else if (horde.queen === 1) {
|
|
693
|
+
// The horde has a lone queen.
|
|
694
|
+
// A lone queen mates a king on A1 bounded by:
|
|
695
|
+
// -- a pawn/rook on A2
|
|
696
|
+
// -- two same color bishops on A2, B1
|
|
697
|
+
// We ignore every other mating case, since it can be reduced to
|
|
698
|
+
// the two previous cases (e.g. a black pawn on A2 and a black
|
|
699
|
+
// bishop on B1).
|
|
700
|
+
return !(
|
|
701
|
+
pieces.pawn >= 1 ||
|
|
702
|
+
pieces.rook >= 1 ||
|
|
703
|
+
piecesBishops("light") >= 2 ||
|
|
704
|
+
piecesBishops("dark") >= 2
|
|
705
|
+
);
|
|
706
|
+
} else if (horde.pawn === 1) {
|
|
707
|
+
// Promote the pawn to a queen or a knight and check whether white
|
|
708
|
+
// can mate.
|
|
709
|
+
const pawnSquare = this.board.pieces(color, "pawn").last()!;
|
|
710
|
+
const promoteToQueen = this.clone();
|
|
711
|
+
promoteToQueen.board.set(pawnSquare, { color, role: "queen" });
|
|
712
|
+
const promoteToKnight = this.clone();
|
|
713
|
+
promoteToKnight.board.set(pawnSquare, { color, role: "knight" });
|
|
714
|
+
return (
|
|
715
|
+
promoteToQueen.hasInsufficientMaterial(color) &&
|
|
716
|
+
promoteToKnight.hasInsufficientMaterial(color)
|
|
717
|
+
);
|
|
718
|
+
} else if (horde.rook === 1) {
|
|
719
|
+
// A lone rook mates a king on A8 bounded by a pawn/rook on A7 and a
|
|
720
|
+
// pawn/knight on B7. We ignore every other case, since it can be
|
|
721
|
+
// reduced to the two previous cases.
|
|
722
|
+
// (e.g. three pawns on A7, B7, C7)
|
|
723
|
+
return !(
|
|
724
|
+
pieces.pawn >= 2 ||
|
|
725
|
+
(pieces.rook >= 1 && pieces.pawn >= 1) ||
|
|
726
|
+
(pieces.rook >= 1 && pieces.knight >= 1) ||
|
|
727
|
+
(pieces.pawn >= 1 && pieces.knight >= 1)
|
|
728
|
+
);
|
|
729
|
+
} else if (horde.bishop === 1) {
|
|
730
|
+
// The horde has a lone bishop.
|
|
731
|
+
return !(
|
|
732
|
+
// The king can be mated on A1 if there is a pawn/opposite-color-bishop
|
|
733
|
+
// on A2 and an opposite-color-bishop on B1.
|
|
734
|
+
// If black has two or more pawns, white gets the benefit of the doubt;
|
|
735
|
+
// there is an outside chance that white promotes its pawns to
|
|
736
|
+
// opposite-color-bishops and selfmates theirself.
|
|
737
|
+
// Every other case that the king is mated by the bishop requires that
|
|
738
|
+
// black has two pawns or two opposite-color-bishop or a pawn and an
|
|
739
|
+
// opposite-color-bishop.
|
|
740
|
+
// For example a king on A3 can be mated if there is
|
|
741
|
+
// a pawn/opposite-color-bishop on A4, a pawn/opposite-color-bishop on
|
|
742
|
+
// B3, a pawn/bishop/rook/queen on A2 and any other piece on B2.
|
|
743
|
+
(
|
|
744
|
+
piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 2 ||
|
|
745
|
+
(piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 1 && pieces.pawn >= 1) ||
|
|
746
|
+
pieces.pawn >= 2
|
|
747
|
+
)
|
|
748
|
+
);
|
|
749
|
+
} else if (horde.knight === 1) {
|
|
750
|
+
// The horde has a lone knight.
|
|
751
|
+
return !(
|
|
752
|
+
// The king on A1 can be smother mated by a knight on C2 if there is
|
|
753
|
+
// a pawn/knight/bishop on B2, a knight/rook on B1 and any other piece
|
|
754
|
+
// on A2.
|
|
755
|
+
// Moreover, when black has four or more pieces and two of them are
|
|
756
|
+
// pawns, black can promote their pawns and selfmate theirself.
|
|
757
|
+
(
|
|
758
|
+
piecesNum >= 4 &&
|
|
759
|
+
(pieces.knight >= 2 ||
|
|
760
|
+
pieces.pawn >= 2 ||
|
|
761
|
+
(pieces.rook >= 1 && pieces.knight >= 1) ||
|
|
762
|
+
(pieces.rook >= 1 && pieces.bishop >= 1) ||
|
|
763
|
+
(pieces.knight >= 1 && pieces.bishop >= 1) ||
|
|
764
|
+
(pieces.rook >= 1 && pieces.pawn >= 1) ||
|
|
765
|
+
(pieces.knight >= 1 && pieces.pawn >= 1) ||
|
|
766
|
+
(pieces.bishop >= 1 && pieces.pawn >= 1) ||
|
|
767
|
+
(hasBishopPair(opposite(color)) && pieces.pawn >= 1)) &&
|
|
768
|
+
(piecesBishops("dark") < 2 || piecesOfRoleNot(piecesBishops("dark")) >= 3) &&
|
|
769
|
+
(piecesBishops("light") < 2 || piecesOfRoleNot(piecesBishops("light")) >= 3)
|
|
770
|
+
)
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// By this point, we only need to deal with white's minor pieces.
|
|
775
|
+
} else if (hordeNum === 2) {
|
|
776
|
+
if (piecesNum === 1) {
|
|
777
|
+
// Two minor pieces cannot mate a lone king.
|
|
778
|
+
return true;
|
|
779
|
+
} else if (horde.knight === 2) {
|
|
780
|
+
// A king on A1 is mated by two knights, if it is obstructed by a
|
|
781
|
+
// pawn/bishop/knight on B2. On the other hand, if black only has
|
|
782
|
+
// major pieces it is a draw.
|
|
783
|
+
return pieces.pawn + pieces.bishop + pieces.knight < 1;
|
|
784
|
+
} else if (hasBishopPair(color)) {
|
|
785
|
+
return !(
|
|
786
|
+
// A king on A1 obstructed by a pawn/bishop on A2 is mated
|
|
787
|
+
// by the bishop pair.
|
|
788
|
+
(
|
|
789
|
+
pieces.pawn >= 1 ||
|
|
790
|
+
pieces.bishop >= 1 ||
|
|
791
|
+
// A pawn/bishop/knight on B4, a pawn/bishop/rook/queen on
|
|
792
|
+
// A4 and the king on A3 enable Boden's mate by the bishop
|
|
793
|
+
// pair. In every other case white cannot win.
|
|
794
|
+
(pieces.knight >= 1 && pieces.rook + pieces.queen >= 1)
|
|
795
|
+
)
|
|
796
|
+
);
|
|
797
|
+
} else if (horde.bishop >= 1 && horde.knight >= 1) {
|
|
798
|
+
// The horde has a bishop and a knight.
|
|
799
|
+
return !(
|
|
800
|
+
// A king on A1 obstructed by a pawn/opposite-color-bishop on
|
|
801
|
+
// A2 is mated by a knight on D2 and a bishop on C3.
|
|
802
|
+
(
|
|
803
|
+
pieces.pawn >= 1 ||
|
|
804
|
+
piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 1 ||
|
|
805
|
+
// A king on A1 bounded by two friendly pieces on A2 and B1 is
|
|
806
|
+
// mated when the knight moves from D4 to C2 so that both the
|
|
807
|
+
// knight and the bishop deliver check.
|
|
808
|
+
piecesOfRoleNot(piecesBishops(hordeBishopColor)) >= 3
|
|
809
|
+
)
|
|
810
|
+
);
|
|
811
|
+
} else {
|
|
812
|
+
// The horde has two or more bishops on the same color.
|
|
813
|
+
// White can only win if black has enough material to obstruct
|
|
814
|
+
// the squares of the opposite color around the king.
|
|
815
|
+
return !(
|
|
816
|
+
// A king on A1 obstructed by a pawn/opposite-bishop/knight
|
|
817
|
+
// on A2 and a opposite-bishop/knight on B1 is mated by two
|
|
818
|
+
// bishops on B2 and C3. This position is theoretically
|
|
819
|
+
// achievable even when black has two pawns or when they
|
|
820
|
+
// have a pawn and an opposite color bishop.
|
|
821
|
+
(
|
|
822
|
+
(pieces.pawn >= 1 && piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 1) ||
|
|
823
|
+
(pieces.pawn >= 1 && pieces.knight >= 1) ||
|
|
824
|
+
(piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 1 && pieces.knight >= 1) ||
|
|
825
|
+
piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 2 ||
|
|
826
|
+
pieces.knight >= 2 ||
|
|
827
|
+
pieces.pawn >= 2
|
|
828
|
+
)
|
|
829
|
+
// In every other case, white can only draw.
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
} else if (hordeNum === 3) {
|
|
833
|
+
// A king in the corner is mated by two knights and a bishop or three
|
|
834
|
+
// knights or the bishop pair and a knight/bishop.
|
|
835
|
+
if (
|
|
836
|
+
(horde.knight === 2 && horde.bishop === 1) ||
|
|
837
|
+
horde.knight === 3 ||
|
|
838
|
+
hasBishopPair(color)
|
|
839
|
+
) {
|
|
840
|
+
return false;
|
|
841
|
+
} else {
|
|
842
|
+
// White has two same color bishops and a knight.
|
|
843
|
+
// A king on A1 is mated by a bishop on B2, a bishop on C1 and a
|
|
844
|
+
// knight on C3, as long as there is another black piece to waste
|
|
845
|
+
// a tempo.
|
|
846
|
+
return piecesNum === 1;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
isVariantEnd(): boolean {
|
|
854
|
+
return this.board.white.isEmpty() || this.board.black.isEmpty();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
variantOutcome(_ctx?: Context): Outcome | undefined {
|
|
858
|
+
if (this.board.white.isEmpty()) return { winner: "black" };
|
|
859
|
+
if (this.board.black.isEmpty()) return { winner: "white" };
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export const defaultPosition = (rules: Rules): Position => {
|
|
865
|
+
switch (rules) {
|
|
866
|
+
case "chess":
|
|
867
|
+
return Chess.default();
|
|
868
|
+
case "antichess":
|
|
869
|
+
return Antichess.default();
|
|
870
|
+
case "atomic":
|
|
871
|
+
return Atomic.default();
|
|
872
|
+
case "horde":
|
|
873
|
+
return Horde.default();
|
|
874
|
+
case "racingkings":
|
|
875
|
+
return RacingKings.default();
|
|
876
|
+
case "kingofthehill":
|
|
877
|
+
return KingOfTheHill.default();
|
|
878
|
+
case "3check":
|
|
879
|
+
return ThreeCheck.default();
|
|
880
|
+
case "crazyhouse":
|
|
881
|
+
return Crazyhouse.default();
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
export const setupPosition = (rules: Rules, setup: Setup): Result<Position, PositionError> => {
|
|
886
|
+
switch (rules) {
|
|
887
|
+
case "chess":
|
|
888
|
+
return Chess.fromSetup(setup);
|
|
889
|
+
case "antichess":
|
|
890
|
+
return Antichess.fromSetup(setup);
|
|
891
|
+
case "atomic":
|
|
892
|
+
return Atomic.fromSetup(setup);
|
|
893
|
+
case "horde":
|
|
894
|
+
return Horde.fromSetup(setup);
|
|
895
|
+
case "racingkings":
|
|
896
|
+
return RacingKings.fromSetup(setup);
|
|
897
|
+
case "kingofthehill":
|
|
898
|
+
return KingOfTheHill.fromSetup(setup);
|
|
899
|
+
case "3check":
|
|
900
|
+
return ThreeCheck.fromSetup(setup);
|
|
901
|
+
case "crazyhouse":
|
|
902
|
+
return Crazyhouse.fromSetup(setup);
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
export const isStandardMaterial = (pos: Position): boolean => {
|
|
907
|
+
switch (pos.rules) {
|
|
908
|
+
case "chess":
|
|
909
|
+
case "antichess":
|
|
910
|
+
case "atomic":
|
|
911
|
+
case "kingofthehill":
|
|
912
|
+
case "3check":
|
|
913
|
+
return COLORS.every((color) => isStandardMaterialSide(pos.board, color));
|
|
914
|
+
case "crazyhouse": {
|
|
915
|
+
const promoted = pos.board.promoted;
|
|
916
|
+
return (
|
|
917
|
+
promoted.size() + pos.board.pawn.size() + (pos.pockets?.count("pawn") || 0) <= 16 &&
|
|
918
|
+
pos.board.knight.diff(promoted).size() + (pos.pockets?.count("knight") || 0) <= 4 &&
|
|
919
|
+
pos.board.bishop.diff(promoted).size() + (pos.pockets?.count("bishop") || 0) <= 4 &&
|
|
920
|
+
pos.board.rook.diff(promoted).size() + (pos.pockets?.count("rook") || 0) <= 4 &&
|
|
921
|
+
pos.board.queen.diff(promoted).size() + (pos.pockets?.count("queen") || 0) <= 2
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
case "horde":
|
|
925
|
+
return COLORS.every((color) =>
|
|
926
|
+
pos.board.pieces(color, "king").nonEmpty()
|
|
927
|
+
? isStandardMaterialSide(pos.board, color)
|
|
928
|
+
: pos.board[color].size() <= 36,
|
|
929
|
+
);
|
|
930
|
+
case "racingkings":
|
|
931
|
+
return COLORS.every(
|
|
932
|
+
(color) =>
|
|
933
|
+
pos.board.pieces(color, "knight").size() <= 2 &&
|
|
934
|
+
pos.board.pieces(color, "bishop").size() <= 2 &&
|
|
935
|
+
pos.board.pieces(color, "rook").size() <= 2 &&
|
|
936
|
+
pos.board.pieces(color, "queen").size() <= 1,
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
};
|