Notation::withTargetRow()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
rs 10
cc 1
nc 1
nop 1
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
        // Check for castling:
163
        if (preg_match('/^(0-0|0-0-0)(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
164
            $this->castling = $matches[1];
165
            $this->check = $matches[2] === '+';
166
            $this->checkmate = $matches[2] === '#';
167
            $this->annotation = isset($matches[3]) ? $matches[3] : null;
168
            return;
169
        }
170
171
        // Pawn movement:
172
        if (preg_match('/^([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
173
            $this->targetColumn = $matches[1];
174
            $this->targetRow = (int)$matches[2];
175
            $this->check = $matches[3] === '+';
176
            $this->checkmate = $matches[3] === '#';
177
            $this->movedPiece = self::PIECE_PAWN;
178
            $this->annotation = isset($matches[4]) ? $matches[4] : null;
179
            return;
180
        }
181
182
        // Pawn movement (long san):
183
        if (preg_match('/^([a-h])([1-8])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
184
            $this->movedPieceDisambiguationColumn = $matches[1];
185
            $this->movedPieceDisambiguationRow = (int)$matches[2];
186
            $this->targetColumn = $matches[3];
187
            $this->targetRow = (int)$matches[4];
188
            $this->check = $matches[5] === '+';
189
            $this->checkmate = $matches[5] === '#';
190
            $this->movedPiece = self::PIECE_PAWN;
191
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
192
            $this->longSan = true;
193
            return;
194
        }
195
196
        // Piece movement:
197
        if (preg_match('/^([KQBNR])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
198
            $this->movedPiece = $matches[1];
199
            $this->targetColumn = $matches[2];
200
            $this->targetRow = (int)$matches[3];
201
            $this->check = $matches[4] === '+';
202
            $this->checkmate = $matches[4] === '#';
203
            $this->annotation = isset($matches[5]) ? $matches[5] : null;
204
            return;
205
        }
206
207
        // Piece movement from a specific column:
208
        if (preg_match('/^([KQBNR])([a-h])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
209
            $this->movedPiece = $matches[1];
210
            $this->movedPieceDisambiguationColumn = $matches[2];
211
            $this->targetColumn = $matches[3];
212
            $this->targetRow = (int)$matches[4];
213
            $this->check = $matches[5] === '+';
214
            $this->checkmate = $matches[5] === '#';
215
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
216
            return;
217
        }
218
219
        // Piece movement from a specific row:
220
        if (preg_match('/^([KQBNR])([0-9])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
221
            $this->movedPiece = $matches[1];
222
            $this->movedPieceDisambiguationRow = (int)$matches[2];
223
            $this->targetColumn = $matches[3];
224
            $this->targetRow = (int)$matches[4];
225
            $this->check = $matches[5] === '+';
226
            $this->checkmate = $matches[5] === '#';
227
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
228
            return;
229
        }
230
231
        // Piece movement from a specific column and row (long san):
232
        if (preg_match('/^([KQBNR])([a-h])([0-9])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
233
            $this->movedPiece = $matches[1];
234
            $this->movedPieceDisambiguationColumn = $matches[2];
235
            $this->movedPieceDisambiguationRow = (int)$matches[3];
236
            $this->targetColumn = $matches[4];
237
            $this->targetRow = (int)$matches[5];
238
            $this->check = $matches[6] === '+';
239
            $this->checkmate = $matches[6] === '#';
240
            $this->annotation = isset($matches[7]) ? $matches[7] : null;
241
            $this->longSan = true;
242
            return;
243
        }
244
245
        // Pawn capture:
246
        if (preg_match('/^([a-h])x([a-h])([1-8])(?:=?([KQBNR]))?(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
247
            $this->targetColumn = $matches[2];
248
            $this->targetRow = (int)$matches[3];
249
            $this->movedPiece = self::PIECE_PAWN;
250
            $this->movedPieceDisambiguationColumn = $matches[1];
251
            $this->capture = true;
252
            $this->promotedPiece = $matches[4] ?: null;
253
            $this->check = $matches[5] === '+';
254
            $this->checkmate = $matches[5] === '#';
255
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
256
            return;
257
        }
258
259
        // Pawn capture (long san):
260
        if (preg_match('/^([a-h])([1-8])x([a-h])([1-8])(?:=?([KQBNR]))?(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
261
            $this->targetColumn = $matches[3];
262
            $this->targetRow = (int)$matches[4];
263
            $this->movedPiece = self::PIECE_PAWN;
264
            $this->movedPieceDisambiguationColumn = $matches[1];
265
            $this->movedPieceDisambiguationRow = (int)$matches[2];
266
            $this->capture = true;
267
            $this->promotedPiece = $matches[5] ?: null;
268
            $this->check = $matches[6] === '+';
269
            $this->checkmate = $matches[6] === '#';
270
            $this->annotation = isset($matches[7]) ? $matches[7] : null;
271
            $this->longSan = true;
272
            return;
273
        }
274
275
        // Piece capture:
276
        if (preg_match('/^([KQBNR])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
277
            $this->movedPiece = $matches[1];
278
            $this->targetColumn = $matches[2];
279
            $this->targetRow = (int)$matches[3];
280
            $this->check = $matches[4] === '+';
281
            $this->checkmate = $matches[4] === '#';
282
            $this->capture = true;
283
            $this->annotation = isset($matches[5]) ? $matches[5] : null;
284
            return;
285
        }
286
287
        // Piece capture from a specific column:
288
        if (preg_match('/^([KQBNR])([a-h])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
289
            $this->movedPiece = $matches[1];
290
            $this->movedPieceDisambiguationColumn = $matches[2];
291
            $this->targetColumn = $matches[3];
292
            $this->targetRow = (int)$matches[4];
293
            $this->check = $matches[5] === '+';
294
            $this->checkmate = $matches[5] === '#';
295
            $this->capture = true;
296
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
297
            return;
298
        }
299
300
        // Piece capture from a specific row:
301
        if (preg_match('/^([KQBNR])([0-9])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
302
            $this->movedPiece = $matches[1];
303
            $this->movedPieceDisambiguationRow = (int)$matches[2];
304
            $this->targetColumn = $matches[3];
305
            $this->targetRow = (int)$matches[4];
306
            $this->check = $matches[5] === '+';
307
            $this->checkmate = $matches[5] === '#';
308
            $this->capture = true;
309
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
310
            return;
311
        }
312
313
        // Piece capture from a specific column and row (long san):
314
        if (preg_match('/^([KQBNR])([a-h])([0-9])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
315
            $this->movedPiece = $matches[1];
316
            $this->movedPieceDisambiguationColumn = $matches[2];
317
            $this->movedPieceDisambiguationRow = (int)$matches[3];
318
            $this->targetColumn = $matches[4];
319
            $this->targetRow = (int)$matches[5];
320
            $this->check = $matches[6] === '+';
321
            $this->checkmate = $matches[6] === '#';
322
            $this->capture = true;
323
            $this->annotation = isset($matches[7]) ? $matches[7] : null;
324
            $this->longSan = true;
325
            return;
326
        }
327
328
        // Check for pawn promotion:
329
        if (preg_match('/^([a-h])([1-8])=?([KQBNR])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
330
            $this->movedPiece = self::PIECE_PAWN;
331
            $this->targetColumn = $matches[1];
332
            $this->targetRow = (int)$matches[2];
333
            $this->promotedPiece = $matches[3];
334
            $this->check = $matches[4] === '+';
335
            $this->checkmate = $matches[4] === '#';
336
            $this->annotation = isset($matches[5]) ? $matches[5] : null;
337
            return;
338
        }
339
340
        throw new InvalidArgumentException(sprintf(
341
            'The value "%s" could not be parsed.',
342
            $value
343
        ));
344
    }
345
346
    /**
347
     * Checks if this move is a castling move.
348
     *
349
     * @return bool Returns true when this is a castling move; false otherwise.
350
     */
351
    public function isCastlingMove(): bool
352
    {
353
        return $this->isCastlingTowardsKingSide() || $this->isCastlingTowardsQueenSide();
354
    }
355
356
    /**
357
     * Checks if this is a castling move towards the king side.
358
     *
359
     * @return bool Returns true if this is a castling move; false otherwise.
360
     */
361
    public function isCastlingTowardsKingSide(): bool
362
    {
363
        return $this->castling === self::CASTLING_KING_SIDE;
364
    }
365
366
    /**
367
     * Checks if this is a castling move towards the queen side.
368
     *
369
     * @return bool Returns true if this is a castling move; false otherwise.
370
     */
371
    public function isCastlingTowardsQueenSide(): bool
372
    {
373
        return $this->castling === self::CASTLING_QUEEN_SIDE;
374
    }
375
376
    /**
377
     * Gets the original value.
378
     *
379
     * @return string
380
     */
381
    public function getValue(): string
382
    {
383
        return $this->value;
384
    }
385
386
    /**
387
     * Gets the castling value.
388
     *
389
     * @return null|string
390
     */
391
    public function getCastling(): ?string
392
    {
393
        return $this->castling;
394
    }
395
396
    /**
397
     * Gets the column to where the move was made.
398
     *
399
     * @return null|string
400
     */
401
    public function getTargetColumn(): ?string
402
    {
403
        return $this->targetColumn;
404
    }
405
406
    /**
407
     * Gets the column index to where the move was made.
408
     *
409
     * @return int|null
410
     */
411
    public function getTargetColumnIndex(): ?int
412
    {
413
        if ($this->targetColumn === null) {
414
            return null;
415
        }
416
417
        return ord($this->targetColumn) - 97;
418
    }
419
420
    /**
421
     * Duplicates the notation with a column based on an index from 0 to 7.
422
     *
423
     * @param int $index The index of the column to use.
424
     * @return Notation
425
     * @throws InvalidArgumentException Thrown when an invalid SAN value is created.
426
     * @throws RuntimeException Thrown when no target row is set.
427
     */
428
    public function withTargetColumnIndex(int $index): Notation
429
    {
430
        if (!$this->getTargetRow()) {
431
            throw new RuntimeException('No row has been set.');
432
        }
433
434
        $notation = chr(97 + $index) . $this->getTargetRow();
435
436
        return new self($notation);
437
    }
438
439
    /**
440
     * Gets the row number to where the move was made.
441
     *
442
     * @return int|null
443
     */
444
    public function getTargetRow(): ?int
445
    {
446
        return $this->targetRow;
447
    }
448
449
    /**
450
     * Duplicates the notation with a new row.
451
     *
452
     * @param int $row A value between 1 and 8.
453
     * @return Notation
454
     * @throws InvalidArgumentException Thrown when an invalid SAN value is created.
455
     */
456
    public function withTargetRow(int $row): Notation
457
    {
458
        $notation = $this->getTargetColumn() . $row;
459
460
        return new self($notation);
461
    }
462
463
    /**
464
     * Gets the target notation.
465
     *
466
     * @return string
467
     */
468
    public function getTargetNotation(): string
469
    {
470
        return $this->getTargetColumn() . $this->getTargetRow();
471
    }
472
473
    /**
474
     * Gets the piece that was moved.
475
     *
476
     * @return null|string Returns the piece or null when a pawn was moved.
477
     */
478
    public function getMovedPiece(): ?string
479
    {
480
        return $this->movedPiece;
481
    }
482
483
    /**
484
     * Gets the disambiguation column.
485
     *
486
     * @return null|string
487
     */
488
    public function getMovedPieceDisambiguationColumn(): ?string
489
    {
490
        return $this->movedPieceDisambiguationColumn;
491
    }
492
493
    /**
494
     * Sets the disambiguation column.
495
     *
496
     * @param null|string $movedPieceDisambiguationColumn
497
     */
498
    public function setMovedPieceDisambiguationColumn(?string $movedPieceDisambiguationColumn): void
499
    {
500
        $this->movedPieceDisambiguationColumn = $movedPieceDisambiguationColumn;
501
    }
502
503
    /**
504
     * Gets the disambiguation row.
505
     *
506
     * @return int|null
507
     */
508
    public function getMovedPieceDisambiguationRow(): ?int
509
    {
510
        return $this->movedPieceDisambiguationRow;
511
    }
512
513
    /**
514
     * Sets the disambiguation row.
515
     *
516
     * @param int|null $movedPieceDisambiguationRow
517
     */
518
    public function setMovedPieceDisambiguationRow(?int $movedPieceDisambiguationRow): void
519
    {
520
        $this->movedPieceDisambiguationRow = $movedPieceDisambiguationRow;
521
    }
522
523
    /**
524
     * Gets the flag that indicates if this was a capture move.
525
     *
526
     * @return bool
527
     */
528
    public function isCapture(): bool
529
    {
530
        return $this->capture;
531
    }
532
533
    /**
534
     * Gets the piece into which the pawn was promoted.
535
     *
536
     * @return null|string
537
     */
538
    public function getPromotedPiece(): ?string
539
    {
540
        return $this->promotedPiece;
541
    }
542
543
    /**
544
     * Gets the flag that indicates whether or not the move returned into a check state.
545
     *
546
     * @return bool
547
     */
548
    public function isCheck(): bool
549
    {
550
        return $this->check;
551
    }
552
553
    /**
554
     * Gets the flag that indicates whether or not the move returned into a checkmate state.
555
     *
556
     * @return bool
557
     */
558
    public function isCheckmate(): bool
559
    {
560
        return $this->checkmate;
561
    }
562
563
    /**
564
     * Gets the annotation that has been given to this move.
565
     *
566
     * @return null|string
567
     */
568
    public function getAnnotation(): ?string
569
    {
570
        return $this->annotation;
571
    }
572
573
    /**
574
     * Checks whether or not this notation is in long-SAN format.
575
     *
576
     * @return bool
577
     */
578
    public function isLongSan(): bool
579
    {
580
        return $this->longSan;
581
    }
582
583
    /**
584
     * Converts the move to a string.
585
     *
586
     * @return string
587
     */
588
    public function toString(): string
589
    {
590
        return $this->getValue();
591
    }
592
593
    /**
594
     * Converts the move to a string.
595
     *
596
     * @return string
597
     */
598
    public function __toString()
599
    {
600
        return $this->toString();
601
    }
602
}
603