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