Passed
Push — master ( 5e1a02...3bc4c6 )
by Walter
02:31
created

Notation::getAnnotation()   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
rs 10
c 0
b 0
f 0
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
     * Initializes a new instance of this class.
123
     *
124
     * @param string $value
125
     * @throws InvalidArgumentException Thrown when an invalid value is provided.
126
     */
127
    public function __construct(string $value)
128
    {
129
        $this->value = $value;
130
        $this->capture = false;
131
        $this->check = false;
132
        $this->checkmate = false;
133
134
        $this->parse($value);
135
    }
136
137
    /**
138
     * Parses a SAN value.
139
     *
140
     * @param string $value The value to parse.
141
     * @throws InvalidArgumentException Thrown when an invalid value is provided.
142
     */
143
    private function parse(string $value): void
144
    {
145
        // Check for castling:
146
        if (preg_match('/^(O-O|O-O-O)(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
147
            $this->castling = $matches[1];
148
            $this->check = $matches[2] === '+';
149
            $this->checkmate = $matches[2] === '#';
150
            $this->annotation = isset($matches[3]) ? $matches[3] : null;
151
            return;
152
        }
153
154
        // Pawn movement:
155
        if (preg_match('/^([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
156
            $this->targetColumn = $matches[1];
157
            $this->targetRow = (int)$matches[2];
158
            $this->check = $matches[3] === '+';
159
            $this->checkmate = $matches[3] === '#';
160
            $this->movedPiece = self::PIECE_PAWN;
161
            $this->annotation = isset($matches[4]) ? $matches[4] : null;
162
            return;
163
        }
164
165
        // Piece movement:
166
        if (preg_match('/^([KQBNR])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
167
            $this->movedPiece = $matches[1];
168
            $this->targetColumn = $matches[2];
169
            $this->targetRow = (int)$matches[3];
170
            $this->check = $matches[4] === '+';
171
            $this->checkmate = $matches[4] === '#';
172
            $this->annotation = isset($matches[5]) ? $matches[5] : null;
173
            return;
174
        }
175
176
        // Piece movement from a specific column:
177
        if (preg_match('/^([KQBNR])([a-h])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
178
            $this->movedPiece = $matches[1];
179
            $this->movedPieceDisambiguationColumn = $matches[2];
180
            $this->targetColumn = $matches[3];
181
            $this->targetRow = (int)$matches[4];
182
            $this->check = $matches[5] === '+';
183
            $this->checkmate = $matches[5] === '#';
184
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
185
            return;
186
        }
187
188
        // Piece movement from a specific row:
189
        if (preg_match('/^([KQBNR])([0-9])([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
190
            $this->movedPiece = $matches[1];
191
            $this->movedPieceDisambiguationRow = (int)$matches[2];
192
            $this->targetColumn = $matches[3];
193
            $this->targetRow = (int)$matches[4];
194
            $this->check = $matches[5] === '+';
195
            $this->checkmate = $matches[5] === '#';
196
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
197
            return;
198
        }
199
200
        // Pawn capture:
201
        if (preg_match('/^([a-h])x([a-h])([1-8])(?:=([KQBNR]))?(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
202
            $this->targetColumn = $matches[2];
203
            $this->targetRow = (int)$matches[3];
204
            $this->movedPiece = self::PIECE_PAWN;
205
            $this->movedPieceDisambiguationColumn = $matches[1];
206
            $this->capture = true;
207
            $this->promotedPiece = $matches[4] ?: null;
208
            $this->check = $matches[5] === '+';
209
            $this->checkmate = $matches[5] === '#';
210
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
211
            return;
212
        }
213
214
        // Piece capture:
215
        if (preg_match('/^([KQBNR])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
216
            $this->movedPiece = $matches[1];
217
            $this->targetColumn = $matches[2];
218
            $this->targetRow = (int)$matches[3];
219
            $this->check = $matches[4] === '+';
220
            $this->checkmate = $matches[4] === '#';
221
            $this->capture = true;
222
            $this->annotation = isset($matches[5]) ? $matches[5] : null;
223
            return;
224
        }
225
226
        // Piece capture from a specific column:
227
        if (preg_match('/^([KQBNR])([a-h])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
228
            $this->movedPiece = $matches[1];
229
            $this->movedPieceDisambiguationColumn = $matches[2];
230
            $this->targetColumn = $matches[3];
231
            $this->targetRow = (int)$matches[4];
232
            $this->check = $matches[5] === '+';
233
            $this->checkmate = $matches[5] === '#';
234
            $this->capture = true;
235
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
236
            return;
237
        }
238
239
        // Piece capture from a specific column:
240
        if (preg_match('/^([KQBNR])([0-9])x([a-h])([1-8])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
241
            $this->movedPiece = $matches[1];
242
            $this->movedPieceDisambiguationRow = (int)$matches[2];
243
            $this->targetColumn = $matches[3];
244
            $this->targetRow = (int)$matches[4];
245
            $this->check = $matches[5] === '+';
246
            $this->checkmate = $matches[5] === '#';
247
            $this->capture = true;
248
            $this->annotation = isset($matches[6]) ? $matches[6] : null;
249
            return;
250
        }
251
252
        // Check for pawn promotion:
253
        if (preg_match('/^([a-h])([1-8])=?([KQBNR])(\+|\#?)(\?\?|\?|\?\!|\!|\!\!)?$/', $value, $matches)) {
254
            $this->movedPiece = self::PIECE_PAWN;
255
            $this->targetColumn = $matches[1];
256
            $this->targetRow = (int)$matches[2];
257
            $this->promotedPiece = $matches[3];
258
            $this->check = $matches[4] === '+';
259
            $this->checkmate = $matches[4] === '#';
260
            $this->annotation = isset($matches[5]) ? $matches[5] : null;
261
            return;
262
        }
263
264
        throw new InvalidArgumentException(sprintf(
265
            'The value "%s" could not be parsed.',
266
            $value
267
        ));
268
    }
269
270
    /**
271
     * Checks if this move is a castling move.
272
     *
273
     * @return bool Returns true when this is a castling move; false otherwise.
274
     */
275
    public function isCastlingMove(): bool
276
    {
277
        return $this->isCastlingTowardsKingSide() || $this->isCastlingTowardsQueenSide();
278
    }
279
280
    /**
281
     * Checks if this is a castling move towards the king side.
282
     *
283
     * @return bool Returns true if this is a castling move; false otherwise.
284
     */
285
    public function isCastlingTowardsKingSide(): bool
286
    {
287
        return $this->castling === self::CASTLING_KING_SIDE;
288
    }
289
290
    /**
291
     * Checks if this is a castling move towards the queen side.
292
     *
293
     * @return bool Returns true if this is a castling move; false otherwise.
294
     */
295
    public function isCastlingTowardsQueenSide(): bool
296
    {
297
        return $this->castling === self::CASTLING_QUEEN_SIDE;
298
    }
299
300
    /**
301
     * Gets the original value.
302
     *
303
     * @return string
304
     */
305
    public function getValue(): string
306
    {
307
        return $this->value;
308
    }
309
310
    /**
311
     * Gets the castling value.
312
     *
313
     * @return null|string
314
     */
315
    public function getCastling(): ?string
316
    {
317
        return $this->castling;
318
    }
319
320
    /**
321
     * Gets the column to where the move was made.
322
     *
323
     * @return null|string
324
     */
325
    public function getTargetColumn(): ?string
326
    {
327
        return $this->targetColumn;
328
    }
329
330
    /**
331
     * Gets the column index to where the move was made.
332
     *
333
     * @return int|null
334
     */
335
    public function getTargetColumnIndex(): ?int
336
    {
337
        if ($this->targetColumn === null) {
338
            return null;
339
        }
340
341
        return ord($this->targetColumn) - 97;
342
    }
343
344
    /**
345
     * Duplicates the notation with a column based on an index from 0 to 7.
346
     *
347
     * @param int $index The index of the column to use.
348
     * @return Notation
349
     * @throws InvalidArgumentException Thrown when an invalid SAN value is created.
350
     * @throws RuntimeException Thrown when no target row is set.
351
     */
352
    public function withTargetColumnIndex(int $index): Notation
353
    {
354
        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...
355
            throw new RuntimeException('No row has been set.');
356
        }
357
358
        $notation = chr(97 + $index) . $this->getTargetRow();
359
360
        return new self($notation);
361
    }
362
363
    /**
364
     * Gets the row number to where the move was made.
365
     *
366
     * @return int|null
367
     */
368
    public function getTargetRow(): ?int
369
    {
370
        return $this->targetRow;
371
    }
372
373
    /**
374
     * Duplicates the notation with a new row.
375
     *
376
     * @param int $row A value between 1 and 8.
377
     * @return Notation
378
     * @throws InvalidArgumentException Thrown when an invalid SAN value is created.
379
     */
380
    public function withTargetRow(int $row): Notation
381
    {
382
        $notation = $this->getTargetColumn() . $row;
383
384
        return new self($notation);
385
    }
386
387
    /**
388
     * Gets the target notation.
389
     *
390
     * @return string
391
     */
392
    public function getTargetNotation(): string
393
    {
394
        return $this->getTargetColumn() . $this->getTargetRow();
395
    }
396
397
    /**
398
     * Gets the piece that was moved.
399
     *
400
     * @return null|string Returns the piece or null when a pawn was moved.
401
     */
402
    public function getMovedPiece(): ?string
403
    {
404
        return $this->movedPiece;
405
    }
406
407
    /**
408
     * Gets the disambiguation column.
409
     *
410
     * @return null|string
411
     */
412
    public function getMovedPieceDisambiguationColumn(): ?string
413
    {
414
        return $this->movedPieceDisambiguationColumn;
415
    }
416
417
    /**
418
     * Sets the disambiguation column.
419
     *
420
     * @param null|string $movedPieceDisambiguationColumn
421
     */
422
    public function setMovedPieceDisambiguationColumn(?string $movedPieceDisambiguationColumn): void
423
    {
424
        $this->movedPieceDisambiguationColumn = $movedPieceDisambiguationColumn;
425
    }
426
427
    /**
428
     * Gets the disambiguation row.
429
     *
430
     * @return int|null
431
     */
432
    public function getMovedPieceDisambiguationRow(): ?int
433
    {
434
        return $this->movedPieceDisambiguationRow;
435
    }
436
437
    /**
438
     * Sets the disambiguation row.
439
     *
440
     * @param int|null $movedPieceDisambiguationRow
441
     */
442
    public function setMovedPieceDisambiguationRow(?int $movedPieceDisambiguationRow): void
443
    {
444
        $this->movedPieceDisambiguationRow = $movedPieceDisambiguationRow;
445
    }
446
447
    /**
448
     * Gets the flag that indicates if this was a capture move.
449
     *
450
     * @return bool
451
     */
452
    public function isCapture(): bool
453
    {
454
        return $this->capture;
455
    }
456
457
    /**
458
     * Gets the piece into which the pawn was promoted.
459
     *
460
     * @return null|string
461
     */
462
    public function getPromotedPiece(): ?string
463
    {
464
        return $this->promotedPiece;
465
    }
466
467
    /**
468
     * Gets the flag that indicates whether or not the move returned into a check state.
469
     *
470
     * @return bool
471
     */
472
    public function isCheck(): bool
473
    {
474
        return $this->check;
475
    }
476
477
    /**
478
     * Gets the flag that indicates whether or not the move returned into a checkmate state.
479
     *
480
     * @return bool
481
     */
482
    public function isCheckmate(): bool
483
    {
484
        return $this->checkmate;
485
    }
486
487
    /**
488
     * Gets the annotation that has been given to this move.
489
     *
490
     * @return null|string
491
     */
492
    public function getAnnotation(): ?string
493
    {
494
        return $this->annotation;
495
    }
496
497
    /**
498
     * Converts the move to a string.
499
     *
500
     * @return string
501
     */
502
    public function toString(): string
503
    {
504
        return $this->getValue();
505
    }
506
507
    /**
508
     * Converts the move to a string.
509
     *
510
     * @return string
511
     */
512
    public function __toString()
513
    {
514
        return $this->toString();
515
    }
516
}
517