Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.22% covered (warning)
68.22%
88 / 129
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChessParser
68.22% covered (warning)
68.22%
88 / 129
44.44% covered (danger)
44.44%
4 / 9
80.95
0.00% covered (danger)
0.00%
0 / 1
 __construct
88.46% covered (warning)
88.46%
23 / 26
0.00% covered (danger)
0.00%
0 / 1
3.01
 getGameByIndex
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 squareToInt
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 createOutputJson
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 convertParserOutput
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
5
 createAnnotationJson
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 checkSpecialMove
77.42% covered (warning)
77.42%
24 / 31
0.00% covered (danger)
0.00%
0 / 1
17.59
 getFenParts
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getPgnParserOutput
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
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 contains code originally 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 ChessParser
34 * @ingroup ChessBrowser
35 * @author Wugapodes
36 * @author DannyS712
37 * @author Alf Magne Kalleland
38 */
39
40namespace MediaWiki\Extension\ChessBrowser;
41
42use MediaWiki\Extension\ChessBrowser\PgnParser\GameParser;
43use MediaWiki\Extension\ChessBrowser\PgnParser\PgnGameParser;
44
45class ChessParser {
46
47    /** @var string[] */
48    private $pgnGames;
49
50    /**
51     * Construct a new ChesParser
52     *
53     * @param string $pgnContent
54     */
55    public function __construct( $pgnContent ) {
56        $clean = $pgnContent;
57
58        $clean = preg_replace( '/"\]\s{0,10}\[/s', "]\n[", $clean );
59        $clean = preg_replace( '/"\]\s{0,10}([\.\d{])/s', "\"]\n\n$1", $clean );
60
61        $clean = preg_replace( "/{\s{0,6}\[%emt[^\}]*?\}/", "", $clean );
62
63        $clean = preg_replace( '/\s(\$\d{1,3})/', '\1', $clean );
64        $clean = str_replace( "({", "( {", $clean );
65        $clean = preg_replace( "/{([^\[]*?)\[([^}]?)}/s", '{$1-SB-$2}', $clean );
66        $clean = preg_replace( "/\r/s", "", $clean );
67        $clean = preg_replace( "/\t/s", "", $clean );
68        $clean = preg_replace( "/\]\s+\[/s", "]\n[", $clean );
69        $clean = str_replace( " [", "[", $clean );
70        $clean = preg_replace( "/([^\]])(\n+)\[/si", "$1\n\n[", $clean );
71        $clean = preg_replace( "/\n{3,}/s", "\n\n", $clean );
72        $clean = str_replace( "-SB-", "[", $clean );
73        $clean = str_replace( "0-0-0", "O-O-O", $clean );
74        $clean = str_replace( "0-0", "O-O", $clean );
75
76        $clean = preg_replace( '/^([^\[])*?\[/', '[', $clean );
77
78        $clean = NagTable::replaceNag( $clean );
79
80        $pgnGames = [];
81        $content = "\n\n" . $clean;
82        $games = preg_split( "/\n\n\[/s", $content, -1, PREG_SPLIT_DELIM_CAPTURE );
83
84        for ( $i = 1, $count = count( $games ); $i < $count; $i++ ) {
85            $gameContent = trim( "[" . $games[$i] );
86            if ( strlen( $gameContent ) > 10 ) {
87                $pgnGames[] = $gameContent;
88            }
89        }
90
91        $this->pgnGames = $pgnGames;
92    }
93
94    /**
95     * Get the game at an index
96     *
97     * @param int $index
98     * @return array|null
99     */
100    private function getGameByIndex( $index ) {
101        $games = $this->pgnGames;
102        if ( $games && count( $games ) > $index ) {
103            $pgnGameParser = new PgnGameParser( $games[$index] );
104            $parsedData = $pgnGameParser->getParsedData();
105
106            $gameParser = new GameParser( $parsedData );
107            return $gameParser->getParsedGame();
108        }
109        return null;
110    }
111
112    /**
113     * To move up a rank, add 1; down a rank subtract 1
114     * To move a file towards queenside, subtract 8; kingside a file add 8
115     * Example: a1 to c5 = 0(a1) + 8(b1) + 8(c1) + 4(c5) = 20
116     *
117     * @param string $square
118     * @return int
119     */
120    private static function squareToInt( string $square ): int {
121        if ( $square === '-' ) {
122            // Special handling
123            return -1;
124        }
125
126        $chessSquare = ChessSquare::newFromCoords( $square );
127        return $chessSquare->getAsVertical64();
128    }
129
130    /**
131     * Returns a JSON string for the javascript module
132     * @since 0.2.0
133     * @param int $gameNum Passed directly to the PGN parsing library
134     * @return array
135     */
136    public function createOutputJson( $gameNum = 0 ) {
137        $gameObject = $this->getPgnParserOutput( $gameNum );
138        $moves = array_pop( $gameObject );
139
140        # The parser guarantees us the initial board state as FEN
141        $fenParts = $this->getFenParts( $gameObject['metadata']['fen'] );
142        $gameObject = $this->convertParserOutput( $moves, $fenParts, $gameObject );
143
144        return $gameObject;
145    }
146
147    /**
148     * Convert the parser output into the board, token, ply object used in
149     * the javascript module.
150     * @param array $moves The list of moves output by the PgnParser
151     * @param array $fenParts The fen string broken down by getFenParts
152     * @param array $moveObject The movereferences from the PgnParser
153     * @return array
154     */
155    public function convertParserOutput( $moves, $fenParts, $moveObject ) {
156        $index = 0;
157        foreach ( $moves as $move ) {
158            $token = $move['m'];
159
160            $fen = $move['fen'];
161            $from = self::squareToInt( $move['from'] );
162            $to = self::squareToInt( $move['to'] );
163
164            $special = $this->checkSpecialMove( $to, $from, $token, $fenParts );
165            if ( array_key_exists( 'comment', $move ) ) {
166                $special[] = $move['comment'];
167            } else {
168                $special[] = null;
169            }
170            if ( array_key_exists( 'variations', $move ) ) {
171                if ( !array_key_exists( 'variations', $moveObject ) ) {
172                    $moveObject['variations'] = [];
173                }
174                $moveObject['variations'][] = $this->createAnnotationJson( $move['variations'], $fenParts, $index );
175            }
176
177            $moveObject['boards'][] = $fen;
178            $moveObject['plys'][] = [ $from, $to, $special ];
179            $moveObject['tokens'][] = $token;
180
181            $fenParts = $this->getFenParts( $fen );
182            $index++;
183        }
184        return $moveObject;
185    }
186
187    /**
188     * Will pass variations to the JS module
189     * to insert into the game.
190     * @param array $variationObj list of variations associated with a move. Organized
191     *   as a list of lists of moves, so 1. e4 (1. c4 c5) (1.d4) would be:
192     *   [
193     *     [
194     *       {
195     *        'm': 'c4',
196     *      'from': 'c2',
197     *      'to': 'c4',
198     *      'fen': '...'
199     *     },
200     *       {
201     *        'm': 'c5',
202     *      'from': 'c7',
203     *      'to': 'c5',
204     *      'fen': '...'
205     *     }
206     *     ],
207     *     [
208     *       {
209     *        'm': 'd4',
210     *        'from': 'd2',
211     *        'to': 'd4',
212     *        'fen': '...'
213     *       }
214     *     ]
215     *   ]
216     * @param array $fenParts The starting fen broken down into semantic chunks
217     * @param int $index Ply of the variation's parent move.
218     * @return array
219     */
220    public function createAnnotationJson( $variationObj, $fenParts, $index ) {
221        $variations = [ $index, [] ];
222        foreach ( $variationObj as $variation ) {
223            $moveObject = [
224                'boards' => [],
225                'plys' => [],
226                'tokens' => []
227            ];
228            $moves = $variation;
229            $variations[1][] = $this->convertParserOutput( $moves, $fenParts, $moveObject );
230        }
231        return $variations;
232        // Will pass variations and notation to the JS module
233        // to insert into the game.
234    }
235
236    /**
237     * Sets the special move field of the JSON "ply" section
238     * @since 0.2.0
239     * @param int $to
240     * @param int $from
241     * @param string $token
242     * @param array $fenParts
243     * @return array
244     */
245    public function checkSpecialMove( $to, $from, $token, $fenParts ) {
246        if ( $to === $fenParts['enPassantTarget'] ) {
247            $specialType = "en passant";
248            if ( $fenParts['toMove'] === 'w' ) {
249                $specialAction = $to - 1;
250            } else {
251                $specialAction = $to + 1;
252            }
253        } elseif ( $token === 'O-O' || $token === 'O-O-O' ) {
254            $specialType = "castle";
255            $rookSource = null;
256            $rookTarget = null;
257            if ( $token === 'O-O' && $fenParts['toMove'] === 'w' ) {
258                $rookSource = 56;
259                $rookTarget = 40;
260            } elseif ( $token === 'O-O' && $fenParts['toMove'] === 'b' ) {
261                $rookSource = 63;
262                $rookTarget = 47;
263            } elseif ( $token === 'O-O-O' && $fenParts['toMove'] === 'w' ) {
264                $rookSource = 0;
265                $rookTarget = 24;
266            } elseif ( $token === 'O-O-O' && $fenParts['toMove'] === 'b' ) {
267                $rookSource = 7;
268                $rookTarget = 31;
269            }
270            $specialAction = [ $rookSource, $rookTarget ];
271        } elseif ( strpos( $token, '=' ) ) {
272            $specialType = "promotion";
273            # Get first char after =; ignore rest
274            $promotedTo = explode( '=', $token )[1][0];
275            if ( $fenParts['toMove'] === 'b' ) {
276                $specialAction = strtolower( $promotedTo );
277            } else {
278                $specialAction = strtoupper( $promotedTo );
279            }
280        } else {
281            $specialType = "move";
282            $specialAction = null;
283        }
284        return [ $specialType, $specialAction ];
285    }
286
287    /**
288     * Extracts en passant target and color to play from FEN
289     * @since 0.2.0
290     * @param string $fen
291     * @return array
292     */
293    public function getFenParts( $fen ) {
294        $fenParts = explode( ' ', $fen );
295        $toMove = $fenParts[1];
296        $enPassantTarget = self::squareToInt( $fenParts[3] );
297        return [
298            'toMove' => $toMove,
299            'enPassantTarget' => $enPassantTarget
300        ];
301    }
302
303    /**
304     * Extract needed data from the PGN parser output
305     * @since 0.2.0
306     * @param int $gameNum Passed directly to the PGN parsing library
307     * @return array
308     */
309    public function getPgnParserOutput( $gameNum = 0 ) {
310        $gameObject = $this->getGameByIndex( $gameNum );
311        // What is in the game Object?
312
313        // Need to document
314        $metadata = [];
315        $moves = [];
316        foreach ( $gameObject as $key => $obj ) {
317            if ( $key === 'metadata' ) {
318                continue;
319            } elseif ( $key === 'moves' ) {
320                $moves = $obj;
321            } elseif ( $key === 'comment' ) {
322                continue;
323            } else {
324                $metadata[$key] = $obj;
325            }
326        }
327        return [
328            'boards' => [],
329            'plys' => [],
330            'tokens' => [],
331            'variations' => [],
332            'metadata' => $metadata,
333            'moves' => $moves
334        ];
335    }
336}