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

Passed
Pull Request — master (#905)
by Dan
05:43
created

ChessGame::createMove()   F

Complexity

Conditions 26
Paths 4608

Size

Total Lines 36
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 26
c 0
b 0
f 0
nc 4608
nop 11
dl 0
loc 36
rs 0

How to fix   Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 $CACHE_CHESS_GAMES = array();
11
12
	private $db;
13
14
	private $chessGameID;
15
	private $gameID;
16
	private $startDate;
17
	private $endDate;
18
	private $winner;
19
	private $whiteID;
20
	private $blackID;
21
22
	private $hasMoved;
23
	private $board;
24
	private $moves;
25
26
	private $lastMove = null;
27
28
	public static function getNPCMoveGames($forceUpdate = false) {
29
		$db = new SmrMySqlDatabase();
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 > ' . 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) {
46
		$db = new SmrMySqlDatabase();
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 > ' . 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($accountID) {
56
		$db = new SmrMySqlDatabase();
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($chessGameID, $forceUpdate = false) {
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($chessGameID) {
73
		$this->db = new SmrMySqlDatabase();
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($x, $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, $colour) {
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() {
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($debugInfo = false) {
127
		$db = new SmrMySqlDatabase();
128
		$db2 = new SmrMySqlDatabase();
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
		$this->board = null;
140
		$this->endDate = null;
141
		$this->winner = null;
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())) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $this->tryMove($db->getI... : $this->getBlackID()) of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
Bug introduced by
The call to ChessGame::tryMove() has too few arguments starting with pawnPromotionPiece. ( Ignorable by Annotation )

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

149
				if (0 != $this->/** @scrutinizer ignore-call */ 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())) {

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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() {
162
		if ($this->board == null) {
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() {
192
		if ($this->moves == null) {
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 = $this->db->getField('piece_taken') == null ? null : $this->db->getInt('piece_taken');
198
				$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'), $this->db->getInt('promote_piece_id'));
199
				$mate = $this->db->getField('checked') == 'MATE';
200
			}
201
			if (!$mate && $this->hasEnded()) {
202
				if ($this->getWinner() != 0) {
203
					$this->moves[] = ($this->getWinner() == $this->getWhiteID() ? 'Black' : 'White') . ' Resigned.';
204
				} elseif (count($this->moves) < 2) {
205
					$this->moves[] = 'Game Cancelled.';
206
				} else {
207
					$this->moves[] = 'Game Drawn.';
208
				}
209
			}
210
		}
211
		return $this->moves;
212
	}
213
214
	public function getFENString() {
215
		$fen = '';
216
		$board = $this->getBoard();
217
		$blanks = 0;
218
		for ($y = 0; $y < 8; $y++) {
219
			if ($y > 0) {
220
				$fen .= '/';
221
			}
222
			for ($x = 0; $x < 8; $x++) {
223
				if ($board[$y][$x] == null) {
224
					$blanks++;
225
				} else {
226
					if ($blanks > 0) {
227
						$fen .= $blanks;
228
						$blanks = 0;
229
					}
230
					$fen .= $board[$y][$x]->getPieceLetter();
231
				}
232
			}
233
			if ($blanks > 0) {
234
				$fen .= $blanks;
235
				$blanks = 0;
236
			}
237
		}
238
		switch ($this->getCurrentTurnColour()) {
239
			case self::PLAYER_WHITE:
240
				$fen .= ' w ';
241
			break;
242
			case self::PLAYER_BLACK:
243
				$fen .= ' b ';
244
			break;
245
		}
246
247
		// Castling
248
		$castling = '';
249
		if ($this->hasMoved[self::PLAYER_WHITE][ChessPiece::KING] !== true) {
250
			if ($this->hasMoved[self::PLAYER_WHITE][ChessPiece::ROOK]['King'] !== true) {
251
				$castling .= 'K';
252
			}
253
			if ($this->hasMoved[self::PLAYER_WHITE][ChessPiece::ROOK]['Queen'] !== true) {
254
				$castling .= 'Q';
255
			}
256
		}
257
		if ($this->hasMoved[self::PLAYER_BLACK][ChessPiece::KING] !== true) {
258
			if ($this->hasMoved[self::PLAYER_BLACK][ChessPiece::ROOK]['King'] !== true) {
259
				$castling .= 'k';
260
			}
261
			if ($this->hasMoved[self::PLAYER_BLACK][ChessPiece::ROOK]['Queen'] !== true) {
262
				$castling .= 'q';
263
			}
264
		}
265
		if ($castling == '') {
266
			$castling = '-';
267
		}
268
		$fen .= $castling . ' ';
269
270
		if ($this->hasMoved[ChessPiece::PAWN][0] != -1) {
271
			$fen .= chr(ord('a') + $this->hasMoved[ChessPiece::PAWN][0]);
272
			switch ($this->hasMoved[ChessPiece::PAWN][1]) {
273
				case 3:
274
					$fen .= '6';
275
				break;
276
				case 4:
277
					$fen .= '3';
278
				break;
279
			}
280
		} else {
281
			$fen .= '-';
282
		}
283
		$fen .= ' 0 ' . floor(count($this->moves) / 2);
284
285
		return $fen;
286
	}
287
288
	private static function parsePieces(array $pieces) {
289
		$board = array();
290
		$row = array();
291
		for ($i = 0; $i < 8; $i++) {
292
			$row[] = null;
293
		}
294
		for ($i = 0; $i < 8; $i++) {
295
			$board[] = $row;
296
		}
297
		foreach ($pieces as $piece) {
298
			if ($board[$piece->getY()][$piece->getX()] != null) {
299
				throw new Exception('Two pieces found in the same tile.');
300
			}
301
			$board[$piece->getY()][$piece->getX()] = $piece;
302
		}
303
		return $board;
304
	}
305
306
	public static function getStandardGame($chessGameID, AbstractSmrPlayer $whitePlayer, AbstractSmrPlayer $blackPlayer) {
307
		$white = $whitePlayer->getAccountID();
308
		$black = $blackPlayer->getAccountID();
309
		return array(
310
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::ROOK, 0, 0),
311
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::KNIGHT, 1, 0),
312
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::BISHOP, 2, 0),
313
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::QUEEN, 3, 0),
314
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::KING, 4, 0),
315
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::BISHOP, 5, 0),
316
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::KNIGHT, 6, 0),
317
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::ROOK, 7, 0),
318
319
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 0, 1),
320
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 1, 1),
321
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 2, 1),
322
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 3, 1),
323
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 4, 1),
324
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 5, 1),
325
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 6, 1),
326
				new ChessPiece($chessGameID, $black, self::PLAYER_BLACK, ChessPiece::PAWN, 7, 1),
327
328
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 0, 6),
329
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 1, 6),
330
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 2, 6),
331
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 3, 6),
332
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 4, 6),
333
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 5, 6),
334
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 6, 6),
335
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::PAWN, 7, 6),
336
337
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::ROOK, 0, 7),
338
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::KNIGHT, 1, 7),
339
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::BISHOP, 2, 7),
340
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::QUEEN, 3, 7),
341
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::KING, 4, 7),
342
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::BISHOP, 5, 7),
343
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::KNIGHT, 6, 7),
344
				new ChessPiece($chessGameID, $white, self::PLAYER_WHITE, ChessPiece::ROOK, 7, 7),
345
			);
346
	}
347
348
	public static function insertNewGame($startDate, $endDate, AbstractSmrPlayer $whitePlayer, AbstractSmrPlayer $blackPlayer) {
349
		if ($startDate == null) {
350
			throw new Exception('Start date cannot be null.');
351
		}
352
353
		$db = new SmrMySqlDatabase();
354
		$db->query('INSERT INTO chess_game' .
355
				'(start_time,end_time,white_id,black_id,game_id)' .
356
				'values' .
357
				'(' . $db->escapeNumber($startDate) . ',' . ($endDate == null ? 'NULL' : $db->escapeNumber($endDate)) . ',' . $db->escapeNumber($whitePlayer->getAccountID()) . ',' . $db->escapeNumber($blackPlayer->getAccountID()) . ',' . $db->escapeNumber($whitePlayer->getGameID()) . ');');
358
		$chessGameID = $db->getInsertID();
359
360
		self::insertPieces($chessGameID, $whitePlayer, $blackPlayer);
361
		return $chessGameID;
362
	}
363
364
	private static function insertPieces($chessGameID, AbstractSmrPlayer $whitePlayer, AbstractSmrPlayer $blackPlayer) {
365
		$db = new SmrMySqlDatabase();
366
		$pieces = self::getStandardGame($chessGameID, $whitePlayer, $blackPlayer);
367
		foreach ($pieces as $p) {
368
			$db->query('INSERT INTO chess_game_pieces' .
369
			'(chess_game_id,account_id,piece_id,x,y)' .
370
			'values' .
371
			'(' . $db->escapeNumber($chessGameID) . ',' . $db->escapeNumber($p->accountID) . ',' . $db->escapeNumber($p->pieceID) . ',' . $db->escapeNumber($p->getX()) . ',' . $db->escapeNumber($p->getY()) . ');');
372
		}
373
	}
374
375
	private function createMove($pieceID, $startX, $startY, $endX, $endY, $pieceTaken, $checking, $playerColour, $castling, $enPassant, $promotionPieceID) {
376
		// This move will be set as the most recent move
377
		$this->lastMove = [
378
			'From' => ['X' => $startX, 'Y' => $startY],
379
			'To'   => ['X' => $endX, 'Y' => $endY],
380
		];
381
382
		$otherPlayerColour = self::getOtherColour($playerColour);
383
		if ($pieceID == ChessPiece::KING) {
384
			$this->hasMoved[$playerColour][ChessPiece::KING] = true;
385
		}
386
		// Check if the piece moving is a rook and mark it as moved to stop castling.
387
		if ($pieceID == ChessPiece::ROOK && ($startX == 0 || $startX == 7) && ($startY == ($playerColour == self::PLAYER_WHITE ? 7 : 0))) {
388
			$this->hasMoved[$playerColour][ChessPiece::ROOK][$startX == 0 ? 'Queen' : 'King'] = true;
389
		}
390
		// 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.
391
		if ($pieceTaken == ChessPiece::ROOK && ($endX == 0 || $endX == 7) && $endY == ($otherPlayerColour == self::PLAYER_WHITE ? 7 : 0)) {
392
			$this->hasMoved[$otherPlayerColour][ChessPiece::ROOK][$endX == 0 ? 'Queen' : 'King'] = true;
393
		}
394
		if ($pieceID == ChessPiece::PAWN && ($startY == 1 || $startY == 6) && ($endY == 3 || $endY == 4)) {
395
			$this->hasMoved[ChessPiece::PAWN] = array($endX, $endY);
396
		} else {
397
			$this->hasMoved[ChessPiece::PAWN] = array(-1, -1);
398
		}
399
		return ($castling == 'Queen' ? '0-0-0' : ($castling == 'King' ? '0-0' : ''))
400
			. ChessPiece::getSymbolForPiece($pieceID, $playerColour)
401
			. chr(ord('a') + $startX)
402
			. (8 - $startY)
403
			. ' '
404
			. ($pieceTaken == null ? '' : ChessPiece::getSymbolForPiece($pieceTaken, $otherPlayerColour))
405
			. chr(ord('a') + $endX)
406
			. (8 - $endY)
407
			. ($promotionPieceID == null ? '' : ChessPiece::getSymbolForPiece($promotionPieceID, $playerColour))
408
			. ' '
409
			. ($checking == null ? '' : ($checking == 'CHECK' ? '+' : '++'))
410
			. ($enPassant ? ' e.p.' : '');
411
	}
412
413
	public function isCheckmated($colour) {
414
		$king = null;
415
		foreach ($this->board as $row) {
416
			foreach ($row as $piece) {
417
				if ($piece != null && $piece->pieceID == ChessPiece::KING && $piece->colour == $colour) {
418
					$king = $piece;
419
					break;
420
				}
421
			}
422
		}
423
		if ($king == null) {
424
			throw new Exception('Could not find the king: game id = ' . $this->chessGameID);
425
		}
426
		if (!self::isPlayerChecked($this->board, $this->getHasMoved(), $colour)) {
427
			return false;
428
		}
429
		foreach ($this->board as $row) {
430
			foreach ($row as $piece) {
431
				if ($piece != null && $piece->colour == $colour) {
432
					$moves = $piece->getPossibleMoves($this->board, $this->getHasMoved());
433
					//There are moves we can make, we are clearly not checkmated.
434
					if (count($moves) > 0) {
435
						return false;
436
					}
437
				}
438
			}
439
		}
440
		return true;
441
	}
442
443
	public static function isCastling($x, $toX) {
444
		$movement = $toX - $x;
445
		if (abs($movement) == 2) {
446
			//To the left.
447
			if ($movement == -2) {
448
				return array('Type' => 'Queen',
449
						'X' => 0,
450
						'ToX' => 3
451
					);
452
			} //To the right
453
			elseif ($movement == 2) {
454
				return array('Type' => 'King',
455
						'X' => 7,
456
						'ToX' => 5
457
					);
458
			}
459
		}
460
		return false;
461
	}
462
463
	public static function movePiece(array &$board, array &$hasMoved, $x, $y, $toX, $toY, $pawnPromotionPiece = ChessPiece::QUEEN) {
464
		if (!self::isValidCoord($x, $y, $board)) {
465
			throw new Exception('Invalid from coordinates, x=' . $x . ', y=' . $y);
466
		}
467
		if (!self::isValidCoord($toX, $toY, $board)) {
468
			throw new Exception('Invalid to coordinates, x=' . $toX . ', y=' . $toY);
469
		}
470
		$pieceTaken = $board[$toY][$toX];
471
		$board[$toY][$toX] = $board[$y][$x];
472
		$p = $board[$toY][$toX];
473
		$board[$y][$x] = null;
474
		if ($p == null) {
475
			throw new Exception('Trying to move non-existent piece: ' . var_export($board, true));
476
		}
477
		$p->setX($toX);
478
		$p->setY($toY);
479
480
		$oldPawnMovement = $hasMoved[ChessPiece::PAWN];
481
		$nextPawnMovement = array(-1, -1);
482
		$castling = false;
483
		$enPassant = false;
484
		$rookMoved = false;
485
		$rookTaken = false;
486
		$pawnPromotion = false;
487
		if ($p->pieceID == ChessPiece::KING) {
488
			//Castling?
489
			$castling = self::isCastling($x, $toX);
490
			if ($castling !== false) {
491
				$hasMoved[$p->colour][ChessPiece::KING] = true;
492
				$hasMoved[$p->colour][ChessPiece::ROOK][$castling['Type']] = true;
493
				if ($board[$y][$castling['X']] == null) {
494
					throw new Exception('Cannot castle with non-existent castle.');
495
				}
496
				$board[$toY][$castling['ToX']] = $board[$y][$castling['X']];
497
				$board[$toY][$castling['ToX']]->setX($castling['ToX']);
498
				$board[$y][$castling['X']] = null;
499
			}
500
		} elseif ($p->pieceID == ChessPiece::PAWN) {
501
			if ($toY == 0 || $toY == 7) {
502
				$pawnPromotion = $p->promote($pawnPromotionPiece, $board);
503
			}
504
			//Double move to track?
505
			elseif (($y == 1 || $y == 6) && ($toY == 3 || $toY == 4)) {
506
				$nextPawnMovement = array($toX, $toY);
507
			}
508
			//En passant?
509
			elseif ($hasMoved[ChessPiece::PAWN][0] == $toX &&
510
					($hasMoved[ChessPiece::PAWN][1] == 3 && $toY == 2 || $hasMoved[ChessPiece::PAWN][1] == 4 && $toY == 5)) {
511
				$enPassant = true;
512
				$pieceTaken = $board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]];
513
				if ($board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]] == null) {
514
					throw new Exception('Cannot en passant a non-existent pawn.');
515
				}
516
				$board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]] = null;
517
			}
518
		} elseif ($p->pieceID == ChessPiece::ROOK && ($x == 0 || $x == 7) && $y == ($p->colour == self::PLAYER_WHITE ? 7 : 0)) {
519
			//Rook moved?
520
			if ($hasMoved[$p->colour][ChessPiece::ROOK][$x == 0 ? 'Queen' : 'King'] === false) {
521
				// We set rook moved in here as it's used for move info.
522
				$rookMoved = $x == 0 ? 'Queen' : 'King';
523
				$hasMoved[$p->colour][ChessPiece::ROOK][$rookMoved] = true;
524
			}
525
		}
526
		// 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.
527
		if ($pieceTaken != null && $pieceTaken->pieceID == ChessPiece::ROOK && ($toX == 0 || $toX == 7) && $toY == ($pieceTaken->colour == self::PLAYER_WHITE ? 7 : 0)) {
528
			if ($hasMoved[$pieceTaken->colour][ChessPiece::ROOK][$toX == 0 ? 'Queen' : 'King'] === false) {
529
				$rookTaken = $toX == 0 ? 'Queen' : 'King';
530
				$hasMoved[$pieceTaken->colour][ChessPiece::ROOK][$rookTaken] = true;
531
			}
532
		}
533
		
534
		$hasMoved[ChessPiece::PAWN] = $nextPawnMovement;
535
		return array('Castling' => $castling,
536
				'PieceTaken' => $pieceTaken,
537
				'EnPassant' => $enPassant,
538
				'RookMoved' => $rookMoved,
539
				'RookTaken' => $rookTaken,
540
				'OldPawnMovement' => $oldPawnMovement,
541
				'PawnPromotion' => $pawnPromotion
542
			);
543
	}
544
545
	public static function undoMovePiece(array &$board, array &$hasMoved, $x, $y, $toX, $toY, $moveInfo) {
546
		if (!self::isValidCoord($x, $y, $board)) {
547
			throw new Exception('Invalid from coordinates, x=' . $x . ', y=' . $y);
548
		}
549
		if (!self::isValidCoord($toX, $toY, $board)) {
550
			throw new Exception('Invalid to coordinates, x=' . $toX . ', y=' . $toY);
551
		}
552
		if ($board[$y][$x] != null) {
553
			throw new Exception('Undoing move onto another piece? x=' . $x . ', y=' . $y);
554
		}
555
		$board[$y][$x] = $board[$toY][$toX];
556
		$p = $board[$y][$x];
557
		if ($p == null) {
558
			throw new Exception('Trying to undo move of a non-existent piece: ' . var_export($board, true));
559
		}
560
		$board[$toY][$toX] = $moveInfo['PieceTaken'];
561
		$p->setX($x);
562
		$p->setY($y);
563
564
		$hasMoved[ChessPiece::PAWN] = $moveInfo['OldPawnMovement'];
565
		//Castling
566
		if ($p->pieceID == ChessPiece::KING) {
567
			$castling = self::isCastling($x, $toX);
568
			if ($castling !== false) {
569
				$hasMoved[$p->colour][ChessPiece::KING] = false;
570
				$hasMoved[$p->colour][ChessPiece::ROOK][$castling['Type']] = false;
571
				if ($board[$toY][$castling['ToX']] == null) {
572
					throw new Exception('Cannot undo castle with non-existent castle.');
573
				}
574
				$board[$y][$castling['X']] = $board[$toY][$castling['ToX']];
575
				$board[$y][$castling['X']]->setX($castling['X']);
576
				$board[$toY][$castling['ToX']] = null;
577
			}
578
		} elseif ($moveInfo['EnPassant'] === true) {
579
			$board[$toY][$toX] = null;
580
			$board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]] = $moveInfo['PieceTaken'];
581
		} elseif ($moveInfo['RookMoved'] !== false) {
582
			$hasMoved[$p->colour][ChessPiece::ROOK][$moveInfo['RookMoved']] = false;
583
		}
584
		if ($moveInfo['RookTaken'] !== false) {
585
			$hasMoved[$moveInfo['PieceTaken']->colour][ChessPiece::ROOK][$moveInfo['RookTaken']] = false;
586
		}
587
	}
588
589
	public function tryAlgebraicMove($move) {
590
		if (strlen($move) != 4 && strlen($move) != 5) {
591
			throw new Exception('Move of length "' . strlen($move) . '" is not valid, full move: ' . $move);
592
		}
593
		$aVal = ord('a');
594
		$hVal = ord('h');
595
		if (ord($move[0]) < $aVal || ord($move[2]) < $aVal
596
				|| ord($move[0]) > $hVal || ord($move[2]) > $hVal
597
				|| $move[1] < 1 || $move[3] < 1
598
				|| $move[1] > 8 || $move[3] > 8) {
599
			throw new Exception('Invalid move: ' . $move);
600
		}
601
		$x = ord($move[0]) - $aVal;
602
		$y = 8 - $move[1];
603
		$toX = ord($move[2]) - $aVal;
604
		$toY = 8 - $move[3];
605
		$pawnPromotionPiece = null;
606
		if (isset($move[4])) {
607
			$pawnPromotionPiece = ChessPiece::getPieceForLetter($move[4]);
608
		}
609
		return $this->tryMove($x, $y, $toX, $toY, $this->getCurrentTurnAccountID(), $pawnPromotionPiece);
610
	}
611
612
	public function tryMove($x, $y, $toX, $toY, $forAccountID, $pawnPromotionPiece) {
613
		if ($this->hasEnded()) {
614
			return 5;
615
		}
616
		if ($this->getCurrentTurnAccountID() != $forAccountID) {
617
			return 4;
618
		}
619
		$lastTurnPlayer = $this->getCurrentTurnPlayer();
620
		$this->getBoard();
621
		$p = $this->board[$y][$x];
622
		if ($p == null || $p->colour != $this->getColourForAccountID($forAccountID)) {
623
			return 2;
624
		}
625
626
		$moves = $p->getPossibleMoves($this->board, $this->getHasMoved(), $forAccountID);
627
		foreach ($moves as $move) {
628
			if ($move[0] == $toX && $move[1] == $toY) {
629
				$chessType = $this->isNPCGame() ? 'Chess (NPC)' : 'Chess';
630
				$currentPlayer = $this->getCurrentTurnPlayer();
631
632
				$moveInfo = ChessGame::movePiece($this->board, $this->getHasMoved(), $x, $y, $toX, $toY, $pawnPromotionPiece);
633
634
				//We have taken the move, we should refresh $p
635
				$p = $this->board[$toY][$toX];
636
637
				$pieceTakenID = null;
638
				if ($moveInfo['PieceTaken'] != null) {
639
					$pieceTakenID = $moveInfo['PieceTaken']->pieceID;
640
					if ($moveInfo['PieceTaken']->pieceID == ChessPiece::KING) {
641
						throw new Exception('King was taken.');
642
					}
643
				}
644
645
				$pieceID = $p->pieceID;
646
				$pieceNo = $p->pieceNo;
647
				$promotionPieceID = null;
648
				if ($moveInfo['PawnPromotion'] !== false) {
649
					$p->pieceID = $moveInfo['PawnPromotion']['PieceID'];
650
					$p->pieceNo = $moveInfo['PawnPromotion']['PieceNo'];
651
					$promotionPieceID = $p->pieceID;
652
				}
653
654
				$checking = null;
655
				if ($p->isAttacking($this->board, $this->getHasMoved(), true)) {
656
					$checking = 'CHECK';
657
				}
658
				if ($this->isCheckmated(self::getOtherColour($p->colour))) {
659
					$checking = 'MATE';
660
				}
661
662
				$castlingType = $moveInfo['Castling'] === false ? null : $moveInfo['Castling']['Type'];
663
664
				if ($this->moves != null) {
665
					$this->moves[] = $this->createMove($pieceID, $x, $y, $toX, $toY, $pieceTakenID, $checking, $this->getCurrentTurnColour(), $castlingType, $moveInfo['EnPassant'], $promotionPieceID);
666
				}
667
				if (self::isPlayerChecked($this->board, $this->getHasMoved(), $p->colour)) {
668
					return 3;
669
				}
670
671
				$otherPlayer = $this->getCurrentTurnPlayer();
672
				if ($moveInfo['PawnPromotion'] !== false) {
673
					$piecePromotedSymbol = $p->getPieceSymbol();
674
					$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Own Pawns Promoted', 'Total'), HOF_PUBLIC);
675
					$otherPlayer->increaseHOF(1, array($chessType, 'Moves', 'Opponent Pawns Promoted', 'Total'), HOF_PUBLIC);
676
					$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Own Pawns Promoted', $piecePromotedSymbol), HOF_PUBLIC);
677
					$otherPlayer->increaseHOF(1, array($chessType, 'Moves', 'Opponent Pawns Promoted', $piecePromotedSymbol), HOF_PUBLIC);
678
				}
679
680
				$this->db->query('INSERT INTO chess_game_moves
681
								(chess_game_id,piece_id,start_x,start_y,end_x,end_y,checked,piece_taken,castling,en_passant,promote_piece_id)
682
								VALUES
683
								(' . $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, true) . ',' . ($moveInfo['PieceTaken'] == null ? 'NULL' : $this->db->escapeNumber($moveInfo['PieceTaken']->pieceID)) . ',' . $this->db->escapeString($castlingType, true, true) . ',' . $this->db->escapeBoolean($moveInfo['EnPassant']) . ',' . ($moveInfo['PawnPromotion'] == false ? 'NULL' : $this->db->escapeNumber($moveInfo['PawnPromotion']['PieceID'])) . ');');
684
685
686
				$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Total Taken'), HOF_PUBLIC);
687
				if ($moveInfo['PieceTaken'] != null) {
688
					$this->db->query('DELETE FROM chess_game_pieces
689
									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) . ';');
690
691
					$pieceTakenSymbol = $moveInfo['PieceTaken']->getPieceSymbol();
692
					$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Opponent Pieces Taken', 'Total'), HOF_PUBLIC);
693
					$otherPlayer->increaseHOF(1, array($chessType, 'Moves', 'Own Pieces Taken', 'Total'), HOF_PUBLIC);
694
					$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Opponent Pieces Taken', $pieceTakenSymbol), HOF_PUBLIC);
695
					$otherPlayer->increaseHOF(1, array($chessType, 'Moves', 'Own Pieces Taken', $pieceTakenSymbol), HOF_PUBLIC);
696
				}
697
				$this->db->query('UPDATE chess_game_pieces
698
							SET x=' . $this->db->escapeNumber($toX) . ', y=' . $this->db->escapeNumber($toY) .
699
								($moveInfo['PawnPromotion'] !== false ? ', piece_id=' . $this->db->escapeNumber($moveInfo['PawnPromotion']['PieceID']) . ', piece_no=' . $this->db->escapeNumber($moveInfo['PawnPromotion']['PieceNo']) : '') . '
700
							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) . ';');
701
				if ($moveInfo['Castling'] !== false) {
702
					$this->db->query('UPDATE chess_game_pieces
703
								SET x=' . $this->db->escapeNumber($moveInfo['Castling']['ToX']) . '
704
								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) . ';');
705
				}
706
				$return = 0;
707
				if ($checking == 'MATE') {
708
					$this->setWinner($forAccountID);
709
					$return = 1;
710
					SmrPlayer::sendMessageFromCasino($lastTurnPlayer->getGameID(), $this->getCurrentTurnAccountID(), 'You have just lost [chess=' . $this->getChessGameID() . '] against [player=' . $lastTurnPlayer->getPlayerID() . '].');
711
				} else {
712
					SmrPlayer::sendMessageFromCasino($lastTurnPlayer->getGameID(), $this->getCurrentTurnAccountID(), 'It is now your turn in [chess=' . $this->getChessGameID() . '] against [player=' . $lastTurnPlayer->getPlayerID() . '].');
713
					if ($checking == 'CHECK') {
714
						$currentPlayer->increaseHOF(1, array($chessType, 'Moves', 'Check Given'), HOF_PUBLIC);
715
						$otherPlayer->increaseHOF(1, array($chessType, 'Moves', 'Check Received'), HOF_PUBLIC);
716
					}
717
				}
718
				$currentPlayer->saveHOF();
719
				$otherPlayer->saveHOF();
720
				return $return;
721
			}
722
		}
723
	}
724
725
	public function getChessGameID() {
726
		return $this->chessGameID;
727
	}
728
729
	public function getStartDate() {
730
		return $this->startDate;
731
	}
732
733
	public function getGameID() {
734
		return $this->gameID;
735
	}
736
737
	public function getWhitePlayer() {
738
		return SmrPlayer::getPlayer($this->whiteID, $this->getGameID());
739
	}
740
741
	public function getWhiteID() {
742
		return $this->whiteID;
743
	}
744
745
	public function getBlackPlayer() {
746
		return SmrPlayer::getPlayer($this->blackID, $this->getGameID());
747
	}
748
749
	public function getBlackID() {
750
		return $this->blackID;
751
	}
752
753
	public function getColourID($colour) {
754
		if ($colour == self::PLAYER_WHITE) {
755
			return $this->getWhiteID();
756
		}
757
		if ($colour == self::PLAYER_BLACK) {
758
			return $this->getBlackID();
759
		}
760
	}
761
762
	public function getColourPlayer($colour) {
763
		return SmrPlayer::getPlayer($this->getColourID($colour), $this->getGameID());
764
	}
765
766
	public function getColourForAccountID($accountID) {
767
		if ($accountID == $this->getWhiteID()) {
768
			return self::PLAYER_WHITE;
769
		}
770
		if ($accountID == $this->getBlackID()) {
771
			return self::PLAYER_BLACK;
772
		}
773
		return false;
774
	}
775
776
	public function getEndDate() {
777
		return $this->endDate;
778
	}
779
780
	public function hasEnded() {
781
		return $this->endDate != 0 && $this->endDate <= TIME;
782
	}
783
784
	public function getWinner() {
785
		return $this->winner;
786
	}
787
788
	public function setWinner($accountID) {
789
		$this->winner = $accountID;
790
		$this->endDate = TIME;
791
		$this->db->query('UPDATE chess_game
792
						SET end_time=' . $this->db->escapeNumber(TIME) . ', winner_id=' . $this->db->escapeNumber($this->winner) . '
793
						WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ';');
794
		$winnerColour = $this->getColourForAccountID($accountID);
795
		$winningPlayer = $this->getColourPlayer($winnerColour);
796
		$losingPlayer = $this->getColourPlayer(self::getOtherColour($winnerColour));
797
		$chessType = $this->isNPCGame() ? 'Chess (NPC)' : 'Chess';
798
		$winningPlayer->increaseHOF(1, array($chessType, 'Games', 'Won'), HOF_PUBLIC);
799
		$losingPlayer->increaseHOF(1, array($chessType, 'Games', 'Lost'), HOF_PUBLIC);
800
		return array('Winner' => $winningPlayer, 'Loser' => $losingPlayer);
801
	}
802
803
	public function &getHasMoved() {
804
		return $this->hasMoved;
805
	}
806
807
	public function getCurrentTurnColour() {
808
		return count($this->getMoves()) % 2 == 0 ? self::PLAYER_WHITE : self::PLAYER_BLACK;
809
	}
810
811
	public function getCurrentTurnAccountID() {
812
		return count($this->getMoves()) % 2 == 0 ? $this->whiteID : $this->blackID;
813
	}
814
815
	public function getCurrentTurnPlayer() {
816
		return SmrPlayer::getPlayer($this->getCurrentTurnAccountID(), $this->getGameID());
817
	}
818
819
	public function getCurrentTurnAccount() {
820
		return SmrAccount::getAccount($this->getCurrentTurnAccountID());
821
	}
822
823
	public function getWhiteAccount() {
824
		return SmrAccount::getAccount($this->getWhiteID());
825
	}
826
827
	public function getBlackAccount() {
828
		return SmrAccount::getAccount($this->getBlackID());
829
	}
830
831
	public function isCurrentTurn($accountID) {
832
		return $accountID == $this->getCurrentTurnAccountID();
833
	}
834
835
	public function isNPCGame() {
836
		return $this->getWhiteAccount()->isNPC() || $this->getBlackAccount()->isNPC();
837
	}
838
839
	public static function getOtherColour($colour) {
840
		if ($colour == self::PLAYER_WHITE) {
841
			return self::PLAYER_BLACK;
842
		}
843
		if ($colour == self::PLAYER_BLACK) {
844
			return self::PLAYER_WHITE;
845
		}
846
		return false;
847
	}
848
849
	public function resign($accountID) {
850
		if ($this->hasEnded() || !$this->getColourForAccountID($accountID)) {
851
			return false;
852
		}
853
		// If only 1 person has moved then just end the game.
854
		if (count($this->getMoves()) < 2) {
855
			$this->endDate = TIME;
856
			$this->db->query('UPDATE chess_game
857
							SET end_time=' . $this->db->escapeNumber(TIME) . '
858
							WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ';');
859
			return 1;
860
		} else {
861
			$loserColour = $this->getColourForAccountID($accountID);
862
			$winnerAccountID = $this->getColourID(self::getOtherColour($loserColour));
863
			$results = $this->setWinner($winnerAccountID);
864
			$chessType = $this->isNPCGame() ? 'Chess (NPC)' : 'Chess';
865
			$results['Loser']->increaseHOF(1, array($chessType, 'Games', 'Resigned'), HOF_PUBLIC);
866
			SmrPlayer::sendMessageFromCasino($results['Winner']->getGameID(), $results['Winner']->getPlayerID(), '[player=' . $results['Loser']->getPlayerID() . '] just resigned against you in [chess=' . $this->getChessGameID() . '].');
867
			return 0;
868
		}
869
	}
870
871
	public function getPlayGameHREF() {
872
		return SmrSession::getNewHREF(create_container('skeleton.php', 'chess_play.php', array('ChessGameID' => $this->chessGameID)));
873
	}
874
875
	public function getResignHREF() {
876
		return SmrSession::getNewHREF(create_container('chess_resign_processing.php', '', array('ChessGameID' => $this->chessGameID)));
877
	}
878
}
879