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    /** @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}