ChessGame::board_to_fen_piece_placement()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 29
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 5
nop 0
dl 0
loc 29
rs 9.5222
c 0
b 0
f 0
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
The property browsing_mode does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
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
Bug Best Practice introduced by
The expression return $result returns the type integer which is incompatible with the documented return type boolean.
Loading history...
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
The variable $king_pos does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
The variable $king_pos does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
The variable $king_y does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $king_x does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
Unused Code introduced by
The assignment to $param is dead and can be removed.
Loading history...
1009
1010
        #var_dump('move_msg, text', $text, 'param', $param);#*#DEBUG#
1011
1012
        return eval("return \"$text\";");
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
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
introduced by
The condition 0 == $pawns is always true.
Loading history...
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
The variable $pawn_line does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
The variable $pos1 does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
The variable $pos2 does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
The variable $hint does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
The variable $dest does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
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
Bug Best Practice introduced by
The property piece_captured does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
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
Best Practice introduced by
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