Passed
Push — master ( b6c62e...3fdbae )
by Michael
07:59
created

ChessGame::tileIsReachable()   D

Complexity

Conditions 36
Paths 44

Size

Total Lines 120
Code Lines 82

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 36
eloc 82
nc 44
nop 3
dl 0
loc 120
rs 4.1666
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
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 0;
339
            }
340
        }
341
342
        return 1;
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 -= 1;
479
            } else {
480
                $change += 1;
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 -= 1;
520
                } else {
521
                    $change += 1;
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;
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 1;
577
            }
578
579
            if ($fig_pos % 8 < 7 && $dest_pos == $fig_pos + 9) {
580
                return 1;
581
            }
582
        } elseif ('b' == $this->board[$fig_pos][0]) {
583
            if ($fig_pos % 8 < 7 && $dest_pos == $fig_pos - 7) {
584
                return 1;
585
            }
586
587
            if ($fig_pos % 8 > 0 && $dest_pos == $fig_pos - 9) {
588
                return 1;
589
            }
590
        }
591
592
        return 0;
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 1;
620
            }
621
622
            if ($first_move && ($dest_pos == $fig_pos + 16)) {
623
                if ($this->is_empty_tile($fig_pos + 8)) {
624
                    return 1;
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 1;
634
            }
635
636
            if ($first_move && ($dest_pos == $fig_pos - 16)) {
637
                if ($this->is_empty_tile($fig_pos - 8)) {
638
                    return 1;
639
                }
640
            }
641
        }
642
643
        return 0;
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 1;
667
                }
668
            }
669
        }
670
671
        return 0;
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);
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);
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 0;
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 1;
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 0;
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 1;
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) {
834
            return 1;
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 0;
857
                    }
858
                }
859
            }
860
        }
861
862
        return 1;
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 0;
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 0;
900
                            }
901
902
                            if ($i % 8 > 0 && $this->board[$i + 7][0] != $player) {
903
                                return 0;
904
                            }
905
906
                            if ($i % 8 < 7 && $this->board[$i + 9][0] != $player) {
907
                                return 0;
908
                            }
909
                        } else {
910
                            if ($this->is_empty_tile($i - 8)) {
911
                                return 0;
912
                            }
913
914
                            if ($i % 8 > 0 && $this->board[$i - 9][0] != $player) {
915
                                return 0;
916
                            }
917
918
                            if ($i % 8 < 7 && $this->board[$i - 7][0] != $player) {
919
                                return 0;
920
                            }
921
                        }
922
                        break;
923
                    case 'B':
924
                        if ($i - 9 >= 0 && $this->board[$i - 9][0] != $player) {
925
                            return 0;
926
                        }
927
                        if ($i - 7 >= 0 && $this->board[$i - 7][0] != $player) {
928
                            return 0;
929
                        }
930
                        if ($i + 9 <= 63 && $this->board[$i + 9][0] != $player) {
931
                            return 0;
932
                        }
933
                        if ($i + 7 <= 63 && $this->board[$i + 7][0] != $player) {
934
                            return 0;
935
                        }
936
                        break;
937
                    case 'R':
938
                        if ($i - 8 >= 0 && $this->board[$i - 8][0] != $player) {
939
                            return 0;
940
                        }
941
                        if ($i - 1 >= 0 && $this->board[$i - 1][0] != $player) {
942
                            return 0;
943
                        }
944
                        if ($i + 8 <= 63 && $this->board[$i + 8][0] != $player) {
945
                            return 0;
946
                        }
947
                        if ($i + 1 <= 63 && $this->board[$i + 1][0] != $player) {
948
                            return 0;
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 0;
956
                            }
957
                        }
958
                        break;
959
                    case 'N':
960
                        if ($i - 17 >= 0 && $this->board[$i - 17][0] != $player) {
961
                            return 0;
962
                        }
963
                        if ($i - 15 >= 0 && $this->board[$i - 15][0] != $player) {
964
                            return 0;
965
                        }
966
                        if ($i - 6 >= 0 && $this->board[$i - 6][0] != $player) {
967
                            return 0;
968
                        }
969
                        if ($i + 10 <= 63 && $this->board[$i + 10][0] != $player) {
970
                            return 0;
971
                        }
972
                        if ($i + 17 <= 63 && $this->board[$i + 17][0] != $player) {
973
                            return 0;
974
                        }
975
                        if ($i + 15 <= 63 && $this->board[$i + 15][0] != $player) {
976
                            return 0;
977
                        }
978
                        if ($i + 6 <= 63 && $this->board[$i + 6][0] != $player) {
979
                            return 0;
980
                        }
981
                        if ($i - 10 >= 0 && $this->board[$i - 10][0] != $player) {
982
                            return 0;
983
                        }
984
                        break;
985
                }
986
            }
987
        }
988
989
        return 1;
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();
1009
1010
        #var_dump('move_msg, text', $text, 'param', $param);#*#DEBUG#
1011
1012
        return eval("return \"$text\";");
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')) {
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_NS_SEPARATOR, expecting T_STRING on line 1053 at column 23
Loading history...
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) {
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;
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,
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),
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') {
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);
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;
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';
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 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
?>
2340