Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
68.22% |
88 / 129 |
|
44.44% |
4 / 9 |
CRAP | |
0.00% |
0 / 1 |
ChessParser | |
68.22% |
88 / 129 |
|
44.44% |
4 / 9 |
80.95 | |
0.00% |
0 / 1 |
__construct | |
88.46% |
23 / 26 |
|
0.00% |
0 / 1 |
3.01 | |||
getGameByIndex | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
squareToInt | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
createOutputJson | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
convertParserOutput | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
5 | |||
createAnnotationJson | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
checkSpecialMove | |
77.42% |
24 / 31 |
|
0.00% |
0 / 1 |
17.59 | |||
getFenParts | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getPgnParserOutput | |
0.00% |
0 / 19 |
|
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 | |
40 | namespace MediaWiki\Extension\ChessBrowser; |
41 | |
42 | use MediaWiki\Extension\ChessBrowser\PgnParser\GameParser; |
43 | use MediaWiki\Extension\ChessBrowser\PgnParser\PgnGameParser; |
44 | |
45 | class 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 | } |