Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

ChessPiece::getPieceForLetter()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 9
rs 10
1
<?php declare(strict_types=1);
2
3
namespace Smr\Chess;
4
5
use Exception;
6
7
class ChessPiece {
8
9
	public const KING = 1;
10
	public const QUEEN = 2;
11
	public const ROOK = 3;
12
	public const BISHOP = 4;
13
	public const KNIGHT = 5;
14
	public const PAWN = 6;
15
16
	public function __construct(
17
		public readonly Colour $colour,
18
		public int $pieceID,
19
		public int $x,
20
		public int $y,
21
		public int $pieceNo = -1) {
22
	}
23
24
	/**
25
	 * @param array<int, array<int, ?ChessPiece>> $board
26
	 * @param array<mixed> $hasMoved
27
	 */
28
	public function isSafeMove(array $board, array $hasMoved, int $toX, int $toY): bool {
29
		// Make a deep copy of the board so that we can inspect possible future
30
		// positions without actually changing the state of the real board.
31
		// (Note $hasMoved is safe to shallow copy since it has no objects.)
32
		$boardCopy = [];
33
		foreach ($board as $y => $row) {
34
			foreach ($row as $x => $piece) {
35
				if ($piece === null) {
36
					$boardCopy[$y][$x] = null;
37
				} else {
38
					$boardCopy[$y][$x] = clone $piece;
39
				}
40
			}
41
		}
42
43
		ChessGame::movePiece($boardCopy, $hasMoved, $this->x, $this->y, $toX, $toY);
44
		return !ChessGame::isPlayerChecked($boardCopy, $hasMoved, $this->colour);
45
	}
46
47
	/**
48
	 * @param array<int, array<int, ?ChessPiece>> $board
49
	 * @param array<mixed> $hasMoved
50
	 */
51
	public function isAttacking(array $board, array $hasMoved, bool $king, int $x = -1, int $y = -1): bool {
52
		$moves = $this->getPossibleMoves($board, $hasMoved, null, true);
53
		foreach ($moves as [$toX, $toY]) {
54
			$p = $board[$toY][$toX];
55
			if (($toX == $x && $toY == $y) || ($king === true && $p != null && $p->pieceID == self::KING && $this->colour != $p->colour)) {
56
				return true;
57
			}
58
		}
59
		return false;
60
	}
61
62
	/**
63
	 * @param array<int, array<int, ?ChessPiece>> $board
64
	 * @param array<mixed> $hasMoved
65
	 * @return array<array{int, int}>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array{int, int}>> at position 4 could not be parsed: Expected ':' at position 4, but found 'int'.
Loading history...
66
	 */
67
	public function getPossibleMoves(array $board, array $hasMoved, Colour $forColour = null, bool $attackingCheck = false): array {
68
		$moves = [];
69
		if ($forColour === null || $this->colour === $forColour) {
70
			if ($this->pieceID == self::PAWN) {
71
				$dirY = $this->colour == Colour::Black ? 1 : -1;
72
				$moveY = $this->y + $dirY;
73
				//Pawn forward movement is not attacking - so don't check it if doing an attacking check.
74
				if (!$attackingCheck) {
75
					if (ChessGame::isValidCoord($this->x, $moveY, $board) && $board[$moveY][$this->x] === null && $this->isSafeMove($board, $hasMoved, $this->x, $moveY)) {
76
						$moves[] = [$this->x, $moveY];
77
					}
78
					$doubleMoveY = $moveY + $dirY;
79
					if ($this->y - $dirY == 0 || $this->y - $dirY * 2 == count($board)) { //Double move first move
80
						if ($board[$moveY][$this->x] === null && $board[$doubleMoveY][$this->x] === null && $this->isSafeMove($board, $hasMoved, $this->x, $doubleMoveY)) {
81
							$moves[] = [$this->x, $doubleMoveY];
82
						}
83
					}
84
				}
85
				for ($i = -1; $i < 2; $i += 2) {
86
					$moveX = $this->x + $i;
87
					if (ChessGame::isValidCoord($moveX, $moveY, $board)) {
88
						if ($attackingCheck ||
89
							((($hasMoved[self::PAWN][0] == $moveX && $hasMoved[self::PAWN][1] == $this->y) ||
90
							($board[$moveY][$moveX] != null && $board[$moveY][$moveX]->colour != $this->colour))
91
							&& $this->isSafeMove($board, $hasMoved, $moveX, $moveY))) {
92
							$moves[] = [$moveX, $moveY];
93
						}
94
					}
95
				}
96
			} elseif ($this->pieceID == self::KING) {
97
				for ($i = -1; $i < 2; $i++) {
98
					for ($j = -1; $j < 2; $j++) {
99
						if ($i != 0 || $j != 0) {
100
							$this->addMove($this->x + $i, $this->y + $j, $board, $moves, $hasMoved, $attackingCheck);
101
						}
102
					}
103
				}
104
				//Castling is not attacking - so don't check it if doing an attacking check.
105
				if (!$attackingCheck && !$hasMoved[$this->colour->value][self::KING] && !ChessGame::isPlayerChecked($board, $hasMoved, $this->colour)) {
106
					if (!$hasMoved[$this->colour->value][self::ROOK]['Queen'] &&
107
							ChessGame::isValidCoord($this->x - 1, $this->y, $board) && $board[$this->y][$this->x - 1] === null &&
108
							ChessGame::isValidCoord($this->x - 3, $this->y, $board) && $board[$this->y][$this->x - 3] === null &&
109
							$this->isSafeMove($board, $hasMoved, $this->x - 1, $this->y)) {
110
						$this->addMove($this->x - 2, $this->y, $board, $moves, $hasMoved, $attackingCheck);
111
					}
112
					if (!$hasMoved[$this->colour->value][self::ROOK]['King'] &&
113
							ChessGame::isValidCoord($this->x + 1, $this->y, $board) && $board[$this->y][$this->x + 1] === null &&
114
							$this->isSafeMove($board, $hasMoved, $this->x + 1, $this->y)) {
115
						$this->addMove($this->x + 2, $this->y, $board, $moves, $hasMoved, $attackingCheck);
116
					}
117
				}
118
			} elseif ($this->pieceID == self::QUEEN) {
119
				$moveX = $this->x;
120
				$moveY = $this->y;
121
				while ($this->addMove(--$moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Left
122
				$moveX = $this->x;
123
				$moveY = $this->y;
124
				while ($this->addMove(++$moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Right
125
				$moveX = $this->x;
126
				$moveY = $this->y;
127
				while ($this->addMove($moveX, ++$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Up
128
				$moveX = $this->x;
129
				$moveY = $this->y;
130
				while ($this->addMove($moveX, --$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Down
131
				$moveX = $this->x;
132
				$moveY = $this->y;
133
				while ($this->addMove(--$moveX, --$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Up-Left
134
				$moveX = $this->x;
135
				$moveY = $this->y;
136
				while ($this->addMove(++$moveX, --$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Up-Right
137
				$moveX = $this->x;
138
				$moveY = $this->y;
139
				while ($this->addMove(--$moveX, ++$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Down-Left
140
				$moveX = $this->x;
141
				$moveY = $this->y;
142
				while ($this->addMove(++$moveX, ++$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Up-Left
143
			} elseif ($this->pieceID == self::ROOK) {
144
				$moveX = $this->x;
145
				$moveY = $this->y;
146
				while ($this->addMove(--$moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Left
147
				$moveX = $this->x;
148
				$moveY = $this->y;
149
				while ($this->addMove(++$moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Right
150
				$moveX = $this->x;
151
				$moveY = $this->y;
152
				while ($this->addMove($moveX, ++$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Up
153
				$moveX = $this->x;
154
				$moveY = $this->y;
155
				while ($this->addMove($moveX, --$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Down
156
			} elseif ($this->pieceID == self::BISHOP) {
157
				$moveX = $this->x;
158
				$moveY = $this->y;
159
				while ($this->addMove(--$moveX, --$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Up-Left
160
				$moveX = $this->x;
161
				$moveY = $this->y;
162
				while ($this->addMove(++$moveX, --$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Up-Right
163
				$moveX = $this->x;
164
				$moveY = $this->y;
165
				while ($this->addMove(--$moveX, ++$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Down-Left
166
				$moveX = $this->x;
167
				$moveY = $this->y;
168
				while ($this->addMove(++$moveX, ++$moveY, $board, $moves, $hasMoved, $attackingCheck) && $board[$moveY][$moveX] === null); //Up-Left
169
			} elseif ($this->pieceID == self::KNIGHT) {
170
				$moveX = $this->x - 1;
171
				$moveY = $this->y - 2;
172
				$this->addMove($moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck); //2up-left
173
				$moveX += 2;
174
				$this->addMove($moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck); //2up-right
175
				$moveY = $this->y + 2;
176
				$this->addMove($moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck); //2down-right
177
				$moveX -= 2;
178
				$this->addMove($moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck); //2down-left
179
				$moveX = $this->x - 2;
180
				$moveY = $this->y - 1;
181
				$this->addMove($moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck); //2left-up
182
				$moveY += 2;
183
				$this->addMove($moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck); //2left-down
184
				$moveX = $this->x + 2;
185
				$this->addMove($moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck); //2right-down
186
				$moveY -= 2;
187
				$this->addMove($moveX, $moveY, $board, $moves, $hasMoved, $attackingCheck); //2right-up
188
			}
189
		}
190
191
		return $moves;
192
	}
193
194
	/**
195
	 * @param array<int, array<int, ?ChessPiece>> $board
196
	 * @param array{int, int} $moves
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{int, int} at position 2 could not be parsed: Expected ':' at position 2, but found 'int'.
Loading history...
197
	 * @param array<mixed> $hasMoved
198
	 */
199
	private function addMove(int $toX, int $toY, array $board, array &$moves, array $hasMoved, bool $attackingCheck = true): bool {
200
		if (ChessGame::isValidCoord($toX, $toY, $board)) {
201
			if (($board[$toY][$toX] === null || $board[$toY][$toX]->colour != $this->colour)) {
202
				//We can only actually move to this position if it is safe to do so, however we can pass through it looking for a safe move so we still want to return true.
203
				if (($attackingCheck === true || $this->isSafeMove($board, $hasMoved, $toX, $toY))) {
204
					$moves[] = [$toX, $toY];
205
				}
206
				return true;
207
			}
208
		}
209
		return false;
210
	}
211
212
	/**
213
	 * @param array<int, array<int, ?ChessPiece>> $board
214
	 * @return array<string, int>
215
	 */
216
	public function promote(int $pawnPromotionPieceID, array $board): array {
217
		$takenNos = [];
218
		foreach ($board as $row) {
219
			foreach ($row as $piece) {
220
				if ($piece != null && $piece->pieceID == $pawnPromotionPieceID && $piece->colour == $this->colour) {
221
					$takenNos[$piece->pieceNo] = true;
222
				}
223
			}
224
		}
225
		$i = 0;
226
		while (isset($takenNos[$i])) {
227
			$i++;
228
		}
229
		return ['PieceID' => $pawnPromotionPieceID, 'PieceNo' => $i];
230
	}
231
232
	public function getPieceLetter(): string {
233
		return self::getLetterForPiece($this->pieceID, $this->colour);
234
	}
235
236
	public function getPieceSymbol(): string {
237
		return self::getSymbolForPiece($this->pieceID, $this->colour);
238
	}
239
240
	public static function getSymbolForPiece(int $pieceID, Colour $colour): string {
241
		return '&#' . (9811 + $pieceID + ($colour == Colour::White ? 0 : 6)) . ';';
242
	}
243
244
	public static function getLetterForPiece(int $pieceID, Colour $colour): string {
245
		$letter = match ($pieceID) {
246
			self::KING => 'k',
247
			self::QUEEN => 'q',
248
			self::ROOK => 'r',
249
			self::BISHOP => 'b',
250
			self::KNIGHT => 'n',
251
			self::PAWN => 'p',
252
			default => throw new Exception('Invalid chess piece ID: ' . $pieceID),
253
		};
254
		if ($colour == Colour::White) {
255
			$letter = strtoupper($letter);
256
		}
257
		return $letter;
258
	}
259
260
	/**
261
	 * @return self::*
262
	 */
263
	public static function getPieceForLetter(string $letter): int {
264
		return match (strtolower($letter)) {
265
			'k' => self::KING,
266
			'q' => self::QUEEN,
267
			'r' => self::ROOK,
268
			'b' => self::BISHOP,
269
			'n' => self::KNIGHT,
270
			'p' => self::PAWN,
271
			default => throw new Exception('Invalid chess piece letter: ' . $letter),
272
		};
273
	}
274
275
}
276