1 import { ArrayFun
} from "@/utils/array";
2 import { randInt
} from "@/utils/alea";
3 import { ChessRules
, PiPo
, Move
} from "@/base_rules";
5 export class FullcavalryRules
extends ChessRules
{
11 static get IMAGE_EXTENSION() {
12 // Temporarily, for the time SVG pieces are being designed:
16 // Lancer directions *from white perspective*
17 static get LANCER_DIRS() {
31 return ChessRules
.PIECES
.concat(Object
.keys(V
.LANCER_DIRS
));
35 const piece
= this.board
[i
][j
].charAt(1);
36 // Special lancer case: 8 possible orientations
37 if (Object
.keys(V
.LANCER_DIRS
).includes(piece
)) return V
.LANCER
;
41 getPpath(b
, color
, score
, orientation
) {
42 if (Object
.keys(V
.LANCER_DIRS
).includes(b
[1])) {
43 if (orientation
== 'w') return "Eightpieces/tmp_png/" + b
;
44 // Find opposite direction for adequate display:
72 return "Eightpieces/tmp_png/" + b
[0] + oppDir
;
74 // TODO: after we have SVG pieces, remove the folder and next prefix:
75 return "Eightpieces/tmp_png/" + b
;
78 getPPpath(m
, orientation
) {
79 // If castle, show choices on m.appear[1]:
80 const index
= (m
.appear
.length
== 2 ? 1 : 0);
83 m
.appear
[index
].c
+ m
.appear
[index
].p
,
91 static GenRandInitFen(randomness
) {
94 return "efbqkbnm/pppppppp/8/8/8/8/PPPPPPPP/EDBQKBNM w 0 ahah -";
96 const baseFen
= ChessRules
.GenRandInitFen(randomness
);
97 // Replace rooks by lancers with expected orientation:
98 const firstBlackRook
= baseFen
.indexOf('r'),
99 lastBlackRook
= baseFen
.lastIndexOf('r'),
100 firstWhiteRook
= baseFen
.indexOf('R'),
101 lastWhiteRook
= baseFen
.lastIndexOf('R');
103 baseFen
.substring(0, firstBlackRook
) +
104 (firstBlackRook
<= 3 ? 'e' : 'm') +
105 baseFen
.substring(firstBlackRook
+ 1, lastBlackRook
) +
106 (lastBlackRook
>= 5 ? 'm' : 'e') +
107 baseFen
.substring(lastBlackRook
+ 1, firstWhiteRook
) +
108 (firstWhiteRook
<= 3 ? 'E' : 'M') +
109 baseFen
.substring(firstWhiteRook
+ 1, lastWhiteRook
) +
110 (lastWhiteRook
>= 5 ? 'M' : 'E') +
111 baseFen
.substring(lastWhiteRook
+ 1)
115 // Because of the lancers, getPiece() could be wrong:
116 // use board[x][y][1] instead (always valid).
117 // TODO: base implementation now uses this too (no?)
118 getBasicMove([sx
, sy
], [ex
, ey
], tr
) {
119 const initColor
= this.getColor(sx
, sy
);
120 const initPiece
= this.board
[sx
][sy
].charAt(1);
126 c: tr
? tr
.c : initColor
,
127 p: tr
? tr
.p : initPiece
140 // The opponent piece disappears if we take it
141 if (this.board
[ex
][ey
] != V
.EMPTY
) {
146 c: this.getColor(ex
, ey
),
147 p: this.board
[ex
][ey
].charAt(1)
155 getPotentialMovesFrom([x
, y
]) {
156 if (this.getPiece(x
, y
) == V
.LANCER
)
157 return this.getPotentialLancerMoves([x
, y
]);
158 return super.getPotentialMovesFrom([x
, y
]);
161 getPotentialPawnMoves([x
, y
]) {
162 const color
= this.getColor(x
, y
);
164 const [sizeX
, sizeY
] = [V
.size
.x
, V
.size
.y
];
165 let shiftX
= (color
== "w" ? -1 : 1);
166 const startRank
= color
== "w" ? sizeX
- 2 : 1;
167 const lastRank
= color
== "w" ? 0 : sizeX
- 1;
169 let finalPieces
= [V
.PAWN
];
170 if (x
+ shiftX
== lastRank
) {
171 // Only allow direction facing inside board:
172 const allowedLancerDirs
=
174 ? ['e', 'f', 'g', 'h', 'm']
175 : ['c', 'd', 'e', 'm', 'o'];
176 finalPieces
= allowedLancerDirs
.concat([V
.KNIGHT
, V
.BISHOP
, V
.QUEEN
]);
178 if (this.board
[x
+ shiftX
][y
] == V
.EMPTY
) {
179 // One square forward
180 for (let piece
of finalPieces
) {
182 this.getBasicMove([x
, y
], [x
+ shiftX
, y
], {
188 if (x
== startRank
&& this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
)
190 moves
.push(this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
]));
193 for (let shiftY
of [-1, 1]) {
196 y
+ shiftY
< sizeY
&&
197 this.board
[x
+ shiftX
][y
+ shiftY
] != V
.EMPTY
&&
198 this.canTake([x
, y
], [x
+ shiftX
, y
+ shiftY
])
200 for (let piece
of finalPieces
) {
202 this.getBasicMove([x
, y
], [x
+ shiftX
, y
+ shiftY
], {
211 // Add en-passant captures
212 Array
.prototype.push
.apply(
214 this.getEnpassantCaptures([x
, y
], shiftX
)
220 // Obtain all lancer moves in "step" direction
221 getPotentialLancerMoves_aux([x
, y
], step
, tr
) {
223 // Add all moves to vacant squares until opponent is met:
224 const color
= this.getColor(x
, y
);
225 const oppCol
= V
.GetOppCol(color
)
226 let sq
= [x
+ step
[0], y
+ step
[1]];
227 while (V
.OnBoard(sq
[0], sq
[1]) && this.getColor(sq
[0], sq
[1]) != oppCol
) {
228 if (this.board
[sq
[0]][sq
[1]] == V
.EMPTY
)
229 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
233 if (V
.OnBoard(sq
[0], sq
[1]))
234 // Add capturing move
235 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
239 getPotentialLancerMoves([x
, y
]) {
241 // Add all lancer possible orientations, similar to pawn promotions.
242 const color
= this.getColor(x
, y
);
243 const dirCode
= this.board
[x
][y
][1];
244 const curDir
= V
.LANCER_DIRS
[dirCode
];
246 this.getPotentialLancerMoves_aux([x
, y
], V
.LANCER_DIRS
[dirCode
]);
247 monodirMoves
.forEach(m
=> {
248 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
249 const newDir
= V
.LANCER_DIRS
[k
];
250 // Prevent orientations toward outer board:
251 if (V
.OnBoard(m
.end
.x
+ newDir
[0], m
.end
.y
+ newDir
[1])) {
252 let mk
= JSON
.parse(JSON
.stringify(m
));
261 getCastleMoves([x
, y
]) {
262 const c
= this.getColor(x
, y
);
265 const oppCol
= V
.GetOppCol(c
);
268 // King, then lancer:
269 const finalSquares
= [ [2, 3], [V
.size
.y
- 2, V
.size
.y
- 3] ];
273 castleSide
++ //large, then small
275 if (this.castleFlags
[c
][castleSide
] >= V
.size
.y
) continue;
276 // If this code is reached, lancer and king are on initial position
278 const lancerPos
= this.castleFlags
[c
][castleSide
];
279 const castlingPiece
= this.board
[x
][lancerPos
].charAt(1);
281 // Nothing on the path of the king ? (and no checks)
282 const finDist
= finalSquares
[castleSide
][0] - y
;
283 let step
= finDist
/ Math
.max(1, Math
.abs(finDist
));
287 (this.isAttacked([x
, i
], oppCol
)) ||
289 this.board
[x
][i
] != V
.EMPTY
&&
290 // NOTE: next check is enough, because of chessboard constraints
291 (this.getColor(x
, i
) != c
|| ![y
, lancerPos
].includes(i
))
294 continue castlingCheck
;
297 } while (i
!= finalSquares
[castleSide
][0]);
299 // Nothing on final squares, except maybe king and castling lancer?
300 for (i
= 0; i
< 2; i
++) {
302 finalSquares
[castleSide
][i
] != lancerPos
&&
303 this.board
[x
][finalSquares
[castleSide
][i
]] != V
.EMPTY
&&
305 finalSquares
[castleSide
][i
] != y
||
306 this.getColor(x
, finalSquares
[castleSide
][i
]) != c
309 continue castlingCheck
;
313 // If this code is reached, castle is valid
314 let allowedLancerDirs
= [castlingPiece
];
315 if (finalSquares
[castleSide
][1] != lancerPos
) {
316 // It moved: allow reorientation
319 ? ['e', 'f', 'g', 'h', 'm']
320 : ['c', 'd', 'e', 'm', 'o'];
322 allowedLancerDirs
.forEach(dir
=> {
328 y: finalSquares
[castleSide
][0],
334 y: finalSquares
[castleSide
][1],
340 new PiPo({ x: x
, y: y
, p: V
.KING
, c: c
}),
341 new PiPo({ x: x
, y: lancerPos
, p: castlingPiece
, c: c
})
344 Math
.abs(y
- lancerPos
) <= 2
345 ? { x: x
, y: lancerPos
}
346 : { x: x
, y: y
+ 2 * (castleSide
== 0 ? -1 : 1) }
355 isAttacked(sq
, color
) {
357 super.isAttacked(sq
, color
) ||
358 this.isAttackedByLancer(sq
, color
)
362 isAttackedByLancer([x
, y
], color
) {
363 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
364 // If in this direction there are only enemy pieces and empty squares,
365 // and we meet a lancer: can he reach us?
366 // NOTE: do not stop at first lancer, there might be several!
367 let coord
= { x: x
+ step
[0], y: y
+ step
[1] };
370 V
.OnBoard(coord
.x
, coord
.y
) &&
372 this.board
[coord
.x
][coord
.y
] == V
.EMPTY
||
373 this.getColor(coord
.x
, coord
.y
) == color
377 this.getPiece(coord
.x
, coord
.y
) == V
.LANCER
&&
378 !this.isImmobilized([coord
.x
, coord
.y
])
380 lancerPos
.push({x: coord
.x
, y: coord
.y
});
385 for (let xy
of lancerPos
) {
386 const dir
= V
.LANCER_DIRS
[this.board
[xy
.x
][xy
.y
].charAt(1)];
387 if (dir
[0] == -step
[0] && dir
[1] == -step
[1]) return true;
393 static get VALUES() {
394 return Object
.assign(
395 { l: 4.8 }, //Jeff K. estimation (for Eightpieces)
400 // For moves notation:
401 static get LANCER_DIRNAMES() {
415 // At move 1, forbid captures (in case of...):
416 if (this.movesCount
>= 2) return moves
;
417 return moves
.filter(m
=> m
.vanish
.length
== 1);
421 let notation
= super.getNotation(move);
422 if (Object
.keys(V
.LANCER_DIRNAMES
).includes(move.vanish
[0].p
))
423 // Lancer: add direction info
424 notation
+= "=" + V
.LANCER_DIRNAMES
[move.appear
[0].p
];
425 else if (move.appear
.length
== 2 && move.vanish
[1].p
!= move.appear
[1].p
)
426 // Same after castle:
427 notation
+= "+L:" + V
.LANCER_DIRNAMES
[move.appear
[1].p
];
429 move.vanish
[0].p
== V
.PAWN
&&
430 Object
.keys(V
.LANCER_DIRNAMES
).includes(move.appear
[0].p
)
432 // Fix promotions in lancer:
433 notation
= notation
.slice(0, -1) +
434 "L:" + V
.LANCER_DIRNAMES
[move.appear
[0].p
];