We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.
Total Complexity | 211 |
Total Lines | 877 |
Duplicated Lines | 0 % |
Changes | 8 | ||
Bugs | 1 | Features | 1 |
Complex classes like ChessGame often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ChessGame, and based on these observations, apply Extract Interface, too.
1 | <?php declare(strict_types=1); |
||
15 | class ChessGame { |
||
16 | |||
17 | public const END_RESIGN = 0; |
||
18 | public const END_CANCEL = 1; |
||
19 | |||
20 | /** @var array<int, self> */ |
||
21 | protected static array $CACHE_CHESS_GAMES = []; |
||
22 | |||
23 | private Database $db; |
||
24 | |||
25 | private readonly int $whiteID; |
||
26 | private readonly int $blackID; |
||
27 | private readonly int $gameID; |
||
28 | private readonly int $startDate; |
||
29 | private ?int $endDate; |
||
30 | private int $winner; |
||
31 | |||
32 | /** @var array<mixed> */ |
||
33 | private array $hasMoved; |
||
34 | /** @var array<int, array<int, ?ChessPiece>> */ |
||
35 | private array $board; |
||
36 | /** @var array<string> */ |
||
37 | private array $moves; |
||
38 | |||
39 | /** @var ?array<string, array<string, int>> */ |
||
40 | private ?array $lastMove = null; |
||
41 | |||
42 | /** |
||
43 | * @return array<self> |
||
44 | */ |
||
45 | public static function getNPCMoveGames(bool $forceUpdate = false): array { |
||
46 | $db = Database::getInstance(); |
||
47 | $dbResult = $db->read('SELECT chess_game_id |
||
48 | FROM npc_logins |
||
49 | JOIN account USING(login) |
||
50 | JOIN chess_game ON account_id = black_id OR account_id = white_id |
||
51 | WHERE end_time > ' . Epoch::time() . ' OR end_time IS NULL;'); |
||
52 | $games = []; |
||
53 | foreach ($dbResult->records() as $dbRecord) { |
||
54 | $game = self::getChessGame($dbRecord->getInt('chess_game_id'), $forceUpdate); |
||
55 | if ($game->getCurrentTurnAccount()->isNPC()) { |
||
56 | $games[] = $game; |
||
57 | } |
||
58 | } |
||
59 | return $games; |
||
60 | } |
||
61 | |||
62 | /** |
||
63 | * @return array<self> |
||
64 | */ |
||
65 | public static function getOngoingPlayerGames(AbstractSmrPlayer $player): array { |
||
66 | $db = Database::getInstance(); |
||
67 | $dbResult = $db->read('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 > ' . Epoch::time() . ' OR end_time IS NULL);'); |
||
68 | $games = []; |
||
69 | foreach ($dbResult->records() as $dbRecord) { |
||
70 | $games[] = self::getChessGame($dbRecord->getInt('chess_game_id')); |
||
71 | } |
||
72 | return $games; |
||
73 | } |
||
74 | |||
75 | public static function getChessGame(int $chessGameID, bool $forceUpdate = false): self { |
||
76 | if ($forceUpdate || !isset(self::$CACHE_CHESS_GAMES[$chessGameID])) { |
||
77 | self::$CACHE_CHESS_GAMES[$chessGameID] = new self($chessGameID); |
||
78 | } |
||
79 | return self::$CACHE_CHESS_GAMES[$chessGameID]; |
||
80 | } |
||
81 | |||
82 | public function __construct(private readonly int $chessGameID) { |
||
83 | $this->db = Database::getInstance(); |
||
84 | $dbResult = $this->db->read('SELECT * |
||
85 | FROM chess_game |
||
86 | WHERE chess_game_id=' . $this->db->escapeNumber($chessGameID)); |
||
87 | if (!$dbResult->hasRecord()) { |
||
88 | throw new Exception('Chess game not found: ' . $chessGameID); |
||
89 | } |
||
90 | $dbRecord = $dbResult->record(); |
||
91 | $this->gameID = $dbRecord->getInt('game_id'); |
||
|
|||
92 | $this->startDate = $dbRecord->getInt('start_time'); |
||
93 | $this->endDate = $dbRecord->getNullableInt('end_time'); |
||
94 | $this->whiteID = $dbRecord->getInt('white_id'); |
||
95 | $this->blackID = $dbRecord->getInt('black_id'); |
||
96 | $this->winner = $dbRecord->getInt('winner_id'); |
||
97 | $this->resetHasMoved(); |
||
98 | } |
||
99 | |||
100 | /** |
||
101 | * @param array<int, array<int, ?ChessPiece>> $board |
||
102 | */ |
||
103 | public static function isValidCoord(int $x, int $y, array $board): bool { |
||
104 | return $y < count($board) && $y >= 0 && $x < count($board[$y]) && $x >= 0; |
||
105 | } |
||
106 | |||
107 | /** |
||
108 | * @param array<int, array<int, ?ChessPiece>> $board |
||
109 | * @param array<mixed> $hasMoved |
||
110 | */ |
||
111 | public static function isPlayerChecked(array $board, array $hasMoved, Colour $colour): bool { |
||
112 | foreach ($board as $row) { |
||
113 | foreach ($row as $p) { |
||
114 | if ($p != null && $p->colour != $colour && $p->isAttacking($board, $hasMoved, true)) { |
||
115 | return true; |
||
116 | } |
||
117 | } |
||
118 | } |
||
119 | return false; |
||
120 | } |
||
121 | |||
122 | private function resetHasMoved(): void { |
||
123 | $this->hasMoved = [ |
||
124 | Colour::White->value => [ |
||
125 | ChessPiece::KING => false, |
||
126 | ChessPiece::ROOK => [ |
||
127 | 'Queen' => false, |
||
128 | 'King' => false, |
||
129 | ], |
||
130 | ], |
||
131 | Colour::Black->value => [ |
||
132 | ChessPiece::KING => false, |
||
133 | ChessPiece::ROOK => [ |
||
134 | 'Queen' => false, |
||
135 | 'King' => false, |
||
136 | ], |
||
137 | ], |
||
138 | ChessPiece::PAWN => [-1, -1], |
||
139 | ]; |
||
140 | } |
||
141 | |||
142 | public function rerunGame(bool $debugInfo = false): void { |
||
143 | $db = Database::getInstance(); |
||
144 | |||
145 | $db->write('UPDATE chess_game |
||
146 | SET end_time = NULL, winner_id = 0 |
||
147 | WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ';'); |
||
148 | $db->write('DELETE FROM chess_game_pieces WHERE chess_game_id = ' . $this->db->escapeNumber($this->chessGameID) . ';'); |
||
149 | self::insertPieces($this->chessGameID); |
||
150 | |||
151 | $dbResult = $db->read('SELECT * FROM chess_game_moves WHERE chess_game_id = ' . $this->db->escapeNumber($this->chessGameID) . ' ORDER BY move_id;'); |
||
152 | $db->write('DELETE FROM chess_game_moves WHERE chess_game_id = ' . $this->db->escapeNumber($this->chessGameID) . ';'); |
||
153 | $this->moves = []; |
||
154 | unset($this->board); |
||
155 | unset($this->endDate); |
||
156 | unset($this->winner); |
||
157 | $this->resetHasMoved(); |
||
158 | |||
159 | try { |
||
160 | foreach ($dbResult->records() as $dbRecord) { |
||
161 | $start_x = $dbRecord->getInt('start_x'); |
||
162 | $start_y = $dbRecord->getInt('start_y'); |
||
163 | $end_x = $dbRecord->getInt('end_x'); |
||
164 | $end_y = $dbRecord->getInt('end_y'); |
||
165 | $colour = $dbRecord->getInt('move_id') % 2 == 1 ? Colour::White : Colour::Black; |
||
166 | $promotePieceID = $dbRecord->getInt('promote_piece_id'); |
||
167 | if ($debugInfo === true) { |
||
168 | echo 'x=', $start_x, ', y=', $start_y, ', endX=', $end_x, ', endY=', $end_y, ', colour=', $colour->name, EOL; |
||
169 | } |
||
170 | if ($this->tryMove($start_x, $start_y, $end_x, $end_y, $colour, $promotePieceID) != 0) { |
||
171 | break; |
||
172 | } |
||
173 | } |
||
174 | } catch (Exception $e) { |
||
175 | if ($debugInfo === true) { |
||
176 | echo $e->getMessage() . EOL . $e->getTraceAsString() . EOL; |
||
177 | } |
||
178 | // We probably tried an invalid move - move on. |
||
179 | } |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * @return array<int, array<int, ?ChessPiece>> |
||
184 | */ |
||
185 | public function getBoard(): array { |
||
186 | if (!isset($this->board)) { |
||
187 | $dbResult = $this->db->read('SELECT * FROM chess_game_pieces WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ';'); |
||
188 | $pieces = []; |
||
189 | foreach ($dbResult->records() as $dbRecord) { |
||
190 | $pieces[] = new ChessPiece(Colour::from($dbRecord->getString('colour')), $dbRecord->getInt('piece_id'), $dbRecord->getInt('x'), $dbRecord->getInt('y'), $dbRecord->getInt('piece_no')); |
||
191 | } |
||
192 | $this->board = $this->parsePieces($pieces); |
||
193 | } |
||
194 | return $this->board; |
||
195 | } |
||
196 | |||
197 | /** |
||
198 | * Get the board from black's perspective |
||
199 | * |
||
200 | * @return array<int, array<int, ?ChessPiece>> |
||
201 | */ |
||
202 | public function getBoardReversed(): array { |
||
203 | // Need to reverse both the rows and the files to rotate the board |
||
204 | $board = array_reverse($this->getBoard(), true); |
||
205 | foreach ($board as $key => $row) { |
||
206 | $board[$key] = array_reverse($row, true); |
||
207 | } |
||
208 | return $board; |
||
209 | } |
||
210 | |||
211 | /** |
||
212 | * @return ?array<string, array<string, int>> |
||
213 | */ |
||
214 | public function getLastMove(): ?array { |
||
215 | $this->getMoves(); |
||
216 | return $this->lastMove; |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * Determines if a board square is part of the last move |
||
221 | * (returns true for both the 'To' and 'From' squares). |
||
222 | */ |
||
223 | public function isLastMoveSquare(int $x, int $y): bool { |
||
224 | $lastMove = $this->getLastMove(); |
||
225 | if ($lastMove === null) { |
||
226 | return false; |
||
227 | } |
||
228 | return ($x == $lastMove['From']['X'] && $y == $lastMove['From']['Y']) || ($x == $lastMove['To']['X'] && $y == $lastMove['To']['Y']); |
||
229 | } |
||
230 | |||
231 | /** |
||
232 | * @return array<string> |
||
233 | */ |
||
234 | public function getMoves(): array { |
||
235 | if (!isset($this->moves)) { |
||
236 | $dbResult = $this->db->read('SELECT * FROM chess_game_moves WHERE chess_game_id = ' . $this->db->escapeNumber($this->chessGameID) . ' ORDER BY move_id;'); |
||
237 | $this->moves = []; |
||
238 | $mate = false; |
||
239 | foreach ($dbResult->records() as $dbRecord) { |
||
240 | $pieceTakenID = $dbRecord->getNullableInt('piece_taken'); |
||
241 | $promotionPieceID = $dbRecord->getNullableInt('promote_piece_id'); |
||
242 | $this->moves[] = $this->createMove( |
||
243 | $dbRecord->getInt('piece_id'), |
||
244 | $dbRecord->getInt('start_x'), |
||
245 | $dbRecord->getInt('start_y'), |
||
246 | $dbRecord->getInt('end_x'), |
||
247 | $dbRecord->getInt('end_y'), |
||
248 | $pieceTakenID, |
||
249 | $dbRecord->getNullableString('checked'), |
||
250 | $dbRecord->getInt('move_id') % 2 == 1 ? Colour::White : Colour::Black, |
||
251 | $dbRecord->getNullableString('castling'), |
||
252 | $dbRecord->getBoolean('en_passant'), |
||
253 | $promotionPieceID |
||
254 | ); |
||
255 | $mate = $dbRecord->getNullableString('checked') == 'MATE'; |
||
256 | } |
||
257 | if (!$mate && $this->hasEnded()) { |
||
258 | if ($this->getWinner() != 0) { |
||
259 | $this->moves[] = ($this->getWinner() == $this->getWhiteID() ? 'Black' : 'White') . ' Resigned.'; |
||
260 | } elseif (count($this->moves) < 2) { |
||
261 | $this->moves[] = 'Game Cancelled.'; |
||
262 | } else { |
||
263 | $this->moves[] = 'Game Drawn.'; |
||
264 | } |
||
265 | } |
||
266 | } |
||
267 | return $this->moves; |
||
268 | } |
||
269 | |||
270 | public function getFENString(): string { |
||
271 | $fen = ''; |
||
272 | $board = $this->getBoard(); |
||
273 | $blanks = 0; |
||
274 | for ($y = 0; $y < 8; $y++) { |
||
275 | if ($y > 0) { |
||
276 | $fen .= '/'; |
||
277 | } |
||
278 | for ($x = 0; $x < 8; $x++) { |
||
279 | if ($board[$y][$x] === null) { |
||
280 | $blanks++; |
||
281 | } else { |
||
282 | if ($blanks > 0) { |
||
283 | $fen .= $blanks; |
||
284 | $blanks = 0; |
||
285 | } |
||
286 | $fen .= $board[$y][$x]->getPieceLetter(); |
||
287 | } |
||
288 | } |
||
289 | if ($blanks > 0) { |
||
290 | $fen .= $blanks; |
||
291 | $blanks = 0; |
||
292 | } |
||
293 | } |
||
294 | $fen .= match ($this->getCurrentTurnColour()) { |
||
295 | Colour::White => ' w ', |
||
296 | Colour::Black => ' b ', |
||
297 | }; |
||
298 | |||
299 | // Castling |
||
300 | $castling = ''; |
||
301 | foreach (Colour::cases() as $colour) { |
||
302 | if ($this->hasMoved[$colour->value][ChessPiece::KING] !== true) { |
||
303 | if ($this->hasMoved[$colour->value][ChessPiece::ROOK]['King'] !== true) { |
||
304 | $castling .= ChessPiece::getLetterForPiece(ChessPiece::KING, $colour); |
||
305 | } |
||
306 | if ($this->hasMoved[$colour->value][ChessPiece::ROOK]['Queen'] !== true) { |
||
307 | $castling .= ChessPiece::getLetterForPiece(ChessPiece::QUEEN, $colour); |
||
308 | } |
||
309 | } |
||
310 | } |
||
311 | if ($castling == '') { |
||
312 | $castling = '-'; |
||
313 | } |
||
314 | $fen .= $castling . ' '; |
||
315 | |||
316 | // En passant |
||
317 | [$pawnX, $pawnY] = $this->hasMoved[ChessPiece::PAWN]; |
||
318 | if ($pawnX != -1) { |
||
319 | $fen .= chr(ord('a') + $pawnX); |
||
320 | $fen .= match ($pawnY) { |
||
321 | 3 => '6', |
||
322 | 4 => '3', |
||
323 | default => throw new Exception('Invalid en passant rank: ' . $pawnY), |
||
324 | }; |
||
325 | } else { |
||
326 | $fen .= '-'; |
||
327 | } |
||
328 | $fen .= ' 0 ' . floor(count($this->moves) / 2); |
||
329 | |||
330 | return $fen; |
||
331 | } |
||
332 | |||
333 | /** |
||
334 | * @param array<ChessPiece> $pieces |
||
335 | * @return array<int, array<int, ?ChessPiece>> |
||
336 | */ |
||
337 | private static function parsePieces(array $pieces): array { |
||
338 | $row = array_fill(0, 8, null); |
||
339 | $board = array_fill(0, 8, $row); |
||
340 | foreach ($pieces as $piece) { |
||
341 | if ($board[$piece->y][$piece->x] != null) { |
||
342 | throw new Exception('Two pieces found in the same tile.'); |
||
343 | } |
||
344 | $board[$piece->y][$piece->x] = $piece; |
||
345 | } |
||
346 | return $board; |
||
347 | } |
||
348 | |||
349 | /** |
||
350 | * @return array<ChessPiece> |
||
351 | */ |
||
352 | public static function getStandardGame(): array { |
||
353 | return [ |
||
354 | new ChessPiece(Colour::Black, ChessPiece::ROOK, 0, 0), |
||
355 | new ChessPiece(Colour::Black, ChessPiece::KNIGHT, 1, 0), |
||
356 | new ChessPiece(Colour::Black, ChessPiece::BISHOP, 2, 0), |
||
357 | new ChessPiece(Colour::Black, ChessPiece::QUEEN, 3, 0), |
||
358 | new ChessPiece(Colour::Black, ChessPiece::KING, 4, 0), |
||
359 | new ChessPiece(Colour::Black, ChessPiece::BISHOP, 5, 0), |
||
360 | new ChessPiece(Colour::Black, ChessPiece::KNIGHT, 6, 0), |
||
361 | new ChessPiece(Colour::Black, ChessPiece::ROOK, 7, 0), |
||
362 | |||
363 | new ChessPiece(Colour::Black, ChessPiece::PAWN, 0, 1), |
||
364 | new ChessPiece(Colour::Black, ChessPiece::PAWN, 1, 1), |
||
365 | new ChessPiece(Colour::Black, ChessPiece::PAWN, 2, 1), |
||
366 | new ChessPiece(Colour::Black, ChessPiece::PAWN, 3, 1), |
||
367 | new ChessPiece(Colour::Black, ChessPiece::PAWN, 4, 1), |
||
368 | new ChessPiece(Colour::Black, ChessPiece::PAWN, 5, 1), |
||
369 | new ChessPiece(Colour::Black, ChessPiece::PAWN, 6, 1), |
||
370 | new ChessPiece(Colour::Black, ChessPiece::PAWN, 7, 1), |
||
371 | |||
372 | new ChessPiece(Colour::White, ChessPiece::PAWN, 0, 6), |
||
373 | new ChessPiece(Colour::White, ChessPiece::PAWN, 1, 6), |
||
374 | new ChessPiece(Colour::White, ChessPiece::PAWN, 2, 6), |
||
375 | new ChessPiece(Colour::White, ChessPiece::PAWN, 3, 6), |
||
376 | new ChessPiece(Colour::White, ChessPiece::PAWN, 4, 6), |
||
377 | new ChessPiece(Colour::White, ChessPiece::PAWN, 5, 6), |
||
378 | new ChessPiece(Colour::White, ChessPiece::PAWN, 6, 6), |
||
379 | new ChessPiece(Colour::White, ChessPiece::PAWN, 7, 6), |
||
380 | |||
381 | new ChessPiece(Colour::White, ChessPiece::ROOK, 0, 7), |
||
382 | new ChessPiece(Colour::White, ChessPiece::KNIGHT, 1, 7), |
||
383 | new ChessPiece(Colour::White, ChessPiece::BISHOP, 2, 7), |
||
384 | new ChessPiece(Colour::White, ChessPiece::QUEEN, 3, 7), |
||
385 | new ChessPiece(Colour::White, ChessPiece::KING, 4, 7), |
||
386 | new ChessPiece(Colour::White, ChessPiece::BISHOP, 5, 7), |
||
387 | new ChessPiece(Colour::White, ChessPiece::KNIGHT, 6, 7), |
||
388 | new ChessPiece(Colour::White, ChessPiece::ROOK, 7, 7), |
||
389 | ]; |
||
390 | } |
||
391 | |||
392 | public static function insertNewGame(int $startDate, ?int $endDate, AbstractSmrPlayer $whitePlayer, AbstractSmrPlayer $blackPlayer): int { |
||
393 | $db = Database::getInstance(); |
||
394 | $chessGameID = $db->insert('chess_game', [ |
||
395 | 'start_time' => $db->escapeNumber($startDate), |
||
396 | 'end_time' => $endDate === null ? 'NULL' : $db->escapeNumber($endDate), |
||
397 | 'white_id' => $db->escapeNumber($whitePlayer->getAccountID()), |
||
398 | 'black_id' => $db->escapeNumber($blackPlayer->getAccountID()), |
||
399 | 'game_id' => $db->escapeNumber($whitePlayer->getGameID()), |
||
400 | ]); |
||
401 | |||
402 | self::insertPieces($chessGameID); |
||
403 | return $chessGameID; |
||
404 | } |
||
405 | |||
406 | private static function insertPieces(int $chessGameID): void { |
||
407 | $db = Database::getInstance(); |
||
408 | $pieces = self::getStandardGame(); |
||
409 | foreach ($pieces as $p) { |
||
410 | $db->insert('chess_game_pieces', [ |
||
411 | 'chess_game_id' => $db->escapeNumber($chessGameID), |
||
412 | 'colour' => $db->escapeString($p->colour->value), |
||
413 | 'piece_id' => $db->escapeNumber($p->pieceID), |
||
414 | 'x' => $db->escapeNumber($p->x), |
||
415 | 'y' => $db->escapeNumber($p->y), |
||
416 | ]); |
||
417 | } |
||
418 | } |
||
419 | |||
420 | private function createMove(int $pieceID, int $startX, int $startY, int $endX, int $endY, ?int $pieceTaken, ?string $checking, Colour $playerColour, ?string $castling, bool $enPassant, ?int $promotionPieceID): string { |
||
421 | // This move will be set as the most recent move |
||
422 | $this->lastMove = [ |
||
423 | 'From' => ['X' => $startX, 'Y' => $startY], |
||
424 | 'To' => ['X' => $endX, 'Y' => $endY], |
||
425 | ]; |
||
426 | |||
427 | $otherPlayerColour = $playerColour->opposite(); |
||
428 | if ($pieceID == ChessPiece::KING) { |
||
429 | $this->hasMoved[$playerColour->value][ChessPiece::KING] = true; |
||
430 | } |
||
431 | // Check if the piece moving is a rook and mark it as moved to stop castling. |
||
432 | if ($pieceID == ChessPiece::ROOK && ($startX == 0 || $startX == 7) && ($startY == ($playerColour == Colour::White ? 7 : 0))) { |
||
433 | $this->hasMoved[$playerColour->value][ChessPiece::ROOK][$startX == 0 ? 'Queen' : 'King'] = true; |
||
434 | } |
||
435 | // 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. |
||
436 | if ($pieceTaken == ChessPiece::ROOK && ($endX == 0 || $endX == 7) && $endY == ($otherPlayerColour == Colour::White ? 7 : 0)) { |
||
437 | $this->hasMoved[$otherPlayerColour->value][ChessPiece::ROOK][$endX == 0 ? 'Queen' : 'King'] = true; |
||
438 | } |
||
439 | if ($pieceID == ChessPiece::PAWN && ($startY == 1 || $startY == 6) && ($endY == 3 || $endY == 4)) { |
||
440 | $this->hasMoved[ChessPiece::PAWN] = [$endX, $endY]; |
||
441 | } else { |
||
442 | $this->hasMoved[ChessPiece::PAWN] = [-1, -1]; |
||
443 | } |
||
444 | return ($castling == 'Queen' ? '0-0-0' : ($castling == 'King' ? '0-0' : '')) |
||
445 | . ChessPiece::getSymbolForPiece($pieceID, $playerColour) |
||
446 | . chr(ord('a') + $startX) |
||
447 | . (8 - $startY) |
||
448 | . ' ' |
||
449 | . ($pieceTaken === null ? '' : ChessPiece::getSymbolForPiece($pieceTaken, $otherPlayerColour)) |
||
450 | . chr(ord('a') + $endX) |
||
451 | . (8 - $endY) |
||
452 | . ($promotionPieceID === null ? '' : ChessPiece::getSymbolForPiece($promotionPieceID, $playerColour)) |
||
453 | . ' ' |
||
454 | . ($checking === null ? '' : ($checking == 'CHECK' ? '+' : '++')) |
||
455 | . ($enPassant ? ' e.p.' : ''); |
||
456 | } |
||
457 | |||
458 | public function isCheckmated(Colour $colour): bool { |
||
459 | $king = null; |
||
460 | foreach ($this->board as $row) { |
||
461 | foreach ($row as $piece) { |
||
462 | if ($piece != null && $piece->pieceID == ChessPiece::KING && $piece->colour == $colour) { |
||
463 | $king = $piece; |
||
464 | break; |
||
465 | } |
||
466 | } |
||
467 | } |
||
468 | if ($king === null) { |
||
469 | throw new Exception('Could not find the king: game id = ' . $this->chessGameID); |
||
470 | } |
||
471 | if (!self::isPlayerChecked($this->board, $this->hasMoved, $colour)) { |
||
472 | return false; |
||
473 | } |
||
474 | foreach ($this->board as $row) { |
||
475 | foreach ($row as $piece) { |
||
476 | if ($piece != null && $piece->colour == $colour) { |
||
477 | $moves = $piece->getPossibleMoves($this->board, $this->hasMoved); |
||
478 | //There are moves we can make, we are clearly not checkmated. |
||
479 | if (count($moves) > 0) { |
||
480 | return false; |
||
481 | } |
||
482 | } |
||
483 | } |
||
484 | } |
||
485 | return true; |
||
486 | } |
||
487 | |||
488 | /** |
||
489 | * @return array{Type: string, X: int, ToX: int}|false |
||
490 | */ |
||
491 | public static function isCastling(int $x, int $toX): array|false { |
||
492 | $movement = $toX - $x; |
||
493 | return match ($movement) { |
||
494 | -2 => ['Type' => 'Queen', 'X' => 0, 'ToX' => 3], |
||
495 | 2 => ['Type' => 'King', 'X' => 7, 'ToX' => 5], |
||
496 | default => false, |
||
497 | }; |
||
498 | } |
||
499 | |||
500 | /** |
||
501 | * @param array<int, array<int, ?ChessPiece>> $board |
||
502 | * @param array<mixed> $hasMoved |
||
503 | * @return array<string, mixed> |
||
504 | */ |
||
505 | public static function movePiece(array &$board, array &$hasMoved, int $x, int $y, int $toX, int $toY, int $pawnPromotionPiece = ChessPiece::QUEEN): array { |
||
506 | if (!self::isValidCoord($x, $y, $board)) { |
||
507 | throw new Exception('Invalid from coordinates, x=' . $x . ', y=' . $y); |
||
508 | } |
||
509 | if (!self::isValidCoord($toX, $toY, $board)) { |
||
510 | throw new Exception('Invalid to coordinates, x=' . $toX . ', y=' . $toY); |
||
511 | } |
||
512 | $pieceTaken = $board[$toY][$toX]; |
||
513 | $board[$toY][$toX] = $board[$y][$x]; |
||
514 | $p = $board[$toY][$toX]; |
||
515 | $board[$y][$x] = null; |
||
516 | if ($p === null) { |
||
517 | throw new Exception('Trying to move non-existent piece: ' . var_export($board, true)); |
||
518 | } |
||
519 | $p->x = $toX; |
||
520 | $p->y = $toY; |
||
521 | |||
522 | $oldPawnMovement = $hasMoved[ChessPiece::PAWN]; |
||
523 | $nextPawnMovement = [-1, -1]; |
||
524 | $castling = false; |
||
525 | $enPassant = false; |
||
526 | $rookMoved = false; |
||
527 | $rookTaken = false; |
||
528 | $pawnPromotion = false; |
||
529 | if ($p->pieceID == ChessPiece::KING) { |
||
530 | //Castling? |
||
531 | $castling = self::isCastling($x, $toX); |
||
532 | if ($castling !== false) { |
||
533 | $hasMoved[$p->colour->value][ChessPiece::KING] = true; |
||
534 | $hasMoved[$p->colour->value][ChessPiece::ROOK][$castling['Type']] = true; |
||
535 | if ($board[$y][$castling['X']] === null) { |
||
536 | throw new Exception('Cannot castle with non-existent rook.'); |
||
537 | } |
||
538 | $board[$toY][$castling['ToX']] = $board[$y][$castling['X']]; |
||
539 | $board[$toY][$castling['ToX']]->x = $castling['ToX']; |
||
540 | $board[$y][$castling['X']] = null; |
||
541 | } |
||
542 | } elseif ($p->pieceID == ChessPiece::PAWN) { |
||
543 | if ($toY == 0 || $toY == 7) { |
||
544 | $pawnPromotion = $p->promote($pawnPromotionPiece, $board); |
||
545 | } elseif (($y == 1 || $y == 6) && ($toY == 3 || $toY == 4)) { |
||
546 | //Double move to track? |
||
547 | $nextPawnMovement = [$toX, $toY]; |
||
548 | } elseif ($hasMoved[ChessPiece::PAWN][0] == $toX && |
||
549 | ($hasMoved[ChessPiece::PAWN][1] == 3 && $toY == 2 || $hasMoved[ChessPiece::PAWN][1] == 4 && $toY == 5)) { |
||
550 | //En passant? |
||
551 | $enPassant = true; |
||
552 | $pieceTaken = $board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]]; |
||
553 | if ($board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]] === null) { |
||
554 | throw new Exception('Cannot en passant a non-existent pawn.'); |
||
555 | } |
||
556 | $board[$hasMoved[ChessPiece::PAWN][1]][$hasMoved[ChessPiece::PAWN][0]] = null; |
||
557 | } |
||
558 | } elseif ($p->pieceID == ChessPiece::ROOK && ($x == 0 || $x == 7) && $y == ($p->colour == Colour::White ? 7 : 0)) { |
||
559 | //Rook moved? |
||
560 | if ($hasMoved[$p->colour->value][ChessPiece::ROOK][$x == 0 ? 'Queen' : 'King'] === false) { |
||
561 | // We set rook moved in here as it's used for move info. |
||
562 | $rookMoved = $x == 0 ? 'Queen' : 'King'; |
||
563 | $hasMoved[$p->colour->value][ChessPiece::ROOK][$rookMoved] = true; |
||
564 | } |
||
565 | } |
||
566 | // 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. |
||
567 | if ($pieceTaken != null && $pieceTaken->pieceID == ChessPiece::ROOK && ($toX == 0 || $toX == 7) && $toY == ($pieceTaken->colour == Colour::White ? 7 : 0)) { |
||
568 | if ($hasMoved[$pieceTaken->colour->value][ChessPiece::ROOK][$toX == 0 ? 'Queen' : 'King'] === false) { |
||
569 | $rookTaken = $toX == 0 ? 'Queen' : 'King'; |
||
570 | $hasMoved[$pieceTaken->colour->value][ChessPiece::ROOK][$rookTaken] = true; |
||
571 | } |
||
572 | } |
||
573 | |||
574 | $hasMoved[ChessPiece::PAWN] = $nextPawnMovement; |
||
575 | return [ |
||
576 | 'Castling' => $castling, |
||
577 | 'PieceTaken' => $pieceTaken, |
||
578 | 'EnPassant' => $enPassant, |
||
579 | 'RookMoved' => $rookMoved, |
||
580 | 'RookTaken' => $rookTaken, |
||
581 | 'OldPawnMovement' => $oldPawnMovement, |
||
582 | 'PawnPromotion' => $pawnPromotion, |
||
583 | ]; |
||
584 | } |
||
585 | |||
586 | /** |
||
587 | * @param string $move Algebraic notation like "b2b4" |
||
588 | */ |
||
589 | public function tryAlgebraicMove(string $move): void { |
||
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 | $file = $move[0]; |
||
594 | $rank = str2int($move[1]); |
||
595 | $toFile = $move[2]; |
||
596 | $toRank = str2int($move[3]); |
||
597 | |||
598 | $aVal = ord('a'); |
||
599 | $x = ord($file) - $aVal; |
||
600 | $toX = ord($toFile) - $aVal; |
||
601 | $y = $rank - 1; |
||
602 | $toY = $toRank - 1; |
||
603 | |||
604 | $pawnPromotionPiece = ChessPiece::QUEEN; |
||
605 | if (isset($move[4])) { |
||
606 | $pawnPromotionPiece = ChessPiece::getPieceForLetter($move[4]); |
||
607 | } |
||
608 | $this->tryMove($x, $y, $toX, $toY, $this->getCurrentTurnColour(), $pawnPromotionPiece); |
||
609 | } |
||
610 | |||
611 | public function tryMove(int $x, int $y, int $toX, int $toY, Colour $forColour, int $pawnPromotionPiece): string { |
||
612 | if ($this->hasEnded()) { |
||
613 | throw new UserError('This game is already over'); |
||
614 | } |
||
615 | if ($this->getCurrentTurnColour() != $forColour) { |
||
616 | throw new UserError('It is not your turn to move'); |
||
617 | } |
||
618 | $lastTurnPlayer = $this->getCurrentTurnPlayer(); |
||
619 | $this->getBoard(); |
||
620 | $p = $this->board[$y][$x]; |
||
621 | if ($p === null || $p->colour != $forColour) { |
||
622 | throw new UserError('There is no piece on that square'); |
||
623 | } |
||
624 | |||
625 | $moves = $p->getPossibleMoves($this->board, $this->hasMoved, $forColour); |
||
626 | $moveIsLegal = false; |
||
627 | foreach ($moves as $move) { |
||
628 | if ($move[0] == $toX && $move[1] == $toY) { |
||
629 | $moveIsLegal = true; |
||
630 | break; |
||
631 | } |
||
632 | } |
||
633 | if (!$moveIsLegal) { |
||
634 | throw new UserError('That move is not legal'); |
||
635 | } |
||
636 | |||
637 | $chessType = $this->isNPCGame() ? 'Chess (NPC)' : 'Chess'; |
||
638 | $currentPlayer = $this->getCurrentTurnPlayer(); |
||
639 | |||
640 | $moveInfo = self::movePiece($this->board, $this->hasMoved, $x, $y, $toX, $toY, $pawnPromotionPiece); |
||
641 | |||
642 | //We have taken the move, we should refresh $p |
||
643 | $p = $this->board[$toY][$toX]; |
||
644 | |||
645 | $pieceTakenID = null; |
||
646 | if ($moveInfo['PieceTaken'] != null) { |
||
647 | $pieceTakenID = $moveInfo['PieceTaken']->pieceID; |
||
648 | if ($moveInfo['PieceTaken']->pieceID == ChessPiece::KING) { |
||
649 | throw new Exception('King was taken.'); |
||
650 | } |
||
651 | } |
||
652 | |||
653 | $pieceID = $p->pieceID; |
||
654 | $pieceNo = $p->pieceNo; |
||
655 | $promotionPieceID = null; |
||
656 | if ($moveInfo['PawnPromotion'] !== false) { |
||
657 | $p->pieceID = $moveInfo['PawnPromotion']['PieceID']; |
||
658 | $p->pieceNo = $moveInfo['PawnPromotion']['PieceNo']; |
||
659 | $promotionPieceID = $p->pieceID; |
||
660 | } |
||
661 | |||
662 | $checking = null; |
||
663 | if ($p->isAttacking($this->board, $this->hasMoved, true)) { |
||
664 | $checking = 'CHECK'; |
||
665 | } |
||
666 | if ($this->isCheckmated($p->colour->opposite())) { |
||
667 | $checking = 'MATE'; |
||
668 | } |
||
669 | |||
670 | $castlingType = $moveInfo['Castling'] === false ? null : $moveInfo['Castling']['Type']; |
||
671 | |||
672 | $this->getMoves(); // make sure $this->moves is initialized |
||
673 | $this->moves[] = $this->createMove($pieceID, $x, $y, $toX, $toY, $pieceTakenID, $checking, $this->getCurrentTurnColour(), $castlingType, $moveInfo['EnPassant'], $promotionPieceID); |
||
674 | if (self::isPlayerChecked($this->board, $this->hasMoved, $p->colour)) { |
||
675 | throw new UserError('You cannot end your turn in check'); |
||
676 | } |
||
677 | |||
678 | $otherPlayer = $this->getCurrentTurnPlayer(); |
||
679 | if ($moveInfo['PawnPromotion'] !== false) { |
||
680 | $piecePromotedSymbol = $p->getPieceSymbol(); |
||
681 | $currentPlayer->increaseHOF(1, [$chessType, 'Moves', 'Own Pawns Promoted', 'Total'], HOF_PUBLIC); |
||
682 | $otherPlayer->increaseHOF(1, [$chessType, 'Moves', 'Opponent Pawns Promoted', 'Total'], HOF_PUBLIC); |
||
683 | $currentPlayer->increaseHOF(1, [$chessType, 'Moves', 'Own Pawns Promoted', $piecePromotedSymbol], HOF_PUBLIC); |
||
684 | $otherPlayer->increaseHOF(1, [$chessType, 'Moves', 'Opponent Pawns Promoted', $piecePromotedSymbol], HOF_PUBLIC); |
||
685 | } |
||
686 | |||
687 | $this->db->insert('chess_game_moves', [ |
||
688 | 'chess_game_id' => $this->db->escapeNumber($this->chessGameID), |
||
689 | 'piece_id' => $this->db->escapeNumber($pieceID), |
||
690 | 'start_x' => $this->db->escapeNumber($x), |
||
691 | 'start_y' => $this->db->escapeNumber($y), |
||
692 | 'end_x' => $this->db->escapeNumber($toX), |
||
693 | 'end_y' => $this->db->escapeNumber($toY), |
||
694 | 'checked' => $this->db->escapeString($checking, true), |
||
695 | 'piece_taken' => $moveInfo['PieceTaken'] === null ? 'NULL' : $this->db->escapeNumber($moveInfo['PieceTaken']->pieceID), |
||
696 | 'castling' => $this->db->escapeString($castlingType, true), |
||
697 | 'en_passant' => $this->db->escapeBoolean($moveInfo['EnPassant']), |
||
698 | 'promote_piece_id' => $moveInfo['PawnPromotion'] == false ? 'NULL' : $this->db->escapeNumber($moveInfo['PawnPromotion']['PieceID']), |
||
699 | ]); |
||
700 | |||
701 | $currentPlayer->increaseHOF(1, [$chessType, 'Moves', 'Total Taken'], HOF_PUBLIC); |
||
702 | if ($moveInfo['PieceTaken'] != null) { |
||
703 | // Get the owner of the taken piece |
||
704 | $this->db->write('DELETE FROM chess_game_pieces |
||
705 | WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ' AND colour=' . $this->db->escapeString($moveInfo['PieceTaken']->colour->value) . ' AND piece_id=' . $this->db->escapeNumber($moveInfo['PieceTaken']->pieceID) . ' AND piece_no=' . $this->db->escapeNumber($moveInfo['PieceTaken']->pieceNo) . ';'); |
||
706 | |||
707 | $pieceTakenSymbol = $moveInfo['PieceTaken']->getPieceSymbol(); |
||
708 | $currentPlayer->increaseHOF(1, [$chessType, 'Moves', 'Opponent Pieces Taken', 'Total'], HOF_PUBLIC); |
||
709 | $otherPlayer->increaseHOF(1, [$chessType, 'Moves', 'Own Pieces Taken', 'Total'], HOF_PUBLIC); |
||
710 | $currentPlayer->increaseHOF(1, [$chessType, 'Moves', 'Opponent Pieces Taken', $pieceTakenSymbol], HOF_PUBLIC); |
||
711 | $otherPlayer->increaseHOF(1, [$chessType, 'Moves', 'Own Pieces Taken', $pieceTakenSymbol], HOF_PUBLIC); |
||
712 | } |
||
713 | |||
714 | $this->db->write('UPDATE chess_game_pieces |
||
715 | SET x=' . $this->db->escapeNumber($toX) . ', y=' . $this->db->escapeNumber($toY) . |
||
716 | ($moveInfo['PawnPromotion'] !== false ? ', piece_id=' . $this->db->escapeNumber($moveInfo['PawnPromotion']['PieceID']) . ', piece_no=' . $this->db->escapeNumber($moveInfo['PawnPromotion']['PieceNo']) : '') . ' |
||
717 | WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ' AND colour=' . $this->db->escapeString($p->colour->value) . ' AND piece_id=' . $this->db->escapeNumber($pieceID) . ' AND piece_no=' . $this->db->escapeNumber($pieceNo) . ';'); |
||
718 | if ($moveInfo['Castling'] !== false) { |
||
719 | $this->db->write('UPDATE chess_game_pieces |
||
720 | SET x=' . $this->db->escapeNumber($moveInfo['Castling']['ToX']) . ' |
||
721 | WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ' AND colour=' . $this->db->escapeString($p->colour->value) . ' AND x = ' . $this->db->escapeNumber($moveInfo['Castling']['X']) . ' AND y = ' . $this->db->escapeNumber($y) . ';'); |
||
722 | } |
||
723 | |||
724 | if ($checking == 'MATE') { |
||
725 | $message = 'You have checkmated your opponent, congratulations!'; |
||
726 | $this->setWinner($this->getColourID($forColour)); |
||
727 | SmrPlayer::sendMessageFromCasino($lastTurnPlayer->getGameID(), $this->getCurrentTurnAccountID(), 'You have just lost [chess=' . $this->getChessGameID() . '] against [player=' . $lastTurnPlayer->getPlayerID() . '].'); |
||
728 | } else { |
||
729 | $message = ''; // non-mating valid move, no message |
||
730 | SmrPlayer::sendMessageFromCasino($lastTurnPlayer->getGameID(), $this->getCurrentTurnAccountID(), 'It is now your turn in [chess=' . $this->getChessGameID() . '] against [player=' . $lastTurnPlayer->getPlayerID() . '].'); |
||
731 | if ($checking == 'CHECK') { |
||
732 | $currentPlayer->increaseHOF(1, [$chessType, 'Moves', 'Check Given'], HOF_PUBLIC); |
||
733 | $otherPlayer->increaseHOF(1, [$chessType, 'Moves', 'Check Received'], HOF_PUBLIC); |
||
734 | } |
||
735 | } |
||
736 | $currentPlayer->saveHOF(); |
||
737 | $otherPlayer->saveHOF(); |
||
738 | return $message; |
||
739 | } |
||
740 | |||
741 | public function getChessGameID(): int { |
||
742 | return $this->chessGameID; |
||
743 | } |
||
744 | |||
745 | public function getStartDate(): int { |
||
746 | return $this->startDate; |
||
747 | } |
||
748 | |||
749 | public function getGameID(): int { |
||
750 | return $this->gameID; |
||
751 | } |
||
752 | |||
753 | public function getWhitePlayer(): AbstractSmrPlayer { |
||
754 | return SmrPlayer::getPlayer($this->whiteID, $this->getGameID()); |
||
755 | } |
||
756 | |||
757 | public function getWhiteID(): int { |
||
758 | return $this->whiteID; |
||
759 | } |
||
760 | |||
761 | public function getBlackPlayer(): AbstractSmrPlayer { |
||
762 | return SmrPlayer::getPlayer($this->blackID, $this->getGameID()); |
||
763 | } |
||
764 | |||
765 | public function getBlackID(): int { |
||
766 | return $this->blackID; |
||
767 | } |
||
768 | |||
769 | public function getColourID(Colour $colour): int { |
||
770 | return match ($colour) { |
||
771 | Colour::White => $this->getWhiteID(), |
||
772 | Colour::Black => $this->getBlackID(), |
||
773 | }; |
||
774 | } |
||
775 | |||
776 | public function getColourPlayer(Colour $colour): AbstractSmrPlayer { |
||
777 | return SmrPlayer::getPlayer($this->getColourID($colour), $this->getGameID()); |
||
778 | } |
||
779 | |||
780 | public function getColourForAccountID(int $accountID): Colour { |
||
781 | return match ($accountID) { |
||
782 | $this->getWhiteID() => Colour::White, |
||
783 | $this->getBlackID() => Colour::Black, |
||
784 | default => throw new Exception('Account ID is not in this chess game: ' . $accountID), |
||
785 | }; |
||
786 | } |
||
787 | |||
788 | /** |
||
789 | * Is the given account ID one of the two players of this game? |
||
790 | */ |
||
791 | public function isPlayer(int $accountID): bool { |
||
792 | return $accountID === $this->getWhiteID() || $accountID === $this->getBlackID(); |
||
793 | } |
||
794 | |||
795 | public function hasEnded(): bool { |
||
796 | return $this->endDate !== null && $this->endDate <= Epoch::time(); |
||
797 | } |
||
798 | |||
799 | public function getWinner(): int { |
||
800 | return $this->winner; |
||
801 | } |
||
802 | |||
803 | /** |
||
804 | * @return array<string, AbstractSmrPlayer> |
||
805 | */ |
||
806 | public function setWinner(int $accountID): array { |
||
807 | $this->winner = $accountID; |
||
808 | $this->endDate = Epoch::time(); |
||
809 | $this->db->write('UPDATE chess_game |
||
810 | SET end_time=' . $this->db->escapeNumber(Epoch::time()) . ', winner_id=' . $this->db->escapeNumber($this->winner) . ' |
||
811 | WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ';'); |
||
812 | $winnerColour = $this->getColourForAccountID($accountID); |
||
813 | $winningPlayer = $this->getColourPlayer($winnerColour); |
||
814 | $losingPlayer = $this->getColourPlayer($winnerColour->opposite()); |
||
815 | $chessType = $this->isNPCGame() ? 'Chess (NPC)' : 'Chess'; |
||
816 | $winningPlayer->increaseHOF(1, [$chessType, 'Games', 'Won'], HOF_PUBLIC); |
||
817 | $losingPlayer->increaseHOF(1, [$chessType, 'Games', 'Lost'], HOF_PUBLIC); |
||
818 | return ['Winner' => $winningPlayer, 'Loser' => $losingPlayer]; |
||
819 | } |
||
820 | |||
821 | /** |
||
822 | * @return array<mixed> |
||
823 | */ |
||
824 | public function &getHasMoved(): array { |
||
825 | return $this->hasMoved; |
||
826 | } |
||
827 | |||
828 | public function getCurrentTurnColour(): Colour { |
||
830 | } |
||
831 | |||
832 | public function getCurrentTurnAccountID(): int { |
||
833 | return count($this->getMoves()) % 2 == 0 ? $this->whiteID : $this->blackID; |
||
834 | } |
||
835 | |||
836 | public function getCurrentTurnPlayer(): AbstractSmrPlayer { |
||
837 | return SmrPlayer::getPlayer($this->getCurrentTurnAccountID(), $this->getGameID()); |
||
838 | } |
||
839 | |||
840 | public function getCurrentTurnAccount(): SmrAccount { |
||
842 | } |
||
843 | |||
844 | public function getWhiteAccount(): SmrAccount { |
||
845 | return SmrAccount::getAccount($this->getWhiteID()); |
||
846 | } |
||
847 | |||
848 | public function getBlackAccount(): SmrAccount { |
||
850 | } |
||
851 | |||
852 | public function isCurrentTurn(int $accountID): bool { |
||
854 | } |
||
855 | |||
856 | public function isNPCGame(): bool { |
||
858 | } |
||
859 | |||
860 | /** |
||
861 | * @return self::END_* |
||
862 | */ |
||
863 | public function resign(int $accountID): int { |
||
864 | if ($this->hasEnded() || !$this->isPlayer($accountID)) { |
||
865 | throw new Exception('Invalid resign conditions'); |
||
866 | } |
||
867 | |||
868 | // If only 1 person has moved then just end the game. |
||
869 | if (count($this->getMoves()) < 2) { |
||
870 | $this->endDate = Epoch::time(); |
||
871 | $this->db->write('UPDATE chess_game |
||
872 | SET end_time=' . $this->db->escapeNumber(Epoch::time()) . ' |
||
873 | WHERE chess_game_id=' . $this->db->escapeNumber($this->chessGameID) . ';'); |
||
874 | return self::END_CANCEL; |
||
875 | } |
||
876 | |||
877 | $loserColour = $this->getColourForAccountID($accountID); |
||
878 | $winnerAccountID = $this->getColourID($loserColour->opposite()); |
||
879 | $results = $this->setWinner($winnerAccountID); |
||
880 | $chessType = $this->isNPCGame() ? 'Chess (NPC)' : 'Chess'; |
||
881 | $results['Loser']->increaseHOF(1, [$chessType, 'Games', 'Resigned'], HOF_PUBLIC); |
||
882 | SmrPlayer::sendMessageFromCasino($results['Winner']->getGameID(), $results['Winner']->getAccountID(), '[player=' . $results['Loser']->getPlayerID() . '] just resigned against you in [chess=' . $this->getChessGameID() . '].'); |
||
883 | return self::END_RESIGN; |
||
884 | } |
||
885 | |||
886 | public function getPlayGameHREF(): string { |
||
887 | return (new MatchPlay($this->chessGameID))->href(); |
||
888 | } |
||
889 | |||
890 | public function getResignHREF(): string { |
||
891 | return (new MatchResignProcessor($this->chessGameID))->href(); |
||
892 | } |
||
893 | |||
894 | } |
||
895 |