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
Pull Request — master (#1072)
by Dan
05:03
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