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

Failed Conditions
Push — master ( 9c91a5...05804d )
by Dan
33s queued 16s
created

ChessGame::isPlayer()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 2
rs 10
1
<?php declare(strict_types=1);
2
/**
3
 * @author Page
4
 *
5
 */
6
class ChessGame {
7
	const GAMETYPE_STANDARD = 'Standard';
8
	const PLAYER_BLACK = 'Black';
9
	const PLAYER_WHITE = 'White';
10
	protected static array $CACHE_CHESS_GAMES = [];
11
12
	private Smr\Database $db;
13
14
	private int $chessGameID;
15
	private int $gameID;
16
	private int $startDate;
17
	private int $endDate;
18
	private int $winner;
19
	private int $whiteID;
20
	private int $blackID;
21
22
	private array $hasMoved;
23
	private array $board;
24
	private array $moves;
25
26
	private ?array $lastMove = null;
27
28
	public static function getNPCMoveGames(bool $forceUpdate = false) : array {
29
		$db = Smr\Database::getInstance();
30
		$db->query('SELECT chess_game_id
31
					FROM npc_logins
32
					JOIN account USING(login)
33
					JOIN chess_game ON account_id = black_id OR account_id = white_id
34
					WHERE end_time > ' . Smr\Epoch::time() . ' OR end_time IS NULL;');
35
		$games = array();
36
		while ($db->nextRecord()) {
37
			$game = self::getChessGame($db->getInt('chess_game_id'), $forceUpdate);
38
			if ($game->getCurrentTurnAccount()->isNPC()) {
39
				$games[] = $game;
40
			}
41
		}
42
		return $games;
43
	}
44
45
	public static function getOngoingPlayerGames(AbstractSmrPlayer $player) : array {
46
		$db = Smr\Database::getInstance();
47
		$db->query('SELECT chess_game_id FROM chess_game WHERE game_id = ' . $db->escapeNumber($player->getGameID()) . ' AND (black_id = ' . $db->escapeNumber($player->getAccountID()) . ' OR white_id = ' . $db->escapeNumber($player->getAccountID()) . ') AND (end_time > ' . Smr\Epoch::time() . ' OR end_time IS NULL);');
48
		$games = array();
49
		while ($db->nextRecord()) {
50
			$games[] = self::getChessGame($db->getInt('chess_game_id'));
51
		}
52
		return $games;
53
	}
54
55
	public static function getAccountGames(int $accountID) : array {
56
		$db = Smr\Database::getInstance();
57
		$db->query('SELECT chess_game_id FROM chess_game WHERE black_id = ' . $db->escapeNumber($accountID) . ' OR white_id = ' . $db->escapeNumber($accountID) . ';');
58
		$games = array();
59
		while ($db->nextRecord()) {
60
			$games[] = self::getChessGame($db->getInt('chess_game_id'));
61
		}
62
		return $games;
63
	}
64
65
	public static function getChessGame(int $chessGameID, bool $forceUpdate = false) : self {
66
		if ($forceUpdate || !isset(self::$CACHE_CHESS_GAMES[$chessGameID])) {
67
			self::$CACHE_CHESS_GAMES[$chessGameID] = new ChessGame($chessGameID);
68
		}
69
		return self::$CACHE_CHESS_GAMES[$chessGameID];
70
	}
71
72
	public function __construct(int $chessGameID) {
73
		$this->db = Smr\Database::getInstance();
74
		$this->db->query('SELECT *
75
						FROM chess_game
76
						WHERE chess_game_id=' . $this->db->escapeNumber($chessGameID) . ' LIMIT 1;');
77
		if ($this->db->nextRecord()) {
78
			$this->chessGameID = $chessGameID;
79
			$this->gameID = $this->db->getInt('game_id');
80
			$this->startDate = $this->db->getInt('start_time');
81
			$this->endDate = $this->db->getInt('end_time');
82
			$this->whiteID = $this->db->getInt('white_id');
83
			$this->blackID = $this->db->getInt('black_id');
84
			$this->winner = $this->db->getInt('winner_id');
85
			$this->resetHasMoved();
86
		} else {
87
			throw new Exception('Chess game not found: ' . $chessGameID);
88
		}
89
	}
90
91
	public static function isValidCoord(int $x, int $y, array &$board) {
92
		return $y < count($board) && $y >= 0 && $x < count($board[$y]) && $x >= 0;
93
	}
94
95
	public static function isPlayerChecked(array &$board, array &$hasMoved, string $colour) : bool {
96
		foreach ($board as &$row) {
97
			foreach ($row as &$p) {
98
				if ($p != null && $p->colour != $colour && $p->isAttacking($board, $hasMoved, true)) {
99
					return true;
100
				}
101
			}
102
		}
103
		return false;
104
	}
105
106
	private function resetHasMoved() : void {
107
		$this->hasMoved = array(
108
			self::PLAYER_WHITE => array(
109
				ChessPiece::KING => false,
110
				ChessPiece::ROOK => array(
111
					'Queen' => false,
112
					'King' => false
113
				)
114
			),
115
			self::PLAYER_BLACK => array(
116
				ChessPiece::KING => false,
117
				ChessPiece::ROOK => array(
118
					'Queen' => false,
119
					'King' => false
120
				)
121
			),
122
			ChessPiece::PAWN => array(-1, -1)
123
		);
124
	}
125
126
	public function rerunGame(bool $debugInfo = false) : void {
127
		$db = Smr\Database::getInstance();
128
		$db2 = Smr\Database::getInstance();
129
130
		$db->query('UPDATE chess_game
131
					SET end_time = NULL, winner_id = 0
132
					WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ';');
133
		$db->query('DELETE FROM chess_game_pieces WHERE chess_game_id = ' . $this->db->escapeNumber($this->chessGameID) . ';');
134
		self::insertPieces($this->chessGameID, $this->getWhitePlayer(), $this->getBlackPlayer());
135
136
		$db->query('SELECT * FROM chess_game_moves WHERE chess_game_id = ' . $this->db->escapeNumber($this->chessGameID) . ' ORDER BY move_id;');
137
		$db2->query('DELETE FROM chess_game_moves WHERE chess_game_id = ' . $this->db->escapeNumber($this->chessGameID) . ';');
138
		$this->moves = array();
139
		unset($this->board);
140
		unset($this->endDate);
141
		unset($this->winner);
142
		$this->resetHasMoved();
143
144
		try {
145
			while ($db->nextRecord()) {
146
				if ($debugInfo === true) {
147
					echo 'x=', $db->getInt('start_x'), ', y=', $db->getInt('start_y'), ', endX=', $db->getInt('end_x'), ', endY=', $db->getInt('end_y'), ', forAccountID=', $db->getInt('move_id') % 2 == 1 ? $this->getWhiteID() : $this->getBlackID(), EOL;
148
				}
149
				if (0 != $this->tryMove($db->getInt('start_x'), $db->getInt('start_y'), $db->getInt('end_x'), $db->getInt('end_y'), $db->getInt('move_id') % 2 == 1 ? $this->getWhiteID() : $this->getBlackID(), $db->getInt('promote_piece_id'))) {
150
					break;
151
				}
152
			}
153
		} catch (Exception $e) {
154
			if ($debugInfo === true) {
155
				echo $e->getMessage() . EOL . $e->getTraceAsString() . EOL;
156
			}
157
			// We probably tried an invalid move - move on.
158
		}
159
	}
160
161
	public function getBoard() : array {
162
		if (!isset($this->board)) {
163
			$this->db->query('SELECT * FROM chess_game_pieces WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ';');
164
			$pieces = array();
165
			while ($this->db->nextRecord()) {
166
				$accountID = $this->db->getInt('account_id');
167
				$pieces[] = new ChessPiece($this->chessGameID, $accountID, $this->getColourForAccountID($accountID), $this->db->getInt('piece_id'), $this->db->getInt('x'), $this->db->getInt('y'), $this->db->getInt('piece_no'));
168
			}
169
			$this->board = $this->parsePieces($pieces);
170
		}
171
		return $this->board;
172
	}
173
174
	/**
175
	 * Get the board from black's perspective
176
	 */
177
	public function getBoardReversed() : array {
178
		// Need to reverse both the rows and the files to rotate the board
179
		$board = array_reverse($this->getBoard(), true);
180
		foreach ($board as $key => $row) {
181
			$board[$key] = array_reverse($row, true);
182
		}
183
		return $board;
184
	}
185
186
	public function getLastMove() {
187
		$this->getMoves();
188
		return $this->lastMove;
189
	}
190
191
	public function getMoves() : array {
192
		if (!isset($this->moves)) {
193
			$this->db->query('SELECT * FROM chess_game_moves WHERE chess_game_id = ' . $this->db->escapeNumber($this->chessGameID) . ' ORDER BY move_id;');
194
			$this->moves = array();
195
			$mate = false;
196
			while ($this->db->nextRecord()) {
197
				$pieceTakenID = null;
198
				if ($this->db->hasField('piece_taken')) {
199
					$pieceTakenID = $this->db->getInt('piece_taken');
200
				}
201
				$promotionPieceID = null;
202
				if ($this->db->hasField('promote_piece_id')) {
203
					$promotionPieceID = $this->db->getInt('promote_piece_id');
204
				}
205
				$this->moves[] = $this->createMove($this->db->getInt('piece_id'), $this->db->getInt('start_x'), $this->db->getInt('start_y'), $this->db->getInt('end_x'), $this->db->getInt('end_y'), $pieceTakenID, $this->db->getField('checked'), $this->db->getInt('move_id') % 2 == 1 ? self::PLAYER_WHITE : self::PLAYER_BLACK, $this->db->getField('castling'), $this->db->getBoolean('en_passant'), $promotionPieceID);
206
				$mate = $this->db->getField('checked') == 'MATE';
207
			}
208
			if (!$mate && $this->hasEnded()) {
209
				if ($this->getWinner() != 0) {
210
					$this->moves[] = ($this->getWinner() == $this->getWhiteID() ? 'Black' : 'White') . ' Resigned.';
211
				} elseif (count($this->moves) < 2) {
212
					$this->moves[] = 'Game Cancelled.';
213
				} else {
214
					$this->moves[] = 'Game Drawn.';
215
				}
216
			}
217
		}
218
		return $this->moves;
219
	}
220
221
	public function getFENString() : string {
222
		$fen = '';
223
		$board = $this->getBoard();
224
		$blanks = 0;
225
		for ($y = 0; $y < 8; $y++) {
226
			if ($y > 0) {
227
				$fen .= '/';
228
			}
229
			for ($x = 0; $x < 8; $x++) {
230
				if ($board[$y][$x] === null) {
231
					$blanks++;
232
				} else {
233
					if ($blanks > 0) {
234
						$fen .= $blanks;
235
						$blanks = 0;
236
					}
237
					$fen .= $board[$y][$x]->getPieceLetter();
238
				}
239
			}
240
			if ($blanks > 0) {
241
				$fen .= $blanks;
242
				$blanks = 0;
243
			}
244
		}
245
		$fen .= match($this->getCurrentTurnColour()) {
246
			self::PLAYER_WHITE => ' w ',
247
			self::PLAYER_BLACK => ' b ',
248
		};
249
250
		// Castling
251
		$castling = '';
252
		if ($this->hasMoved[self::PLAYER_WHITE][ChessPiece::KING] !== true) {
253
			if ($this->hasMoved[self::PLAYER_WHITE][ChessPiece::ROOK]['King'] !== true) {
254
				$castling .= 'K';
255
			}
256
			if ($this->hasMoved[self::PLAYER_WHITE][ChessPiece::ROOK]['Queen'] !== true) {
257
				$castling .= 'Q';
258
			}
259
		}
260
		if ($this->hasMoved[self::PLAYER_BLACK][ChessPiece::KING] !== true) {
261
			if ($this->hasMoved[self::PLAYER_BLACK][ChessPiece::ROOK]['King'] !== true) {
262
				$castling .= 'k';
263
			}
264
			if ($this->hasMoved[self::PLAYER_BLACK][ChessPiece::ROOK]['Queen'] !== true) {
265
				$castling .= 'q';
266
			}
267
		}
268
		if ($castling == '') {
269
			$castling = '-';
270
		}
271
		$fen .= $castling . ' ';
272
273
		if ($this->hasMoved[ChessPiece::PAWN][0] != -1) {
274
			$fen .= chr(ord('a') + $this->hasMoved[ChessPiece::PAWN][0]);
275
			$fen .= match($this->hasMoved[ChessPiece::PAWN][1]) {
276
				3 => '6',
277
				4 => '3',
278
			};
279
		} else {
280
			$fen .= '-';
281
		}
282
		$fen .= ' 0 ' . floor(count($this->moves) / 2);
283
284
		return $fen;
285
	}
286
287
	private static function parsePieces(array $pieces) : array {
288
		$board = array();
289
		$row = array();
290
		for ($i = 0; $i < 8; $i++) {
291
			$row[] = null;
292
		}
293
		for ($i = 0; $i < 8; $i++) {
294
			$board[] = $row;
295
		}
296
		foreach ($pieces as $piece) {
297
			if ($board[$piece->y][$piece->x] != null) {
298
				throw new Exception('Two pieces found in the same tile.');
299
			}
300
			$board[$piece->y][$piece->x] = $piece;
301
		}
302
		return $board;
303
	}
304
305
	public static function getStandardGame(int $chessGameID, AbstractSmrPlayer $whitePlayer, AbstractSmrPlayer $blackPlayer) : array {
306
		$white = $whitePlayer->getAccountID();
307
		$black = $blackPlayer->getAccountID();
308
		return array(
309
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::ROOK, 0, 0),
310
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::KNIGHT, 1, 0),
311
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::BISHOP, 2, 0),
312
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::QUEEN, 3, 0),
313
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::KING, 4, 0),
314
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::BISHOP, 5, 0),
315
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::KNIGHT, 6, 0),
316
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::ROOK, 7, 0),
317
318
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 0, 1),
319
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 1, 1),
320
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 2, 1),
321
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 3, 1),
322
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 4, 1),
323
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 5, 1),
324
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 6, 1),
325
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 7, 1),
326
327
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 0, 6),
328
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 1, 6),
329
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 2, 6),
330
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 3, 6),
331
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 4, 6),
332
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 5, 6),
333
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 6, 6),
334
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 7, 6),
335
336
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::ROOK, 0, 7),
337
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::KNIGHT, 1, 7),
338
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::BISHOP, 2, 7),
339
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::QUEEN, 3, 7),
340
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::KING, 4, 7),
341
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::BISHOP, 5, 7),
342
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::KNIGHT, 6, 7),
343
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::ROOK, 7, 7),
344
			);
345
	}
346
347
	public static function insertNewGame(int $startDate, ?int $endDate, AbstractSmrPlayer $whitePlayer, AbstractSmrPlayer $blackPlayer) : int {
348
		$db = Smr\Database::getInstance();
349
		$db->query('INSERT INTO chess_game' .
350
				'(start_time,end_time,white_id,black_id,game_id)' .
351
				'values' .
352
				'(' . $db->escapeNumber($startDate) . ',' . ($endDate === null ? 'NULL' : $db->escapeNumber($endDate)) . ',' . $db->escapeNumber($whitePlayer->getAccountID()) . ',' . $db->escapeNumber($blackPlayer->getAccountID()) . ',' . $db->escapeNumber($whitePlayer->getGameID()) . ');');
353
		$chessGameID = $db->getInsertID();
354
355
		self::insertPieces($chessGameID, $whitePlayer, $blackPlayer);
356
		return $chessGameID;
357
	}
358
359
	private static function insertPieces($chessGameID, AbstractSmrPlayer $whitePlayer, AbstractSmrPlayer $blackPlayer) : void {
360
		$db = Smr\Database::getInstance();
361
		$pieces = self::getStandardGame($chessGameID, $whitePlayer, $blackPlayer);
362
		foreach ($pieces as $p) {
363
			$db->query('INSERT INTO chess_game_pieces' .
364
			'(chess_game_id,account_id,piece_id,x,y)' .
365
			'values' .
366
			'(' . $db->escapeNumber($chessGameID) . ',' . $db->escapeNumber($p->accountID) . ',' . $db->escapeNumber($p->pieceID) . ',' . $db->escapeNumber($p->x) . ',' . $db->escapeNumber($p->y) . ');');
367
		}
368
	}
369
370
	private function createMove(int $pieceID, int $startX, int $startY, int $endX, int $endY, ?int $pieceTaken, ?string $checking, string $playerColour, ?string $castling, bool $enPassant, ?int $promotionPieceID) {
371
		// This move will be set as the most recent move
372
		$this->lastMove = [
373
			'From' => ['X' => $startX, 'Y' => $startY],
374
			'To'   => ['X' => $endX, 'Y' => $endY],
375
		];
376
377
		$otherPlayerColour = self::getOtherColour($playerColour);
378
		if ($pieceID == ChessPiece::KING) {
379
			$this->hasMoved[$playerColour][ChessPiece::KING] = true;
380
		}
381
		// Check if the piece moving is a rook and mark it as moved to stop castling.
382
		if ($pieceID == ChessPiece::ROOK && ($startX == 0 || $startX == 7) && ($startY == ($playerColour == self::PLAYER_WHITE ? 7 : 0))) {
383
			$this->hasMoved[$playerColour][ChessPiece::ROOK][$startX == 0 ? 'Queen' : 'King'] = true;
384
		}
385
		// Check if we've taken a rook and marked them as moved, if they've already moved this does nothing, but if they were taken before moving this stops an issue with trying to castle with a non-existent castle.
386
		if ($pieceTaken == ChessPiece::ROOK && ($endX == 0 || $endX == 7) && $endY == ($otherPlayerColour == self::PLAYER_WHITE ? 7 : 0)) {
387
			$this->hasMoved[$otherPlayerColour][ChessPiece::ROOK][$endX == 0 ? 'Queen' : 'King'] = true;
388
		}
389
		if ($pieceID == ChessPiece::PAWN && ($startY == 1 || $startY == 6) && ($endY == 3 || $endY == 4)) {
390
			$this->hasMoved[ChessPiece::PAWN] = array($endX, $endY);
391
		} else {
392
			$this->hasMoved[ChessPiece::PAWN] = array(-1, -1);
393
		}
394
		return ($castling == 'Queen' ? '0-0-0' : ($castling == 'King' ? '0-0' : ''))
395
			. ChessPiece::getSymbolForPiece($pieceID, $playerColour)
396
			. chr(ord('a') + $startX)
397
			. (8 - $startY)
398
			. ' '
399
			. ($pieceTaken === null ? '' : ChessPiece::getSymbolForPiece($pieceTaken, $otherPlayerColour))
400
			. chr(ord('a') + $endX)
401
			. (8 - $endY)
402
			. ($promotionPieceID === null ? '' : ChessPiece::getSymbolForPiece($promotionPieceID, $playerColour))
403
			. ' '
404
			. ($checking === null ? '' : ($checking == 'CHECK' ? '+' : '++'))
405
			. ($enPassant ? ' e.p.' : '');
406
	}
407
408
	public function isCheckmated(string $colour) : bool {
409
		$king = null;
410
		foreach ($this->board as $row) {
411
			foreach ($row as $piece) {
412
				if ($piece != null && $piece->pieceID == ChessPiece::KING && $piece->colour == $colour) {
413
					$king = $piece;
414
					break;
415
				}
416
			}
417
		}
418
		if ($king === null) {
419
			throw new Exception('Could not find the king: game id = ' . $this->chessGameID);
420
		}
421
		if (!self::isPlayerChecked($this->board, $this->getHasMoved(), $colour)) {
422
			return false;
423
		}
424
		foreach ($this->board as $row) {
425
			foreach ($row as $piece) {
426
				if ($piece != null && $piece->colour == $colour) {
427
					$moves = $piece->getPossibleMoves($this->board, $this->getHasMoved());
428
					//There are moves we can make, we are clearly not checkmated.
429
					if (count($moves) > 0) {
430
						return false;
431
					}
432
				}
433
			}
434
		}
435
		return true;
436
	}
437
438
	public static function isCastling(int $x, int $toX) : array|false {
439
		$movement = $toX - $x;
440
		if (abs($movement) == 2) {
441
			//To the left.
442
			if ($movement == -2) {
443
				return array('Type' => 'Queen',
444
						'X' => 0,
445
						'ToX' => 3
446
					);
447
			} //To the right
448
			elseif ($movement == 2) {
449
				return array('Type' => 'King',
450
						'X' => 7,
451
						'ToX' => 5
452
					);
453
			}
454
		}
455
		return false;
456
	}
457
458
	public static function movePiece(array &$board, array &$hasMoved, int $x, int $y, int $toX, int $toY, int $pawnPromotionPiece = ChessPiece::QUEEN) : array {
459
		if (!self::isValidCoord($x, $y, $board)) {
460
			throw new Exception('Invalid from coordinates, x=' . $x . ', y=' . $y);
461
		}
462
		if (!self::isValidCoord($toX, $toY, $board)) {
463
			throw new Exception('Invalid to coordinates, x=' . $toX . ', y=' . $toY);
464
		}
465
		$pieceTaken = $board[$toY][$toX];
466
		$board[$toY][$toX] = $board[$y][$x];
467
		$p = $board[$toY][$toX];
468
		$board[$y][$x] = null;
469
		if ($p === null) {
470
			throw new Exception('Trying to move non-existent piece: ' . var_export($board, true));
471
		}
472
		$p->x = $toX;
473
		$p->y = $toY;
474
475
		$oldPawnMovement = $hasMoved[ChessPiece::PAWN];
476
		$nextPawnMovement = array(-1, -1);
477
		$castling = false;
478
		$enPassant = false;
479
		$rookMoved = false;
480
		$rookTaken = false;
481
		$pawnPromotion = false;
482
		if ($p->pieceID == ChessPiece::KING) {
483
			//Castling?
484
			$castling = self::isCastling($x, $toX);
485
			if ($castling !== false) {
486
				$hasMoved[$p->colour][ChessPiece::KING] = true;
487
				$hasMoved[$p->colour][ChessPiece::ROOK][$castling['Type']] = true;
488
				if ($board[$y][$castling['X']] === null) {
489
					throw new Exception('Cannot castle with non-existent castle.');
490
				}
491
				$board[$toY][$castling['ToX']] = $board[$y][$castling['X']];
492
				$board[$toY][$castling['ToX']]->x = $castling['ToX'];
493
				$board[$y][$castling['X']] = null;
494
			}
495
		} elseif ($p->pieceID == ChessPiece::PAWN) {
496
			if ($toY == 0 || $toY == 7) {
497
				$pawnPromotion = $p->promote($pawnPromotionPiece, $board);
498
			}
499
			//Double move to track?
500
			elseif (($y == 1 || $y == 6) && ($toY == 3 || $toY == 4)) {
501
				$nextPawnMovement = array($toX, $toY);
502
			}
503
			//En passant?
504
			elseif ($hasMoved[ChessPiece::PAWN][0] == $toX &&
505
					($hasMoved[ChessPiece::PAWN][1] == 3 && $toY == 2 || $hasMoved[ChessPiece::PAWN][1] == 4 && $toY == 5)) {
506
				$enPassant = true;
507
				$pieceTaken = $board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]];
508
				if ($board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]] === null) {
509
					throw new Exception('Cannot en passant a non-existent pawn.');
510
				}
511
				$board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]] = null;
512
			}
513
		} elseif ($p->pieceID == ChessPiece::ROOK && ($x == 0 || $x == 7) && $y == ($p->colour == self::PLAYER_WHITE ? 7 : 0)) {
514
			//Rook moved?
515
			if ($hasMoved[$p->colour][ChessPiece::ROOK][$x == 0 ? 'Queen' : 'King'] === false) {
516
				// We set rook moved in here as it's used for move info.
517
				$rookMoved = $x == 0 ? 'Queen' : 'King';
518
				$hasMoved[$p->colour][ChessPiece::ROOK][$rookMoved] = true;
519
			}
520
		}
521
		// Check if we've taken a rook and marked them as moved, if they've already moved this does nothing, but if they were taken before moving this stops an issue with trying to castle with a non-existent castle.
522
		if ($pieceTaken != null && $pieceTaken->pieceID == ChessPiece::ROOK && ($toX == 0 || $toX == 7) && $toY == ($pieceTaken->colour == self::PLAYER_WHITE ? 7 : 0)) {
523
			if ($hasMoved[$pieceTaken->colour][ChessPiece::ROOK][$toX == 0 ? 'Queen' : 'King'] === false) {
524
				$rookTaken = $toX == 0 ? 'Queen' : 'King';
525
				$hasMoved[$pieceTaken->colour][ChessPiece::ROOK][$rookTaken] = true;
526
			}
527
		}
528
529
		$hasMoved[ChessPiece::PAWN] = $nextPawnMovement;
530
		return array('Castling' => $castling,
531
				'PieceTaken' => $pieceTaken,
532
				'EnPassant' => $enPassant,
533
				'RookMoved' => $rookMoved,
534
				'RookTaken' => $rookTaken,
535
				'OldPawnMovement' => $oldPawnMovement,
536
				'PawnPromotion' => $pawnPromotion
537
			);
538
	}
539
540
	public static function undoMovePiece(array &$board, array &$hasMoved, int $x, int $y, int $toX, int $toY, array $moveInfo) : void {
541
		if (!self::isValidCoord($x, $y, $board)) {
542
			throw new Exception('Invalid from coordinates, x=' . $x . ', y=' . $y);
543
		}
544
		if (!self::isValidCoord($toX, $toY, $board)) {
545
			throw new Exception('Invalid to coordinates, x=' . $toX . ', y=' . $toY);
546
		}
547
		if ($board[$y][$x] != null) {
548
			throw new Exception('Undoing move onto another piece? x=' . $x . ', y=' . $y);
549
		}
550
		$board[$y][$x] = $board[$toY][$toX];
551
		$p = $board[$y][$x];
552
		if ($p === null) {
553
			throw new Exception('Trying to undo move of a non-existent piece: ' . var_export($board, true));
554
		}
555
		$board[$toY][$toX] = $moveInfo['PieceTaken'];
556
		$p->x = $x;
557
		$p->y = $y;
558
559
		$hasMoved[ChessPiece::PAWN] = $moveInfo['OldPawnMovement'];
560
		//Castling
561
		if ($p->pieceID == ChessPiece::KING) {
562
			$castling = self::isCastling($x, $toX);
563
			if ($castling !== false) {
564
				$hasMoved[$p->colour][ChessPiece::KING] = false;
565
				$hasMoved[$p->colour][ChessPiece::ROOK][$castling['Type']] = false;
566
				if ($board[$toY][$castling['ToX']] === null) {
567
					throw new Exception('Cannot undo castle with non-existent castle.');
568
				}
569
				$board[$y][$castling['X']] = $board[$toY][$castling['ToX']];
570
				$board[$y][$castling['X']]->x = $castling['X'];
571
				$board[$toY][$castling['ToX']] = null;
572
			}
573
		} elseif ($moveInfo['EnPassant'] === true) {
574
			$board[$toY][$toX] = null;
575
			$board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]] = $moveInfo['PieceTaken'];
576
		} elseif ($moveInfo['RookMoved'] !== false) {
577
			$hasMoved[$p->colour][ChessPiece::ROOK][$moveInfo['RookMoved']] = false;
578
		}
579
		if ($moveInfo['RookTaken'] !== false) {
580
			$hasMoved[$moveInfo['PieceTaken']->colour][ChessPiece::ROOK][$moveInfo['RookTaken']] = false;
581
		}
582
	}
583
584
	public function tryAlgebraicMove(string $move) : int {
585
		if (strlen($move) != 4 && strlen($move) != 5) {
586
			throw new Exception('Move of length "' . strlen($move) . '" is not valid, full move: ' . $move);
587
		}
588
		$aVal = ord('a');
589
		$hVal = ord('h');
590
		if (ord($move[0]) < $aVal || ord($move[2]) < $aVal
591
				|| ord($move[0]) > $hVal || ord($move[2]) > $hVal
592
				|| $move[1] < 1 || $move[3] < 1
593
				|| $move[1] > 8 || $move[3] > 8) {
594
			throw new Exception('Invalid move: ' . $move);
595
		}
596
		$x = ord($move[0]) - $aVal;
597
		$y = 8 - $move[1];
598
		$toX = ord($move[2]) - $aVal;
599
		$toY = 8 - $move[3];
600
		$pawnPromotionPiece = null;
601
		if (isset($move[4])) {
602
			$pawnPromotionPiece = ChessPiece::getPieceForLetter($move[4]);
603
		}
604
		return $this->tryMove($x, $y, $toX, $toY, $this->getCurrentTurnAccountID(), $pawnPromotionPiece);
0 ignored issues
show
Bug introduced by
It seems like $pawnPromotionPiece can also be of type null; however, parameter $pawnPromotionPiece of ChessGame::tryMove() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

604
		return $this->tryMove($x, $y, $toX, $toY, $this->getCurrentTurnAccountID(), /** @scrutinizer ignore-type */ $pawnPromotionPiece);
Loading history...
605
	}
606
607
	public function tryMove(int $x, int $y, int $toX, int $toY, int $forAccountID, int $pawnPromotionPiece) : int {
608
		if ($this->hasEnded()) {
609
			return 5;
610
		}
611
		if ($this->getCurrentTurnAccountID() != $forAccountID) {
612
			return 4;
613
		}
614
		$lastTurnPlayer = $this->getCurrentTurnPlayer();
615
		$this->getBoard();
616
		$p = $this->board[$y][$x];
617
		if ($p === null || $p->colour != $this->getColourForAccountID($forAccountID)) {
618
			return 2;
619
		}
620
621
		$moves = $p->getPossibleMoves($this->board, $this->getHasMoved(), $forAccountID);
622
		foreach ($moves as $move) {
623
			if ($move[0] == $toX && $move[1] == $toY) {
624
				$chessType = $this->isNPCGame() ? 'Chess (NPC)' : 'Chess';
625
				$currentPlayer = $this->getCurrentTurnPlayer();
626
627
				$moveInfo = ChessGame::movePiece($this->board, $this->getHasMoved(), $x, $y, $toX, $toY, $pawnPromotionPiece);
628
629
				//We have taken the move, we should refresh $p
630
				$p = $this->board[$toY][$toX];
631
632
				$pieceTakenID = null;
633
				if ($moveInfo['PieceTaken'] != null) {
634
					$pieceTakenID = $moveInfo['PieceTaken']->pieceID;
635
					if ($moveInfo['PieceTaken']->pieceID == ChessPiece::KING) {
636
						throw new Exception('King was taken.');
637
					}
638
				}
639
640
				$pieceID = $p->pieceID;
641
				$pieceNo = $p->pieceNo;
642
				$promotionPieceID = null;
643
				if ($moveInfo['PawnPromotion'] !== false) {
644
					$p->pieceID = $moveInfo['PawnPromotion']['PieceID'];
645
					$p->pieceNo = $moveInfo['PawnPromotion']['PieceNo'];
646
					$promotionPieceID = $p->pieceID;
647
				}
648
649
				$checking = null;
650
				if ($p->isAttacking($this->board, $this->getHasMoved(), true)) {
651
					$checking = 'CHECK';
652
				}
653
				if ($this->isCheckmated(self::getOtherColour($p->colour))) {
654
					$checking = 'MATE';
655
				}
656
657
				$castlingType = $moveInfo['Castling'] === false ? null : $moveInfo['Castling']['Type'];
658
659
				$this->getMoves(); // make sure $this->moves is initialized
660
				$this->moves[] = $this->createMove($pieceID, $x, $y, $toX, $toY, $pieceTakenID, $checking, $this->getCurrentTurnColour(), $castlingType, $moveInfo['EnPassant'], $promotionPieceID);
661
				if (self::isPlayerChecked($this->board, $this->getHasMoved(), $p->colour)) {
662
					return 3;
663
				}
664
665
				$otherPlayer = $this->getCurrentTurnPlayer();
666
				if ($moveInfo['PawnPromotion'] !== false) {
667
					$piecePromotedSymbol = $p->getPieceSymbol();
668
					$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Own Pawns Promoted', 'Total'), HOF_PUBLIC);
669
					$otherPlayer->increaseHOF(1, array($chessType, 'Moves', 'Opponent Pawns Promoted', 'Total'), HOF_PUBLIC);
670
					$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Own Pawns Promoted', $piecePromotedSymbol), HOF_PUBLIC);
671
					$otherPlayer->increaseHOF(1, array($chessType, 'Moves', 'Opponent Pawns Promoted', $piecePromotedSymbol), HOF_PUBLIC);
672
				}
673
674
				$this->db->query('INSERT INTO chess_game_moves
675
								(chess_game_id,piece_id,start_x,start_y,end_x,end_y,checked,piece_taken,castling,en_passant,promote_piece_id)
676
								VALUES
677
								(' . $this->db->escapeNumber($p->chessGameID) . ',' . $this->db->escapeNumber($pieceID) . ',' . $this->db->escapeNumber($x) . ',' . $this->db->escapeNumber($y) . ',' . $this->db->escapeNumber($toX) . ',' . $this->db->escapeNumber($toY) . ',' . $this->db->escapeString($checking, true) . ',' . ($moveInfo['PieceTaken'] === null ? 'NULL' : $this->db->escapeNumber($moveInfo['PieceTaken']->pieceID)) . ',' . $this->db->escapeString($castlingType, true) . ',' . $this->db->escapeBoolean($moveInfo['EnPassant']) . ',' . ($moveInfo['PawnPromotion'] == false ? 'NULL' : $this->db->escapeNumber($moveInfo['PawnPromotion']['PieceID'])) . ');');
678
679
680
				$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Total Taken'), HOF_PUBLIC);
681
				if ($moveInfo['PieceTaken'] != null) {
682
					$this->db->query('DELETE FROM chess_game_pieces
683
									WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ' AND account_id=' . $this->db->escapeNumber($moveInfo['PieceTaken']->accountID) . ' AND piece_id=' . $this->db->escapeNumber($moveInfo['PieceTaken']->pieceID) . ' AND piece_no=' . $this->db->escapeNumber($moveInfo['PieceTaken']->pieceNo) . ';');
684
685
					$pieceTakenSymbol = $moveInfo['PieceTaken']->getPieceSymbol();
686
					$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Opponent Pieces Taken', 'Total'), HOF_PUBLIC);
687
					$otherPlayer->increaseHOF(1, array($chessType, 'Moves', 'Own Pieces Taken', 'Total'), HOF_PUBLIC);
688
					$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Opponent Pieces Taken', $pieceTakenSymbol), HOF_PUBLIC);
689
					$otherPlayer->increaseHOF(1, array($chessType, 'Moves', 'Own Pieces Taken', $pieceTakenSymbol), HOF_PUBLIC);
690
				}
691
				$this->db->query('UPDATE chess_game_pieces
692
							SET x=' . $this->db->escapeNumber($toX) . ', y=' . $this->db->escapeNumber($toY) .
693
								($moveInfo['PawnPromotion'] !== false ? ', piece_id=' . $this->db->escapeNumber($moveInfo['PawnPromotion']['PieceID']) . ', piece_no=' . $this->db->escapeNumber($moveInfo['PawnPromotion']['PieceNo']) : '') . '
694
							WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ' AND account_id=' . $this->db->escapeNumber($p->accountID) . ' AND piece_id=' . $this->db->escapeNumber($pieceID) . ' AND piece_no=' . $this->db->escapeNumber($pieceNo) . ';');
695
				if ($moveInfo['Castling'] !== false) {
696
					$this->db->query('UPDATE chess_game_pieces
697
								SET x=' . $this->db->escapeNumber($moveInfo['Castling']['ToX']) . '
698
								WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ' AND account_id=' . $this->db->escapeNumber($p->accountID) . ' AND x = ' . $this->db->escapeNumber($moveInfo['Castling']['X']) . ' AND y = ' . $this->db->escapeNumber($y) . ';');
699
				}
700
				$return = 0;
701
				if ($checking == 'MATE') {
702
					$this->setWinner($forAccountID);
703
					$return = 1;
704
					SmrPlayer::sendMessageFromCasino($lastTurnPlayer->getGameID(), $this->getCurrentTurnAccountID(), 'You have just lost [chess=' . $this->getChessGameID() . '] against [player=' . $lastTurnPlayer->getPlayerID() . '].');
705
				} else {
706
					SmrPlayer::sendMessageFromCasino($lastTurnPlayer->getGameID(), $this->getCurrentTurnAccountID(), 'It is now your turn in [chess=' . $this->getChessGameID() . '] against [player=' . $lastTurnPlayer->getPlayerID() . '].');
707
					if ($checking == 'CHECK') {
708
						$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Check Given'), HOF_PUBLIC);
709
						$otherPlayer->increaseHOF(1, array($chessType, 'Moves', 'Check Received'), HOF_PUBLIC);
710
					}
711
				}
712
				$currentPlayer->saveHOF();
713
				$otherPlayer->saveHOF();
714
				return $return;
715
			}
716
		}
717
		// Invalid move was attempted
718
		return 6;
719
	}
720
721
	public function getChessGameID() : int {
722
		return $this->chessGameID;
723
	}
724
725
	public function getStartDate() : int {
726
		return $this->startDate;
727
	}
728
729
	public function getGameID() : int {
730
		return $this->gameID;
731
	}
732
733
	public function getWhitePlayer() : AbstractSmrPlayer {
734
		return SmrPlayer::getPlayer($this->whiteID, $this->getGameID());
735
	}
736
737
	public function getWhiteID() : int {
738
		return $this->whiteID;
739
	}
740
741
	public function getBlackPlayer() : AbstractSmrPlayer {
742
		return SmrPlayer::getPlayer($this->blackID, $this->getGameID());
743
	}
744
745
	public function getBlackID() : int {
746
		return $this->blackID;
747
	}
748
749
	public function getColourID(string $colour) : int {
750
		return match($colour) {
751
			self::PLAYER_WHITE => $this->getWhiteID(),
752
			self::PLAYER_BLACK => $this->getBlackID(),
753
		};
754
	}
755
756
	public function getColourPlayer(string $colour) : AbstractSmrPlayer {
757
		return SmrPlayer::getPlayer($this->getColourID($colour), $this->getGameID());
758
	}
759
760
	public function getColourForAccountID(int $accountID) : string {
761
		return match($accountID) {
762
			$this->getWhiteID() => self::PLAYER_WHITE,
763
			$this->getBlackID() => self::PLAYER_BLACK,
764
		};
765
	}
766
767
	/**
768
	 * Is the given account ID one of the two players of this game?
769
	 */
770
	public function isPlayer(int $accountID) : bool {
771
		return $accountID === $this->getWhiteID() || $accountID === $this->getBlackID();
772
	}
773
774
	public function getEndDate() : ?int {
775
		return $this->endDate;
776
	}
777
778
	public function hasEnded() : bool {
779
		return $this->endDate != 0 && $this->endDate <= Smr\Epoch::time();
780
	}
781
782
	public function getWinner() : int {
783
		return $this->winner;
784
	}
785
786
	public function setWinner(int $accountID) : array {
787
		$this->winner = $accountID;
788
		$this->endDate = Smr\Epoch::time();
789
		$this->db->query('UPDATE chess_game
790
						SET end_time=' . $this->db->escapeNumber(Smr\Epoch::time()) . ', winner_id=' . $this->db->escapeNumber($this->winner) . '
791
						WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ';');
792
		$winnerColour = $this->getColourForAccountID($accountID);
793
		$winningPlayer = $this->getColourPlayer($winnerColour);
794
		$losingPlayer = $this->getColourPlayer(self::getOtherColour($winnerColour));
795
		$chessType = $this->isNPCGame() ? 'Chess (NPC)' : 'Chess';
796
		$winningPlayer->increaseHOF(1, array($chessType, 'Games', 'Won'), HOF_PUBLIC);
797
		$losingPlayer->increaseHOF(1, array($chessType, 'Games', 'Lost'), HOF_PUBLIC);
798
		return array('Winner' => $winningPlayer, 'Loser' => $losingPlayer);
799
	}
800
801
	public function &getHasMoved() : array {
802
		return $this->hasMoved;
803
	}
804
805
	public function getCurrentTurnColour() : string {
806
		return count($this->getMoves()) % 2 == 0 ? self::PLAYER_WHITE : self::PLAYER_BLACK;
807
	}
808
809
	public function getCurrentTurnAccountID() : int {
810
		return count($this->getMoves()) % 2 == 0 ? $this->whiteID : $this->blackID;
811
	}
812
813
	public function getCurrentTurnPlayer() : AbstractSmrPlayer {
814
		return SmrPlayer::getPlayer($this->getCurrentTurnAccountID(), $this->getGameID());
815
	}
816
817
	public function getCurrentTurnAccount() : SmrAccount {
818
		return SmrAccount::getAccount($this->getCurrentTurnAccountID());
819
	}
820
821
	public function getWhiteAccount() : SmrAccount {
822
		return SmrAccount::getAccount($this->getWhiteID());
823
	}
824
825
	public function getBlackAccount() : SmrAccount {
826
		return SmrAccount::getAccount($this->getBlackID());
827
	}
828
829
	public function isCurrentTurn(int $accountID) : bool {
830
		return $accountID == $this->getCurrentTurnAccountID();
831
	}
832
833
	public function isNPCGame() : bool {
834
		return $this->getWhiteAccount()->isNPC() || $this->getBlackAccount()->isNPC();
835
	}
836
837
	public static function getOtherColour(string $colour) : string {
838
		return match($colour) {
839
			self::PLAYER_WHITE => self::PLAYER_BLACK,
840
			self::PLAYER_BLACK => self::PLAYER_WHITE,
841
		};
842
	}
843
844
	public function resign(int $accountID) : int {
845
		if ($this->hasEnded() || !$this->isPlayer($accountID)) {
846
			throw new Exception('Invalid resign conditions');
847
		}
848
		// If only 1 person has moved then just end the game.
849
		if (count($this->getMoves()) < 2) {
850
			$this->endDate = Smr\Epoch::time();
851
			$this->db->query('UPDATE chess_game
852
							SET end_time=' . $this->db->escapeNumber(Smr\Epoch::time()) . '
853
							WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ';');
854
			return 1;
855
		} else {
856
			$loserColour = $this->getColourForAccountID($accountID);
857
			$winnerAccountID = $this->getColourID(self::getOtherColour($loserColour));
858
			$results = $this->setWinner($winnerAccountID);
859
			$chessType = $this->isNPCGame() ? 'Chess (NPC)' : 'Chess';
860
			$results['Loser']->increaseHOF(1, array($chessType, 'Games', 'Resigned'), HOF_PUBLIC);
861
			SmrPlayer::sendMessageFromCasino($results['Winner']->getGameID(), $results['Winner']->getPlayerID(), '[player=' . $results['Loser']->getPlayerID() . '] just resigned against you in [chess=' . $this->getChessGameID() . '].');
862
			return 0;
863
		}
864
	}
865
866
	public function getPlayGameHREF() : string {
867
		return Page::create('skeleton.php', 'chess_play.php', array('ChessGameID' => $this->chessGameID))->href();
868
	}
869
870
	public function getResignHREF() : string {
871
		return Page::create('chess_resign_processing.php', '', array('ChessGameID' => $this->chessGameID))->href();
872
	}
873
}
874