Completed
Push — master ( 3bc4c6...ebaca4 )
by Walter
04:38
created

Notation::isCapture()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
c 0
b 0
f 0
rs 10
cc 1
eloc 1
nc 1
nop 0
1
<?php declare(strict_types=1);
2
/**
3
 * standard-algebraic-notation (https://github.com/chesszebra/standard-algebraic-notation)
4
 *
5
 * @link https://github.com/chesszebra/standard-algebraic-notation for the canonical source repository
6
 * @copyright Copyright (c) 2017 Chess Zebra (https://chesszebra.com)
7
 * @license https://github.com/chesszebra/standard-algebraic-notation/blob/master/LICENSE.md MIT
8
 */
9
10
namespace ChessZebra\StandardAlgebraicNotation;
11
12
use ChessZebra\StandardAlgebraicNotation\Exception\InvalidArgumentException;
13
use ChessZebra\StandardAlgebraicNotation\Exception\RuntimeException;
14
15
/**
16
 * The representation of a Standard Algebraic Notation (SAN) notation.
17
 */
18
final class Notation
19
{
20
    public const PIECE_PAWN = null;
21
    public const PIECE_BISHOP = 'B';
22
    public const PIECE_KING = 'K';
23
    public const PIECE_KNIGHT = 'N';
24
    public const PIECE_QUEEN = 'Q';
25
    public const PIECE_ROOK = 'R';
26
27
    public const CASTLING_KING_SIDE = 'O-O';
28
    public const CASTLING_QUEEN_SIDE = 'O-O-O';
29
30
    public const ANNOTATION_BLUNDER = '??';
31
    public const ANNOTATION_MISTAKE = '?';
32
    public const ANNOTATION_INTERESTING_MOVE = '?!';
33
    public const ANNOTATION_GOOD_MOVE = '!';
34
    public const ANNOTATION_BRILLIANT_MOVE = '!!';
35
36
    /**
37
     * The original value.
38
     *
39
     * @var string
40
     */
41
    private $value;
42
43
    /**
44
     * The type of castling move that was made.
45
     *
46
     * @var null|string
47
     */
48
    private $castling;
49
50
    /**
51
     * The target column.
52
     *
53
     * @var string
54
     */
55
    private $targetColumn;
56
57
    /**
58
     * The target row.
59
     *
60
     * @var int
61
     */
62
    private $targetRow;
63
64
    /**
65
     * The piece that was moved.
66
     *
67
     * @var string
68
     */
69
    private $movedPiece;
70
71
    /**
72
     * The column from where the move was made.
73
     *
74
     * @var string
75
     */
76
    private $movedPieceDisambiguationColumn;
77
78
    /**
79
     * The row from where the move was made.
80
     *
81
     * @var int
82
     */
83
    private $movedPieceDisambiguationRow;
84
85
    /**
86
     * The piece to which the pawn was promoted into.
87
     *
88
     * @var string
89
     */
90
    private $promotedPiece;
91
92
    /**
93
     * A flag that indicates whether or not a piece was captured.
94
     *
95
     * @var bool
96
     */
97
    private $capture;
98
99
    /**
100
     * A flag that indicates that a check move was made.
101
     *
102
     * @var bool
103
     */
104
    private $check;
105
106
    /**
107
     * A flag that indicates that a checkmate move was made.
108
     *
109
     * @var bool
110
     */
111
    private $checkmate;
112
113
    /**
114
     * An annotation given to a move.
115
     * For example "Nbd7?!"
116
     *
117
     * @var null|string
118
     */
119
    private $annotation;
120
121
    /**
122
     * A flag that indicates whether or not this notation is a long-SAN.
123
     *
124
     * @var bool
125
     */
126
    private $longSan;
127
128
    /**
129
     * Initializes a new instance of this class.
130
     *
131
     * @param string $value
132
     * @throws InvalidArgumentException Thrown when an invalid value is provided.
133
     */
134
    public function __construct(string $value)
135
    {
136
        $this->value = $value;
137
        $this->capture = false;
138
        $this->check = false;
139
        $this->checkmate = false;
140
        $this->longSan = false;
141
142
        $this->parse($value);
143
    }
144
145
    /**
146
     * Parses a SAN value.
147
     *
148
     * @param string $value The value to parse.
149
     * @throws InvalidArgumentException Thrown when an invalid value is provided.
150
     */
151
    private function parse(string $value): void
152
    {
153
        // Check for castling:
154
        if (preg_match('/^(O-O|O-O-O)(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
155
            $this->castling = $matches[1];
156
            $this->check = $matches[2] === '+';
157
            $this->checkmate = $matches[2] === '#';
158
            $this->annotation = isset($matches[3]) ? $matches[3] : null;
159
            return;
160
        }
161
162
        // Pawn movement:
163
        if (preg_match('/^([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
164
            $this->targetColumn = $matches[1];
165
            $this->targetRow = (int)$matches[2];
166
            $this->check = $matches[3] === '+';
167
            $this->checkmate = $matches[3] === '#';
168
            $this->movedPiece = self::PIECE_PAWN;
169
            $this->annotation = isset($matches[4]) ? $matches[4] : null;
170
            return;
171
        }
172
173
        // Pawn movement (long san):
174
        if (preg_match('/^([a-h])([1-8])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
175
            $this->movedPieceDisambiguationColumn = $matches[1];
176
            $this->movedPieceDisambiguationRow = (int)$matches[2];
177
            $this->targetColumn = $matches[3];
178
            $this->targetRow = (int)$matches[4];
179
            $this->check = $matches[5] === '+';
180
            $this->checkmate = $matches[5] === '#';
181
            $this->movedPiece = self::PIECE_PAWN;
182
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
183
            $this->longSan = true;
184
            return;
185
        }
186
187
        // Piece movement:
188
        if (preg_match('/^([KQBNR])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
189
            $this->movedPiece = $matches[1];
190
            $this->targetColumn = $matches[2];
191
            $this->targetRow = (int)$matches[3];
192
            $this->check = $matches[4] === '+';
193
            $this->checkmate = $matches[4] === '#';
194
            $this->annotation = isset($matches[5]) ? $matches[5] : null;
195
            return;
196
        }
197
198
        // Piece movement from a specific column:
199
        if (preg_match('/^([KQBNR])([a-h])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
200
            $this->movedPiece = $matches[1];
201
            $this->movedPieceDisambiguationColumn = $matches[2];
202
            $this->targetColumn = $matches[3];
203
            $this->targetRow = (int)$matches[4];
204
            $this->check = $matches[5] === '+';
205
            $this->checkmate = $matches[5] === '#';
206
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
207
            return;
208
        }
209
210
        // Piece movement from a specific row:
211
        if (preg_match('/^([KQBNR])([0-9])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
212
            $this->movedPiece = $matches[1];
213
            $this->movedPieceDisambiguationRow = (int)$matches[2];
214
            $this->targetColumn = $matches[3];
215
            $this->targetRow = (int)$matches[4];
216
            $this->check = $matches[5] === '+';
217
            $this->checkmate = $matches[5] === '#';
218
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
219
            return;
220
        }
221
222
        // Piece movement from a specific column and row (long san):
223
        if (preg_match('/^([KQBNR])([a-h])([0-9])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
224
            $this->movedPiece = $matches[1];
225
            $this->movedPieceDisambiguationColumn = $matches[2];
226
            $this->movedPieceDisambiguationRow = (int)$matches[3];
227
            $this->targetColumn = $matches[4];
228
            $this->targetRow = (int)$matches[5];
229
            $this->check = $matches[6] === '+';
230
            $this->checkmate = $matches[6] === '#';
231
            $this->annotation = isset($matches[7]) ? $matches[7] : null;
232
            $this->longSan = true;
233
            return;
234
        }
235
236
        // Pawn capture:
237
        if (preg_match('/^([a-h])x([a-h])([1-8])(?:=([KQBNR]))?(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
238
            $this->targetColumn = $matches[2];
239
            $this->targetRow = (int)$matches[3];
240
            $this->movedPiece = self::PIECE_PAWN;
241
            $this->movedPieceDisambiguationColumn = $matches[1];
242
            $this->capture = true;
243
            $this->promotedPiece = $matches[4] ?: null;
244
            $this->check = $matches[5] === '+';
245
            $this->checkmate = $matches[5] === '#';
246
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
247
            return;
248
        }
249
250
        // Pawn capture (long san):
251
        if (preg_match('/^([a-h])([1-8])x([a-h])([1-8])(?:=([KQBNR]))?(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
252
            $this->targetColumn = $matches[3];
253
            $this->targetRow = (int)$matches[4];
254
            $this->movedPiece = self::PIECE_PAWN;
255
            $this->movedPieceDisambiguationColumn = $matches[1];
256
            $this->movedPieceDisambiguationRow = (int)$matches[2];
257
            $this->capture = true;
258
            $this->promotedPiece = $matches[5] ?: null;
259
            $this->check = $matches[6] === '+';
260
            $this->checkmate = $matches[6] === '#';
261
            $this->annotation = isset($matches[7]) ? $matches[7] : null;
262
            $this->longSan = true;
263
            return;
264
        }
265
266
        // Piece capture:
267
        if (preg_match('/^([KQBNR])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
268
            $this->movedPiece = $matches[1];
269
            $this->targetColumn = $matches[2];
270
            $this->targetRow = (int)$matches[3];
271
            $this->check = $matches[4] === '+';
272
            $this->checkmate = $matches[4] === '#';
273
            $this->capture = true;
274
            $this->annotation = isset($matches[5]) ? $matches[5] : null;
275
            return;
276
        }
277
278
        // Piece capture from a specific column:
279
        if (preg_match('/^([KQBNR])([a-h])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
280
            $this->movedPiece = $matches[1];
281
            $this->movedPieceDisambiguationColumn = $matches[2];
282
            $this->targetColumn = $matches[3];
283
            $this->targetRow = (int)$matches[4];
284
            $this->check = $matches[5] === '+';
285
            $this->checkmate = $matches[5] === '#';
286
            $this->capture = true;
287
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
288
            return;
289
        }
290
291
        // Piece capture from a specific row:
292
        if (preg_match('/^([KQBNR])([0-9])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
293
            $this->movedPiece = $matches[1];
294
            $this->movedPieceDisambiguationRow = (int)$matches[2];
295
            $this->targetColumn = $matches[3];
296
            $this->targetRow = (int)$matches[4];
297
            $this->check = $matches[5] === '+';
298
            $this->checkmate = $matches[5] === '#';
299
            $this->capture = true;
300
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
301
            return;
302
        }
303
304
        // Piece capture from a specific column and row (long san):
305
        if (preg_match('/^([KQBNR])([a-h])([0-9])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
306
            $this->movedPiece = $matches[1];
307
            $this->movedPieceDisambiguationColumn = $matches[2];
308
            $this->movedPieceDisambiguationRow = (int)$matches[3];
309
            $this->targetColumn = $matches[4];
310
            $this->targetRow = (int)$matches[5];
311
            $this->check = $matches[6] === '+';
312
            $this->checkmate = $matches[6] === '#';
313
            $this->capture = true;
314
            $this->annotation = isset($matches[7]) ? $matches[7] : null;
315
            $this->longSan = true;
316
            return;
317
        }
318
319
        // Check for pawn promotion:
320
        if (preg_match('/^([a-h])([1-8])=?([KQBNR])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
321
            $this->movedPiece = self::PIECE_PAWN;
322
            $this->targetColumn = $matches[1];
323
            $this->targetRow = (int)$matches[2];
324
            $this->promotedPiece = $matches[3];
325
            $this->check = $matches[4] === '+';
326
            $this->checkmate = $matches[4] === '#';
327
            $this->annotation = isset($matches[5]) ? $matches[5] : null;
328
            return;
329
        }
330
331
        throw new InvalidArgumentException(sprintf(
332
            'The value "%s" could not be parsed.',
333
            $value
334
        ));
335
    }
336
337
    /**
338
     * Checks if this move is a castling move.
339
     *
340
     * @return bool Returns true when this is a castling move; false otherwise.
341
     */
342
    public function isCastlingMove(): bool
343
    {
344
        return $this->isCastlingTowardsKingSide() || $this->isCastlingTowardsQueenSide();
345
    }
346
347
    /**
348
     * Checks if this is a castling move towards the king side.
349
     *
350
     * @return bool Returns true if this is a castling move; false otherwise.
351
     */
352
    public function isCastlingTowardsKingSide(): bool
353
    {
354
        return $this->castling === self::CASTLING_KING_SIDE;
355
    }
356
357
    /**
358
     * Checks if this is a castling move towards the queen side.
359
     *
360
     * @return bool Returns true if this is a castling move; false otherwise.
361
     */
362
    public function isCastlingTowardsQueenSide(): bool
363
    {
364
        return $this->castling === self::CASTLING_QUEEN_SIDE;
365
    }
366
367
    /**
368
     * Gets the original value.
369
     *
370
     * @return string
371
     */
372
    public function getValue(): string
373
    {
374
        return $this->value;
375
    }
376
377
    /**
378
     * Gets the castling value.
379
     *
380
     * @return null|string
381
     */
382
    public function getCastling(): ?string
383
    {
384
        return $this->castling;
385
    }
386
387
    /**
388
     * Gets the column to where the move was made.
389
     *
390
     * @return null|string
391
     */
392
    public function getTargetColumn(): ?string
393
    {
394
        return $this->targetColumn;
395
    }
396
397
    /**
398
     * Gets the column index to where the move was made.
399
     *
400
     * @return int|null
401
     */
402
    public function getTargetColumnIndex(): ?int
403
    {
404
        if ($this->targetColumn === null) {
405
            return null;
406
        }
407
408
        return ord($this->targetColumn) - 97;
409
    }
410
411
    /**
412
     * Duplicates the notation with a column based on an index from 0 to 7.
413
     *
414
     * @param int $index The index of the column to use.
415
     * @return Notation
416
     * @throws InvalidArgumentException Thrown when an invalid SAN value is created.
417
     * @throws RuntimeException Thrown when no target row is set.
418
     */
419
    public function withTargetColumnIndex(int $index): Notation
420
    {
421
        if (!$this->getTargetRow()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getTargetRow() of type null|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
422
            throw new RuntimeException('No row has been set.');
423
        }
424
425
        $notation = chr(97 + $index) . $this->getTargetRow();
426
427
        return new self($notation);
428
    }
429
430
    /**
431
     * Gets the row number to where the move was made.
432
     *
433
     * @return int|null
434
     */
435
    public function getTargetRow(): ?int
436
    {
437
        return $this->targetRow;
438
    }
439
440
    /**
441
     * Duplicates the notation with a new row.
442
     *
443
     * @param int $row A value between 1 and 8.
444
     * @return Notation
445
     * @throws InvalidArgumentException Thrown when an invalid SAN value is created.
446
     */
447
    public function withTargetRow(int $row): Notation
448
    {
449
        $notation = $this->getTargetColumn() . $row;
450
451
        return new self($notation);
452
    }
453
454
    /**
455
     * Gets the target notation.
456
     *
457
     * @return string
458
     */
459
    public function getTargetNotation(): string
460
    {
461
        return $this->getTargetColumn() . $this->getTargetRow();
462
    }
463
464
    /**
465
     * Gets the piece that was moved.
466
     *
467
     * @return null|string Returns the piece or null when a pawn was moved.
468
     */
469
    public function getMovedPiece(): ?string
470
    {
471
        return $this->movedPiece;
472
    }
473
474
    /**
475
     * Gets the disambiguation column.
476
     *
477
     * @return null|string
478
     */
479
    public function getMovedPieceDisambiguationColumn(): ?string
480
    {
481
        return $this->movedPieceDisambiguationColumn;
482
    }
483
484
    /**
485
     * Sets the disambiguation column.
486
     *
487
     * @param null|string $movedPieceDisambiguationColumn
488
     */
489
    public function setMovedPieceDisambiguationColumn(?string $movedPieceDisambiguationColumn): void
490
    {
491
        $this->movedPieceDisambiguationColumn = $movedPieceDisambiguationColumn;
492
    }
493
494
    /**
495
     * Gets the disambiguation row.
496
     *
497
     * @return int|null
498
     */
499
    public function getMovedPieceDisambiguationRow(): ?int
500
    {
501
        return $this->movedPieceDisambiguationRow;
502
    }
503
504
    /**
505
     * Sets the disambiguation row.
506
     *
507
     * @param int|null $movedPieceDisambiguationRow
508
     */
509
    public function setMovedPieceDisambiguationRow(?int $movedPieceDisambiguationRow): void
510
    {
511
        $this->movedPieceDisambiguationRow = $movedPieceDisambiguationRow;
512
    }
513
514
    /**
515
     * Gets the flag that indicates if this was a capture move.
516
     *
517
     * @return bool
518
     */
519
    public function isCapture(): bool
520
    {
521
        return $this->capture;
522
    }
523
524
    /**
525
     * Gets the piece into which the pawn was promoted.
526
     *
527
     * @return null|string
528
     */
529
    public function getPromotedPiece(): ?string
530
    {
531
        return $this->promotedPiece;
532
    }
533
534
    /**
535
     * Gets the flag that indicates whether or not the move returned into a check state.
536
     *
537
     * @return bool
538
     */
539
    public function isCheck(): bool
540
    {
541
        return $this->check;
542
    }
543
544
    /**
545
     * Gets the flag that indicates whether or not the move returned into a checkmate state.
546
     *
547
     * @return bool
548
     */
549
    public function isCheckmate(): bool
550
    {
551
        return $this->checkmate;
552
    }
553
554
    /**
555
     * Gets the annotation that has been given to this move.
556
     *
557
     * @return null|string
558
     */
559
    public function getAnnotation(): ?string
560
    {
561
        return $this->annotation;
562
    }
563
564
    /**
565
     * Checks whether or not this notation is in long-SAN format.
566
     *
567
     * @return bool
568
     */
569
    public function isLongSan(): bool
570
    {
571
        return $this->longSan;
572
    }
573
574
    /**
575
     * Converts the move to a string.
576
     *
577
     * @return string
578
     */
579
    public function toString(): string
580
    {
581
        return $this->getValue();
582
    }
583
584
    /**
585
     * Converts the move to a string.
586
     *
587
     * @return string
588
     */
589
    public function __toString()
590
    {
591
        return $this->toString();
592
    }
593
}
594