Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
30.99% |
207 / 668 |
|
0.00% |
0 / 27 |
CRAP | |
0.00% |
0 / 1 |
FenParser0x88 | |
30.99% |
207 / 668 |
|
0.00% |
0 / 27 |
37887.73 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
setFen | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
2 | |||
parseFen | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
56 | |||
isValid | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getEnPassantSquare | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getColor | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
getValidMovesAndResult | |
97.56% |
40 / 41 |
|
0.00% |
0 / 1 |
15 | |||
getValidMovePathsForPiece | |
75.79% |
72 / 95 |
|
0.00% |
0 / 1 |
161.14 | |||
excludeInvalidSquares | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCaptureAndProtectiveMoves | |
91.67% |
33 / 36 |
|
0.00% |
0 / 1 |
23.31 | |||
getSlidingPiecesAttackingKing | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
420 | |||
getPinned | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
72 | |||
getValidSquaresOnCheck | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
702 | |||
getBishopCheckPath | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
72 | |||
getRookCheckPath | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
90 | |||
getCountChecks | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
isEnPassantMove | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
42 | |||
isCastleMove | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
getFromAndToByLongNotation | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getParsed | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
56 | |||
getFromAndToByNotation | |
28.87% |
28 / 97 |
|
0.00% |
0 / 1 |
740.85 | |||
move | |
80.95% |
17 / 21 |
|
0.00% |
0 / 1 |
8.44 | |||
updateBoardData | |
0.00% |
0 / 60 |
|
0.00% |
0 / 1 |
306 | |||
updatePieces | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
56 | |||
getFen | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getNotationForAMove | |
47.22% |
17 / 36 |
|
0.00% |
0 / 1 |
134.17 | |||
getNewFen | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
72 |
1 | <?php |
2 | /** |
3 | * This file is a part of ChessBrowser. |
4 | * |
5 | * ChessBrowser is free software: you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation, either version 3 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License |
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
17 | * |
18 | * This file is a part of PgnParser |
19 | * |
20 | * PgnParser is free software: you can redistribute it and/or modify |
21 | * it under the terms of the GNU Lesser General Public License as published by |
22 | * the Free Software Foundation, either version 3 of the License, or |
23 | * (at your option) any later version. |
24 | * |
25 | * This program is distributed in the hope that it will be useful, |
26 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
27 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
28 | * GNU Lesser General Public License for more details. |
29 | * |
30 | * You should have received a copy of the GNU Lesser General Public License |
31 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
32 | * |
33 | * @file FenParser0x88 |
34 | * @ingroup ChessBrowser |
35 | * @author Alf Magne Kalleland |
36 | */ |
37 | |
38 | namespace MediaWiki\Extension\ChessBrowser\PgnParser; |
39 | |
40 | use MediaWiki\Extension\ChessBrowser\CastlingTracker; |
41 | use MediaWiki\Extension\ChessBrowser\ChessBrowserException; |
42 | use MediaWiki\Extension\ChessBrowser\ChessPiece; |
43 | use MediaWiki\Extension\ChessBrowser\ChessSquare; |
44 | use MediaWiki\Extension\ChessBrowser\NotationAnalyzer; |
45 | use MediaWiki\Extension\ChessBrowser\SquareRelations; |
46 | |
47 | class FenParser0x88 { |
48 | /** @var string|null */ |
49 | private $fen; |
50 | /** @var array */ |
51 | private $cache; |
52 | |
53 | /** @var string|null */ |
54 | private $notation; |
55 | /** @var array|null */ |
56 | private $validMoves = null; |
57 | /** @var array */ |
58 | private $fenParts = []; |
59 | /** @var array */ |
60 | private $keySquares; |
61 | /** @var CastlingTracker|null */ |
62 | private $castlingTracker; |
63 | |
64 | private const FEN_SQUARES = [ |
65 | 'a8', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8', |
66 | 'a7', 'b7', 'c7', 'd7', 'e7', 'f7', 'g7', 'h7', |
67 | 'a6', 'b6', 'c6', 'd6', 'e6', 'f6', 'g6', 'h6', |
68 | 'a5', 'b5', 'c5', 'd5', 'e5', 'f5', 'g5', 'h5', |
69 | 'a4', 'b4', 'c4', 'd4', 'e4', 'f4', 'g4', 'h4', |
70 | 'a3', 'b3', 'c3', 'd3', 'e3', 'f3', 'g3', 'h3', |
71 | 'a2', 'b2', 'c2', 'd2', 'e2', 'f2', 'g2', 'h2', |
72 | 'a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1' |
73 | ]; |
74 | |
75 | private const VALID_NUMBERS = [ |
76 | '0' => 1, |
77 | '1' => 1, |
78 | '2' => 1, |
79 | '3' => 1, |
80 | '4' => 1, |
81 | '5' => 1, |
82 | '6' => 1, |
83 | '7' => 1, |
84 | '8' => 1, |
85 | '9' => 0 |
86 | ]; |
87 | |
88 | /** |
89 | * Create a new FenParser |
90 | * |
91 | * @param string $fen |
92 | */ |
93 | public function __construct( string $fen ) { |
94 | // Set up $keySquares |
95 | $keySquares = []; |
96 | foreach ( range( 0, 119 ) as $square ) { |
97 | $keySquares[] = ",$square,"; |
98 | } |
99 | $this->keySquares = $keySquares; |
100 | |
101 | $this->setFen( $fen ); |
102 | } |
103 | |
104 | /** |
105 | * Set new fen position |
106 | * Example: |
107 | * $parser = new FenParser0x88(); |
108 | * $parser->setFen('8/7P/8/8/1k15/8/P7/K7 w - - 0 1'); |
109 | * |
110 | * @param string $fen |
111 | */ |
112 | public function setFen( $fen ) { |
113 | $this->cache = [ |
114 | // Create the default board as empty |
115 | 'board' => array_fill( 0, 120, 0 ), |
116 | 'white' => [], |
117 | 'black' => [], |
118 | 'whiteSliding' => [], |
119 | 'blackSliding' => [], |
120 | 'king' => [ 'white' => null, 'black' => null ] |
121 | ]; |
122 | |
123 | $this->fen = $fen; |
124 | |
125 | $fenParts = explode( " ", $fen ); |
126 | |
127 | $this->castlingTracker = new CastlingTracker( $fenParts[2] ); |
128 | |
129 | $this->fenParts = [ |
130 | 'color' => $fenParts[1], |
131 | 'castle' => $fenParts[2], |
132 | 'enPassant' => $fenParts[3], |
133 | 'halfMoves' => $fenParts[4], |
134 | 'fullMoves' => $fenParts[5] |
135 | ]; |
136 | |
137 | // Pieces |
138 | $this->parseFen( $fenParts[0] ); |
139 | } |
140 | |
141 | /** |
142 | * Parse the stored fenParts |
143 | * |
144 | * @param string $pieceStr |
145 | */ |
146 | private function parseFen( string $pieceStr ) { |
147 | $pos = 0; |
148 | $squares = self::FEN_SQUARES; |
149 | $pieces = str_split( $pieceStr ); |
150 | $length = count( $pieces ); |
151 | foreach ( range( 0, $length - 1 ) as $index ) { |
152 | // Need to use index to be able to check position |
153 | $token = $pieces[$index]; |
154 | |
155 | try { |
156 | $pieceObject = new ChessPiece( $token ); |
157 | } catch ( ChessBrowserException $ex ) { |
158 | $pieceObject = false; |
159 | } |
160 | |
161 | if ( $pieceObject !== false ) { |
162 | $square = ChessSquare::newFromCoords( $squares[$pos] )->getNumber(); |
163 | $type = $pieceObject->getAsHex(); |
164 | $piece = [ |
165 | 't' => $type, |
166 | 's' => $square |
167 | ]; |
168 | // Board array |
169 | $this->cache['board'][$square] = $type; |
170 | |
171 | $color = $pieceObject->getColor(); |
172 | $this->cache[$color][] = $piece; |
173 | |
174 | // King array |
175 | if ( $pieceObject->getType() === 'k' ) { |
176 | $this->cache['king' . $color] = $piece; |
177 | } |
178 | $pos++; |
179 | } elseif ( $index < $length - 1 && isset( self::VALID_NUMBERS[$token] ) ) { |
180 | // TODO should 9 really be valid? |
181 | $pos += intval( $token ); |
182 | } |
183 | } |
184 | } |
185 | |
186 | /** |
187 | * Check if a move is valid |
188 | * |
189 | * @param array $move |
190 | * @param string $fen |
191 | * @return bool |
192 | */ |
193 | public function isValid( $move, $fen ) { |
194 | $this->setFen( $fen ); |
195 | if ( !isset( $move['from'] ) ) { |
196 | $fromAndTo = $this->getFromAndToByNotation( $move[ChessJson::MOVE_NOTATION] ); |
197 | $move['from'] = $fromAndTo['from']; |
198 | $move['to'] = $fromAndTo['to']; |
199 | |
200 | } |
201 | $from = ChessSquare::newFromCoords( $move['from'] )->getNumber(); |
202 | $to = ChessSquare::newFromCoords( $move['to'] )->getNumber(); |
203 | |
204 | $obj = $this->getValidMovesAndResult(); |
205 | $moves = $obj['moves']; |
206 | return isset( $moves[$from] ) && in_array( $to, $moves[$from] ); |
207 | } |
208 | |
209 | /** |
210 | * Returns en passant square or null |
211 | * |
212 | * @return int|null |
213 | */ |
214 | public function getEnPassantSquare() { |
215 | $enPassantSquare = $this->fenParts['enPassant']; |
216 | if ( $enPassantSquare === '-' ) { |
217 | return null; |
218 | } |
219 | return ChessSquare::newFromCoords( $enPassantSquare )->getNumber(); |
220 | } |
221 | |
222 | /** |
223 | * Return color to move, "white" or "black" |
224 | * |
225 | * @return string |
226 | * @throws ChessBrowserException |
227 | */ |
228 | public function getColor() { |
229 | $color = $this->fenParts['color']; |
230 | switch ( $color ) { |
231 | case 'w': |
232 | return 'white'; |
233 | case 'b': |
234 | return 'black'; |
235 | default: |
236 | throw new ChessBrowserException( "Unknown color: $color" ); |
237 | } |
238 | } |
239 | |
240 | /** |
241 | * Returns valid moves in 0x88 numeric format and result |
242 | * |
243 | * @return array |
244 | */ |
245 | public function getValidMovesAndResult() { |
246 | $color = $this->getColor(); |
247 | $ret = []; |
248 | $enPassantSquare = $this->getEnPassantSquare(); |
249 | |
250 | $isWhite = $color === 'white'; |
251 | |
252 | $kingSideCastle = $this->castlingTracker->checkCastle( $isWhite ? 'K' : 'k' ); |
253 | $queenSideCastle = $this->castlingTracker->checkCastle( $isWhite ? 'Q' : 'q' ); |
254 | |
255 | $oppositeColor = $isWhite ? 'black' : 'white'; |
256 | |
257 | $protectiveMoves = $this->getCaptureAndProtectiveMoves( $oppositeColor ); |
258 | |
259 | $checks = $this->getCountChecks( $color, $protectiveMoves ); |
260 | $validSquares = null; |
261 | $pinned = []; |
262 | if ( $checks === 2 ) { |
263 | $pieces = [ $this->cache['king' . $color] ]; |
264 | } else { |
265 | $pieces = $this->cache[$color]; |
266 | $pinned = $this->getPinned( $color ); |
267 | if ( $checks === 1 ) { |
268 | $validSquares = $this->getValidSquaresOnCheck( $color ); |
269 | } |
270 | } |
271 | |
272 | # New code section |
273 | # The above should be spun out |
274 | $totalCountMoves = 0; |
275 | foreach ( $pieces as $piece ) { |
276 | '@phan-var int[] $piece'; |
277 | $paths = $this->getValidMovePathsForPiece( |
278 | $piece, |
279 | $pinned, |
280 | $isWhite, |
281 | $protectiveMoves, |
282 | $kingSideCastle, |
283 | $queenSideCastle, |
284 | $enPassantSquare |
285 | ); |
286 | if ( $validSquares |
287 | && $piece['t'] != ChessPiece::WHITE_KING |
288 | && $piece['t'] != ChessPiece::BLACK_KING |
289 | ) { |
290 | $paths = $this->excludeInvalidSquares( $paths, $validSquares ); |
291 | } |
292 | $ret[$piece['s']] = $paths; |
293 | $totalCountMoves += count( $paths ); |
294 | } |
295 | $result = 0; |
296 | if ( $checks && !$totalCountMoves ) { |
297 | $result = $color === 'black' ? 1 : -1; |
298 | } elseif ( !$checks && !$totalCountMoves ) { |
299 | $result = 0.5; |
300 | } |
301 | $this->validMoves = [ 'moves' => $ret, 'result' => $result, 'check' => $checks ]; |
302 | return $this->validMoves; |
303 | } |
304 | |
305 | /** |
306 | * Analyze the possible moves for a specific piece |
307 | * |
308 | * @param array $piece |
309 | * @param array $pinned |
310 | * @param bool $isWhite |
311 | * @param string $protectiveMoves |
312 | * @param bool $kingSideCastle |
313 | * @param bool $queenSideCastle |
314 | * @param int|null $enPassantSquare |
315 | * @return array |
316 | */ |
317 | private function getValidMovePathsForPiece( |
318 | array $piece, |
319 | array $pinned, |
320 | bool $isWhite, |
321 | string $protectiveMoves, |
322 | bool $kingSideCastle, |
323 | bool $queenSideCastle, |
324 | $enPassantSquare |
325 | ): array { |
326 | $paths = []; |
327 | $type = $piece['t']; |
328 | $square = $piece['s']; |
329 | $isPinned = isset( $pinned[$square] ); |
330 | $pin = $pinned[$square] ?? [ 'by' => -1 ]; |
331 | $board = $this->cache['board']; |
332 | '@phan-var int[] $board'; |
333 | $directions = ChessPiece::newFromHex( $type )->getMovePatterns(); |
334 | |
335 | switch ( $type ) { |
336 | // pawns |
337 | case ChessPiece::WHITE_PAWN: |
338 | if ( !$isPinned || |
339 | SquareRelations::new( $square, $pin['by'] )->haveSameFile() |
340 | ) { |
341 | if ( !$board[$square + 16] ) { |
342 | $paths[] = $square + 16; |
343 | if ( $square < 32 && !$board[$square + 32] ) { |
344 | $paths[] = $square + 32; |
345 | } |
346 | } |
347 | } |
348 | if ( !$isPinned || $pin['by'] === $square + 15 ) { |
349 | if ( $enPassantSquare == $square + 15 || $board[$square + 15] & 0x8 ) { |
350 | $paths[] = $square + 15; |
351 | } |
352 | } |
353 | if ( isset( $board[$square + 17] ) && |
354 | ( !$isPinned || $pin['by'] === $square + 17 ) |
355 | ) { |
356 | if ( |
357 | $enPassantSquare == $square + 17 |
358 | || ( $board[$square + 17] && $board[$square + 17] & 0x8 ) |
359 | ) { |
360 | $paths[] = $square + 17; |
361 | } |
362 | } |
363 | break; |
364 | case ChessPiece::BLACK_PAWN: |
365 | if ( !$isPinned || |
366 | SquareRelations::new( $square, $pin['by'] )->haveSameFile() |
367 | ) { |
368 | if ( !$board[$square - 16] ) { |
369 | $paths[] = $square - 16; |
370 | if ( $square > 87 && !$board[$square - 32] ) { |
371 | $paths[] = $square - 32; |
372 | } |
373 | } |
374 | } |
375 | if ( !$isPinned || $pin['by'] === $square - 15 ) { |
376 | if ( |
377 | $enPassantSquare == $square - 15 |
378 | || ( $board[$square - 15] && !( $board[$square - 15] & 0x8 ) ) |
379 | ) { |
380 | $paths[] = $square - 15; |
381 | } |
382 | } |
383 | if ( $square - 17 >= 0 ) { |
384 | if ( !$isPinned || $pin['by'] === $square - 17 ) { |
385 | if ( |
386 | $enPassantSquare == $square - 17 |
387 | || ( $board[$square - 17] && !( $board[$square - 17] & 0x8 ) ) |
388 | ) { |
389 | $paths[] = $square - 17; |
390 | } |
391 | } |
392 | } |
393 | |
394 | break; |
395 | // Sliding pieces |
396 | case ChessPiece::WHITE_BISHOP: |
397 | case ChessPiece::WHITE_ROOK: |
398 | case ChessPiece::WHITE_QUEEN: |
399 | case ChessPiece::BLACK_BISHOP: |
400 | case ChessPiece::BLACK_ROOK: |
401 | case ChessPiece::BLACK_QUEEN: |
402 | if ( $isPinned ) { |
403 | if ( array_search( $pin['direction'], $directions ) !== false ) { |
404 | $directions = [ $pin['direction'], $pin['direction'] * -1 ]; |
405 | } else { |
406 | $directions = []; |
407 | } |
408 | } |
409 | $oldSquare = $square; |
410 | foreach ( $directions as $aValue ) { |
411 | $square = $oldSquare + $aValue; |
412 | while ( ( $square & 0x88 ) === 0 ) { |
413 | if ( $board[$square] ) { |
414 | if ( !( $isWhite xor $board[$square] & 0x8 ) ) { |
415 | $paths[] = $square; |
416 | } |
417 | break; |
418 | } |
419 | $paths[] = $square; |
420 | $square += $aValue; |
421 | } |
422 | } |
423 | break; |
424 | case ChessPiece::WHITE_KNIGHT: |
425 | case ChessPiece::BLACK_KNIGHT: |
426 | if ( $isPinned ) { |
427 | break; |
428 | } |
429 | $oldSquare = $square; |
430 | for ( $a = 0, $lenD = count( $directions ); $a < $lenD; $a++ ) { |
431 | $square = $oldSquare + $directions[$a]; |
432 | if ( ( $square & 0x88 ) === 0 ) { |
433 | if ( $board[$square] ) { |
434 | if ( !( $isWhite xor $board[$square] & 0x8 ) ) { |
435 | $paths[] = $square; |
436 | } |
437 | } else { |
438 | $paths[] = $square; |
439 | } |
440 | } |
441 | } |
442 | break; |
443 | case ChessPiece::WHITE_KING: |
444 | case ChessPiece::BLACK_KING: |
445 | $oldSquare = $square; |
446 | for ( $a = 0, $lenD = count( $directions ); $a < $lenD; $a++ ) { |
447 | $square = $oldSquare + $directions[$a]; |
448 | if ( ( $square & 0x88 ) === 0 && |
449 | strpos( $protectiveMoves, $this->keySquares[$square] ) === false |
450 | ) { |
451 | if ( $board[$square] ) { |
452 | if ( !( $isWhite xor $board[$square] & 0x8 ) ) { |
453 | $paths[] = $square; |
454 | } |
455 | } else { |
456 | $paths[] = $square; |
457 | } |
458 | } |
459 | } |
460 | $square = $oldSquare; |
461 | |
462 | if ( $kingSideCastle |
463 | && !( $board[$square + 1] ) |
464 | && !( $board[$square + 2] ) |
465 | && $board[$square + 3] |
466 | && strpos( $protectiveMoves, $this->keySquares[$square] ) === false |
467 | && $square < 117 |
468 | && strpos( $protectiveMoves, $this->keySquares[$square + 1] ) === false |
469 | && strpos( $protectiveMoves, $this->keySquares[$square + 2] ) === false |
470 | ) { |
471 | $paths[] = $square + 2; |
472 | } |
473 | |
474 | if ( $queenSideCastle |
475 | && $square - 2 != -1 |
476 | && !( $board[$square - 1] ) |
477 | && !( $board[$square - 2] ) |
478 | && !( $board[$square - 3] ) |
479 | && $board[$square - 4] |
480 | && strpos( $protectiveMoves, $this->keySquares[$square] ) === false |
481 | && strpos( $protectiveMoves, $this->keySquares[$square - 1] ) === false |
482 | && strpos( $protectiveMoves, $this->keySquares[$square - 2] ) === false |
483 | ) { |
484 | $paths[] = $square - 2; |
485 | } |
486 | break; |
487 | } |
488 | return $paths; |
489 | } |
490 | |
491 | /** |
492 | * From a list of squares and valid squares, return the valid ones |
493 | * |
494 | * TODO document how is this different from using $validSquares? |
495 | * |
496 | * @param array $squares |
497 | * @param array $validSquares |
498 | * @return array |
499 | */ |
500 | private function excludeInvalidSquares( $squares, $validSquares ) { |
501 | return array_intersect( $squares, $validSquares ); |
502 | } |
503 | |
504 | /** |
505 | * Returns comma-separated string of moves (since it's faster to work with than arrays). |
506 | * |
507 | * @param string $color |
508 | * @return string |
509 | */ |
510 | public function getCaptureAndProtectiveMoves( $color ) { |
511 | $possible = []; |
512 | $actual = []; |
513 | |
514 | $pieces = $this->cache[$color]; |
515 | |
516 | $oppositeColor = $color === 'white' ? 'black' : 'white'; |
517 | $oppositeKing = $this->cache['king' . $oppositeColor]; |
518 | $oppositeKingSquare = $oppositeKing['s']; |
519 | |
520 | foreach ( $pieces as $piece ) { |
521 | '@phan-var int[] $piece'; |
522 | switch ( $piece['t'] ) { |
523 | // pawns |
524 | case ChessPiece::WHITE_PAWN: |
525 | $possible[] = $piece['s'] + 15; |
526 | $possible[] = $piece['s'] + 17; |
527 | break; |
528 | case ChessPiece::BLACK_PAWN: |
529 | $possible[] = $piece['s'] - 15; |
530 | $possible[] = $piece['s'] - 17; |
531 | break; |
532 | // Sliding pieces |
533 | case ChessPiece::WHITE_BISHOP: |
534 | case ChessPiece::WHITE_ROOK: |
535 | case ChessPiece::WHITE_QUEEN: |
536 | case ChessPiece::BLACK_BISHOP: |
537 | case ChessPiece::BLACK_ROOK: |
538 | case ChessPiece::BLACK_QUEEN: |
539 | $directions = ChessPiece::newFromHex( $piece['t'] )->getMovePatterns(); |
540 | for ( $a = 0, $lenA = count( $directions ); $a < $lenA; $a++ ) { |
541 | $square = $piece['s'] + $directions[$a]; |
542 | while ( ( $square & 0x88 ) === 0 ) { |
543 | if ( $this->cache['board'][$square] && $square !== $oppositeKingSquare ) { |
544 | $actual[] = $square; |
545 | break; |
546 | } |
547 | $actual[] = $square; |
548 | $square += $directions[$a]; |
549 | } |
550 | } |
551 | break; |
552 | case ChessPiece::WHITE_KNIGHT: |
553 | case ChessPiece::BLACK_KNIGHT: |
554 | // White knight |
555 | $directions = ChessPiece::newFromHex( $piece['t'] )->getMovePatterns(); |
556 | for ( $a = 0, $lenA = count( $directions ); $a < $lenA; $a++ ) { |
557 | $possible[] = $piece['s'] + $directions[$a]; |
558 | } |
559 | break; |
560 | case ChessPiece::WHITE_KING: |
561 | case ChessPiece::BLACK_KING: |
562 | $directions = ChessPiece::newFromHex( $piece['t'] )->getMovePatterns(); |
563 | foreach ( $directions as $v ) { |
564 | $possible[] = $piece['s'] + $v; |
565 | } |
566 | break; |
567 | } |
568 | } |
569 | |
570 | foreach ( $possible as $square ) { |
571 | if ( ( $square & 0x88 ) === 0 ) { |
572 | $actual[] = $square; |
573 | } |
574 | } |
575 | |
576 | return ',' . implode( ",", $actual ) . ','; |
577 | } |
578 | |
579 | /** |
580 | * Get a list of sliding pieces attacking the king of a color |
581 | * |
582 | * @param string $color |
583 | * @return array |
584 | */ |
585 | public function getSlidingPiecesAttackingKing( $color ) { |
586 | $ret = []; |
587 | $king = $this->cache['king' . ( $color === 'white' ? 'black' : 'white' )]; |
588 | '@phan-var int[] $king'; |
589 | $pieces = $this->cache[$color]; |
590 | |
591 | foreach ( $pieces as $piece ) { |
592 | '@phan-var int[] $piece'; |
593 | if ( $piece['t'] & 0x4 ) { |
594 | $numericDistance = $king['s'] - $piece['s']; |
595 | $squareDistance = SquareRelations::new( $king['s'], $piece['s'] )->getDistance(); |
596 | $boardDistance = $numericDistance / $squareDistance; |
597 | |
598 | switch ( $piece['t'] ) { |
599 | case ChessPiece::WHITE_BISHOP: |
600 | case ChessPiece::BLACK_BISHOP: |
601 | if ( $numericDistance % 15 === 0 || $numericDistance % 17 === 0 ) { |
602 | $ret[] = ( [ 's' => $piece['s'], 'p' => $boardDistance ] ); |
603 | } |
604 | break; |
605 | case ChessPiece::WHITE_ROOK: |
606 | case ChessPiece::BLACK_ROOK: |
607 | if ( $numericDistance % 16 === 0 ) { |
608 | $ret[] = [ 's' => $piece['s'], 'p' => $boardDistance ]; |
609 | } elseif ( ( $piece['s'] & 240 ) == ( $king['s'] & 240 ) ) { |
610 | $ret[] = [ 's' => $piece['s'], 'p' => $numericDistance > 0 ? 1 : -1 ]; |
611 | } |
612 | break; |
613 | case ChessPiece::WHITE_QUEEN: |
614 | case ChessPiece::BLACK_QUEEN: |
615 | if ( |
616 | $numericDistance % 15 === 0 |
617 | || $numericDistance % 16 === 0 |
618 | || $numericDistance % 17 === 0 |
619 | ) { |
620 | $ret[] = [ 's' => $piece['s'], 'p' => $boardDistance ]; |
621 | } elseif ( ( $piece['s'] & 240 ) == ( $king['s'] & 240 ) ) { |
622 | $ret[] = ( [ 's' => $piece['s'], 'p' => $numericDistance > 0 ? 1 : -1 ] ); |
623 | } |
624 | break; |
625 | } |
626 | } |
627 | } |
628 | return $ret; |
629 | } |
630 | |
631 | /** |
632 | * Return numeric squares(0x88) of pinned pieces |
633 | * |
634 | * @param string $color |
635 | * @return array |
636 | */ |
637 | public function getPinned( $color ): array { |
638 | $ret = []; |
639 | $isWhite = $color === 'white'; |
640 | $pieces = $this->getSlidingPiecesAttackingKing( $isWhite ? 'black' : 'white' ); |
641 | $king = $this->cache['king' . $color]; |
642 | $i = 0; |
643 | $countPieces = count( $pieces ); |
644 | $board = $this->cache['board']; |
645 | '@phan-var int[] $board'; |
646 | while ( $i < $countPieces ) { |
647 | $piece = $pieces[$i]; |
648 | $square = $piece['s'] + $piece['p']; |
649 | $countOpposite = 0; |
650 | |
651 | $squares = [ $piece['s'] ]; |
652 | $pinning = ''; |
653 | while ( $square !== $king['s'] && $countOpposite < 2 ) { |
654 | $squares[] = $square; |
655 | if ( $board[$square] ) { |
656 | $countOpposite++; |
657 | |
658 | if ( $isWhite xor ( $board[$square] & 0x8 ) ) { |
659 | $pinning = $square; |
660 | } else { |
661 | break; |
662 | } |
663 | } |
664 | $square += $piece['p']; |
665 | } |
666 | if ( $countOpposite === 1 ) { |
667 | $ret[$pinning] = [ 'by' => $piece['s'], 'direction' => $piece['p'] ]; |
668 | } |
669 | $i++; |
670 | } |
671 | return $ret; |
672 | } |
673 | |
674 | /** |
675 | * Get valid squares for other pieces than king to move to when in check |
676 | * |
677 | * i.e. squares which avoids the check. |
678 | * |
679 | * Example: if white king on g1 is checked by rook on g8, then valid squares for other pieces |
680 | * are the squares g2,g3,g4,g5,g6,g7,g8. |
681 | * Squares are returned in numeric format |
682 | * |
683 | * @param string $color |
684 | * @return array|null |
685 | */ |
686 | public function getValidSquaresOnCheck( $color ) { |
687 | $king = $this->cache['king' . $color]; |
688 | $pieces = $this->cache[$color === 'white' ? 'black' : 'white']; |
689 | |
690 | $enPassantSquare = $this->getEnPassantSquare(); |
691 | |
692 | foreach ( $pieces as $piece ) { |
693 | '@phan-var int[] $piece'; |
694 | switch ( $piece['t'] ) { |
695 | case ChessPiece::WHITE_PAWN: |
696 | if ( $king['s'] === $piece['s'] + 15 || $king['s'] === $piece['s'] + 17 ) { |
697 | if ( $enPassantSquare === $piece['s'] - 16 ) { |
698 | return [ $piece['s'], $enPassantSquare ]; |
699 | } |
700 | return [ $piece['s'] ]; |
701 | } |
702 | break; |
703 | case ChessPiece::BLACK_PAWN: |
704 | if ( $king['s'] === $piece['s'] - 15 || $king['s'] === $piece['s'] - 17 ) { |
705 | if ( $enPassantSquare === $piece['s'] + 16 ) { |
706 | return [ $piece['s'], $enPassantSquare ]; |
707 | } |
708 | return [ $piece['s'] ]; |
709 | } |
710 | break; |
711 | case ChessPiece::WHITE_KNIGHT: |
712 | case ChessPiece::BLACK_KNIGHT: |
713 | if ( SquareRelations::new( $piece['s'], $king['s'] )->getDistance() === 2 ) { |
714 | $directions = ChessPiece::newFromHex( $piece['t'] )->getMovePatterns(); |
715 | for ( $a = 0, $lenD = count( $directions ); $a < $lenD; $a++ ) { |
716 | $square = $piece['s'] + $directions[$a]; |
717 | if ( $square === $king['s'] ) { |
718 | return [ $piece['s'] ]; |
719 | } |
720 | } |
721 | } |
722 | break; |
723 | case ChessPiece::WHITE_BISHOP: |
724 | case ChessPiece::BLACK_BISHOP: |
725 | $checks = $this->getBishopCheckPath( $piece, $king ); |
726 | if ( $checks !== [] ) { |
727 | return $checks; |
728 | } |
729 | break; |
730 | case ChessPiece::WHITE_ROOK: |
731 | case ChessPiece::BLACK_ROOK: |
732 | $checks = $this->getRookCheckPath( $piece, $king ); |
733 | if ( $checks !== [] ) { |
734 | return $checks; |
735 | } |
736 | break; |
737 | case ChessPiece::WHITE_QUEEN: |
738 | case ChessPiece::BLACK_QUEEN: |
739 | $checks = $this->getRookCheckPath( $piece, $king ); |
740 | if ( $checks !== [] ) { |
741 | return $checks; |
742 | } |
743 | $checks = $this->getBishopCheckPath( $piece, $king ); |
744 | if ( $checks !== [] ) { |
745 | return $checks; |
746 | } |
747 | break; |
748 | } |
749 | } |
750 | |
751 | return null; |
752 | } |
753 | |
754 | /** |
755 | * Get the path by which a bishop is checking a king |
756 | * |
757 | * @param array $piece |
758 | * @param array $king |
759 | * @return array |
760 | */ |
761 | public function getBishopCheckPath( $piece, $king ): array { |
762 | if ( ( $king['s'] - $piece['s'] ) % 15 === 0 || ( $king['s'] - $piece['s'] ) % 17 === 0 ) { |
763 | $distance = SquareRelations::new( $piece['s'], $king['s'] )->getDistance(); |
764 | $direction = ( $king['s'] - $piece['s'] ) / $distance; |
765 | $square = $piece['s'] + $direction; |
766 | $pieceFound = false; |
767 | $squares = [ $piece['s'] ]; |
768 | while ( $square !== $king['s'] && !$pieceFound ) { |
769 | $squares[] = $square; |
770 | if ( isset( $this->cache['board'][$square] ) && $this->cache['board'][$square] ) { |
771 | $pieceFound = true; |
772 | } |
773 | $square += $direction; |
774 | } |
775 | if ( !$pieceFound ) { |
776 | return $squares; |
777 | } |
778 | } |
779 | return []; |
780 | } |
781 | |
782 | /** |
783 | * Get the path by which a rook is chekcing a king |
784 | * |
785 | * @param array $piece |
786 | * @param array $king |
787 | * @return array |
788 | */ |
789 | public function getRookCheckPath( $piece, $king ): array { |
790 | $direction = null; |
791 | $relations = SquareRelations::new( $piece['s'], $king['s'] ); |
792 | if ( $relations->haveSameFile() ) { |
793 | $direction = ( $king['s'] - $piece['s'] ) / $relations->getDistance(); |
794 | } elseif ( $relations->haveSameRank() ) { |
795 | $direction = $king['s'] > $piece['s'] ? 1 : -1; |
796 | } |
797 | |
798 | if ( $direction ) { |
799 | $square = $piece['s'] + $direction; |
800 | $pieceFound = false; |
801 | $squares = [ $piece['s'] ]; |
802 | while ( $square !== $king['s'] && !$pieceFound ) { |
803 | $squares[] = $square; |
804 | if ( $this->cache['board'][$square] ) { |
805 | $pieceFound = true; |
806 | } |
807 | $square += $direction; |
808 | } |
809 | if ( !$pieceFound ) { |
810 | return $squares; |
811 | } |
812 | } |
813 | return []; |
814 | } |
815 | |
816 | /** |
817 | * Counts the number of pieces checking the $king |
818 | * |
819 | * $king is set by FenParser0x88::parseFen and FenParser0x88::updatePieces |
820 | * where $king['s'] is the square the king occupies. To determine if the |
821 | * $king is in check, this method evaluates whether the square the king is |
822 | * on---$king['s']---is one (or more) of the squares the opponent's pieces |
823 | * can move to. |
824 | * |
825 | * Because FenParser0x88::getCaptureAndProtectiveMoves returns a string, this |
826 | * method searches that string to see how many times the king's square is in |
827 | * that list. Because the string '2' would match both '2' and '22', we |
828 | * ensure that it matches all and only the integer representing the king's |
829 | * square by searching for the integer surrounded by commas. The mapping from |
830 | * integer to substring (needle) is stored in Board0x88Config::$keySquares. |
831 | * |
832 | * @param string $kingColor Color of the king being checked; either |
833 | * 'white' or 'black' |
834 | * @param string $moveString a comma-delimited string of move targets, output |
835 | * by FenParser0x88::getCaptureAndProtectiveMoves. Each field is a |
836 | * square (represented by an integer) where the other player's pieces |
837 | * can move to. |
838 | * @see FenParser0x88::getCaptureAndProtectiveMoves |
839 | * @see Board0x88Config::$keySquares |
840 | * @return int |
841 | */ |
842 | public function getCountChecks( $kingColor, $moveString ) { |
843 | $king = $this->cache['king' . $kingColor]; |
844 | return substr_count( $moveString, $this->keySquares[$king['s']] ); |
845 | } |
846 | |
847 | /** |
848 | * Check if a move is en passant |
849 | * |
850 | * TODO combine ifs |
851 | * |
852 | * @param array $move |
853 | * @return bool |
854 | */ |
855 | public function isEnPassantMove( $move ) { |
856 | if ( |
857 | ( $this->cache['board'][$move['from']] === ChessPiece::WHITE_PAWN |
858 | || $this->cache['board'][$move['from']] === ChessPiece::BLACK_PAWN ) |
859 | ) { |
860 | if ( |
861 | !$this->cache['board'][$move['to']] && |
862 | ( |
863 | ( $move['from'] - $move['to'] ) % 17 === 0 |
864 | || ( $move['from'] - $move['to'] ) % 15 === 0 |
865 | ) |
866 | ) { |
867 | return true; |
868 | } |
869 | } |
870 | return false; |
871 | } |
872 | |
873 | /** |
874 | * Check if a move is castling |
875 | * |
876 | * TODO combine ifs |
877 | * |
878 | * @param array $move |
879 | * @return bool |
880 | */ |
881 | public function isCastleMove( $move ) { |
882 | if ( |
883 | ( $this->cache['board'][$move['from']] === ChessPiece::WHITE_KING |
884 | || $this->cache['board'][$move['from']] === ChessPiece::BLACK_KING ) |
885 | ) { |
886 | if ( SquareRelations::new( $move['from'], $move['to'] )->getDistance() === 2 ) { |
887 | return true; |
888 | } |
889 | } |
890 | return false; |
891 | } |
892 | |
893 | /** |
894 | * Convert string notation to array |
895 | * |
896 | * TODO make static |
897 | * |
898 | * @param string $notation |
899 | * @return array |
900 | */ |
901 | private function getFromAndToByLongNotation( $notation ) { |
902 | $notation = preg_replace( '/[^a-h0-8]/si', '', $notation ); |
903 | return [ |
904 | 'from' => substr( $notation, 0, 2 ), |
905 | 'to' => substr( $notation, 2, 2 ) |
906 | ]; |
907 | } |
908 | |
909 | /** |
910 | * Get the parsed form of a move |
911 | * |
912 | * @param string|array $move |
913 | * @return array |
914 | */ |
915 | public function getParsed( $move ) { |
916 | if ( is_string( $move ) ) { |
917 | $move = [ 'm' => $move ]; |
918 | } |
919 | |
920 | $move["m"] = preg_replace( "/([a-h])([a-h])([0-8])/s", "$1x$2$3", $move["m"] ); |
921 | |
922 | if ( isset( $move['m'] ) ) { |
923 | if ( $move['m'] == '--' ) { |
924 | $this->fen = null; |
925 | |
926 | // Switch active color |
927 | $this->fenParts['color'] = $this->fenParts['color'] == 'w' ? 'b' : 'w'; |
928 | |
929 | return [ |
930 | 'm' => $move['m'], |
931 | 'fen' => $this->getFen() |
932 | ]; |
933 | } |
934 | if ( is_string( $move['m'] ) && preg_match( '/^[a-h][0-8][a-h][0-8]$/', $move['m'] ) ) { |
935 | $fromAndTo = $this->getFromAndToByLongNotation( $move['m'] ); |
936 | } else { |
937 | $fromAndTo = $this->getFromAndToByNotation( $move['m'] ); |
938 | |
939 | } |
940 | } else { |
941 | $fromAndTo = $move; |
942 | } |
943 | |
944 | // Make the move and then recreate the fen |
945 | $this->updateBoardData( $fromAndTo ); |
946 | $this->fen = null; |
947 | |
948 | $newProperties = [ |
949 | 'from' => $fromAndTo['from'], |
950 | 'to' => $fromAndTo['to'], |
951 | 'fen' => $this->getFen() |
952 | ]; |
953 | return array_merge( $move, $newProperties ); |
954 | } |
955 | |
956 | /** |
957 | * Get from and to by notation |
958 | * |
959 | * TODO document |
960 | * |
961 | * @param string $notation |
962 | * @return array |
963 | * @throws ChessBrowserException |
964 | */ |
965 | public function getFromAndToByNotation( $notation ) { |
966 | $notation = str_replace( ".", "", $notation ); |
967 | $notationAnalyzer = new NotationAnalyzer( $notation ); |
968 | |
969 | $ret = [ 'promoteTo' => $notationAnalyzer->getPromotion() ]; |
970 | $color = $this->getColor(); |
971 | |
972 | $offset = ( $color === 'black' ? 112 : 0 ); |
973 | |
974 | $foundPieces = []; |
975 | $fromRank = $notationAnalyzer->getFromRank(); |
976 | $fromFile = $notationAnalyzer->getFromFile(); |
977 | |
978 | if ( strlen( $notation ) === 2 ) { |
979 | $square = ChessSquare::newFromCoords( $notation )->getNumber(); |
980 | $ret['to'] = $square; |
981 | $direction = $color === 'white' ? -16 : 16; |
982 | if ( $this->cache['board'][$square + $direction] ) { |
983 | $foundPieces[] = $square + $direction; |
984 | } else { |
985 | $foundPieces[] = $square + ( $direction * 2 ); |
986 | } |
987 | |
988 | } else { |
989 | $notation = preg_replace( "/=[QRBN]/", "", $notation ); |
990 | $notation = preg_replace( "/[\+#!\?]/s", "", $notation ); |
991 | $notation = preg_replace( "/^(.*?)[QRBN]$/s", "$1", $notation ); |
992 | $pieceType = $notationAnalyzer->getPieceType( $color ); |
993 | |
994 | $capture = strpos( $notation, "x" ) > 0; |
995 | |
996 | $ret['to'] = $notationAnalyzer->getTargetSquare(); |
997 | switch ( $pieceType ) { |
998 | case ChessPiece::WHITE_PAWN: |
999 | case ChessPiece::BLACK_PAWN: |
1000 | if ( $color === 'black' ) { |
1001 | $offsets = $capture ? [ 15, 17 ] : [ 16 ]; |
1002 | if ( $ret['to'] >= 64 ) { |
1003 | $offsets[] = 32; |
1004 | } |
1005 | } else { |
1006 | $offsets = $capture ? [ -15, -17 ] : [ -16 ]; |
1007 | if ( $ret['to'] < 64 ) { |
1008 | $offsets[] = -32; |
1009 | } |
1010 | } |
1011 | |
1012 | foreach ( $offsets as $iValue ) { |
1013 | $sq = $ret['to'] + $iValue; |
1014 | if ( $this->cache['board'][$sq] && $this->cache['board'][$sq] === $pieceType ) { |
1015 | $foundPieces[] = ( $sq ); |
1016 | } |
1017 | } |
1018 | break; |
1019 | case ChessPiece::WHITE_KING: |
1020 | case ChessPiece::BLACK_KING: |
1021 | if ( $notation === 'O-O' ) { |
1022 | $foundPieces[] = ( $offset + 4 ); |
1023 | $ret['to'] = $offset + 6; |
1024 | } elseif ( $notation === 'O-O-O' ) { |
1025 | $foundPieces[] = ( $offset + 4 ); |
1026 | $ret['to'] = $offset + 2; |
1027 | } else { |
1028 | $k = $this->cache['king' . $color]; |
1029 | $foundPieces[] = $k['s']; |
1030 | } |
1031 | break; |
1032 | case ChessPiece::WHITE_KNIGHT: |
1033 | case ChessPiece::BLACK_KNIGHT: |
1034 | $pattern = ChessPiece::newFromHex( $pieceType )->getMovePatterns(); |
1035 | for ( $i = 0, $len = count( $pattern ); $i < $len; $i++ ) { |
1036 | $sq = $ret['to'] + $pattern[$i]; |
1037 | if ( !( $sq & 0x88 ) ) { |
1038 | if ( $this->cache['board'][$sq] && $this->cache['board'][$sq] === $pieceType ) { |
1039 | $foundPieces[] = ( $sq ); |
1040 | } |
1041 | } |
1042 | } |
1043 | break; |
1044 | // Sliding pieces |
1045 | default: |
1046 | $patterns = ChessPiece::newFromHex( $pieceType )->getMovePatterns(); |
1047 | for ( $i = 0, $len = count( $patterns ); $i < $len; $i++ ) { |
1048 | $sq = $ret['to'] + $patterns[$i]; |
1049 | while ( !( $sq & 0x88 ) ) { |
1050 | if ( $this->cache['board'][$sq] && $this->cache['board'][$sq] === $pieceType ) { |
1051 | $foundPieces[] = ( $sq ); |
1052 | } |
1053 | if ( $this->cache['board'][$sq] ) { |
1054 | break; |
1055 | } |
1056 | $sq += $patterns[$i]; |
1057 | } |
1058 | } |
1059 | break; |
1060 | } |
1061 | } |
1062 | |
1063 | if ( count( $foundPieces ) === 1 ) { |
1064 | $ret['from'] = $foundPieces[0]; |
1065 | } else { |
1066 | if ( $fromRank !== null && $fromRank >= 0 ) { |
1067 | for ( $i = 0, $len = count( $foundPieces ); $i < $len; $i++ ) { |
1068 | if ( SquareRelations::new( $foundPieces[$i], $fromRank )->haveSameRank() ) { |
1069 | $ret['from'] = $foundPieces[$i]; |
1070 | break; |
1071 | } |
1072 | } |
1073 | } elseif ( $fromFile !== null && $fromFile >= 0 ) { |
1074 | for ( $i = 0, $len = count( $foundPieces ); $i < $len; $i++ ) { |
1075 | if ( SquareRelations::new( $foundPieces[$i], $fromFile )->haveSameFile() ) { |
1076 | $ret['from'] = $foundPieces[$i]; |
1077 | break; |
1078 | } |
1079 | } |
1080 | } |
1081 | |
1082 | if ( !isset( $ret['from'] ) ) { |
1083 | $config = $this->getValidMovesAndResult(); |
1084 | $moves = $config['moves']; |
1085 | foreach ( $foundPieces as $piece ) { |
1086 | if ( in_array( $ret['to'], $moves[$piece] ) ) { |
1087 | $ret['from'] = $piece; |
1088 | break; |
1089 | } |
1090 | } |
1091 | } |
1092 | } |
1093 | // TODO some pgn files may not have correct notations for all moves. |
1094 | // Example Nd7 which may be from b2 or f6. |
1095 | // this may cause problems later on in the game. Figure out a way to handle this. |
1096 | #if (count($foundPieces) === 2){ |
1097 | #$ret['from'] = $foundPieces[1]; |
1098 | #throw new Exception("Unable to decide which move to take for notation: ". $notation); |
1099 | #} |
1100 | |
1101 | if ( !isset( $ret['from'] ) ) { |
1102 | $msg = "Fen: " |
1103 | . $this->fen |
1104 | . "\ncolor: " |
1105 | . $color |
1106 | . "\nnotation: " |
1107 | . $notation |
1108 | . "\nRank:" |
1109 | . $fromRank |
1110 | . "\nFile:" |
1111 | . $fromFile |
1112 | . "\n" |
1113 | . count( $foundPieces ) |
1114 | . ", " |
1115 | . implode( ",", $foundPieces ); |
1116 | throw new ChessBrowserException( $msg ); |
1117 | } |
1118 | $ret['from'] = ChessSquare::newFromNumber( $ret['from'] )->getCoords(); |
1119 | $ret['to'] = ChessSquare::newFromNumber( $ret['to'] )->getCoords(); |
1120 | |
1121 | return $ret; |
1122 | } |
1123 | |
1124 | /** |
1125 | * Make a move on the board |
1126 | * |
1127 | * Example: |
1128 | * |
1129 | * $parser = new FenParser0x88(); |
1130 | * $parser->newGame(); |
1131 | * $parser->move("Nf3"); |
1132 | * |
1133 | * $move can be a string like Nf3, g1f3 or an array with from and to squares, |
1134 | * like array("from" => "g1", "to"=>"f3") |
1135 | * |
1136 | * @param string|array $move |
1137 | * @throws ChessBrowserException |
1138 | */ |
1139 | public function move( $move ) { |
1140 | if ( is_string( $move ) ) { |
1141 | if ( strlen( $move ) === 4 ) { |
1142 | $move = $this->getFromAndToByLongNotation( $move ); |
1143 | } else { |
1144 | $move = $this->getFromAndToByNotation( $move ); |
1145 | } |
1146 | } |
1147 | |
1148 | $validMovesAndResult = $this->getValidMovesAndResult(); |
1149 | $validMoves = $validMovesAndResult["moves"]; |
1150 | |
1151 | $from = ChessSquare::newFromCoords( $move['from'] )->getNumber(); |
1152 | $to = ChessSquare::newFromCoords( $move['to'] )->getNumber(); |
1153 | |
1154 | if ( empty( $validMoves[$from] ) || !in_array( $to, $validMoves[$from] ) ) { |
1155 | throw new ChessBrowserException( |
1156 | "Invalid move " . $this->getColor() . " - " . json_encode( $move ) |
1157 | ); |
1158 | } |
1159 | |
1160 | $this->fen = null; |
1161 | $this->validMoves = null; |
1162 | $this->notation = $this->getNotationForAMove( $move ); |
1163 | $this->updateBoardData( $move ); |
1164 | |
1165 | $config = $this->getValidMovesAndResult(); |
1166 | |
1167 | if ( $config['result'] === 1 || $config['result'] === -1 ) { |
1168 | $this->notation .= '#'; |
1169 | } elseif ( $config['check'] > 0 ) { |
1170 | $this->notation .= '+'; |
1171 | } |
1172 | } |
1173 | |
1174 | /** |
1175 | * updateBoardData based on passed move |
1176 | * |
1177 | * @param array $move |
1178 | */ |
1179 | private function updateBoardData( $move ) { |
1180 | $move = [ |
1181 | 'from' => ChessSquare::newFromCoords( $move['from'] )->getNumber(), |
1182 | 'to' => ChessSquare::newFromCoords( $move['to'] )->getNumber(), |
1183 | 'promoteTo' => $move['promoteTo'] ?? '' |
1184 | ]; |
1185 | $board = $this->cache['board']; |
1186 | '@phan-var int[] $board'; |
1187 | $movedPiece = $board[$move['from']]; |
1188 | $color = ( $movedPiece & 0x8 ) ? 'black' : 'white'; |
1189 | $enPassant = '-'; |
1190 | |
1191 | $incrementHalfMoves = !( $board[$move['to']] ); |
1192 | |
1193 | if ( $board[$move['from']] === ChessPiece::WHITE_PAWN |
1194 | || $board[$move['from']] === ChessPiece::BLACK_PAWN |
1195 | ) { |
1196 | $incrementHalfMoves = false; |
1197 | if ( $this->isEnPassantMove( $move ) ) { |
1198 | if ( $color == 'black' ) { |
1199 | $this->cache['board'][$move['to'] + 16] = null; |
1200 | } else { |
1201 | $this->cache['board'][$move['to'] - 16] = null; |
1202 | } |
1203 | } |
1204 | |
1205 | if ( |
1206 | ( $move['from'] & 15 ) == ( $move['to'] & 15 ) |
1207 | && SquareRelations::new( $move['from'], $move['to'] )->getDistance() == 2 |
1208 | ) { |
1209 | if ( $color === 'white' ) { |
1210 | $number = $move['from'] + 16; |
1211 | } else { |
1212 | $number = $move['from'] - 16; |
1213 | } |
1214 | $enPassant = ChessSquare::newFromNumber( $number )->getCoords(); |
1215 | } |
1216 | } |
1217 | |
1218 | $this->fenParts['enPassant'] = $enPassant; |
1219 | |
1220 | if ( $this->isCastleMove( [ 'from' => $move['from'], 'to' => $move['to'] ] ) ) { |
1221 | if ( $color == 'white' ) { |
1222 | $castleNotation = '/[KQ]/s'; |
1223 | $pieceType = ChessPiece::WHITE_ROOK; |
1224 | $offset = 0; |
1225 | } else { |
1226 | $castleNotation = '/[kq]/s'; |
1227 | $pieceType = ChessPiece::BLACK_ROOK; |
1228 | $offset = 112; |
1229 | } |
1230 | |
1231 | if ( $move['from'] < $move['to'] ) { |
1232 | $this->cache['board'][7 + $offset] = null; |
1233 | $this->cache['board'][5 + $offset] = $pieceType; |
1234 | |
1235 | } else { |
1236 | $this->cache['board'][0 + $offset] = null; |
1237 | $this->cache['board'][3 + $offset] = $pieceType; |
1238 | } |
1239 | $newCastle = preg_replace( $castleNotation, '', $this->fenParts['castle'] ); |
1240 | $this->castlingTracker = new CastlingTracker( $newCastle ); |
1241 | $this->fenParts['castle'] = $newCastle; |
1242 | } else { |
1243 | $this->castlingTracker = new CastlingTracker( $this->fenParts['castle'] ); |
1244 | $this->fenParts['castle'] = $this->castlingTracker->updateForMove( |
1245 | $movedPiece, |
1246 | $move['from'] |
1247 | ); |
1248 | } |
1249 | |
1250 | if ( $color === 'black' ) { |
1251 | $this->fenParts['fullMoves']++; |
1252 | } |
1253 | if ( $incrementHalfMoves ) { |
1254 | $this->fenParts['halfMoves']++; |
1255 | } else { |
1256 | $this->fenParts['halfMoves'] = 0; |
1257 | } |
1258 | |
1259 | $this->cache['board'][$move['to']] = $this->cache['board'][$move['from']]; |
1260 | $this->cache['board'][$move['from']] = null; |
1261 | if ( $move['promoteTo'] ) { |
1262 | $pieceStr = $move['promoteTo']; |
1263 | if ( $color === 'white' ) { |
1264 | // $move stores target as lowercase, regardless of color |
1265 | $pieceStr = strtoupper( $pieceStr ); |
1266 | } |
1267 | $piece = new ChessPiece( $pieceStr ); |
1268 | $this->cache['board'][$move['to']] = $piece->getAsHex(); |
1269 | } |
1270 | |
1271 | $this->fenParts['color'] = ( $this->fenParts['color'] == 'w' ) ? 'b' : 'w'; |
1272 | |
1273 | $this->updatePieces(); |
1274 | } |
1275 | |
1276 | /** |
1277 | * updatePieces |
1278 | * |
1279 | * TODO document |
1280 | */ |
1281 | private function updatePieces() { |
1282 | $this->cache['white'] = []; |
1283 | $this->cache['black'] = []; |
1284 | |
1285 | foreach ( range( 0, 119 ) as $i ) { |
1286 | if ( $i & 0x88 ) { |
1287 | continue; |
1288 | } |
1289 | $piece = $this->cache['board'][$i]; |
1290 | if ( $piece ) { |
1291 | $color = $piece & 0x8 ? 'black' : 'white'; |
1292 | $obj = [ |
1293 | 't' => $piece, |
1294 | 's' => $i |
1295 | ]; |
1296 | $this->cache[$color][] = $obj; |
1297 | |
1298 | if ( $piece === ChessPiece::WHITE_KING |
1299 | || $piece == ChessPiece::BLACK_KING |
1300 | ) { |
1301 | $this->cache['king' . $color] = $obj; |
1302 | } |
1303 | } |
1304 | } |
1305 | } |
1306 | |
1307 | /** |
1308 | * Returns FEN for current position |
1309 | * |
1310 | * @return string |
1311 | */ |
1312 | public function getFen() { |
1313 | if ( !$this->fen ) { |
1314 | $this->fen = $this->getNewFen(); |
1315 | } |
1316 | return $this->fen; |
1317 | } |
1318 | |
1319 | /** |
1320 | * Convert a move to notation |
1321 | * |
1322 | * Does NOT add + or #, so it is an incomplete notation |
1323 | * |
1324 | * @param array $move |
1325 | * @return string |
1326 | */ |
1327 | private function getNotationForAMove( $move ) { |
1328 | $fromSquare = ChessSquare::newFromCoords( $move['from'] ); |
1329 | $toSquare = ChessSquare::newFromCoords( $move['to'] ); |
1330 | $move['from'] = $fromSquare->getNumber(); |
1331 | $move['to'] = $toSquare->getNumber(); |
1332 | $type = $this->cache['board'][$move['from']]; |
1333 | |
1334 | $ret = ChessPiece::newFromHex( $type )->getNotation(); |
1335 | |
1336 | switch ( $type ) { |
1337 | case ChessPiece::WHITE_PAWN: |
1338 | case ChessPiece::BLACK_PAWN: |
1339 | if ( $this->isEnPassantMove( $move ) || $this->cache['board'][$move['to']] ) { |
1340 | $ret .= $fromSquare->getFile() . 'x'; |
1341 | } |
1342 | |
1343 | $ret .= $toSquare->getCoords(); |
1344 | |
1345 | if ( isset( $move['promoteTo'] ) && $move['promoteTo'] ) { |
1346 | $promotedTo = new ChessPiece( $move['promoteTo'] ); |
1347 | $ret .= '=' . $promotedTo->getNotation(); |
1348 | } |
1349 | break; |
1350 | case ChessPiece::WHITE_KNIGHT: |
1351 | case ChessPiece::BLACK_KNIGHT: |
1352 | case ChessPiece::WHITE_BISHOP: |
1353 | case ChessPiece::BLACK_BISHOP: |
1354 | case ChessPiece::WHITE_ROOK: |
1355 | case ChessPiece::BLACK_ROOK: |
1356 | case ChessPiece::WHITE_QUEEN: |
1357 | case ChessPiece::BLACK_QUEEN: |
1358 | $config = $this->getValidMovesAndResult(); |
1359 | |
1360 | $configMoves = $config['moves']; |
1361 | foreach ( $configMoves as $square => $moves ) { |
1362 | if ( |
1363 | $square != $move['from'] |
1364 | && $this->cache['board'][$square] === $type |
1365 | && array_search( $move['to'], $moves ) !== false |
1366 | ) { |
1367 | if ( ( $square & 15 ) != ( $move['from'] & 15 ) ) { |
1368 | $ret .= $fromSquare->getFile(); |
1369 | } elseif ( ( $square & 240 ) != ( $move['from'] & 240 ) ) { |
1370 | $ret .= (string)$fromSquare->getRank(); |
1371 | } |
1372 | } |
1373 | } |
1374 | |
1375 | if ( $this->cache['board'][$move['to']] ) { |
1376 | $ret .= 'x'; |
1377 | } |
1378 | |
1379 | $ret .= $toSquare->getCoords(); |
1380 | break; |
1381 | case ChessPiece::WHITE_KING: |
1382 | case ChessPiece::BLACK_KING: |
1383 | if ( $this->isCastleMove( $move ) ) { |
1384 | if ( $move['to'] > $move['from'] ) { |
1385 | $ret = 'O-O'; |
1386 | } else { |
1387 | $ret = 'O-O-O'; |
1388 | } |
1389 | } else { |
1390 | if ( $this->cache['board'][$move['to']] ) { |
1391 | $ret .= 'x'; |
1392 | } |
1393 | |
1394 | $ret .= $toSquare->getCoords(); |
1395 | } |
1396 | break; |
1397 | |
1398 | } |
1399 | |
1400 | return $ret; |
1401 | } |
1402 | |
1403 | /** |
1404 | * Get a new fen |
1405 | * |
1406 | * @return string |
1407 | */ |
1408 | private function getNewFen() { |
1409 | $board = $this->cache['board']; |
1410 | $fen = ''; |
1411 | $emptyCounter = 0; |
1412 | |
1413 | // @phan-suppress-next-line PhanTypeInvalidUnaryOperandIncOrDec |
1414 | for ( $rank = 7; $rank >= 0; $rank-- ) { |
1415 | for ( $file = 0; $file < 8; $file++ ) { |
1416 | $index = ( $rank * 8 ) + $file; |
1417 | $mapped = ChessSquare::newFromLateral64( $index )->getNumber(); |
1418 | if ( $board[$mapped] ) { |
1419 | if ( $emptyCounter ) { |
1420 | $fen .= $emptyCounter; |
1421 | } |
1422 | $fen .= ChessPiece::newFromHex( $board[$mapped] ) |
1423 | ->getSymbol(); |
1424 | $emptyCounter = 0; |
1425 | } else { |
1426 | $emptyCounter++; |
1427 | } |
1428 | } |
1429 | if ( $rank ) { |
1430 | if ( $emptyCounter ) { |
1431 | $fen .= $emptyCounter; |
1432 | } |
1433 | $fen .= '/'; |
1434 | $emptyCounter = 0; |
1435 | } |
1436 | } |
1437 | |
1438 | if ( $emptyCounter ) { |
1439 | $fen .= $emptyCounter; |
1440 | } |
1441 | return $fen |
1442 | . ' ' |
1443 | . $this->fenParts['color'] |
1444 | . ' ' |
1445 | . $this->fenParts['castle'] |
1446 | . ' ' |
1447 | . $this->fenParts['enPassant'] |
1448 | . ' ' |
1449 | . $this->fenParts['halfMoves'] |
1450 | . ' ' |
1451 | . $this->fenParts['fullMoves']; |
1452 | } |
1453 | } |