mambax7 /
chess
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
| 1 | <?php |
||
| 2 | |||
| 3 | namespace XoopsModules\Chess; |
||
| 4 | |||
| 5 | // ------------------------------------------------------------------------- // |
||
| 6 | // This program is free software; you can redistribute it and/or modify // |
||
| 7 | // it under the terms of the GNU General Public License as published by // |
||
| 8 | // the Free Software Foundation; either version 2 of the License, or // |
||
| 9 | // (at your option) any later version. // |
||
| 10 | // // |
||
| 11 | // You may not change or alter any portion of this comment or credits // |
||
| 12 | // of supporting developers from this source code or any supporting // |
||
| 13 | // source code which is considered copyrighted (c) material of the // |
||
| 14 | // original comment or credit authors. // |
||
| 15 | // // |
||
| 16 | // This program is distributed in the hope that it will be useful, // |
||
| 17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of // |
||
| 18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // |
||
| 19 | // GNU General Public License for more details. // |
||
| 20 | // // |
||
| 21 | // You should have received a copy of the GNU General Public License // |
||
| 22 | // along with this program; if not, write to the Free Software // |
||
| 23 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // |
||
| 24 | // ------------------------------------------------------------------------ // |
||
| 25 | // Author: Dave Lerner <http://Dave-L.com> // |
||
| 26 | // ------------------------------------------------------------------------ // |
||
| 27 | // Adapted from Online Chess Club (OCC) version 1.0.10, which was written // |
||
| 28 | // by Michael Speck <http://lgames.sf.net> and published under the GNU // |
||
| 29 | // General Public License. // |
||
| 30 | // ------------------------------------------------------------------------ // |
||
| 31 | |||
| 32 | /** |
||
| 33 | * class ChessGame |
||
| 34 | * |
||
| 35 | * @package chess |
||
| 36 | * @subpackage game |
||
| 37 | */ |
||
| 38 | |||
| 39 | /** |
||
| 40 | * The purpose of this class is to handle chess moves. |
||
| 41 | * |
||
| 42 | * An instantiation of this class comprises the data essential for handling chess |
||
| 43 | * moves in a specific game, and provides the requisite methods. |
||
| 44 | * |
||
| 45 | * - Input: Game state and proposed move. |
||
| 46 | * - Processing: Check the legality of the move, and update the game state if the move is legal. |
||
| 47 | * - Output: Indication of the move's legality, and the (possibly) updated game state. |
||
| 48 | * |
||
| 49 | * In addition to the above, there are utility methods for converting between Standard Algebraic |
||
| 50 | * Notation (SAN) and a notation similar to Long Algebraic Notation. |
||
| 51 | * |
||
| 52 | * @package chess |
||
| 53 | * @subpackage game |
||
| 54 | */ |
||
| 55 | class ChessGame |
||
| 56 | { |
||
| 57 | /** |
||
| 58 | * Indicates whether object is valid. |
||
| 59 | * |
||
| 60 | * If empty string (''), indicates this is a valid object; otherwise contains an error message. |
||
| 61 | * Should be checked after creating an instance of this class. |
||
| 62 | * |
||
| 63 | * @var string $error |
||
| 64 | */ |
||
| 65 | |||
| 66 | public $error; |
||
| 67 | /** |
||
| 68 | * gamestate |
||
| 69 | * |
||
| 70 | * The game state is represented as an array with the following elements: |
||
| 71 | * |
||
| 72 | * - 'fen_piece_placement' |
||
| 73 | * - 'fen_active_color' |
||
| 74 | * - 'fen_castling_availability' |
||
| 75 | * - 'fen_en_passant_target_square' |
||
| 76 | * - 'fen_halfmove_clock' |
||
| 77 | * - 'fen_fullmove_number' |
||
| 78 | * - 'pgn_result' |
||
| 79 | * - 'pgn_fen' |
||
| 80 | * - 'pgn_movetext' |
||
| 81 | * |
||
| 82 | * The elements prefixed with 'fen_' are standard Forsyth-Edwards Notation (FEN) elements, |
||
| 83 | * and the elements prefixed with 'pgn_' are standard Portable Game Notation (PGN) elements. |
||
| 84 | * |
||
| 85 | * Each element is a string. |
||
| 86 | * |
||
| 87 | * @var array $gamestate |
||
| 88 | */ |
||
| 89 | |||
| 90 | public $gamestate; |
||
| 91 | /** |
||
| 92 | * board |
||
| 93 | * |
||
| 94 | * A 64-element array, constructed from fen_piece_placement, is used for handling moves. |
||
| 95 | * Its indices are related to the standard tile coordinates as follows: |
||
| 96 | * |
||
| 97 | * <pre> |
||
| 98 | * 8 | 56 57 58 59 60 61 62 63 |
||
| 99 | * 7 | 48 49 50 51 52 53 54 55 |
||
| 100 | * 6 | 40 41 42 43 44 45 46 47 |
||
| 101 | * 5 | 32 33 34 35 36 37 38 39 |
||
| 102 | * 4 | 24 25 26 27 28 29 30 31 |
||
| 103 | * 3 | 16 17 18 19 20 21 22 23 |
||
| 104 | * 2 | 8 9 10 11 12 13 14 15 |
||
| 105 | * 1 | 0 1 2 3 4 5 6 7 |
||
| 106 | * ------------------------ |
||
| 107 | * a b c d e f g h |
||
| 108 | * </pre> |
||
| 109 | * |
||
| 110 | * For example, $board[17] is tile b3 and $board[55] is tile h7. |
||
| 111 | * |
||
| 112 | * @var array $board |
||
| 113 | */ |
||
| 114 | |||
| 115 | public $board; |
||
| 116 | /** |
||
| 117 | * for auto-completion of moves |
||
| 118 | * @var string $ac_move |
||
| 119 | */ |
||
| 120 | |||
| 121 | public $ac_move; |
||
| 122 | /** |
||
| 123 | * array of white's pieces |
||
| 124 | * @var array $w_figures |
||
| 125 | */ |
||
| 126 | |||
| 127 | public $w_figures; |
||
| 128 | /** |
||
| 129 | * array of black's pieces |
||
| 130 | * @var array $b_figures |
||
| 131 | */ |
||
| 132 | |||
| 133 | public $b_figures; |
||
| 134 | /** |
||
| 135 | * updated by handleMove, not used now but might be used in future |
||
| 136 | * @var string $last_move |
||
| 137 | */ |
||
| 138 | |||
| 139 | public $last_move; |
||
| 140 | /** |
||
| 141 | * updated by handleMove, not used now but might be used in future |
||
| 142 | * @var string $captured_piece |
||
| 143 | */ |
||
| 144 | |||
| 145 | public $captured_piece; |
||
| 146 | // -------------- |
||
| 147 | |||
| 148 | // PUBLIC METHODS |
||
| 149 | |||
| 150 | // -------------- |
||
| 151 | |||
| 152 | /** |
||
| 153 | * constructor |
||
| 154 | * |
||
| 155 | * If a failure occurs, $this->error is set to a string containing an error message; |
||
| 156 | * otherwise $this->error is set to an empty string. |
||
| 157 | * |
||
| 158 | * Example: |
||
| 159 | * <pre> |
||
| 160 | * $chessgame = new ChessGame($fen); |
||
| 161 | * if (!empty($chessgame->error)) { |
||
| 162 | * echo "'$fen' invalid: $chessgame->error\n"; |
||
| 163 | * } |
||
| 164 | * </pre> |
||
| 165 | * |
||
| 166 | * @param mixed $param If $param is an array, an existing game is loaded using $param as the nine-element gamestate array described above. |
||
| 167 | * If $param is a non-empty string, a new game is created using $param as a FEN setup position. |
||
| 168 | * Otherwise, a new game is created using the standard starting position. |
||
| 169 | */ |
||
| 170 | public function __construct($param = null) |
||
| 171 | { |
||
| 172 | // for now |
||
| 173 | |||
| 174 | $this->browsing_mode = 0; |
||
|
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
| 175 | |||
| 176 | if (\is_array($param)) { |
||
| 177 | $this->gamestate = $param; |
||
| 178 | |||
| 179 | $this->error = ''; |
||
| 180 | } elseif (\is_string($param) && !empty($param)) { |
||
| 181 | $this->error = $this->init_gamestate($param); |
||
| 182 | } else { |
||
| 183 | $this->init_gamestate(); |
||
| 184 | |||
| 185 | $this->error = ''; |
||
| 186 | } |
||
| 187 | } |
||
| 188 | |||
| 189 | /** |
||
| 190 | * Handle a move. |
||
| 191 | * |
||
| 192 | * @param string $move |
||
| 193 | * @return array A two-element array: |
||
| 194 | * - $move_performed: true if the move was performed and the game state has been updated, false otherwise |
||
| 195 | * - $move_result_text: text message |
||
| 196 | */ |
||
| 197 | public function move($move) |
||
| 198 | { |
||
| 199 | empty($this->error) or \trigger_error(_MD_CHESS_ERROR, \E_USER_ERROR); |
||
| 200 | |||
| 201 | return $this->handleMove($move); |
||
| 202 | } |
||
| 203 | |||
| 204 | /** |
||
| 205 | * get game state |
||
| 206 | * |
||
| 207 | * @return array |
||
| 208 | */ |
||
| 209 | public function gamestate() |
||
| 210 | { |
||
| 211 | empty($this->error) or \trigger_error(_MD_CHESS_ERROR, \E_USER_ERROR); |
||
| 212 | |||
| 213 | return $this->gamestate; |
||
| 214 | } |
||
| 215 | |||
| 216 | // ---------------------------------------------------------------- |
||
| 217 | |||
| 218 | // PRIVATE METHODS - intended for use only by methods of this class |
||
| 219 | |||
| 220 | // ---------------------------------------------------------------- |
||
| 221 | |||
| 222 | /** |
||
| 223 | * Initialize gamestate for a new game. |
||
| 224 | * |
||
| 225 | * If a non-empty string $fen is provided, the game is initialized using $fen as a FEN setup position. |
||
| 226 | * Otherwise the game is initialized using the standard starting position. |
||
| 227 | * |
||
| 228 | * @param string $fen |
||
| 229 | * @return string empty string on success, or error message on failure |
||
| 230 | * |
||
| 231 | * @access private |
||
| 232 | */ |
||
| 233 | public function init_gamestate($fen = null) |
||
| 234 | { |
||
| 235 | $this->gamestate = []; |
||
| 236 | |||
| 237 | if (!empty($fen)) { |
||
| 238 | $setup = true; |
||
| 239 | } else { |
||
| 240 | $setup = false; |
||
| 241 | |||
| 242 | $fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; |
||
| 243 | } |
||
| 244 | |||
| 245 | // check that data is not unreasonably short or long |
||
| 246 | |||
| 247 | if (\mb_strlen($fen) < 23 || \mb_strlen($fen) > 100) { |
||
| 248 | return _MD_CHESS_FENBAD_LENGTH; // invalid length |
||
| 249 | } |
||
| 250 | |||
| 251 | $fen_data = \explode(' ', $fen); |
||
| 252 | |||
| 253 | if (6 != \count($fen_data)) { |
||
| 254 | return _MD_CHESS_FENBAD_FIELD_COUNT; // wrong number of fields |
||
| 255 | } |
||
| 256 | |||
| 257 | $this->gamestate['fen_piece_placement'] = $fen_data[0]; |
||
| 258 | |||
| 259 | $this->gamestate['fen_active_color'] = $fen_data[1]; |
||
| 260 | |||
| 261 | $this->gamestate['fen_castling_availability'] = $fen_data[2]; |
||
| 262 | |||
| 263 | $this->gamestate['fen_en_passant_target_square'] = $fen_data[3]; |
||
| 264 | |||
| 265 | $this->gamestate['fen_halfmove_clock'] = $fen_data[4]; |
||
| 266 | |||
| 267 | $this->gamestate['fen_fullmove_number'] = $fen_data[5]; |
||
| 268 | |||
| 269 | $this->gamestate['pgn_fen'] = $setup ? $fen : null; |
||
| 270 | |||
| 271 | $this->gamestate['pgn_result'] = '*'; |
||
| 272 | |||
| 273 | $this->gamestate['pgn_movetext'] = '*'; |
||
| 274 | |||
| 275 | if (!$this->fen_piece_placement_to_board()) { |
||
| 276 | return _MD_CHESS_FENBAD_PP_INVALID; // piece_placement invalid |
||
| 277 | } elseif ('w' != $this->gamestate['fen_active_color'] && 'b' != $this->gamestate['fen_active_color']) { |
||
| 278 | return _MD_CHESS_FENBAD_AC_INVALID; // active_color invalid |
||
| 279 | } // Since fen_piece_placement_to_board() checked $fen for the correct number of fields above, $castling_availability is non-empty. |
||
| 280 | |||
| 281 | elseif ('-' != $this->gamestate['fen_castling_availability'] && !\preg_match('/^K?Q?k?q?$/', $this->gamestate['fen_castling_availability'])) { |
||
| 282 | return _MD_CHESS_FENBAD_CA_INVALID; // castling_availability invalid |
||
| 283 | } elseif ('-' != $this->gamestate['fen_en_passant_target_square'] && !\preg_match('/^[a-h][36]$/', $this->gamestate['fen_en_passant_target_square'])) { |
||
| 284 | return _MD_CHESS_FENBAD_EP_INVALID; // en_passant_target_square invalid |
||
| 285 | } elseif (!\preg_match('/^\d{0,4}$/', $this->gamestate['fen_halfmove_clock'])) { |
||
| 286 | return _MD_CHESS_FENBAD_HC_INVALID; // halfmove_clock invalid |
||
| 287 | } elseif (!\preg_match('/^\d{0,4}$/', $this->gamestate['fen_fullmove_number']) || $this->gamestate['fen_fullmove_number'] < 1) { |
||
| 288 | return _MD_CHESS_FENBAD_FN_INVALID; // fullmove_number invalid |
||
| 289 | } elseif ($this->insufficient_mating_material()) { |
||
| 290 | return _MD_CHESS_FENBAD_MATERIAL; // insufficient mating material |
||
| 291 | } elseif (('w' == $this->gamestate['fen_active_color'] && $this->kingIsUnderAttack('b', 'w')) |
||
| 292 | || ('b' == $this->gamestate['fen_active_color'] && $this->kingIsUnderAttack('w', 'b'))) { |
||
| 293 | return _MD_CHESS_FENBAD_IN_CHECK; // player to move cannot have opponent in check |
||
| 294 | } elseif ((\mb_strstr($this->gamestate['fen_castling_availability'], 'K') && ('wK' != $this->board[4] || 'wR' != $this->board[7])) |
||
| 295 | || (\mb_strstr($this->gamestate['fen_castling_availability'], 'Q') && ('wK' != $this->board[4] || 'wR' != $this->board[0])) |
||
| 296 | || (\mb_strstr($this->gamestate['fen_castling_availability'], 'k') && ('bK' != $this->board[60] || 'bR' != $this->board[63])) |
||
| 297 | || (\mb_strstr($this->gamestate['fen_castling_availability'], 'q') && ('bK' != $this->board[60] || 'bR' != $this->board[56]))) { |
||
| 298 | return _MD_CHESS_FENBAD_CA_INCONSISTENT; // castling availability inconsistent with piece placement |
||
| 299 | } elseif (('-' != $this->gamestate['fen_en_passant_target_square'] && 3 == $this->gamestate['fen_en_passant_target_square'][1] && 'b' != $this->gamestate['fen_active_color']) |
||
| 300 | || ('-' != $this->gamestate['fen_en_passant_target_square'] && 6 == $this->gamestate['fen_en_passant_target_square'][1] && 'w' != $this->gamestate['fen_active_color'])) { |
||
| 301 | return _MD_CHESS_FENBAD_EP_COLOR; // en passant target square wrong color |
||
| 302 | } elseif ('-' != $this->gamestate['fen_en_passant_target_square'] && 3 == $this->gamestate['fen_en_passant_target_square'][1] |
||
| 303 | && 'wP' != $this->board[$this->boardCoordToIndex($this->gamestate['fen_en_passant_target_square'][0] . '4')]) { |
||
| 304 | return _MD_CHESS_FENBAD_EP_NO_PAWN; // en passant target square for nonexistent pawn |
||
| 305 | } elseif ('-' != $this->gamestate['fen_en_passant_target_square'] && 6 == $this->gamestate['fen_en_passant_target_square'][1] |
||
| 306 | && 'bP' != $this->board[$this->boardCoordToIndex($this->gamestate['fen_en_passant_target_square'][0] . '5')]) { |
||
| 307 | return _MD_CHESS_FENBAD_EP_NO_PAWN; // en passant target square for nonexistent pawn |
||
| 308 | } |
||
| 309 | |||
| 310 | #echo "In " . __CLASS__ . '::' . __FUNCTION__ . "\n";#*#DEBUG# |
||
| 311 | |||
| 312 | #var_dump('gamestate', $this->gamestate);#*#DEBUG# |
||
| 313 | |||
| 314 | return ''; // successful |
||
| 315 | } |
||
| 316 | |||
| 317 | /** |
||
| 318 | * Check whether a path is blocked. |
||
| 319 | * |
||
| 320 | * check a series of tiles given a start, an end tile |
||
| 321 | * which is not included to the check and a position |
||
| 322 | * change for each iteration. return true if not blocked. |
||
| 323 | * all values are given for 1dim board. |
||
| 324 | * |
||
| 325 | * @param int $start |
||
| 326 | * @param int $end |
||
| 327 | * @param int $change |
||
| 328 | * @return bool |
||
| 329 | * |
||
| 330 | * @access private |
||
| 331 | */ |
||
| 332 | public function pathIsNotBlocked($start, $end, $change) |
||
| 333 | { |
||
| 334 | for ($pos = $start; $pos != $end; $pos += $change) { |
||
| 335 | #echo "path: $pos: '$this->board[$pos]' "; #*#DEBUG# |
||
| 336 | |||
| 337 | if (!$this->is_empty_tile($pos)) { |
||
| 338 | return false; |
||
| 339 | } |
||
| 340 | } |
||
| 341 | |||
| 342 | return true; |
||
| 343 | } |
||
| 344 | |||
| 345 | /** |
||
| 346 | * Get path. |
||
| 347 | * |
||
| 348 | * get the empty tiles between start and end as an 1dim array. |
||
| 349 | * whether the path is clear is not checked. |
||
| 350 | * |
||
| 351 | * @param int $start |
||
| 352 | * @param int $end |
||
| 353 | * @param int $change |
||
| 354 | * @return array |
||
| 355 | * |
||
| 356 | * @access private |
||
| 357 | */ |
||
| 358 | public function getPath($start, $end, $change) |
||
| 359 | { |
||
| 360 | $path = []; |
||
| 361 | |||
| 362 | for ($pos = $start; $pos != $end; $pos += $change) { |
||
| 363 | $path[] = $pos; |
||
| 364 | } |
||
| 365 | |||
| 366 | return $path; |
||
| 367 | } |
||
| 368 | |||
| 369 | /** |
||
| 370 | * get path change |
||
| 371 | * |
||
| 372 | * get the change value that must be added to create |
||
| 373 | * the 1dim path for figure moving from fig_pos to |
||
| 374 | * dest_pos. it is assumed that the movement is valid! |
||
| 375 | * no additional checks as in tileIsReachable are |
||
| 376 | * performed. rook, queen and bishop are the only |
||
| 377 | * units that can have empty tiles in between. |
||
| 378 | * |
||
| 379 | * @param string $fig |
||
| 380 | * @param int $fig_pos |
||
| 381 | * @param int $dest_pos |
||
| 382 | * @return int |
||
| 383 | * |
||
| 384 | * @access private |
||
| 385 | */ |
||
| 386 | public function getPathChange($fig, $fig_pos, $dest_pos) |
||
| 387 | { |
||
| 388 | $change = 0; |
||
| 389 | |||
| 390 | $fy = \floor($fig_pos / 8); |
||
| 391 | |||
| 392 | $fx = $fig_pos % 8; |
||
| 393 | |||
| 394 | $dx = $dest_pos % 8; |
||
| 395 | |||
| 396 | $dy = \floor($dest_pos / 8); |
||
| 397 | |||
| 398 | switch ($fig) { |
||
| 399 | /* bishop */ case 'B': |
||
| 400 | $change = $dy < $fy ? -8 : 8; |
||
| 401 | $change += $dx < $fx ? -1 : 1; |
||
| 402 | break; |
||
| 403 | /* rook */ case 'R': |
||
| 404 | if ($fx == $dx) { |
||
| 405 | $change = $dy < $fy ? -8 : 8; |
||
| 406 | } else { |
||
| 407 | $change = $dx < $fx ? -1 : 1; |
||
| 408 | } |
||
| 409 | break; |
||
| 410 | /* queen */ case 'Q': |
||
| 411 | if (\abs($fx - $dx) == \abs($fy - $dy)) { |
||
| 412 | $change = $dy < $fy ? -8 : 8; |
||
| 413 | |||
| 414 | $change += $dx < $fx ? -1 : 1; |
||
| 415 | } elseif ($fx == $dx) { |
||
| 416 | $change = $dy < $fy ? -8 : 8; |
||
| 417 | } else { |
||
| 418 | $change = $dx < $fx ? -1 : 1; |
||
| 419 | } |
||
| 420 | break; |
||
| 421 | } |
||
| 422 | |||
| 423 | return $change; |
||
| 424 | } |
||
| 425 | |||
| 426 | /** |
||
| 427 | * Check whether a tile is reachable. |
||
| 428 | * |
||
| 429 | * check whether dest_pos is in reach for unit of fig_type |
||
| 430 | * at tile fig_pos. it is not checked whether the tile |
||
| 431 | * itself is occupied but only the tiles in between. |
||
| 432 | * this function does not check pawns. |
||
| 433 | * |
||
| 434 | * @param string $fig |
||
| 435 | * @param int $fig_pos |
||
| 436 | * @param int $dest_pos |
||
| 437 | * @return bool |
||
| 438 | * |
||
| 439 | * @access private |
||
| 440 | */ |
||
| 441 | public function tileIsReachable($fig, $fig_pos, $dest_pos) |
||
| 442 | { |
||
| 443 | if ($fig_pos == $dest_pos) { |
||
| 444 | return; |
||
| 445 | } |
||
| 446 | |||
| 447 | $result = 0; |
||
| 448 | |||
| 449 | $fy = \floor($fig_pos / 8); |
||
| 450 | |||
| 451 | $fx = $fig_pos % 8; |
||
| 452 | |||
| 453 | $dy = \floor($dest_pos / 8); |
||
| 454 | |||
| 455 | $dx = $dest_pos % 8; |
||
| 456 | |||
| 457 | /* DEBUG: echo "$fx,$fy --> $dx,$dy: "; */ |
||
| 458 | |||
| 459 | switch ($fig) { |
||
| 460 | /* knight */ case 'N': |
||
| 461 | if (1 == \abs($fx - $dx) && 2 == \abs($fy - $dy)) { |
||
| 462 | $result = 1; |
||
| 463 | } |
||
| 464 | if (1 == \abs($fy - $dy) && 2 == \abs($fx - $dx)) { |
||
| 465 | $result = 1; |
||
| 466 | } |
||
| 467 | break; |
||
| 468 | /* bishop */ case 'B': |
||
| 469 | if (\abs($fx - $dx) != \abs($fy - $dy)) { |
||
| 470 | break; |
||
| 471 | } |
||
| 472 | if ($dy < $fy) { |
||
| 473 | $change = -8; |
||
| 474 | } else { |
||
| 475 | $change = 8; |
||
| 476 | } |
||
| 477 | if ($dx < $fx) { |
||
| 478 | --$change; |
||
| 479 | } else { |
||
| 480 | ++$change; |
||
| 481 | } |
||
| 482 | if ($this->pathIsNotBlocked($fig_pos + $change, $dest_pos, $change)) { |
||
| 483 | $result = 1; |
||
| 484 | } |
||
| 485 | break; |
||
| 486 | /* rook */ case 'R': |
||
| 487 | if ($fx != $dx && $fy != $dy) { |
||
| 488 | break; |
||
| 489 | } |
||
| 490 | if ($fx == $dx) { |
||
| 491 | if ($dy < $fy) { |
||
| 492 | $change = -8; |
||
| 493 | } else { |
||
| 494 | $change = 8; |
||
| 495 | } |
||
| 496 | } else { |
||
| 497 | if ($dx < $fx) { |
||
| 498 | $change = -1; |
||
| 499 | } else { |
||
| 500 | $change = 1; |
||
| 501 | } |
||
| 502 | } |
||
| 503 | if ($this->pathIsNotBlocked($fig_pos + $change, $dest_pos, $change)) { |
||
| 504 | $result = 1; |
||
| 505 | } |
||
| 506 | break; |
||
| 507 | /* queen */ case 'Q': |
||
| 508 | if (\abs($fx - $dx) != \abs($fy - $dy) && $fx != $dx && $fy != $dy) { |
||
| 509 | break; |
||
| 510 | } |
||
| 511 | if (\abs($fx - $dx) == \abs($fy - $dy)) { |
||
| 512 | if ($dy < $fy) { |
||
| 513 | $change = -8; |
||
| 514 | } else { |
||
| 515 | $change = 8; |
||
| 516 | } |
||
| 517 | |||
| 518 | if ($dx < $fx) { |
||
| 519 | --$change; |
||
| 520 | } else { |
||
| 521 | ++$change; |
||
| 522 | } |
||
| 523 | } elseif ($fx == $dx) { |
||
| 524 | if ($dy < $fy) { |
||
| 525 | $change = -8; |
||
| 526 | } else { |
||
| 527 | $change = 8; |
||
| 528 | } |
||
| 529 | } else { |
||
| 530 | if ($dx < $fx) { |
||
| 531 | $change = -1; |
||
| 532 | } else { |
||
| 533 | $change = 1; |
||
| 534 | } |
||
| 535 | } |
||
| 536 | if ($this->pathIsNotBlocked($fig_pos + $change, $dest_pos, $change)) { |
||
| 537 | $result = 1; |
||
| 538 | } |
||
| 539 | break; |
||
| 540 | /* king */ case 'K': |
||
| 541 | if (\abs($fx - $dx) > 1 || \abs($fy - $dy) > 1) { |
||
| 542 | break; |
||
| 543 | } |
||
| 544 | $kings = 0; |
||
| 545 | $adj_tiles = $this->getAdjTiles($dest_pos); |
||
| 546 | foreach ($adj_tiles as $tile) { |
||
| 547 | if ('K' == $this->board[$tile][1]) { |
||
| 548 | $kings++; |
||
| 549 | } |
||
| 550 | } |
||
| 551 | if (2 == $kings) { |
||
| 552 | break; |
||
| 553 | } |
||
| 554 | $result = 1; |
||
| 555 | break; |
||
| 556 | } |
||
| 557 | |||
| 558 | /* DEBUG: echo " $result<br>"; */ |
||
| 559 | |||
| 560 | return $result; |
||
|
0 ignored issues
–
show
|
|||
| 561 | } |
||
| 562 | |||
| 563 | /** |
||
| 564 | * Check whether a pawn can attack a tile. |
||
| 565 | * |
||
| 566 | * @param int $fig_pos Position of pawn |
||
| 567 | * @param int $dest_pos Tile to check |
||
| 568 | * @return bool True if pawn can attack |
||
| 569 | * |
||
| 570 | * @access private |
||
| 571 | */ |
||
| 572 | public function checkPawnAttack($fig_pos, $dest_pos) |
||
| 573 | { |
||
| 574 | if ('w' == $this->board[$fig_pos][0]) { |
||
| 575 | if ($fig_pos % 8 > 0 && $dest_pos == $fig_pos + 7) { |
||
| 576 | return true; |
||
| 577 | } |
||
| 578 | |||
| 579 | if ($fig_pos % 8 < 7 && $dest_pos == $fig_pos + 9) { |
||
| 580 | return true; |
||
| 581 | } |
||
| 582 | } elseif ('b' == $this->board[$fig_pos][0]) { |
||
| 583 | if ($fig_pos % 8 < 7 && $dest_pos == $fig_pos - 7) { |
||
| 584 | return true; |
||
| 585 | } |
||
| 586 | |||
| 587 | if ($fig_pos % 8 > 0 && $dest_pos == $fig_pos - 9) { |
||
| 588 | return true; |
||
| 589 | } |
||
| 590 | } |
||
| 591 | |||
| 592 | return false; |
||
| 593 | } |
||
| 594 | |||
| 595 | /** |
||
| 596 | * Check whether a pawn move is legal. |
||
| 597 | * |
||
| 598 | * check whether pawn at figpos may move to destpos. |
||
| 599 | * first move may be two tiles instead of just one. |
||
| 600 | * again the last tile is not checked but just the path |
||
| 601 | * in between. |
||
| 602 | * |
||
| 603 | * @param int $fig_pos Position of pawn. |
||
| 604 | * @param int $dest_pos Destination tile. |
||
| 605 | * @return bool True if move is legal |
||
| 606 | * |
||
| 607 | * @access private |
||
| 608 | */ |
||
| 609 | public function checkPawnMove($fig_pos, $dest_pos) |
||
| 610 | { |
||
| 611 | $first_move = 0; |
||
| 612 | |||
| 613 | if ('w' == $this->board[$fig_pos][0]) { |
||
| 614 | if ($fig_pos >= 8 && $fig_pos <= 15) { |
||
| 615 | $first_move = 1; |
||
| 616 | } |
||
| 617 | |||
| 618 | if ($dest_pos == $fig_pos + 8) { |
||
| 619 | return true; |
||
| 620 | } |
||
| 621 | |||
| 622 | if ($first_move && ($dest_pos == $fig_pos + 16)) { |
||
| 623 | if ($this->is_empty_tile($fig_pos + 8)) { |
||
| 624 | return true; |
||
| 625 | } |
||
| 626 | } |
||
| 627 | } elseif ('b' == $this->board[$fig_pos][0]) { |
||
| 628 | if ($fig_pos >= 48 && $fig_pos <= 55) { |
||
| 629 | $first_move = 1; |
||
| 630 | } |
||
| 631 | |||
| 632 | if ($dest_pos == $fig_pos - 8) { |
||
| 633 | return true; |
||
| 634 | } |
||
| 635 | |||
| 636 | if ($first_move && ($dest_pos == $fig_pos - 16)) { |
||
| 637 | if ($this->is_empty_tile($fig_pos - 8)) { |
||
| 638 | return true; |
||
| 639 | } |
||
| 640 | } |
||
| 641 | } |
||
| 642 | |||
| 643 | return false; |
||
| 644 | } |
||
| 645 | |||
| 646 | /** |
||
| 647 | * Check whether a tile is under attack by the specified player. |
||
| 648 | * |
||
| 649 | * @param string $opp Attacking color ('w' or 'b') |
||
| 650 | * @param int $dest_pos Tile to check |
||
| 651 | * @return bool |
||
| 652 | * |
||
| 653 | * @access private |
||
| 654 | */ |
||
| 655 | public function tileIsUnderAttack($opp, $dest_pos) |
||
| 656 | { |
||
| 657 | #var_dump('tileIsUnderAttack, opp', $opp, 'dest_pos', $dest_pos, 'board', $board);#*#DEBUG# |
||
| 658 | |||
| 659 | for ($i = 0; $i < 64; $i++) { |
||
| 660 | if ($this->board[$i][0] == $opp) { |
||
| 661 | if (('P' == $this->board[$i][1] && $this->checkPawnAttack($i, $dest_pos)) |
||
| 662 | || ('P' != $this->board[$i][1] |
||
| 663 | && $this->tileIsReachable($this->board[$i][1], $i, $dest_pos))) { |
||
| 664 | /*DEBUG: echo "attack test: $i: ",$opp,"P<br>"; */ |
||
| 665 | |||
| 666 | return true; |
||
| 667 | } |
||
| 668 | } |
||
| 669 | } |
||
| 670 | |||
| 671 | return false; |
||
| 672 | } |
||
| 673 | |||
| 674 | /** |
||
| 675 | * Check whether a player's king can be attacked by his opponent. |
||
| 676 | * |
||
| 677 | * @param string $player Player's color ('w' or 'b') |
||
| 678 | * @param string $opp Opponent's color ('w' or 'b') |
||
| 679 | * @return bool |
||
| 680 | * |
||
| 681 | * @access private |
||
| 682 | */ |
||
| 683 | public function kingIsUnderAttack($player, $opp) |
||
| 684 | { |
||
| 685 | #var_dump('kingIsUnderAttack, player', $player, 'opp', $opp, 'board', $board);#*#DEBUG# |
||
| 686 | |||
| 687 | for ($i = 0; $i < 64; $i++) { |
||
| 688 | if ($this->board[$i][0] == $player && 'K' == $this->board[$i][1]) { |
||
| 689 | $king_pos = $i; |
||
| 690 | |||
| 691 | break; |
||
| 692 | } |
||
| 693 | } |
||
| 694 | |||
| 695 | /*DEBUG: echo "$player king is at $king_pos<br>"; */ |
||
| 696 | |||
| 697 | return $this->tileIsUnderAttack($opp, $king_pos); |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
| 698 | } |
||
| 699 | |||
| 700 | /** |
||
| 701 | * Check whether player's king is checkmated by his opponent. |
||
| 702 | * |
||
| 703 | * @param string $player Player's color ('w' or 'b') |
||
| 704 | * @param string $opp Opponent's color ('w' or 'b') |
||
| 705 | * @return bool |
||
| 706 | * |
||
| 707 | * @access private |
||
| 708 | */ |
||
| 709 | public function isCheckMate($player, $opp) |
||
| 710 | { |
||
| 711 | for ($i = 0; $i < 64; $i++) { |
||
| 712 | if ($this->board[$i][0] == $player && 'K' == $this->board[$i][1]) { |
||
| 713 | $king_pos = $i; |
||
| 714 | |||
| 715 | $king_x = $i % 8; |
||
| 716 | |||
| 717 | $king_y = \floor($i / 8); |
||
| 718 | |||
| 719 | break; |
||
| 720 | } |
||
| 721 | } |
||
| 722 | |||
| 723 | /* test adjacent tiles while king is temporarly removed */ |
||
| 724 | |||
| 725 | $adj_tiles = $this->getAdjTiles($king_pos); |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
| 726 | |||
| 727 | $contents = $this->board[$king_pos]; |
||
| 728 | |||
| 729 | $this->clear_tile($king_pos); |
||
| 730 | |||
| 731 | foreach ($adj_tiles as $dest_pos) { |
||
| 732 | if ($this->board[$dest_pos][0] == $player) { |
||
| 733 | continue; |
||
| 734 | } |
||
| 735 | |||
| 736 | if ($this->tileIsUnderAttack($opp, $dest_pos)) { |
||
| 737 | continue; |
||
| 738 | } |
||
| 739 | |||
| 740 | $this->board[$king_pos] = $contents; |
||
| 741 | |||
| 742 | return false; |
||
| 743 | } |
||
| 744 | |||
| 745 | $this->board[$king_pos] = $contents; |
||
| 746 | |||
| 747 | /* DEBUG: echo "King cannot escape by itself! "; */ |
||
| 748 | |||
| 749 | /* get all figures that attack the king */ |
||
| 750 | |||
| 751 | $attackers = []; |
||
| 752 | |||
| 753 | $count = 0; |
||
| 754 | |||
| 755 | for ($i = 0; $i < 64; $i++) { |
||
| 756 | if ($this->board[$i][0] == $opp) { |
||
| 757 | if (('P' == $this->board[$i][1] && $this->checkPawnAttack($i, $king_pos)) |
||
| 758 | || ('P' != $this->board[$i][1] |
||
| 759 | && $this->tileIsReachable($this->board[$i][1], $i, $king_pos))) { |
||
| 760 | $attackers[$count++] = $i; |
||
| 761 | } |
||
| 762 | } |
||
| 763 | } |
||
| 764 | |||
| 765 | /* DEBUG: |
||
| 766 | for( $i = 0; $i < $count; $i++ ) |
||
| 767 | echo "Attacker: $attackers[$i] "; |
||
| 768 | echo "Attackercount: ",count($attackers), " "; */ |
||
| 769 | |||
| 770 | /* if more than one there is no chance to escape */ |
||
| 771 | |||
| 772 | if ($count > 1) { |
||
| 773 | return true; |
||
| 774 | } |
||
| 775 | |||
| 776 | /* check whether attacker can be killed by own figure */ |
||
| 777 | |||
| 778 | $dest_pos = $attackers[0]; |
||
| 779 | |||
| 780 | for ($i = 0; $i < 64; $i++) { |
||
| 781 | if ($this->board[$i][0] == $player) { |
||
| 782 | if (('P' == $this->board[$i][1] && $this->checkPawnAttack($i, $dest_pos)) |
||
| 783 | || ('P' != $this->board[$i][1] && 'K' != $this->board[$i][1] |
||
| 784 | && $this->tileIsReachable($this->board[$i][1], $i, $dest_pos)) |
||
| 785 | || ('K' == $this->board[$i][1] |
||
| 786 | && $this->tileIsReachable($this->board[$i][1], $i, $dest_pos) |
||
| 787 | && !$this->tileIsUnderAttack($opp, $dest_pos))) { |
||
| 788 | /* DEBUG: echo "candidate: $i "; */ |
||
| 789 | |||
| 790 | $can_kill_atk = 0; |
||
| 791 | |||
| 792 | $contents_def = $this->board[$i]; |
||
| 793 | |||
| 794 | $contents_atk = $this->board[$dest_pos]; |
||
| 795 | |||
| 796 | $this->board[$dest_pos] = $this->board[$i]; |
||
| 797 | |||
| 798 | $this->clear_tile($i); |
||
| 799 | |||
| 800 | if (!$this->tileIsUnderAttack($opp, $king_pos)) { |
||
| 801 | $can_kill_atk = 1; |
||
| 802 | } |
||
| 803 | |||
| 804 | $this->board[$i] = $contents_def; |
||
| 805 | |||
| 806 | $this->board[$dest_pos] = $contents_atk; |
||
| 807 | |||
| 808 | if ($can_kill_atk) { |
||
| 809 | /* DEBUG: echo "$i can kill attacker"; */ |
||
| 810 | |||
| 811 | return false; |
||
| 812 | } |
||
| 813 | } |
||
| 814 | } |
||
| 815 | } |
||
| 816 | |||
| 817 | /* check whether own unit can block the way */ |
||
| 818 | |||
| 819 | /* if attacking unit is a knight there |
||
| 820 | * is no way to block the path */ |
||
| 821 | |||
| 822 | if ('N' == $this->board[$dest_pos][1]) { |
||
| 823 | return true; |
||
| 824 | } |
||
| 825 | |||
| 826 | /* if enemy is adjacent to king there is no |
||
| 827 | * way either */ |
||
| 828 | |||
| 829 | $dest_x = $dest_pos % 8; |
||
| 830 | |||
| 831 | $dest_y = \floor($dest_pos / 8); |
||
| 832 | |||
| 833 | if (\abs($dest_x - $king_x) <= 1 && \abs($dest_y - $king_y) <= 1) { |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Comprehensibility
Best Practice
introduced
by
|
|||
| 834 | return true; |
||
| 835 | } |
||
| 836 | |||
| 837 | /* get the list of tiles between king and attacking |
||
| 838 | * unit that can be blocked to stop the attack */ |
||
| 839 | |||
| 840 | $change = $this->getPathChange($this->board[$dest_pos][1], $dest_pos, $king_pos); |
||
| 841 | |||
| 842 | /* DEBUG: echo "path change: $change "; */ |
||
| 843 | |||
| 844 | $path = $this->getPath($dest_pos + $change, $king_pos, $change); |
||
| 845 | |||
| 846 | /* DEBUG: foreach( $path as $tile ) echo "tile: $tile "; */ |
||
| 847 | |||
| 848 | foreach ($path as $pos) { |
||
| 849 | for ($i = 0; $i < 64; $i++) { |
||
| 850 | if ($this->board[$i][0] == $player) { |
||
| 851 | if (('P' == $this->board[$i][1] && $this->checkPawnMove($i, $pos)) |
||
| 852 | || ('P' != $this->board[$i][1] && 'K' != $this->board[$i][1] |
||
| 853 | && $this->tileIsReachable($this->board[$i][1], $i, $pos))) { |
||
| 854 | /* DEBUG: echo "$i can block "; */ |
||
| 855 | |||
| 856 | return false; |
||
| 857 | } |
||
| 858 | } |
||
| 859 | } |
||
| 860 | } |
||
| 861 | |||
| 862 | return true; |
||
| 863 | } |
||
| 864 | |||
| 865 | /** |
||
| 866 | * Check whether player is stalemated. |
||
| 867 | * |
||
| 868 | * @param string $player Color of player who has the move ('w' or 'b') |
||
| 869 | * @param string $opp Opponent's color ('w' or 'b') |
||
| 870 | * @return bool |
||
| 871 | * |
||
| 872 | * @todo recognize when move is not possible because of check |
||
| 873 | * |
||
| 874 | * @access private |
||
| 875 | */ |
||
| 876 | public function isStaleMate($player, $opp) |
||
| 877 | { |
||
| 878 | for ($i = 0; $i < 64; $i++) { |
||
| 879 | if ($this->board[$i][0] == $player) { |
||
| 880 | switch ($this->board[$i][1]) { |
||
| 881 | case 'K': |
||
| 882 | $adj_tiles = $this->getAdjTiles($i); |
||
| 883 | foreach ($adj_tiles as $pos) { |
||
| 884 | if ($this->board[$pos][0] == $player) { |
||
| 885 | continue; |
||
| 886 | } |
||
| 887 | |||
| 888 | if ($this->tileIsUnderAttack($opp, $pos)) { |
||
| 889 | continue; |
||
| 890 | } |
||
| 891 | |||
| 892 | return false; |
||
| 893 | } |
||
| 894 | /* DEBUG: echo "King cannot escape by itself! "; */ |
||
| 895 | break; |
||
| 896 | case 'P': |
||
| 897 | if ('w' == $player) { |
||
| 898 | if ($this->is_empty_tile($i + 8)) { |
||
| 899 | return false; |
||
| 900 | } |
||
| 901 | |||
| 902 | if ($i % 8 > 0 && $this->board[$i + 7][0] != $player) { |
||
| 903 | return false; |
||
| 904 | } |
||
| 905 | |||
| 906 | if ($i % 8 < 7 && $this->board[$i + 9][0] != $player) { |
||
| 907 | return false; |
||
| 908 | } |
||
| 909 | } else { |
||
| 910 | if ($this->is_empty_tile($i - 8)) { |
||
| 911 | return false; |
||
| 912 | } |
||
| 913 | |||
| 914 | if ($i % 8 > 0 && $this->board[$i - 9][0] != $player) { |
||
| 915 | return false; |
||
| 916 | } |
||
| 917 | |||
| 918 | if ($i % 8 < 7 && $this->board[$i - 7][0] != $player) { |
||
| 919 | return false; |
||
| 920 | } |
||
| 921 | } |
||
| 922 | break; |
||
| 923 | case 'B': |
||
| 924 | if ($i - 9 >= 0 && $this->board[$i - 9][0] != $player) { |
||
| 925 | return false; |
||
| 926 | } |
||
| 927 | if ($i - 7 >= 0 && $this->board[$i - 7][0] != $player) { |
||
| 928 | return false; |
||
| 929 | } |
||
| 930 | if ($i + 9 <= 63 && $this->board[$i + 9][0] != $player) { |
||
| 931 | return false; |
||
| 932 | } |
||
| 933 | if ($i + 7 <= 63 && $this->board[$i + 7][0] != $player) { |
||
| 934 | return false; |
||
| 935 | } |
||
| 936 | break; |
||
| 937 | case 'R': |
||
| 938 | if ($i - 8 >= 0 && $this->board[$i - 8][0] != $player) { |
||
| 939 | return false; |
||
| 940 | } |
||
| 941 | if ($i - 1 >= 0 && $this->board[$i - 1][0] != $player) { |
||
| 942 | return false; |
||
| 943 | } |
||
| 944 | if ($i + 8 <= 63 && $this->board[$i + 8][0] != $player) { |
||
| 945 | return false; |
||
| 946 | } |
||
| 947 | if ($i + 1 <= 63 && $this->board[$i + 1][0] != $player) { |
||
| 948 | return false; |
||
| 949 | } |
||
| 950 | break; |
||
| 951 | case 'Q': |
||
| 952 | $adj_tiles = $this->getAdjTiles($i); |
||
| 953 | foreach ($adj_tiles as $pos) { |
||
| 954 | if ($this->board[$pos][0] != $player) { |
||
| 955 | return false; |
||
| 956 | } |
||
| 957 | } |
||
| 958 | break; |
||
| 959 | case 'N': |
||
| 960 | if ($i - 17 >= 0 && $this->board[$i - 17][0] != $player) { |
||
| 961 | return false; |
||
| 962 | } |
||
| 963 | if ($i - 15 >= 0 && $this->board[$i - 15][0] != $player) { |
||
| 964 | return false; |
||
| 965 | } |
||
| 966 | if ($i - 6 >= 0 && $this->board[$i - 6][0] != $player) { |
||
| 967 | return false; |
||
| 968 | } |
||
| 969 | if ($i + 10 <= 63 && $this->board[$i + 10][0] != $player) { |
||
| 970 | return false; |
||
| 971 | } |
||
| 972 | if ($i + 17 <= 63 && $this->board[$i + 17][0] != $player) { |
||
| 973 | return false; |
||
| 974 | } |
||
| 975 | if ($i + 15 <= 63 && $this->board[$i + 15][0] != $player) { |
||
| 976 | return false; |
||
| 977 | } |
||
| 978 | if ($i + 6 <= 63 && $this->board[$i + 6][0] != $player) { |
||
| 979 | return false; |
||
| 980 | } |
||
| 981 | if ($i - 10 >= 0 && $this->board[$i - 10][0] != $player) { |
||
| 982 | return false; |
||
| 983 | } |
||
| 984 | break; |
||
| 985 | } |
||
| 986 | } |
||
| 987 | } |
||
| 988 | |||
| 989 | return true; |
||
| 990 | } |
||
| 991 | |||
| 992 | /** |
||
| 993 | * Generate informational text message with parameters. |
||
| 994 | * |
||
| 995 | * Example: |
||
| 996 | * <pre> |
||
| 997 | * echo move_msg('cannot find {$param[1]} pawn in column {$param[2]}', 'b', 'e'); |
||
| 998 | * - prints: "cannot find b pawn in column e" |
||
| 999 | * </pre> |
||
| 1000 | * |
||
| 1001 | * @param string $text |
||
| 1002 | * @return string |
||
| 1003 | * |
||
| 1004 | * @access private |
||
| 1005 | */ |
||
| 1006 | public function move_msg($text) |
||
| 1007 | { |
||
| 1008 | $param = \func_get_args(); |
||
|
0 ignored issues
–
show
|
|||
| 1009 | |||
| 1010 | #var_dump('move_msg, text', $text, 'param', $param);#*#DEBUG# |
||
| 1011 | |||
| 1012 | return eval("return \"$text\";"); |
||
|
0 ignored issues
–
show
|
|||
| 1013 | } |
||
| 1014 | |||
| 1015 | /** |
||
| 1016 | * Translate Standard Algebraic Notation (SAN) into a full move description. |
||
| 1017 | * |
||
| 1018 | * The completed move is placed in $this->ac_move. |
||
| 1019 | * |
||
| 1020 | * @param string $player 'w' or 'b' |
||
| 1021 | * @param string $move |
||
| 1022 | * <pre> |
||
| 1023 | * [a-h][1-8|a-h][RNBQK] pawn move/attack |
||
| 1024 | * [PRNBQK][a-h][1-8] figure move |
||
| 1025 | * [PRNBQK][:x][a-h][1-8] figure attack |
||
| 1026 | * [PRNBQK][1-8|a-h][a-h][1-8] ambigous figure move |
||
| 1027 | * [a-h][:x][a-h][1-8][[RNBQK] ambigous pawn attack |
||
| 1028 | * [PRNBQK][1-8|a-h][:x][a-h][1-8] ambigous figure attack |
||
| 1029 | * </pre> |
||
| 1030 | * @return string Empty string if successful, otherwise error message |
||
| 1031 | * |
||
| 1032 | * @access private |
||
| 1033 | */ |
||
| 1034 | public function completeMove($player, $move) |
||
| 1035 | { |
||
| 1036 | /* |
||
| 1037 | * [a-h][1-8|a-h][RNBQK] pawn move/attack |
||
| 1038 | * [PRNBQK][a-h][1-8] figure move |
||
| 1039 | * [PRNBQK][:x][a-h][1-8] figure attack |
||
| 1040 | * [PRNBQK][1-8|a-h][a-h][1-8] ambigous figure move |
||
| 1041 | * [a-h][:x][a-h][1-8][[RNBQK] ambigous pawn attack |
||
| 1042 | * [PRNBQK][1-8|a-h][:x][a-h][1-8] ambigous figure attack |
||
| 1043 | */ |
||
| 1044 | |||
| 1045 | $error = _MD_CHESS_MOVE_UNKNOWN; // "format is totally unknown!" |
||
| 1046 | |||
| 1047 | $this->ac_move = $move; |
||
| 1048 | |||
| 1049 | if (\mb_strlen($move) >= 6) { |
||
| 1050 | /* full move: a pawn requires a ? in the end |
||
| 1051 | * to automatically choose a queen on last line */ |
||
| 1052 | |||
| 1053 | if (0 === \strpos($move, 'P')) { |
||
| 1054 | if ($move[\mb_strlen($move) - 1] < 'A' || $move[\mb_strlen($move) - 1] > 'Z') { |
||
| 1055 | $this->ac_move = "$move?"; |
||
| 1056 | } |
||
| 1057 | } |
||
| 1058 | |||
| 1059 | return ''; |
||
| 1060 | } |
||
| 1061 | |||
| 1062 | /* allow last letter to be a capital one indicating |
||
| 1063 | * the chessmen a pawn is supposed to transform into, |
||
| 1064 | * when entering the last file. we split this character |
||
| 1065 | * to keep the autocompletion process the same. */ |
||
| 1066 | |||
| 1067 | $pawn_upg = '?'; |
||
| 1068 | |||
| 1069 | if ($move[\mb_strlen($move) - 1] >= 'A' && $move[\mb_strlen($move) - 1] <= 'Z') { |
||
| 1070 | $pawn_upg = $move[\mb_strlen($move) - 1]; |
||
| 1071 | |||
| 1072 | $move = \mb_substr($move, 0, -1); |
||
| 1073 | } |
||
| 1074 | |||
| 1075 | // remove trailing '=', if present |
||
| 1076 | |||
| 1077 | if ('=' == $move[mb_strlen($move) - 1]) { |
||
| 1078 | $move = \mb_substr($move, 0, -1); |
||
| 1079 | } |
||
| 1080 | |||
| 1081 | if ('P' == $pawn_upg || 'K' == $pawn_upg) { |
||
| 1082 | return _MD_CHESS_MOVE_PAWN_MAY_BECOME; |
||
| 1083 | } // "A pawn may only become either a knight, a bishop, a rook or a queen!" |
||
| 1084 | |||
| 1085 | if ($move[0] >= 'a' && $move[0] <= 'h') { |
||
| 1086 | /* pawn move. either it's 2 or for characters as |
||
| 1087 | * listed above */ |
||
| 1088 | |||
| 1089 | if (4 == mb_strlen($move)) { |
||
| 1090 | if ('x' != $move[1]) { |
||
| 1091 | return _MD_CHESS_MOVE_USE_X; |
||
| 1092 | } // "use x to indicate an attack" |
||
| 1093 | |||
| 1094 | $dest_x = $move[2]; |
||
| 1095 | |||
| 1096 | $dest_y = $move[3]; |
||
| 1097 | |||
| 1098 | $src_x = $move[0]; |
||
| 1099 | |||
| 1100 | if ('w' == $player) { |
||
| 1101 | $src_y = $dest_y - 1; |
||
| 1102 | } else { |
||
| 1103 | $src_y = $dest_y + 1; |
||
| 1104 | } |
||
| 1105 | |||
| 1106 | $this->ac_move = \sprintf( |
||
| 1107 | 'P%s%dx%s%d%s', |
||
| 1108 | $src_x, |
||
| 1109 | $src_y, |
||
| 1110 | $dest_x, |
||
| 1111 | $dest_y, |
||
| 1112 | $pawn_upg |
||
| 1113 | ); |
||
| 1114 | |||
| 1115 | return ''; |
||
| 1116 | } elseif (2 == mb_strlen($move)) { |
||
| 1117 | $fig = \sprintf('%sP', $player); |
||
| 1118 | |||
| 1119 | if ($move[1] >= '1' && $move[1] <= '8') { |
||
| 1120 | /* pawn move */ |
||
| 1121 | |||
| 1122 | $pos = $this->boardCoordToIndex($move); |
||
| 1123 | |||
| 1124 | if (64 == $pos) { |
||
| 1125 | return $this->move_msg(_MD_CHESS_MOVE_COORD_INVALID, $move); |
||
| 1126 | } // "coordinate $move is invalid" |
||
| 1127 | |||
| 1128 | if ('w' == $player) { |
||
| 1129 | while ($pos >= 0 && $this->board[$pos] != $fig) { |
||
| 1130 | $pos -= 8; |
||
| 1131 | } |
||
| 1132 | |||
| 1133 | if ($pos < 0) { |
||
| 1134 | $not_found = 1; |
||
| 1135 | } |
||
| 1136 | } else { |
||
| 1137 | while ($pos <= 63 && $this->board[$pos] != $fig) { |
||
| 1138 | $pos += 8; |
||
| 1139 | } |
||
| 1140 | |||
| 1141 | if ($pos > 63) { |
||
| 1142 | $not_found = 1; |
||
| 1143 | } |
||
| 1144 | } |
||
| 1145 | |||
| 1146 | $pos = $this->boardIndexToCoord($pos); |
||
| 1147 | |||
| 1148 | if ((isset($not_found) && $not_found) || '' == $pos) { |
||
| 1149 | return $this->move_msg(_MD_CHESS_MOVE_CANNOT_FIND_PAWN, $player, $move[0]); // "cannot find $player pawn in column $move[0]" |
||
| 1150 | } |
||
| 1151 | |||
| 1152 | $this->ac_move = \sprintf('P%s-%s%s', $pos, $move, $pawn_upg); |
||
| 1153 | |||
| 1154 | return ''; |
||
| 1155 | } |
||
| 1156 | |||
| 1157 | /* notation: [a-h][a-h] for pawn attack no longer allowed |
||
| 1158 | * except for history browser */ |
||
| 1159 | |||
| 1160 | if (0 == $this->browsing_mode) { |
||
| 1161 | return _MD_CHESS_MOVE_USE_NOTATION; |
||
| 1162 | } // "please use denotation [a-h]x[a-h][1-8] for pawn attacks (see help for more information)" |
||
| 1163 | |||
| 1164 | /* pawn attack must be only one pawn in column! */ |
||
| 1165 | |||
| 1166 | $pawns = 0; |
||
| 1167 | |||
| 1168 | $start = $this->boardCoordToIndex(\sprintf('%s1', $move[0])); |
||
| 1169 | |||
| 1170 | if (64 == $start) { |
||
| 1171 | return $this->move_msg(_MD_CHESS_MOVE_COORD_INVALID, $move[0]); |
||
| 1172 | } // "coordinate $move[0] is invalid" |
||
| 1173 | |||
| 1174 | for ($i = 1; $i <= 8; $i++, $start += 8) { |
||
| 1175 | if ($this->board[$start] == $fig) { |
||
| 1176 | $pawns++; |
||
| 1177 | |||
| 1178 | $pawn_line = $i; |
||
| 1179 | } |
||
| 1180 | } |
||
| 1181 | |||
| 1182 | if (0 == $pawns) { |
||
|
0 ignored issues
–
show
|
|||
| 1183 | return $this->move_msg(_MD_CHESS_MOVE_NO_PAWN, $move[0]); |
||
| 1184 | } // "there is no pawn in column $move[0]" |
||
| 1185 | |||
| 1186 | elseif ($pawns > 1) { |
||
| 1187 | return $this->move_msg(_MD_CHESS_MOVE_TWO_PAWNS, $move[0]); |
||
| 1188 | } // "there is more than one pawn in column $move[0]" |
||
| 1189 | |||
| 1190 | if ('w' == $player) { |
||
| 1191 | $dest_line = $pawn_line + 1; |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
| 1192 | } else { |
||
| 1193 | $dest_line = $pawn_line - 1; |
||
| 1194 | } |
||
| 1195 | |||
| 1196 | $this->ac_move = \sprintf( |
||
| 1197 | 'P%s%dx%s%d', |
||
| 1198 | $move[0], |
||
| 1199 | $pawn_line, |
||
| 1200 | $move[1], |
||
| 1201 | $dest_line |
||
| 1202 | ); |
||
| 1203 | |||
| 1204 | return ''; |
||
| 1205 | } |
||
| 1206 | } else { |
||
| 1207 | /* figure move */ |
||
| 1208 | |||
| 1209 | $dest_coord = \mb_substr($move, \mb_strlen($move) - 2, 2); |
||
| 1210 | |||
| 1211 | $action = $move[\mb_strlen($move) - 3]; |
||
| 1212 | |||
| 1213 | if ('x' != $action) { |
||
| 1214 | $action = '-'; |
||
| 1215 | } |
||
| 1216 | |||
| 1217 | if ('w' == $player) { |
||
| 1218 | $figures = $this->w_figures; |
||
| 1219 | } else { |
||
| 1220 | $figures = $this->b_figures; |
||
| 1221 | } |
||
| 1222 | |||
| 1223 | $fig_count = 0; |
||
| 1224 | |||
| 1225 | foreach ($figures as $figure) { |
||
| 1226 | if ($figure[0] == $move[0]) { |
||
| 1227 | $fig_count++; |
||
| 1228 | |||
| 1229 | if (1 == $fig_count) { |
||
| 1230 | $pos1 = \mb_substr($figure, 1, 2); |
||
| 1231 | } else { |
||
| 1232 | $pos2 = \mb_substr($figure, 1, 2); |
||
| 1233 | } |
||
| 1234 | } |
||
| 1235 | } |
||
| 1236 | |||
| 1237 | if (0 == $fig_count) { |
||
| 1238 | return $this->move_msg(_MD_CHESS_MOVE_NO_FIGURE, $move[0], $this->getFullFigureName($move[0])); |
||
| 1239 | } // sprintf("there is no figure %s = %s", $move[0], $this->getFullFigureName($move[0])) |
||
| 1240 | |||
| 1241 | elseif (1 == $fig_count) { |
||
| 1242 | $this->ac_move = \sprintf( |
||
| 1243 | '%s%s%s%s', |
||
| 1244 | $move[0], |
||
| 1245 | $pos1, |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
| 1246 | $action, |
||
| 1247 | $dest_coord |
||
| 1248 | ); |
||
| 1249 | |||
| 1250 | return ''; |
||
| 1251 | } |
||
| 1252 | |||
| 1253 | /* two figures which may cause ambiguity */ |
||
| 1254 | |||
| 1255 | $dest_pos = $this->boardCoordToIndex($dest_coord); |
||
| 1256 | |||
| 1257 | if (64 == $dest_pos) { |
||
| 1258 | return $this->move_msg(_MD_CHESS_MOVE_COORD_INVALID, $dest_coord); |
||
| 1259 | } // "coordinate $dest_coord is invalid" |
||
| 1260 | |||
| 1261 | $fig1_can_reach = $this->tileIsReachable( |
||
| 1262 | $move[0], |
||
| 1263 | $this->boardCoordToIndex($pos1), |
||
| 1264 | $dest_pos |
||
| 1265 | ); |
||
| 1266 | |||
| 1267 | $fig2_can_reach = $this->tileIsReachable( |
||
| 1268 | $move[0], |
||
| 1269 | $this->boardCoordToIndex($pos2), |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
| 1270 | $dest_pos |
||
| 1271 | ); |
||
| 1272 | |||
| 1273 | if (!$fig1_can_reach && !$fig2_can_reach) { |
||
| 1274 | return $this->move_msg(_MD_CHESS_MOVE_NEITHER_CAN_REACH, $move[0], $this->getFullFigureName($move[0]), $dest_coord); |
||
| 1275 | } // sprintf("neither of the %s = %s can reach %s", $move[0], $this->getFullFigureName($move[0]), $dest_coord) |
||
| 1276 | |||
| 1277 | elseif ($fig1_can_reach && $fig2_can_reach) { |
||
| 1278 | /* ambiguity - check whether a hint is given */ |
||
| 1279 | |||
| 1280 | if (('-' == $action && 4 == mb_strlen($move)) |
||
| 1281 | || ('x' == $action && 5 == mb_strlen($move))) { |
||
| 1282 | $hint = $move[1]; |
||
| 1283 | } |
||
| 1284 | |||
| 1285 | if (empty($hint)) { |
||
| 1286 | return $this->move_msg(_MD_CHESS_MOVE_BOTH_CAN_REACH, $move[0], $this->getFullFigureName($move[0]), $dest_coord); |
||
| 1287 | } // sprintf("both of the %s = %s can reach %s", $move[0], $this->getFullFigureName($move[0]), $dest_coord) |
||
| 1288 | |||
| 1289 | $move_fig1 = 0; |
||
| 1290 | |||
| 1291 | $move_fig2 = 0; |
||
| 1292 | |||
| 1293 | if ($hint >= '1' && $hint <= '8') { |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
| 1294 | if ($pos1[1] == $hint && $pos2[1] != $hint) { |
||
| 1295 | $move_fig1 = 1; |
||
| 1296 | } |
||
| 1297 | |||
| 1298 | if ($pos2[1] == $hint && $pos1[1] != $hint) { |
||
| 1299 | $move_fig2 = 1; |
||
| 1300 | } |
||
| 1301 | } else { |
||
| 1302 | if ($pos1[0] == $hint && $pos2[0] != $hint) { |
||
| 1303 | $move_fig1 = 1; |
||
| 1304 | } |
||
| 1305 | |||
| 1306 | if ($pos2[0] == $hint && $pos1[0] != $hint) { |
||
| 1307 | $move_fig2 = 1; |
||
| 1308 | } |
||
| 1309 | } |
||
| 1310 | |||
| 1311 | if (!$move_fig1 && !$move_fig2) { |
||
| 1312 | return _MD_CHESS_MOVE_AMBIGUOUS; |
||
| 1313 | } // "ambiguity is not properly resolved" |
||
| 1314 | |||
| 1315 | if ($move_fig1) { |
||
| 1316 | $this->ac_move = \sprintf( |
||
| 1317 | '%s%s%s%s', |
||
| 1318 | $move[0], |
||
| 1319 | $pos1, |
||
| 1320 | $action, |
||
| 1321 | $dest_coord |
||
| 1322 | ); |
||
| 1323 | } else { |
||
| 1324 | $this->ac_move = \sprintf( |
||
| 1325 | '%s%s%s%s', |
||
| 1326 | $move[0], |
||
| 1327 | $pos2, |
||
| 1328 | $action, |
||
| 1329 | $dest_coord |
||
| 1330 | ); |
||
| 1331 | } |
||
| 1332 | |||
| 1333 | return; |
||
| 1334 | } |
||
| 1335 | |||
| 1336 | if ($fig1_can_reach) { |
||
| 1337 | $this->ac_move = \sprintf( |
||
| 1338 | '%s%s%s%s', |
||
| 1339 | $move[0], |
||
| 1340 | $pos1, |
||
| 1341 | $action, |
||
| 1342 | $dest_coord |
||
| 1343 | ); |
||
| 1344 | } else { |
||
| 1345 | $this->ac_move = \sprintf( |
||
| 1346 | '%s%s%s%s', |
||
| 1347 | $move[0], |
||
| 1348 | $pos2, |
||
| 1349 | $action, |
||
| 1350 | $dest_coord |
||
| 1351 | ); |
||
| 1352 | } |
||
| 1353 | |||
| 1354 | return ''; |
||
| 1355 | } |
||
| 1356 | |||
| 1357 | return $error; |
||
| 1358 | } |
||
| 1359 | |||
| 1360 | /** |
||
| 1361 | * A hacky function that uses autocomplete to short |
||
| 1362 | * a full move. if this fails there is no warning |
||
| 1363 | * but the move is kept unchanged. |
||
| 1364 | * |
||
| 1365 | * @param string $player 'w' or 'b' |
||
| 1366 | * @param string $move |
||
| 1367 | * @return string new move |
||
| 1368 | * |
||
| 1369 | * @access private |
||
| 1370 | */ |
||
| 1371 | public function convertFullToChessNotation($player, $move) |
||
| 1372 | { |
||
| 1373 | $new_move = $move; |
||
| 1374 | |||
| 1375 | $old_ac_move = $this->ac_move; /* backup required as autocomplete |
||
| 1376 | will overwrite it */ |
||
| 1377 | |||
| 1378 | /* valid pawn moves are always non-ambigious */ |
||
| 1379 | |||
| 1380 | if (0 === \strpos($move, 'P')) { |
||
| 1381 | /* skip P anycase. for attacks skip source digit |
||
| 1382 | and for moves skip source pos and - */ |
||
| 1383 | |||
| 1384 | if ('-' == $move[3]) { |
||
| 1385 | $new_move = \mb_substr($move, 4); |
||
| 1386 | } elseif ('x' == $move[3]) { |
||
| 1387 | $new_move = \sprintf('%s%s', $move[1], \mb_substr($move, 3)); |
||
| 1388 | } |
||
| 1389 | } else { |
||
| 1390 | /* try to remove the source position and check whether it |
||
| 1391 | * is a non-ambigious move. if it is add one of the components |
||
| 1392 | * and check again */ |
||
| 1393 | |||
| 1394 | if ('-' == $move[3]) { |
||
| 1395 | $dest = \mb_substr($move, 4); |
||
| 1396 | } elseif ('x' == $move[3]) { |
||
| 1397 | $dest = \mb_substr($move, 3); |
||
| 1398 | } |
||
| 1399 | |||
| 1400 | $new_move = \sprintf('%s%s', $move[0], $dest); |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
| 1401 | |||
| 1402 | if ('' != $this->completeMove($player, $new_move)) { |
||
| 1403 | /* add a component */ |
||
| 1404 | |||
| 1405 | $new_move = \sprintf('%s%s%s', $move[0], $move[1], $dest); |
||
| 1406 | |||
| 1407 | if ('' != $this->completeMove($player, $new_move)) { |
||
| 1408 | /* add other component */ |
||
| 1409 | |||
| 1410 | $new_move = \sprintf('%s%s%s', $move[0], $move[2], $dest); |
||
| 1411 | |||
| 1412 | if ('' != $this->completeMove($player, $new_move)) { |
||
| 1413 | $new_move = $move; |
||
| 1414 | } /* give up */ |
||
| 1415 | } |
||
| 1416 | } |
||
| 1417 | } |
||
| 1418 | |||
| 1419 | $this->ac_move = $old_ac_move; |
||
| 1420 | |||
| 1421 | return $new_move; |
||
| 1422 | } |
||
| 1423 | |||
| 1424 | /** |
||
| 1425 | * Handle a move. |
||
| 1426 | * |
||
| 1427 | * check whether it is user's turn and the move is valid. |
||
| 1428 | * if the move is okay update the game file. |
||
| 1429 | * |
||
| 1430 | * @param string $move |
||
| 1431 | * @return array A two-element array: |
||
| 1432 | * - $move_performed: true if the move was performed and the game state has been updated, false otherwise |
||
| 1433 | * - $move_result_text: text message |
||
| 1434 | * |
||
| 1435 | * @access private |
||
| 1436 | */ |
||
| 1437 | public function handleMove($move) |
||
| 1438 | { |
||
| 1439 | /* DEBUG: echo "HANDLE: $move, $comment<br>"; */ |
||
| 1440 | |||
| 1441 | $result = _MD_CHESS_MOVE_UNDEFINED; |
||
|
0 ignored issues
–
show
|
|||
| 1442 | |||
| 1443 | $move_handled = 0; |
||
| 1444 | |||
| 1445 | // Use $this->gamestate['fen_piece_placement'] to initialize $this->board, $this->w_figures and $this->b_figures. |
||
| 1446 | |||
| 1447 | $this->fen_piece_placement_to_board() || \trigger_error('handleMove, piece_placement invalid', \E_USER_ERROR); |
||
| 1448 | |||
| 1449 | // get color of current player |
||
| 1450 | |||
| 1451 | $cur_player = $this->gamestate['fen_active_color']; /* b or w */ |
||
| 1452 | |||
| 1453 | if ('w' != $cur_player && 'b' != $cur_player) { |
||
| 1454 | return [false, "handleMove, internal error: player='$cur_player'"]; |
||
| 1455 | } |
||
| 1456 | |||
| 1457 | $cur_opp = 'w' == $cur_player ? 'b' : 'w'; |
||
| 1458 | |||
| 1459 | if ('*' != $this->gamestate['pgn_result']) { |
||
| 1460 | return [false, _MD_CHESS_MOVE_GAME_OVER]; |
||
| 1461 | } |
||
| 1462 | |||
| 1463 | // get castling availability flags |
||
| 1464 | |||
| 1465 | $white_may_castle_short = \mb_strstr($this->gamestate['fen_castling_availability'], 'K') ? true : false; |
||
| 1466 | |||
| 1467 | $white_may_castle_long = \mb_strstr($this->gamestate['fen_castling_availability'], 'Q') ? true : false; |
||
| 1468 | |||
| 1469 | $black_may_castle_short = \mb_strstr($this->gamestate['fen_castling_availability'], 'k') ? true : false; |
||
| 1470 | |||
| 1471 | $black_may_castle_long = \mb_strstr($this->gamestate['fen_castling_availability'], 'q') ? true : false; |
||
| 1472 | |||
| 1473 | // Castling is supposed to use ohs, not zeros. Allow zeros on input anyway. |
||
| 1474 | |||
| 1475 | if ('0-0' == $move) { |
||
| 1476 | $move = 'O-O'; |
||
| 1477 | } elseif ('0-0-0' == $move) { |
||
| 1478 | $move = 'O-O-O'; |
||
| 1479 | } |
||
| 1480 | |||
| 1481 | // allow two-step of king to indicate castling |
||
| 1482 | |||
| 1483 | if ('w' == $cur_player && 'Ke1-g1' == $move) { |
||
| 1484 | $move = 'O-O'; |
||
| 1485 | } elseif ('w' == $cur_player && 'Ke1-c1' == $move) { |
||
| 1486 | $move = 'O-O-O'; |
||
| 1487 | } elseif ('b' == $cur_player && 'Ke8-g8' == $move) { |
||
| 1488 | $move = 'O-O'; |
||
| 1489 | } elseif ('b' == $cur_player && 'Ke8-c8' == $move) { |
||
| 1490 | $move = 'O-O-O'; |
||
| 1491 | } |
||
| 1492 | |||
| 1493 | /* backup full move input for game history before |
||
| 1494 | * splitting figure type apart */ |
||
| 1495 | |||
| 1496 | $history_move = $move; |
||
| 1497 | |||
| 1498 | /* clear last move - won't be saved yet if anything |
||
| 1499 | goes wrong */ |
||
| 1500 | |||
| 1501 | $this->last_move = 'x'; |
||
| 1502 | |||
| 1503 | $this->piece_captured = 'x'; |
||
|
0 ignored issues
–
show
|
|||
| 1504 | |||
| 1505 | /* HANDLE MOVES: |
||
| 1506 | * --- surrender |
||
| 1507 | * O-O short castling |
||
| 1508 | * O-O-O long castling |
||
| 1509 | * [PRNBQK][a-h][1-8][-:x][a-h][1-8] unshortened move |
||
| 1510 | */ |
||
| 1511 | |||
| 1512 | if ('O-O' == $move) { |
||
| 1513 | /* short castling */ |
||
| 1514 | |||
| 1515 | if ('b' == $cur_player) { |
||
| 1516 | if (!$black_may_castle_short) { |
||
| 1517 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // You cannot castle short any longer! |
||
| 1518 | } |
||
| 1519 | |||
| 1520 | if (!$this->is_empty_tile(61) || !$this->is_empty_tile(62)) { |
||
| 1521 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // Cannot castle short because the way is blocked! |
||
| 1522 | } |
||
| 1523 | |||
| 1524 | if ($this->kingIsUnderAttack('b', 'w')) { |
||
| 1525 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // You cannot escape check by castling! |
||
| 1526 | } |
||
| 1527 | |||
| 1528 | if ($this->tileIsUnderAttack('w', 62) || $this->tileIsUnderAttack('w', 61)) { |
||
| 1529 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // A tile the king passes over or into would be under attack after short castling! |
||
| 1530 | } |
||
| 1531 | |||
| 1532 | $this->clear_tile(60); |
||
| 1533 | |||
| 1534 | $this->board[62] = 'bK'; |
||
| 1535 | |||
| 1536 | $this->board[61] = 'bR'; |
||
| 1537 | |||
| 1538 | $this->clear_tile(63); |
||
| 1539 | |||
| 1540 | $black_may_castle_short = false; |
||
| 1541 | |||
| 1542 | $black_may_castle_long = false; |
||
| 1543 | } else { |
||
| 1544 | if (!$white_may_castle_short) { |
||
| 1545 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // You cannot castle short any longer! |
||
| 1546 | } |
||
| 1547 | |||
| 1548 | if (!$this->is_empty_tile(5) || !$this->is_empty_tile(6)) { |
||
| 1549 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // Cannot castle short because the way is blocked! |
||
| 1550 | } |
||
| 1551 | |||
| 1552 | if ($this->kingIsUnderAttack('w', 'b')) { |
||
| 1553 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // You cannot escape check by castling! |
||
| 1554 | } |
||
| 1555 | |||
| 1556 | if ($this->tileIsUnderAttack('b', 5) || $this->tileIsUnderAttack('b', 6)) { |
||
| 1557 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // A tile the king passes over or into would be under attack after short castling! |
||
| 1558 | } |
||
| 1559 | |||
| 1560 | $this->clear_tile(4); |
||
| 1561 | |||
| 1562 | $this->board[6] = 'wK'; |
||
| 1563 | |||
| 1564 | $this->board[5] = 'wR'; |
||
| 1565 | |||
| 1566 | $this->clear_tile(7); |
||
| 1567 | |||
| 1568 | $white_may_castle_short = false; |
||
| 1569 | |||
| 1570 | $white_may_castle_long = false; |
||
| 1571 | } |
||
| 1572 | |||
| 1573 | $result = _MD_CHESS_MOVE_CASTLED_SHORT; |
||
| 1574 | |||
| 1575 | $move_handled = 1; |
||
| 1576 | |||
| 1577 | $this->last_move = 'O-O'; |
||
| 1578 | } elseif ('O-O-O' == $move) { |
||
| 1579 | /* long castling */ |
||
| 1580 | |||
| 1581 | if ('b' == $cur_player) { |
||
| 1582 | if (!$black_may_castle_long) { |
||
| 1583 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // You cannot castle long any longer! |
||
| 1584 | } |
||
| 1585 | |||
| 1586 | if (!$this->is_empty_tile(57) || !$this->is_empty_tile(58) || !$this->is_empty_tile(59)) { |
||
| 1587 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // Cannot castle long because the way is blocked! |
||
| 1588 | } |
||
| 1589 | |||
| 1590 | if ($this->kingIsUnderAttack('b', 'w')) { |
||
| 1591 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // You cannot escape check by castling! |
||
| 1592 | } |
||
| 1593 | |||
| 1594 | if ($this->tileIsUnderAttack('w', 58) || $this->tileIsUnderAttack('w', 59)) { |
||
| 1595 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // A tile the king passes over or into would be under attack after short castling! |
||
| 1596 | } |
||
| 1597 | |||
| 1598 | $this->clear_tile(56); |
||
| 1599 | |||
| 1600 | $this->board[58] = 'bK'; |
||
| 1601 | |||
| 1602 | $this->board[59] = 'bR'; |
||
| 1603 | |||
| 1604 | $this->clear_tile(60); |
||
| 1605 | |||
| 1606 | $black_may_castle_short = false; |
||
| 1607 | |||
| 1608 | $black_may_castle_long = false; |
||
| 1609 | } else { |
||
| 1610 | if (!$white_may_castle_long) { |
||
| 1611 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // You cannot castle long any longer! |
||
| 1612 | } |
||
| 1613 | |||
| 1614 | if (!$this->is_empty_tile(1) || !$this->is_empty_tile(2) || !$this->is_empty_tile(3)) { |
||
| 1615 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // Cannot castle long because the way is blocked! |
||
| 1616 | } |
||
| 1617 | |||
| 1618 | if ($this->kingIsUnderAttack('w', 'b')) { |
||
| 1619 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // You cannot escape check by castling! |
||
| 1620 | } |
||
| 1621 | |||
| 1622 | if ($this->tileIsUnderAttack('b', 2) || $this->tileIsUnderAttack('b', 3)) { |
||
| 1623 | return [false, _MD_CHESS_MOVE_NO_CASTLE]; // A tile the king passes over or into would be under attack after short castling! |
||
| 1624 | } |
||
| 1625 | |||
| 1626 | $this->clear_tile(0); |
||
| 1627 | |||
| 1628 | $this->board[2] = 'wK'; |
||
| 1629 | |||
| 1630 | $this->board[3] = 'wR'; |
||
| 1631 | |||
| 1632 | $this->clear_tile(4); |
||
| 1633 | |||
| 1634 | $white_may_castle_short = false; |
||
| 1635 | |||
| 1636 | $white_may_castle_long = false; |
||
| 1637 | } |
||
| 1638 | |||
| 1639 | $result = _MD_CHESS_MOVE_CASTLED_LONG; |
||
| 1640 | |||
| 1641 | $move_handled = 1; |
||
| 1642 | |||
| 1643 | $this->last_move = 'O-O-O'; |
||
| 1644 | } else { |
||
| 1645 | /* [PRNBQK][a-h][1-8][-:x][a-h][1-8][RNBQK] full move */ |
||
| 1646 | |||
| 1647 | /* allow short move description by autocompleting to |
||
| 1648 | * full description */ |
||
| 1649 | |||
| 1650 | $ac_error = $this->completeMove($cur_player, \trim($move)); |
||
| 1651 | |||
| 1652 | if ('' != $ac_error) { |
||
| 1653 | return [false, $ac_error]; |
||
| 1654 | } // "ERROR: autocomplete: $ac_error" |
||
| 1655 | |||
| 1656 | $move = $this->ac_move; |
||
| 1657 | |||
| 1658 | $this->last_move = \str_replace('?', '', $move); |
||
| 1659 | |||
| 1660 | /* a final captial letter may only be N,B,R,Q for the |
||
| 1661 | * appropiate chessman */ |
||
| 1662 | |||
| 1663 | $c = $move[\mb_strlen($move) - 1]; |
||
| 1664 | |||
| 1665 | if ($c >= 'A' && $c <= 'Z') { |
||
| 1666 | if ('N' != $c && 'B' != $c && 'R' != $c && 'Q' != $c) { |
||
| 1667 | return [false, _MD_CHESS_MOVE_INVALID_PIECE]; |
||
| 1668 | } |
||
| 1669 | } // "ERROR: only N (knight), B (bishop), R (rook) and Q (queen) are valid chessman identifiers" |
||
| 1670 | |||
| 1671 | /* if it is a full move, try to shorten the history move */ |
||
| 1672 | |||
| 1673 | if (\mb_strlen($history_move) >= 6) { |
||
| 1674 | $history_move = $this->convertFullToChessNotation($cur_player, $history_move); |
||
| 1675 | } |
||
| 1676 | |||
| 1677 | /* DEBUG: echo "Move: $move ($history_move)<br>"; */ |
||
| 1678 | |||
| 1679 | /* validate figure and position */ |
||
| 1680 | |||
| 1681 | $fig_type = $move[0]; |
||
| 1682 | |||
| 1683 | $fig_name = $this->getFullFigureName($fig_type); |
||
| 1684 | |||
| 1685 | if ('empty' == $fig_name) { |
||
| 1686 | return [false, $this->move_msg(_MD_CHESS_MOVE_UNKNOWN_FIGURE, $fig_type)]; |
||
| 1687 | } // "ERROR: Figure $fig_type is unknown!" |
||
| 1688 | |||
| 1689 | $fig_coord = \mb_substr($move, 1, 2); |
||
| 1690 | |||
| 1691 | $fig_pos = $this->boardCoordToIndex($fig_coord); |
||
| 1692 | |||
| 1693 | if (64 == $fig_pos) { |
||
| 1694 | return [false, $this->move_msg(_MD_CHESS_MOVE_COORD_INVALID, $fig_coord)]; |
||
| 1695 | } // "ERROR: $fig_coord is invalid!" |
||
| 1696 | |||
| 1697 | /* DEBUG echo "fig_type: $fig_type, fig_pos: $fig_pos<br>"; */ |
||
| 1698 | |||
| 1699 | if ($this->is_empty_tile($fig_pos)) { |
||
| 1700 | return [false, $this->move_msg(_MD_CHESS_MOVE_TILE_EMPTY, $fig_coord)]; |
||
| 1701 | } // "ERROR: Tile $fig_coord is empty." |
||
| 1702 | |||
| 1703 | if ($this->board[$fig_pos][0] != $cur_player) { |
||
| 1704 | return [false, _MD_CHESS_MOVE_NOT_YOUR_PIECE]; |
||
| 1705 | } // "ERROR: Figure does not belong to you!" |
||
| 1706 | |||
| 1707 | if ($this->board[$fig_pos][1] != $fig_type) { |
||
| 1708 | return [false, _MD_CHESS_MOVE_NOEXIST_FIGURE]; |
||
| 1709 | } // "ERROR: Figure does not exist!" |
||
| 1710 | |||
| 1711 | /* get target index */ |
||
| 1712 | |||
| 1713 | $dest_coord = \mb_substr($move, 4, 2); |
||
| 1714 | |||
| 1715 | $dest_pos = $this->boardCoordToIndex($dest_coord); |
||
| 1716 | |||
| 1717 | if (64 == $dest_pos) { |
||
| 1718 | return [false, $this->move_msg(_MD_CHESS_MOVE_COORD_INVALID, $dest_coord)]; |
||
| 1719 | } // "ERROR: $dest_coord is invalid!" |
||
| 1720 | |||
| 1721 | if ($dest_pos == $fig_pos) { |
||
| 1722 | return [false, _MD_CHESS_MOVE_START_END_SAME]; |
||
| 1723 | } |
||
| 1724 | |||
| 1725 | /* DEBUG echo "dest_pos: $dest_pos<br>"; */ |
||
| 1726 | |||
| 1727 | /* get action */ |
||
| 1728 | |||
| 1729 | $action = $move[3]; |
||
| 1730 | |||
| 1731 | if ('-' == $move[3]) { |
||
| 1732 | $action = 'M'; |
||
| 1733 | } /* move */ |
||
| 1734 | |||
| 1735 | elseif ('x' == $move[3]) { |
||
| 1736 | $action = 'A'; |
||
| 1737 | } /* attack */ |
||
| 1738 | |||
| 1739 | else { |
||
| 1740 | return [false, $this->move_msg(_MD_CHESS_MOVE_UNKNOWN_ACTION, $action)]; |
||
| 1741 | } // "ERROR: $action is unknown! Please use \"-\" for a move and \"x\" for an attack." |
||
| 1742 | |||
| 1743 | /* if attack an enemy unit must be present on tile |
||
| 1744 | * and if move then tile must be empty. in both cases |
||
| 1745 | * the king must not be checked after moving. */ |
||
| 1746 | |||
| 1747 | /* check whether the move is along a valid path and |
||
| 1748 | * whether all tiles in between are empty thus the path |
||
| 1749 | * is not blocked. the final destination tile is not |
||
| 1750 | * checked here. */ |
||
| 1751 | |||
| 1752 | if ('P' != $fig_type) { |
||
| 1753 | if (!$this->tileIsReachable($fig_type, $fig_pos, $dest_pos)) { |
||
| 1754 | return [false, $this->move_msg(_MD_CHESS_MOVE_OUT_OF_RANGE, $dest_coord, $fig_name, $fig_coord)]; |
||
| 1755 | } // "ERROR: Tile $dest_coord is out of moving range for $fig_name at $fig_coord!" |
||
| 1756 | } else { |
||
| 1757 | if ('M' == $action && !$this->checkPawnMove($fig_pos, $dest_pos)) { |
||
| 1758 | return [false, $this->move_msg(_MD_CHESS_MOVE_OUT_OF_RANGE, $dest_coord, $fig_name, $fig_coord)]; |
||
| 1759 | } // "ERROR: Tile $dest_coord is out of moving range for $fig_name at $fig_coord!" |
||
| 1760 | |||
| 1761 | if ('A' == $action && !$this->checkPawnAttack($fig_pos, $dest_pos)) { |
||
| 1762 | return [false, $this->move_msg(_MD_CHESS_MOVE_OUT_OF_RANGE, $dest_coord, $fig_name, $fig_coord)]; |
||
| 1763 | } // "ERROR: Tile $dest_coord is out of attacking range for $fig_name at $fig_coord!" |
||
| 1764 | } |
||
| 1765 | |||
| 1766 | $en_passant_capture_performed = 0; // 1 if en passant captured occurred, else 0 |
||
| 1767 | |||
| 1768 | /* check action */ |
||
| 1769 | |||
| 1770 | if ('M' == $action && !$this->is_empty_tile($dest_pos)) { |
||
| 1771 | return [false, $this->move_msg(_MD_CHESS_MOVE_OCCUPIED, $dest_coord)]; // "ERROR: Tile $dest_coord is occupied. You cannot move there." |
||
| 1772 | } |
||
| 1773 | |||
| 1774 | if ('A' == $action && $this->is_empty_tile($dest_pos)) { |
||
| 1775 | /* en passant of pawn? */ |
||
| 1776 | |||
| 1777 | if ('P' == $fig_type) { |
||
| 1778 | if ($this->boardIndexToCoord($dest_pos) == $this->gamestate['fen_en_passant_target_square']) { |
||
| 1779 | $en_passant_capture_performed = 1; |
||
| 1780 | } |
||
| 1781 | |||
| 1782 | if (0 == $en_passant_capture_performed) { |
||
| 1783 | return [false, _MD_CHESS_MOVE_NO_EN_PASSANT]; // "ERROR: en-passant no longer possible!" |
||
| 1784 | } |
||
| 1785 | } else { |
||
| 1786 | return [false, $this->move_msg(_MD_CHESS_MOVE_ATTACK_EMPTY, $dest_coord)]; // "ERROR: Tile $dest_coord is empty. You cannot attack it." |
||
| 1787 | } |
||
| 1788 | } |
||
| 1789 | |||
| 1790 | if ('A' == $action && $this->board[$dest_pos][0] == $cur_player) { |
||
| 1791 | return [false, $this->move_msg(_MD_CHESS_MOVE_ATTACK_SELF, $dest_coord)]; // "ERROR: You cannot attack own unit at $dest_coord." |
||
| 1792 | } |
||
| 1793 | |||
| 1794 | /* backup affected tiles */ |
||
| 1795 | |||
| 1796 | $old_fig_tile = $this->board[$fig_pos]; |
||
| 1797 | |||
| 1798 | $old_dest_tile = $this->board[$dest_pos]; |
||
| 1799 | |||
| 1800 | /* perform move */ |
||
| 1801 | |||
| 1802 | $this->clear_tile($fig_pos); |
||
| 1803 | |||
| 1804 | if (!$this->is_empty_tile($dest_pos)) { |
||
| 1805 | $this->piece_captured = \sprintf('%s%s', $this->board[$dest_pos], $dest_pos); |
||
| 1806 | } |
||
| 1807 | |||
| 1808 | $this->board[$dest_pos] = "$cur_player$fig_type"; |
||
| 1809 | |||
| 1810 | if ($en_passant_capture_performed) { |
||
| 1811 | /* kill pawn */ |
||
| 1812 | |||
| 1813 | if ('w' == $cur_player) { |
||
| 1814 | $this->clear_tile($dest_pos - 8); |
||
| 1815 | |||
| 1816 | $this->piece_captured = \sprintf('bP%s', $dest_pos - 8); |
||
| 1817 | } else { |
||
| 1818 | $this->clear_tile($dest_pos + 8); |
||
| 1819 | |||
| 1820 | $this->piece_captured = \sprintf('wP%s', $dest_pos + 8); |
||
| 1821 | } |
||
| 1822 | } |
||
| 1823 | |||
| 1824 | /* check check :) */ |
||
| 1825 | |||
| 1826 | if ($this->kingIsUnderAttack($cur_player, $cur_opp)) { |
||
| 1827 | $this->board[$fig_pos] = $old_fig_tile; |
||
| 1828 | |||
| 1829 | $this->board[$dest_pos] = $old_dest_tile; |
||
| 1830 | |||
| 1831 | if ($en_passant_capture_performed) { |
||
| 1832 | // restore pawn that was captured above, since that move is invalid |
||
| 1833 | |||
| 1834 | if ('w' == $cur_player) { |
||
| 1835 | $this->board[$dest_pos - 8] = 'bP'; |
||
| 1836 | } else { |
||
| 1837 | $this->board[$dest_pos + 8] = 'wP'; |
||
| 1838 | } |
||
| 1839 | } |
||
| 1840 | |||
| 1841 | return [false, _MD_CHESS_MOVE_IN_CHECK]; // "ERROR: Move is invalid because king would be under attack then." |
||
| 1842 | } |
||
| 1843 | |||
| 1844 | // check whether this forbids any castling |
||
| 1845 | |||
| 1846 | if ('K' == $fig_type) { |
||
| 1847 | if ('w' == $cur_player) { |
||
| 1848 | $white_may_castle_short = false; |
||
| 1849 | |||
| 1850 | $white_may_castle_long = false; |
||
| 1851 | } else { |
||
| 1852 | $black_may_castle_short = false; |
||
| 1853 | |||
| 1854 | $black_may_castle_long = false; |
||
| 1855 | } |
||
| 1856 | } |
||
| 1857 | |||
| 1858 | if ('R' == $fig_type) { |
||
| 1859 | if ('w' == $cur_player) { |
||
| 1860 | if (7 == $fig_pos) { |
||
| 1861 | $white_may_castle_short = false; |
||
| 1862 | } elseif (0 == $fig_pos) { |
||
| 1863 | $white_may_castle_long = false; |
||
| 1864 | } |
||
| 1865 | } else { |
||
| 1866 | if (63 == $fig_pos) { |
||
| 1867 | $black_may_castle_short = false; |
||
| 1868 | } elseif (56 == $fig_pos) { |
||
| 1869 | $black_may_castle_long = false; |
||
| 1870 | } |
||
| 1871 | } |
||
| 1872 | } |
||
| 1873 | |||
| 1874 | // if a pawn moved two tiles, this will allow 'en passant' on next move |
||
| 1875 | |||
| 1876 | if ('P' == $fig_type && 16 == \abs($fig_pos - $dest_pos)) { |
||
| 1877 | $file_chars = 'abcdefgh'; |
||
| 1878 | |||
| 1879 | $this->gamestate['fen_en_passant_target_square'] = $file_chars[$fig_pos % 8] . ('w' == $cur_player ? '3' : '6'); |
||
| 1880 | } else { |
||
| 1881 | // clear 'en passant' |
||
| 1882 | |||
| 1883 | $this->gamestate['fen_en_passant_target_square'] = '-'; |
||
| 1884 | } |
||
| 1885 | |||
| 1886 | if ('M' == $action) { |
||
| 1887 | $result = $this->move_msg(_MD_CHESS_MOVE_MOVED, $fig_name, $fig_coord, $dest_coord); |
||
| 1888 | } else { |
||
| 1889 | $result = $this->move_msg(_MD_CHESS_MOVE_CAPTURED, $fig_name, $dest_coord, $fig_coord); |
||
| 1890 | } |
||
| 1891 | |||
| 1892 | /* if pawn reached last line convert into a queen */ |
||
| 1893 | |||
| 1894 | if ('P' == $fig_type) { |
||
| 1895 | if (('w' == $cur_player && $dest_pos >= 56) |
||
| 1896 | || ('b' == $cur_player && $dest_pos <= 7)) { |
||
| 1897 | $pawn_upg = $move[\mb_strlen($move) - 1]; |
||
| 1898 | |||
| 1899 | if ('?' == $pawn_upg) { |
||
| 1900 | $pawn_upg = 'Q'; |
||
| 1901 | |||
| 1902 | $history_move = \sprintf('%sQ', $history_move); |
||
| 1903 | } |
||
| 1904 | |||
| 1905 | $this->board[$dest_pos] = "$cur_player$pawn_upg"; |
||
| 1906 | |||
| 1907 | $result .= ' ... ' . $this->move_msg(_MD_CHESS_MOVE_PROMOTED, $this->getFullFigureName($pawn_upg)); |
||
| 1908 | } |
||
| 1909 | } |
||
| 1910 | |||
| 1911 | $move_handled = 1; |
||
| 1912 | } |
||
| 1913 | |||
| 1914 | /* if a legal move was performed test whether you |
||
| 1915 | * check the opponent or even check-mate him. then |
||
| 1916 | * update castling and en-passant flags, select the |
||
| 1917 | * next player and add the move to the history. */ |
||
| 1918 | |||
| 1919 | if ($move_handled) { |
||
| 1920 | // Use $this->board to update $this->gamestate['fen_piece_placement']. |
||
| 1921 | |||
| 1922 | $this->board_to_fen_piece_placement(); |
||
| 1923 | |||
| 1924 | // handle check, checkmate and stalemate |
||
| 1925 | |||
| 1926 | $comment = ''; |
||
| 1927 | |||
| 1928 | if ($this->kingIsUnderAttack($cur_opp, $cur_player)) { |
||
| 1929 | // if this is check mate finish the game, otherwise if not just add a + to the move |
||
| 1930 | |||
| 1931 | if ($this->isCheckMate($cur_opp, $cur_player)) { |
||
| 1932 | $this->gamestate['pgn_result'] = 'w' == $cur_player ? '1-0' : '0-1'; |
||
| 1933 | |||
| 1934 | $history_move .= '#'; |
||
| 1935 | |||
| 1936 | $result .= ' ... ' . _MD_CHESS_MOVE_CHECKMATE; |
||
| 1937 | } else { |
||
| 1938 | $history_move .= '+'; |
||
| 1939 | } |
||
| 1940 | } elseif ($this->isStaleMate($cur_opp, $cur_player)) { |
||
| 1941 | $this->gamestate['pgn_result'] = '1/2-1/2'; |
||
| 1942 | |||
| 1943 | $result .= ' ... ' . _MD_CHESS_MOVE_STALEMATE; |
||
| 1944 | |||
| 1945 | $comment = _MD_CHESS_DRAW_STALEMATE; |
||
| 1946 | } elseif ($this->insufficient_mating_material()) { |
||
| 1947 | $this->gamestate['pgn_result'] = '1/2-1/2'; |
||
| 1948 | |||
| 1949 | $result .= ' ... ' . _MD_CHESS_MOVE_MATERIAL; |
||
| 1950 | |||
| 1951 | $comment = _MD_CHESS_DRAW_NO_MATE; |
||
| 1952 | } |
||
| 1953 | |||
| 1954 | // store possible castling-availability modification |
||
| 1955 | |||
| 1956 | $KQkq = ($white_may_castle_short ? 'K' : '') . ($white_may_castle_long ? 'Q' : '') . ($black_may_castle_short ? 'k' : '') . ($black_may_castle_long ? 'q' : ''); |
||
| 1957 | |||
| 1958 | $this->gamestate['fen_castling_availability'] = !empty($KQkq) ? $KQkq : '-'; |
||
| 1959 | |||
| 1960 | // strip old result-string from end of movetext |
||
| 1961 | |||
| 1962 | // It's assumed that the movetext for a game in play is terminated by a '*', possibly preceded by whitespace. |
||
| 1963 | |||
| 1964 | // (The whitespace will be present unless there are no moves in the game yet.) |
||
| 1965 | |||
| 1966 | $this->gamestate['pgn_movetext'] = \preg_replace('/\s*\*$/', '', $this->gamestate['pgn_movetext']); |
||
| 1967 | |||
| 1968 | // if white move, output move-number |
||
| 1969 | |||
| 1970 | if ('w' == $this->gamestate['fen_active_color']) { |
||
| 1971 | if (!empty($this->gamestate['pgn_movetext'])) { |
||
| 1972 | $this->gamestate['pgn_movetext'] .= ' '; |
||
| 1973 | } |
||
| 1974 | |||
| 1975 | $this->gamestate['pgn_movetext'] .= $this->gamestate['fen_fullmove_number'] . '.'; |
||
| 1976 | // if black move, no moves yet, and game is setup, output move number with special '...' terminator |
||
| 1977 | } elseif (empty($this->gamestate['pgn_movetext']) && !empty($this->gamestate['pgn_fen'])) { |
||
| 1978 | $this->gamestate['pgn_movetext'] .= $this->gamestate['fen_fullmove_number'] . '...'; |
||
| 1979 | } |
||
| 1980 | |||
| 1981 | // update movetext |
||
| 1982 | |||
| 1983 | // comment is added only for a concluded game |
||
| 1984 | |||
| 1985 | $this->gamestate['pgn_movetext'] .= ' ' . $history_move . ' ' . $this->gamestate['pgn_result']; |
||
| 1986 | |||
| 1987 | if (!empty($comment)) { |
||
| 1988 | $this->gamestate['pgn_movetext'] .= ' {' . $comment . '}'; |
||
| 1989 | } |
||
| 1990 | |||
| 1991 | // if black move, increment move-number |
||
| 1992 | |||
| 1993 | if ('b' == $this->gamestate['fen_active_color']) { |
||
| 1994 | ++$this->gamestate['fen_fullmove_number']; |
||
| 1995 | } |
||
| 1996 | |||
| 1997 | // If pawn advance or capturing move, reset the halfmove clock. Otherwise increment it. |
||
| 1998 | |||
| 1999 | if ('O-O' != $move && 'O-O-O' != $move && (0 === \strpos($move, 'P') || 'x' == $move[3])) { |
||
| 2000 | $this->gamestate['fen_halfmove_clock'] = 0; |
||
| 2001 | } else { |
||
| 2002 | ++$this->gamestate['fen_halfmove_clock']; |
||
| 2003 | } |
||
| 2004 | |||
| 2005 | // set next player |
||
| 2006 | |||
| 2007 | $this->gamestate['fen_active_color'] = 'w' == $this->gamestate['fen_active_color'] ? 'b' : 'w'; |
||
| 2008 | } |
||
| 2009 | |||
| 2010 | return [$move_handled, $result]; |
||
| 2011 | } |
||
| 2012 | |||
| 2013 | /** |
||
| 2014 | * Check whether a tile is empty. |
||
| 2015 | * |
||
| 2016 | * @param int $position |
||
| 2017 | * @return bool |
||
| 2018 | * |
||
| 2019 | * @access private |
||
| 2020 | */ |
||
| 2021 | public function is_empty_tile($position) |
||
| 2022 | { |
||
| 2023 | return '00' == $this->board[$position]; |
||
| 2024 | } |
||
| 2025 | |||
| 2026 | /** |
||
| 2027 | * Clear a tile. |
||
| 2028 | * |
||
| 2029 | * @param int $position Position of tile |
||
| 2030 | * |
||
| 2031 | * @access private |
||
| 2032 | */ |
||
| 2033 | public function clear_tile($position) |
||
| 2034 | { |
||
| 2035 | $this->board[$position] = '00'; |
||
| 2036 | } |
||
| 2037 | |||
| 2038 | /** |
||
| 2039 | * Convert FEN piece placement field to array. |
||
| 2040 | * |
||
| 2041 | * Use $this->gamestate['fen_piece_placement'] to initialize $this->board, $this->w_figures and $this->b_figures. |
||
| 2042 | * |
||
| 2043 | * @return bool True if piece placement is valid, otherwise false. |
||
| 2044 | * |
||
| 2045 | * @access private |
||
| 2046 | */ |
||
| 2047 | public function fen_piece_placement_to_board() |
||
| 2048 | { |
||
| 2049 | if (empty($this->gamestate['fen_piece_placement']) || \mb_strlen($this->gamestate['fen_piece_placement']) > 71) { |
||
| 2050 | #trigger_error('piece placement empty or length invalid', E_USER_WARNING); #*#DEBUG# |
||
| 2051 | return false; // invalid length |
||
| 2052 | } |
||
| 2053 | |||
| 2054 | $this->board = []; |
||
| 2055 | |||
| 2056 | $piece_map = [ |
||
| 2057 | 'K' => 'wK', |
||
| 2058 | 'Q' => 'wQ', |
||
| 2059 | 'R' => 'wR', |
||
| 2060 | 'B' => 'wB', |
||
| 2061 | 'N' => 'wN', |
||
| 2062 | 'P' => 'wP', |
||
| 2063 | 'k' => 'bK', |
||
| 2064 | 'q' => 'bQ', |
||
| 2065 | 'r' => 'bR', |
||
| 2066 | 'b' => 'bB', |
||
| 2067 | 'n' => 'bN', |
||
| 2068 | 'p' => 'bP', |
||
| 2069 | ]; |
||
| 2070 | |||
| 2071 | $tiles = \implode('', \array_reverse(\explode('/', $this->gamestate['fen_piece_placement']))); |
||
| 2072 | |||
| 2073 | for ($i = 0, $iMax = mb_strlen($tiles); $i < $iMax; ++$i) { |
||
| 2074 | $tile = $tiles[$i]; |
||
| 2075 | |||
| 2076 | if (isset($piece_map[$tile])) { |
||
| 2077 | $this->board[] = $piece_map[$tile]; |
||
| 2078 | } elseif (\is_numeric($tile) && $tile >= 1 && $tile <= 8) { |
||
| 2079 | for ($j = 0; $j < $tile; ++$j) { |
||
| 2080 | $this->board[] = '00'; |
||
| 2081 | } |
||
| 2082 | } else { |
||
| 2083 | #trigger_error("tile='$tile'", E_USER_WARNING); #*#DEBUG# |
||
| 2084 | return false; // unexpected character in piece_placement |
||
| 2085 | } |
||
| 2086 | } |
||
| 2087 | |||
| 2088 | if (64 != \count($this->board)) { |
||
| 2089 | #trigger_error('count(board)=' . count($this->board), E_USER_WARNING); #*#DEBUG# |
||
| 2090 | return false; // piece_placement has incorrect number of tiles |
||
| 2091 | } |
||
| 2092 | |||
| 2093 | $this->w_figures = []; |
||
| 2094 | |||
| 2095 | $this->b_figures = []; |
||
| 2096 | |||
| 2097 | for ($i = 0; $i < 64; ++$i) { |
||
| 2098 | $tile = $this->board[$i]; |
||
| 2099 | |||
| 2100 | $coordinates = $this->boardIndexToCoord($i); |
||
| 2101 | |||
| 2102 | if ('w' == $tile[0]) { |
||
| 2103 | $this->w_figures[] = $tile[1] . $coordinates; |
||
| 2104 | } elseif ('b' == $tile[0]) { |
||
| 2105 | $this->b_figures[] = $tile[1] . $coordinates; |
||
| 2106 | } |
||
| 2107 | } |
||
| 2108 | |||
| 2109 | return true; |
||
| 2110 | } |
||
| 2111 | |||
| 2112 | /** |
||
| 2113 | * Convert array to FEN piece placement field. |
||
| 2114 | * |
||
| 2115 | * Use $this->board to initialize $this->gamestate['fen_piece_placement']. |
||
| 2116 | * |
||
| 2117 | * @access private |
||
| 2118 | */ |
||
| 2119 | public function board_to_fen_piece_placement() |
||
| 2120 | { |
||
| 2121 | $rows = []; |
||
| 2122 | |||
| 2123 | for ($rank = 7; $rank >= 0; --$rank) { |
||
| 2124 | $row = ''; |
||
| 2125 | |||
| 2126 | for ($file = 0; $file < 8; ++$file) { |
||
| 2127 | $index = 8 * $rank + $file; |
||
| 2128 | |||
| 2129 | if (!$this->is_empty_tile($index)) { |
||
| 2130 | $tile = $this->board[$index]; |
||
| 2131 | |||
| 2132 | $piece = 'w' == $tile[0] ? $tile[1] : \mb_strtolower($tile[1]); // 'K','Q','R','B','N' or 'P' (uppercase for white, lowercase for black) |
||
| 2133 | } else { |
||
| 2134 | $piece = 'x'; // temporarily mark each empty tile with 'x' |
||
| 2135 | } |
||
| 2136 | |||
| 2137 | $row .= $piece; // append piece symbol to row-string |
||
| 2138 | } |
||
| 2139 | |||
| 2140 | $rows[] = $row; |
||
| 2141 | } |
||
| 2142 | |||
| 2143 | // Concatenate the eight row-strings with the separator '/'. |
||
| 2144 | |||
| 2145 | // Then replace each string of x's with the string length. |
||
| 2146 | |||
| 2147 | $this->gamestate['fen_piece_placement'] = \preg_replace_callback('/(x+)/', static function($matches) {return \strlen($matches[1]);}, \implode('/', $rows)); |
||
| 2148 | } |
||
| 2149 | |||
| 2150 | /** |
||
| 2151 | * Determine whether there is insufficient material to mate. |
||
| 2152 | * |
||
| 2153 | * @return bool True if only the following pieces remain: K vs. K, K vs. K+B or K vs. K+N; otherwise false. |
||
| 2154 | * |
||
| 2155 | * @access private |
||
| 2156 | */ |
||
| 2157 | public function insufficient_mating_material() |
||
| 2158 | { |
||
| 2159 | $pieces = \mb_strtoupper($this->gamestate['fen_piece_placement']); |
||
| 2160 | |||
| 2161 | $counts = \count_chars($pieces, 1); |
||
| 2162 | |||
| 2163 | $num_queens = (int)@$counts[\ord('Q')]; |
||
| 2164 | |||
| 2165 | $num_rooks = (int)@$counts[\ord('R')]; |
||
| 2166 | |||
| 2167 | $num_bishops = (int)@$counts[\ord('B')]; |
||
| 2168 | |||
| 2169 | $num_knights = (int)@$counts[\ord('N')]; |
||
| 2170 | |||
| 2171 | $num_pawns = (int)@$counts[\ord('P')]; |
||
| 2172 | |||
| 2173 | return 0 == $num_queens && 0 == $num_rooks && ($num_bishops + $num_knights) <= 1 && 0 == $num_pawns; |
||
| 2174 | } |
||
| 2175 | |||
| 2176 | // -------------------------- |
||
| 2177 | |||
| 2178 | // INCIDENTAL PRIVATE METHODS |
||
| 2179 | |||
| 2180 | // -------------------------- |
||
| 2181 | |||
| 2182 | // These functions don't really need to be class methods, since they don't access class properties. |
||
| 2183 | |||
| 2184 | // They're placed within the class only for name-scoping. |
||
| 2185 | |||
| 2186 | /** |
||
| 2187 | * Convert board coordinates [a-h][1-8] to board index [0..63] |
||
| 2188 | * |
||
| 2189 | * @param string $coord Example: 'b3' |
||
| 2190 | * @return int Example: 17 |
||
| 2191 | * |
||
| 2192 | * @access private |
||
| 2193 | */ |
||
| 2194 | public function boardCoordToIndex($coord) |
||
| 2195 | { |
||
| 2196 | //echo $coord," --> "; |
||
| 2197 | |||
| 2198 | switch ($coord[0]) { |
||
| 2199 | case 'a': |
||
| 2200 | $x = 0; |
||
| 2201 | break; |
||
| 2202 | case 'b': |
||
| 2203 | $x = 1; |
||
| 2204 | break; |
||
| 2205 | case 'c': |
||
| 2206 | $x = 2; |
||
| 2207 | break; |
||
| 2208 | case 'd': |
||
| 2209 | $x = 3; |
||
| 2210 | break; |
||
| 2211 | case 'e': |
||
| 2212 | $x = 4; |
||
| 2213 | break; |
||
| 2214 | case 'f': |
||
| 2215 | $x = 5; |
||
| 2216 | break; |
||
| 2217 | case 'g': |
||
| 2218 | $x = 6; |
||
| 2219 | break; |
||
| 2220 | case 'h': |
||
| 2221 | $x = 7; |
||
| 2222 | break; |
||
| 2223 | default: |
||
| 2224 | return 64; /* erronous coord */ |
||
| 2225 | } |
||
| 2226 | |||
| 2227 | $y = $coord[1] - 1; |
||
| 2228 | |||
| 2229 | if ($y < 0 || $y > 7) { |
||
| 2230 | return 64; |
||
| 2231 | } /* erronous coord */ |
||
| 2232 | |||
| 2233 | return $y * 8 + $x; |
||
| 2234 | //echo "$index | "; |
||
| 2235 | } |
||
| 2236 | |||
| 2237 | /** |
||
| 2238 | * Convert board index [0..63] to board coordinates [a-h][1-8]. |
||
| 2239 | * |
||
| 2240 | * @param int $index Example: 17 |
||
| 2241 | * @return string Example: 'b3' |
||
| 2242 | * |
||
| 2243 | * @access private |
||
| 2244 | */ |
||
| 2245 | public function boardIndexToCoord($index) |
||
| 2246 | { |
||
| 2247 | //echo $index," --> "; |
||
| 2248 | |||
| 2249 | if ($index < 0 || $index > 63) { |
||
| 2250 | return ''; |
||
| 2251 | } |
||
| 2252 | |||
| 2253 | $y = \floor($index / 8) + 1; |
||
| 2254 | |||
| 2255 | $x = \chr(($index % 8) + 97); |
||
| 2256 | |||
| 2257 | return "$x$y"; |
||
| 2258 | } |
||
| 2259 | |||
| 2260 | /** |
||
| 2261 | * Get piece name from piece symbol. |
||
| 2262 | * |
||
| 2263 | * @param string $short Piece symbol |
||
| 2264 | * @return string Piece name |
||
| 2265 | * |
||
| 2266 | * @access private |
||
| 2267 | */ |
||
| 2268 | public function getFullFigureName($short) |
||
| 2269 | { |
||
| 2270 | static $names = [ |
||
| 2271 | 'K' => _MD_CHESS_MOVE_KING, |
||
| 2272 | 'Q' => _MD_CHESS_MOVE_QUEEN, |
||
| 2273 | 'R' => _MD_CHESS_MOVE_ROOK, |
||
| 2274 | 'B' => _MD_CHESS_MOVE_BISHOP, |
||
| 2275 | 'N' => _MD_CHESS_MOVE_KNIGHT, |
||
| 2276 | 'P' => _MD_CHESS_MOVE_PAWN, |
||
| 2277 | ]; |
||
| 2278 | |||
| 2279 | return $names[$short] ?? _MD_CHESS_MOVE_EMPTY; |
||
| 2280 | } |
||
| 2281 | |||
| 2282 | /** |
||
| 2283 | * Get tiles adjacent to specified tile. |
||
| 2284 | * |
||
| 2285 | * @param int $fig_pos |
||
| 2286 | * @return array |
||
| 2287 | * |
||
| 2288 | * @access private |
||
| 2289 | */ |
||
| 2290 | public function getAdjTiles($fig_pos) |
||
| 2291 | { |
||
| 2292 | $adj_tiles = []; |
||
| 2293 | |||
| 2294 | $i = 0; |
||
| 2295 | |||
| 2296 | $x = $fig_pos % 8; |
||
| 2297 | |||
| 2298 | $y = \floor($fig_pos / 8); |
||
| 2299 | |||
| 2300 | if ($x > 0 && $y > 0) { |
||
| 2301 | $adj_tiles[$i++] = $fig_pos - 9; |
||
| 2302 | } |
||
| 2303 | |||
| 2304 | if ($y > 0) { |
||
| 2305 | $adj_tiles[$i++] = $fig_pos - 8; |
||
| 2306 | } |
||
| 2307 | |||
| 2308 | if ($x < 7 && $y > 0) { |
||
| 2309 | $adj_tiles[$i++] = $fig_pos - 7; |
||
| 2310 | } |
||
| 2311 | |||
| 2312 | if ($x < 7) { |
||
| 2313 | $adj_tiles[$i++] = $fig_pos + 1; |
||
| 2314 | } |
||
| 2315 | |||
| 2316 | if ($x < 7 && $y < 7) { |
||
| 2317 | $adj_tiles[$i++] = $fig_pos + 9; |
||
| 2318 | } |
||
| 2319 | |||
| 2320 | if ($y < 7) { |
||
| 2321 | $adj_tiles[$i++] = $fig_pos + 8; |
||
| 2322 | } |
||
| 2323 | |||
| 2324 | if ($x > 0 && $y < 7) { |
||
| 2325 | $adj_tiles[$i++] = $fig_pos + 7; |
||
| 2326 | } |
||
| 2327 | |||
| 2328 | if ($x > 0) { |
||
| 2329 | $adj_tiles[$i++] = $fig_pos - 1; |
||
| 2330 | } |
||
| 2331 | |||
| 2332 | /* DEBUG: foreach( $adj_tiles as $tile ) |
||
| 2333 | echo "adj: $tile "; */ |
||
| 2334 | |||
| 2335 | return $adj_tiles; |
||
| 2336 | } |
||
| 2337 | } // class ChessGame |
||
| 2338 | |||
| 2339 | ?> |
||
|
0 ignored issues
–
show
It is not recommended to use PHP's closing tag
?> in files other than templates.
Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore. A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever. Loading history...
|
|||
| 2340 |